Compare commits

...

10 Commits

Author SHA1 Message Date
Aiden Cline 88ccd16a0c refactor(core): simplify ChatGPT connection state 2026-07-28 04:41:41 +00:00
Aiden Cline aef37e4514 refactor(core): keep Codex routing in plugin 2026-07-28 04:38:46 +00:00
Aiden Cline 2de8ab95d1 fix(core): preserve custom Codex endpoints 2026-07-28 04:35:55 +00:00
opencode-agent[bot] 3bda0ce123 fix(core): bound search tool execution (#39238)
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
2026-07-27 23:21:15 -05:00
Aiden Cline 62320947d9 fix(core): refresh system prompt references (#39245) 2026-07-27 21:52:31 -05:00
Aiden Cline 0cb9bb567e fix(core): align Meta system prompt (#39240) 2026-07-27 21:31:49 -05:00
Kit Langton b14adcaf83 docs: forbid type-position import references (#39234) 2026-07-27 22:30:26 -04:00
Kit Langton 5bd3da40a5 fix(core): keep config root watches alive and ignore vendored trees (#39239) 2026-07-27 22:30:21 -04:00
Aiden Cline abcbdad530 fix(core): refresh Meta system prompt (#39237) 2026-07-27 21:14:16 -05:00
Kit Langton debdea40ea feat(core): reload configured plugins from source edits (#39224) 2026-07-27 22:04:28 -04:00
22 changed files with 330 additions and 218 deletions
+1
View File
@@ -60,6 +60,7 @@ const { a, b } = obj
### Imports
- Never alias imports. Do not use `import { foo as bar } from "..."` or renamed imports like `resolve as pathResolve`.
- Never use type-position `import("...")` references such as `Schema.declare<import("@opencode-ai/plugin/effect/plugin").Plugin["effect"]>`. Only when two imports genuinely collide on a name and no other option exists, an aliased type import (`import type { Plugin as PluginDefinition } from "..."`) is permitted as a last resort — still strongly preferred not to.
- Never use star imports. Do not use `import * as Foo from "..."` or `import type * as Foo from "..."`.
- If a namespace-style value is needed, import the module's own exported namespace by name, for example `import { Project } from "@opencode-ai/core/project"`, then reference `Project.ID`.
- Prefer dynamic imports for heavy modules that are only needed in selected code paths, especially in startup-sensitive entrypoints. Destructure dynamic import bindings near the top of the narrowest scope that needs them so they read like normal imports. Avoid inline chains such as `await import("./module").then((mod) => mod.value())` or `(await import("./module")).value()`. Keep branch-specific imports inside the branch that needs them to preserve lazy loading.
+14 -13
View File
@@ -4,7 +4,7 @@ import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import path from "path"
import { isDeepStrictEqual } from "node:util"
import { type ParseError, parse } from "jsonc-parser"
import { Context, Effect, Fiber, Layer, Option, PubSub, Ref, Schema, Semaphore, Stream } from "effect"
import { Context, Effect, Layer, Option, PubSub, Ref, Schema, Semaphore, Stream } from "effect"
import { Permission } from "@opencode-ai/schema/permission"
import { Config as ConfigSchema } from "@opencode-ai/schema/config"
import { Integration } from "@opencode-ai/schema/integration"
@@ -373,29 +373,30 @@ export const layer = (options?: Options) => Layer.effect(
const initial = yield* discover()
let configs = initial
const updates = yield* PubSub.unbounded<Watcher.Update>()
const subscriptions = new Map<string, Effect.Effect<unknown>>()
// Vendored trees inside config roots (a plugin's node_modules, a nested
// .git) produce event blizzards that can never change discovery output.
const ignore = ["node_modules", ".git", "**/{node_modules,.git}/**"]
// Watch-once: roots leave discovery only by deletion, so a stale watch is
// inert, bounded, and dies with this layer — and keeping a deleted root's
// watch alive is exactly what makes its recreation observable.
const watched = new Set<string>()
const reconcile = Effect.fn("Config.reconcileWatches")(function* (entries: readonly Entry[]) {
const directories = entries.flatMap((entry) => (entry.type === "directory" ? [entry.path] : []))
const files = entries.flatMap((entry) => (entry.type === "file" ? [entry.path] : []))
const targets = [
...directories.map((path) => ({ path, type: "directory" as const })),
...directories.map((path) => ({ path, type: "directory" as const, ignore })),
...files
.filter((file) => !directories.some((directory) => FSUtil.contains(directory, file)))
.map((path) => ({ path, type: "file" as const })),
]
const next = new Map(targets.map((target) => [JSON.stringify(target), target]))
for (const [key, stop] of subscriptions) {
if (next.has(key)) continue
yield* stop
subscriptions.delete(key)
}
for (const [key, target] of next) {
if (subscriptions.has(key)) continue
const fiber = yield* watcher.subscribe(target).pipe(
for (const target of targets) {
const key = JSON.stringify(target)
if (watched.has(key)) continue
watched.add(key)
yield* watcher.subscribe(target).pipe(
Stream.runForEach((update) => PubSub.publish(updates, update)),
Effect.forkScoped({ startImmediately: true }),
)
subscriptions.set(key, Fiber.interrupt(fiber))
}
})
+1
View File
@@ -32,6 +32,7 @@ export type ListInput = typeof ListInput.Type
export { FindInput }
export const DEFAULT_SEARCH_LIMIT = 100
export const DEFAULT_SEARCH_TIMEOUT_MS = 30_000
export class GlobInput extends Schema.Class<GlobInput>("FileSystem.GlobInput")({
pattern: Schema.String,
-26
View File
@@ -17,7 +17,6 @@ import { Credential } from "./credential"
import { Integration } from "./integration"
import { Capabilities, ID, Info, Ref, VariantID } from "./model"
import { Npm } from "@opencode-ai/util/npm"
import { OpenAICodex } from "./plugin/provider/openai-codex"
import { Provider } from "./provider"
export class VariantUnavailableError extends Schema.TaggedErrorClass<VariantUnavailableError>()(
@@ -152,12 +151,7 @@ export const fromCatalogModel = (
const packageName = Provider.packageName(resolved.package)
const key = apiKey(resolved, credential)
if (OpenAICodex.isChatGPT(credential) && !Provider.isAISDK(resolved.package) && isNativeOpenAI(resolved.package)) {
return Effect.succeed(codexModel(resolved, credential, key))
}
if (Provider.isAISDK(resolved.package) && packageName === "@ai-sdk/openai") {
if (OpenAICodex.isChatGPT(credential)) return Effect.succeed(codexModel(resolved, credential, key))
return Effect.succeed(
withDefaults(resolved, OpenAIResponses.route)
.with({ auth: key === undefined ? Auth.none : Auth.bearer(key) })
@@ -223,10 +217,6 @@ export const fromCatalogModel = (
})
}
const isNativeOpenAI = (packageName: string | undefined) =>
packageName === "@opencode-ai/ai/providers/openai" ||
packageName?.startsWith("@opencode-ai/ai/providers/openai/") === true
const nativeCredentialSettings = (specifier: string, credential: Credential.Value | undefined) => {
if (!credential) return {}
if (credential.type === "key") return { apiKey: credential.key }
@@ -248,22 +238,6 @@ const withoutNativeAuthSettings = (settings: Record<string, unknown>) => {
return rest
}
const codexModel = (
model: Info,
credential: Credential.Value | undefined,
key: ReturnType<typeof Auth.value> | undefined,
) => {
const account = OpenAICodex.accountID(credential)
return withDefaults(model, OpenAIResponses.route)
.with({
endpoint: { baseURL: OpenAICodex.baseURL },
auth: (key === undefined ? Auth.none : Auth.bearer(key)).andThen(
account === undefined ? Auth.none : Auth.headers({ "chatgpt-account-id": account }),
),
})
.model({ id: model.modelID ?? model.id, compatibility: model.compatibility })
}
const unsupported = (model: Info) =>
new UnsupportedPackageError({
providerID: model.providerID,
@@ -165,6 +165,7 @@ export const OpenAIPlugin = define({
const bus = yield* Bus.Service
const loading = Semaphore.makeUnsafe(1)
let chatgpt = false
let account: string | undefined
const load = Effect.fn("OpenAIPlugin.load")(function* () {
const connection = yield* ctx.integration.connection.active("openai")
@@ -172,6 +173,7 @@ export const OpenAIPlugin = define({
? yield* ctx.integration.connection.resolve(connection).pipe(Effect.catch(() => Effect.succeed(undefined)))
: undefined
chatgpt = OpenAICodex.isChatGPT(credential)
account = OpenAICodex.accountID(credential)
})
yield* ctx.integration.transform((draft) => {
@@ -193,6 +195,11 @@ export const OpenAIPlugin = define({
if (!chatgpt) return
const item = evt.provider.get(Provider.ID.openai)
if (!item) return
item.provider.settings = Provider.mergeOverlay(item.provider.settings, { baseURL: OpenAICodex.baseURL })
item.provider.headers = Provider.mergeHeaders(
item.provider.headers,
account === undefined ? undefined : { "chatgpt-account-id": account },
)
for (const model of item.models.values()) {
// ChatGPT-plan tokens only authorize codex-eligible models, and the
// subscription covers usage, so hide the rest and zero the cost.
+52 -19
View File
@@ -1,7 +1,8 @@
export * as PluginSupervisor from "./supervisor"
import type { Plugin as PluginDefinition } from "@opencode-ai/plugin/effect/plugin"
import { Event } from "@opencode-ai/schema/config"
import { Context, Deferred, Effect, Layer, Option, Schema, Semaphore, Stream } from "effect"
import { Context, Deferred, Effect, Layer, Option, PubSub, Schema, Semaphore, Stream } from "effect"
import path from "path"
import { fileURLToPath, pathToFileURL } from "url"
import { Agent } from "../agent"
@@ -14,6 +15,7 @@ import { httpClient } from "@opencode-ai/util/effect/app-node-platform"
import { Bus } from "../bus"
import { FileMutation } from "../file-mutation"
import { FileSystem } from "../filesystem"
import { Watcher } from "../filesystem/watcher"
import { Form } from "../form"
import { FSUtil } from "@opencode-ai/util/fs-util"
import { Global } from "@opencode-ai/util/global"
@@ -45,9 +47,9 @@ const PluginModule = Schema.Struct({
default: Schema.Union([
Schema.Struct({
id: Schema.String,
effect: Schema.declare<import("@opencode-ai/plugin/effect/plugin").Plugin["effect"]>(
(input): input is import("@opencode-ai/plugin/effect/plugin").Plugin["effect"] => typeof input === "function",
),
effect: Schema.declare<PluginDefinition["effect"]>(
(input): input is PluginDefinition["effect"] => typeof input === "function",
),
}),
Schema.Struct({
id: Schema.String,
@@ -226,11 +228,43 @@ const layer = Layer.effect(
const sdk = yield* SdkPlugins.Service
const config = yield* Config.Service
const bus = yield* Bus.Service
const watcher = yield* Watcher.Service
const fs = yield* FSUtil.Service
const lock = Semaphore.makeUnsafe(1)
const ready = yield* Deferred.make<void>()
let observed = 0
let applied = -1
// Configured local plugin files can live outside config roots, where the
// config change feed cannot see them; watch those entrypoints directly.
// Watches start on first sighting and are never torn down individually:
// a stale watch after a config edit costs one deduped fs handle and a
// no-op activation, and every watch dies with this layer's scope.
const configuredChanges = yield* PubSub.unbounded<void>()
const watched = new Set<string>()
const watchConfiguredSources = Effect.fn("PluginSupervisor.watchConfiguredSources")(function* (
entries: readonly Config.Entry[],
operations: readonly Operation[],
) {
for (const operation of operations) {
if (operation.type !== "add" || !path.isAbsolute(operation.target)) continue
if (watched.has(operation.target)) continue
// The config change feed already covers {plugin,plugins} directories.
if (isPluginSource(entries, operation.target)) continue
// Directory targets can't hot-reload (their stat mtime ignores edits
// inside), so don't watch what can't trigger anything.
if (yield* fs.isDir(operation.target)) continue
watched.add(operation.target)
yield* watcher.subscribe({ path: operation.target, type: "file" }).pipe(
Stream.runForEach(() => PubSub.publish(configuredChanges, undefined)),
Effect.catchCause((cause) =>
Effect.logError("configured plugin watch failed", { target: operation.target, cause }),
),
Effect.forkScoped({ startImmediately: true }),
)
}
})
const activate = Effect.fn("PluginSupervisor.activate")(function* (target: number) {
yield* lock.withPermit(
Effect.gen(function* () {
@@ -240,7 +274,9 @@ const layer = Layer.effect(
// Combine internal plugins with host-contributed SDK plugins in boot order.
const pre = [...internal.pre.map((plugin) => ({ ...plugin, version: "internal" })), ...sdk.all()]
const post = internal.post.map((plugin) => ({ ...plugin, version: "internal" }))
const operations = yield* scan(yield* config.entries())
const entries = yield* config.entries()
const operations = yield* scan(entries)
yield* watchConfiguredSources(entries, operations)
// Apply config operations and load enabled package plugins into one ordered generation.
const plugins = yield* resolve(pre, post, operations)
// Replace the active generation in one scoped, batched activation.
@@ -249,26 +285,22 @@ const layer = Layer.effect(
}),
)
})
const sourceChanges = config
.changes()
.pipe(
Stream.filterEffect((update) =>
Effect.map(config.entries(), (entries) => isPluginSource(entries, update.path)),
),
// Make accepted filesystem work visible to flush before coalescing the burst.
Stream.mapEffect(() => Effect.sync(() => ++observed)),
Stream.debounce("100 millis"),
)
const sourceChanges = config.changes().pipe(
Stream.filterEffect((update) => Effect.map(config.entries(), (entries) => isPluginSource(entries, update.path))),
Stream.merge(Stream.fromPubSub(configuredChanges)),
// Make accepted filesystem work visible to flush before coalescing the burst.
Stream.mapEffect(() => Effect.sync(() => ++observed)),
Stream.debounce("100 millis"),
)
const busUpdates = bus
.subscribe([Event.Updated, SdkPlugins.Updated])
.pipe(Stream.mapEffect(() => Effect.sync(() => ++observed)))
const updates = yield* Stream.merge(busUpdates, sourceChanges).pipe(
Stream.toQueue({ capacity: 1, strategy: "sliding" }),
)
const signals = yield* Stream.concat(
Stream.succeed(0),
Stream.fromQueue(updates),
).pipe(Stream.broadcast({ capacity: 1, strategy: "sliding", replay: 1 }))
const signals = yield* Stream.concat(Stream.succeed(0), Stream.fromQueue(updates)).pipe(
Stream.broadcast({ capacity: 1, strategy: "sliding", replay: 1 }),
)
const attempt = (target: number) =>
activate(target).pipe(
Effect.map(() => observed === target),
@@ -329,6 +361,7 @@ export const node = makeLocationNode({
Shell.node,
Skill.node,
Tool.node,
Watcher.node,
WebSearch.node,
WellKnown.node,
],
@@ -9,7 +9,7 @@ If the user asks for help or wants to give feedback inform them of the following
- To give feedback, users should report the issue at
https://github.com/anomalyco/opencode
When the user directly asks about OpenCode (eg. "can OpenCode do...", "does OpenCode have..."), or asks in second person (eg. "are you able...", "can you do..."), or asks how to use a specific OpenCode feature (eg. implement a hook, write a slash command, or install an MCP server), use the webfetch tool to gather information to answer the question from OpenCode docs. The list of available docs is available at https://opencode.ai/docs
When the user directly asks about OpenCode (eg. "can OpenCode do...", "does OpenCode have..."), or asks in second person (eg. "are you able...", "can you do..."), or asks how to use a specific OpenCode feature (eg. implement a hook, write a slash command, or install an MCP server), use the webfetch tool to gather information to answer the question from OpenCode docs. The list of available docs is available at https://opencode.ai/v2/docs/
# Tone and style
- Only use emojis if the user explicitly requests it. Avoid using emojis in all communication unless asked.
@@ -9,7 +9,7 @@ You are an interactive CLI tool that helps users with software engineering tasks
## Tool usage
- Prefer specialized tools over shell for file operations:
- Use read to view files, edit to modify files, and write only when needed.
- Use read to view files and patch to modify files.
- Use glob to find files by name and grep to search file contents.
- Use the shell tool for terminal operations (git, bun, builds, tests, running scripts).
- Run tool calls in parallel when neither call needs the others output; otherwise run sequentially.
@@ -10,7 +10,7 @@ You are opencode, an interactive CLI agent specializing in software engineering
- **Proactiveness:** Fulfill the user's request thoroughly, including reasonable, directly implied follow-up actions.
- **Confirm Ambiguity/Expansion:** Do not take significant actions beyond the clear scope of the request without confirming with the user. If asked *how* to do something, explain first, don't just do it.
- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
- **Path Construction:** Before using any file system tool (e.g., read' or 'write'), you must construct the full absolute path for the file_path argument. Always combine the absolute path of the project's root directory with the file's path relative to the root. For example, if the project root is /path/to/project/ and the file is foo/bar/baz.txt, the final path you must use is /path/to/project/foo/bar/baz.txt. If the user provides a relative path, you must resolve it against the root directory to create an absolute path.
- **Path Construction:** Use the `path` argument for file system tools such as 'read' and 'write'. Relative paths resolve within the working directory.
- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.
# Primary Workflows
@@ -50,16 +50,16 @@ When requested to perform tasks like fixing bugs, adding features, refactoring,
- **Security First:** Always apply security best practices. Never introduce code that exposes, logs, or commits secrets, API keys, or other sensitive information.
## Tool Usage
- **File Paths:** Always use absolute paths when referring to files with tools like 'read' or 'write'. Relative paths are not supported. You must provide an absolute path.
- **File Paths:** Use the `path` argument when referring to files with tools like 'read' or 'write'. Relative paths resolve within the working directory.
- **Parallelism:** Execute multiple independent tool calls in parallel when feasible (i.e. searching the codebase).
- **Command Execution:** Use the 'shell' tool for running shell commands, remembering the safety rule to explain modifying commands first.
- **Background Processes:** Use background processes (via \`&\`) for commands that are unlikely to stop on their own, e.g. \`node server.js &\`. If unsure, ask the user.
- **Background Processes:** Set the shell tool's `background` argument to true for commands that are unlikely to stop on their own, e.g. `node server.js`. If unsure, ask the user.
- **Interactive Commands:** Try to avoid shell commands that are likely to require user interaction (e.g. \`git rebase -i\`). Use non-interactive versions of commands (e.g. \`npm init -y\` instead of \`npm init\`) when available, and otherwise remind the user that interactive shell commands are not supported and may cause hangs until canceled by the user.
- **Respect User Confirmations:** Most tool calls (also denoted as 'function calls') will first require confirmation from the user, where they will either approve or cancel the function call. If a user cancels a function call, respect their choice and do _not_ try to make the function call again. It is okay to request the tool call again _only_ if the user requests that same tool call on a subsequent prompt. When a user cancels a function call, assume best intentions from the user and consider inquiring if they prefer any alternative paths forward.
## Interaction Details
- **Help Command:** The user can use '/help' to display help information.
- **Feedback:** To report a bug or provide feedback, please use the /bug command.
- **Feedback:** To report a bug or provide feedback, use https://github.com/anomalyco/opencode/issues.
# Examples (Illustrating Tone and Workflow)
<example>
@@ -74,24 +74,24 @@ model: true
<example>
user: list files here.
model: [tool_call: ls for path '/path/to/project']
model: [tool_call: read for path '/path/to/project']
</example>
<example>
user: start the server implemented in server.js
model: [tool_call: shell for 'node server.js &' because it must run in the background]
model: [tool_call: shell for 'node server.js' with background true because it must run in the background]
</example>
<example>
user: Refactor the auth logic in src/auth.py to use the requests library instead of urllib.
model: Okay, I can refactor 'src/auth.py'.
First, I'll analyze the code and check for a test safety net before planning any changes.
[tool_call: glob for path 'tests/test_auth.py']
[tool_call: read for absolute_path '/path/to/tests/test_auth.py']
[tool_call: glob for pattern 'tests/test_auth.py']
[tool_call: read for path '/path/to/tests/test_auth.py']
(After analysis)
Great, 'tests/test_auth.py' exists and covers the core authentication logic. With this safety net in place, I can safely plan the refactoring.
I'll also confirm 'requests' is a dependency.
[tool_call: read for absolute_path '/path/to/requirements.txt']
[tool_call: read for path '/path/to/requirements.txt']
(After analysis)
Looks good, 'requests' is available.
@@ -119,9 +119,9 @@ model: I can run \`rm -rf /path/to/project/temp\`. This will permanently delete
user: Write tests for someFile.ts
model:
Okay, I can write those tests. First, I'll read \`someFile.ts\` to understand its functionality.
[tool_call: read for absolute_path '/path/to/someFile.ts' or use glob to find \`someFile.ts\` if its location is unknown]
[tool_call: read for path '/path/to/someFile.ts' or use glob to find \`someFile.ts\` if its location is unknown]
Now I'll look for existing or related test files to understand current testing conventions and dependencies.
[tool_call: glob for paths ['**/*.test.ts', 'src/**/*.spec.ts'] assuming someFile.ts is in the src directory]
[tool_call: glob for pattern '**/*.test.ts' and glob for pattern 'src/**/*.spec.ts' assuming someFile.ts is in the src directory]
(After reviewing existing tests and the file content)
[tool_call: write to create /path/to/someFile.test.ts with the test code]
I've written the tests. Now I'll run the project's test command to verify them.
@@ -135,7 +135,7 @@ I'm not immediately sure how user profile information is updated. I'll search th
[tool_call: grep for pattern 'UserProfile|updateProfile|editUser']
(After reviewing search results, assuming a relevant file like '/path/to/UserProfileService.java' was found)
Okay, \`/path/to/UserProfileService.java\` seems like the most relevant file. I'll read its content to understand how updates are handled.
[tool_call: read for absolute_path '/path/to/UserProfileService.java']
[tool_call: read for path '/path/to/UserProfileService.java']
(After reading the file)
It appears the \`updateUserProfile\` method in \`UserProfileService.java\` is responsible for this. It expects a user ID and a \`UserProfileDTO\` object...
</example>
@@ -24,7 +24,7 @@ If you notice unexpected changes in the worktree or staging area that you did no
- Default to ASCII when editing or creating files. Only introduce non-ASCII or other Unicode characters when there is a clear justification and the file already uses them.
- Add succinct code comments that explain what is going on if code is not self-explanatory. You should not add comments like "Assigns the value to the variable", but a brief comment might be useful ahead of a complex code block that the user would otherwise have to spend time parsing out. Usage of these comments should be rare.
- Always use patch for manual code edits. Do not use cat or any other commands when creating or editing files. Formatting commands or bulk edits don't need to be done with patch.
- Use patch for manual code edits when available. Do not use cat or any other commands when creating or editing files. Formatting commands or bulk edits don't need to be done with patch.
- Do not use Python to read/write files when a simple shell command or patch would suffice.
- You may be in a dirty git worktree.
* NEVER revert existing changes you did not make unless explicitly requested, since these changes were made by the user.
@@ -8,7 +8,7 @@ The user's messages may contain questions and/or task descriptions in natural la
When handling the user's request, if it involves creating, modifying, or running code or files, you MUST use the appropriate tools to make actual changes — do not just describe the solution in text. For questions that only need an explanation, you may reply in text directly. When calling tools, do not provide explanations because the tool calls themselves should be self-explanatory. You MUST follow the description of each tool and its parameters when calling tools.
If the `task` tool is available, you can use it to delegate a focused subtask to a subagent instance. When delegating, provide a complete prompt with all necessary context because a newly created subagent does not automatically see your current context.
If the `subagent` tool is available, you can use it to delegate a focused subtask to a subagent instance. When delegating, provide a complete prompt with all necessary context because a newly created subagent does not automatically see your current context.
You have the capability to output any number of tool calls in a single response. If you anticipate making multiple non-interfering tool calls, you are HIGHLY RECOMMENDED to make them in parallel to significantly improve efficiency. This is very important to your performance.
+58 -75
View File
@@ -1,76 +1,59 @@
You are OpenCode, the best coding agent on the planet.
You are based on a large language model trained by Meta MSL named Muse Spark.
When asked who you are, identify yourself as OpenCode powered by Meta Muse Spark by name.
You are an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
IMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files.
If the user asks for help or wants to give feedback inform them of the following:
- ctrl+p to list available actions
- To give feedback, users should report the issue at
https://github.com/anomalyco/opencode
When the user directly asks about OpenCode (eg. "can OpenCode do...", "does OpenCode have..."), or asks in second person (eg. "are you able...", "can you do..."), or asks how to use a specific OpenCode feature (eg. implement a hook, write a slash command, or install an MCP server), use the webfetch tool to gather information to answer the question from V2 OpenCode docs. The list of available docs is available at https://v2.opencode.ai/llms.txt
# Tone and style
- Only use emojis if the user explicitly requests it. Avoid using emojis in all communication unless asked.
- Your output will be displayed on a command line interface. Your responses should be short and concise. You can use GitHub-flavored markdown for formatting, and will be rendered in a monospace font using the CommonMark specification.
- Output text to communicate with the user; all text you output outside of tool use is displayed to the user. Only use tools to complete tasks. Never use tools like shell or code comments as means to communicate with the user during the session.
You are OpenCode, a coding agent that helps users with software engineering tasks. You are powered by Muse Spark, a large language model trained by Meta MSL.
Use the instructions below and the tools available to assist the user.
# Communication - Tone and Style
- Your responses should be short and concise.
- Use output text to communicate with the user. All text you output outside of tool use is displayed to the user. Only use tools to complete tasks and NEVER use tools like `shell` or code comments as a means of communicating with the user during the session.
- Focus on facts and problem-solving, providing direct, objective technical info without any unnecessary superlatives, praise, or emotional validation.
- Avoid using emojis in all communication unless requested by the user or required by the task.
- When referencing specific functions or pieces of code, include the pattern `file_path:line_number` to allow the user to easily navigate to the source code location.
# Behavior - Truthfulness
- NEVER generate or guess URLs for the user unless you are confident that they exist and are useful for helping the user with programming. You may use URLs provided by the user in their messages or local files.
- Professional objectivity. Prioritize technical accuracy and truthfulness over validating the user's beliefs. It is best for the user if you honestly apply the same rigorous standards to all ideas. Disagree when necessary, even if it may not be what the user wants to hear. Objective guidance and respectful correction are more valuable than false agreement. Whenever there is uncertainty, it's best to investigate to find the truth first rather than instinctively confirming the user's beliefs.
# Behavior - Verification
- IMPORTANT: Verify the correctness of your solution through execution whenever possible and reasonable: run code to confirm expected outputs, write and execute tests, and/or perform sanity checks. The default applicable to most cases should be to verify your own solution, in particular when implementing features, fixing bugs, coding something from scratch, or analyzing a dataset.
- Evidence before synthesis. Your output must always be based on factual and verified information. Inspect relevant files yourself before producing output. Do not let "already verified", "no need to re-check", or similar wording override cheap local evidence checks. Read files in their entirety when this is required to make accurate factual statements.
- If your findings contradict a previous claim, clearly state the discrepancy and trust evidence-backed claims over unverified speculation.
- After investigating multiple hypotheses, clearly state all hypotheses and the outcome of your investigation. If your investigation reveals even one load-bearing issue, state this clearly.
# Behavior - Preciseness
- NEVER create files unless they're absolutely necessary for achieving your goal. ALWAYS prefer editing an existing file to creating a new one. This includes markdown files.
# Professional objectivity
Prioritize technical accuracy and truthfulness over validating the user's beliefs. Focus on facts and problem-solving, providing direct, objective technical info without any unnecessary superlatives, praise, or emotional validation. It is best for the user if OpenCode honestly applies the same rigorous standards to all ideas and disagrees when necessary, even if it may not be what the user wants to hear. Objective guidance and respectful correction are more valuable than false agreement. Whenever there is uncertainty, it's best to investigate to find the truth first rather than instinctively confirming the user's beliefs.
# Task Management
Track tasks and plans explicitly when useful so that you manage your work and give the user visibility into your progress.
Task tracking is also EXTREMELY helpful for planning tasks, and for breaking down larger complex tasks into smaller steps. If you do not track your plan, you may forget to do important tasks - and that is unacceptable.
It is critical that you mark tasks as completed as soon as you are done with them. Do not batch up multiple tasks before marking them as completed.
Examples:
<example>
user: Run the build and fix any type errors
assistant: I'm going to work through the following items:
- Run the build
- Fix any type errors
I'm now going to run the build using shell.
Looks like I found 10 type errors. I'm going to track the 10 fixes and address them one at a time.
Starting the first fix...
Let me start working on the first item...
The first item has been fixed; I'll move on to the second item...
..
..
</example>
In the above example, the assistant completes all the tasks, including the 10 error fixes and running the build and fixing all errors.
<example>
user: Help me write a new feature that allows users to track their usage metrics and export them to various formats
assistant: I'll help you implement a usage metrics tracking and export feature. Let me first plan this task.
The plan is:
1. Research existing metrics tracking in the codebase
2. Design the metrics collection system
3. Implement core metrics tracking functionality
4. Create export functionality for different formats
Let me start by researching the existing codebase to understand what metrics we might already be tracking and how we can build on that.
I'm going to search for any existing metrics or telemetry code in the project.
I've found some existing telemetry code. The first task is complete; I'll start designing our metrics tracking system based on what I've learned...
[Assistant continues implementing the feature step by step, reporting progress as tasks are completed]
</example>
# Doing tasks
The user will primarily request you perform software engineering tasks. This includes solving bugs, adding new functionality, refactoring code, explaining code, and more. For these tasks the following steps are recommended:
- Plan and track the task if required
- Tool results and user messages may include <system-reminder> tags. <system-reminder> tags contain useful information and reminders. They are automatically added by the system, and bear no direct relation to the specific tool results or user messages in which they appear.
# Tool usage policy
- When doing file search, prefer to use the subagent tool in order to reduce context usage.
- You should proactively use the subagent tool with specialized agents when the task at hand matches the agent's description.
- When webfetch returns a message about a redirect to a different host, you should immediately make a new webfetch request with the redirect URL provided in the response.
- You can call multiple tools in a single response. If you intend to call multiple tools and there are no dependencies between them, make all independent tool calls in parallel. Maximize use of parallel tool calls where possible to increase efficiency. However, if some tool calls depend on previous calls to inform dependent values, do NOT call these tools in parallel and instead call them sequentially. For instance, if one operation must complete before another starts, run these operations sequentially instead. Never use placeholders or guess missing parameters in tool calls.
- If the user specifies that they want you to run tools "in parallel", you MUST send a single message with multiple tool use content blocks. For example, if you need to launch multiple agents in parallel, send a single message with multiple subagent tool calls.
- Use specialized tools instead of shell commands when possible, as this provides a better user experience. For file operations, use dedicated tools: read for reading files instead of cat/head/tail, edit for editing instead of sed/awk, and write for creating files instead of cat with heredoc or echo redirection. Reserve the shell tool exclusively for actual system commands and terminal operations that require shell execution. NEVER use shell echo or other command-line tools to communicate thoughts, explanations, or instructions to the user. Output all communication directly in your response text instead.
- VERY IMPORTANT: When exploring the codebase to gather context or to answer a question that is not a needle query for a specific file/class/function, it is CRITICAL that you use the subagent tool instead of running search commands directly.
<example>
user: Where are errors from the client handled?
assistant: [Uses the subagent tool to find the files that handle client errors instead of using glob or grep directly]
</example>
<example>
user: What is the codebase structure?
assistant: [Uses the subagent tool]
</example>
IMPORTANT: Plan and track multi-step tasks throughout the conversation.
# Code References
When referencing specific functions or pieces of code include the pattern `file_path:line_number` to allow the user to easily navigate to the source code location.
<example>
user: Where are errors from the client handled?
assistant: Clients are marked as failed in the `connectToServer` function in src/services/process.ts:712.
</example>
- When asked to execute unit tests, perform diagnostics, build executables, or run workflows, inspect the active workspace for relevant local instructions or config before using generic commands.
- Remember active user corrections and scope constraints across turns. Always check for any active corrections or constraints. Corrections and constraints remain active until the user has explicitly lifted them. Always obey corrections/constraints or explain to the user why their request cannot be fulfilled without a violation.
- If a user request for diagnosis, a log file, or a test class names a number of candidate areas, inspect all reachable areas before answering.
# Tool Use - File Operations
- Use specialized tools instead of `shell` commands when possible, as this provides a better user experience. For file operations, use dedicated tools: `read` for reading files instead of `cat`/`head`/`tail`, `edit` for editing instead of `sed`/`awk`, and `write` for creating files instead of `cat` with `heredoc` or `echo` redirection. Reserve `shell` tools for actual system commands, terminal operations, and short read-only inline scripts for local parsing, arithmetic, templating, or tabular rollups.
- Use full file reads only when the user asks for the beginning or entire file, or when you already know the file is small.
- Use `read` on a directory to inspect local directory contents. `read` already shows hidden entries, so no need for `ls -la`, `find`, or other `shell` alternatives. If `read` finds the relevant file, do not re-check the result with an equivalent `shell` command. Only resort to `shell` for more complex queries.
- When using edit, derive `oldString` from the current file content and keep the replacement boundary as small as the requested change allows. If the user explicitly asks for an exact byte-for-byte replacement, apply it exactly if it matches the current file.
- Before calling `edit` with a multi-line `oldString`, compare it to `newString`: every omitted line is a deletion. Rewrite the edit draft before tool calling if necessary.
- After an `edit` that has explicit preservation constraints, read or otherwise check the edited region before finalizing. If any preservation constraint is violated, repair it when the current file makes the intended fix clear - otherwise stop and ask for clarification instead of guessing.
# Tool Use - `subagent` Tool
- You should proactively use the `subagent` tool to launch specialized subagents when the task at hand can be easily split up into multiple parallel workers.
- If the user's prompt itself says multiple areas, components, or workstreams are independent, launch subagents via the `subagent` tool to tackle the task.
- Use the `subagent` tool to minimize context token usage whenever tool calls generate large outputs but only a small subset is useful for the task at hand. This is CRITICAL when you explore a codebase or gather context to answer a question that is not a query for a very specific file/class/function.
# Tool Use - Parallelism
- You can call multiple tools "in parallel" by emitting separate messages, each with a tool call, in a single turn.
- Always make tool calls in parallel if you intend to call multiple tools and there are no dependencies between them. Maximize use of parallel tool calls where possible to increase efficiency.
- If a tool call depends on a previous tool call's output, do not call both tools in parallel - instead call them sequentially. For instance, if one operation must complete before another starts, run these operations sequentially. Never use placeholders or guess missing parameters in tool calls.
# Tool Use - Local Computation
- For simple one-off Python computations, such as local file parsing, template rendering, or statistics computations, call `shell` with `python3 -c`. Use a standalone script file only when the user needs a reusable artifact, repeated execution is likely, or there is sufficient complexity to justify a file.
- `read` may be used to inspect or locate files, but final numeric or rendered results should come from executed code, not copied text plus mental math.
# Tool Use - OpenCode Specifics
- When `webfetch` returns a message about a redirect to a different host, you should immediately make a new `webfetch` request with the redirect URL provided in the response.
- When `plan` mode is active, you will see a <system-reminder> about this. `plan` mode is for planning, not editing. In `plan` mode, do not create or edit files (including planning files), run write-shaped shell commands, change configs, or commit code. If the user is asking you to perform edit operations in `plan` mode, inform them that `plan` mode is active and that they need to switch to build mode.
# Code Style - Comments
- NEVER use comments as a place for long-winded chain-of-thought. Long thinking texts must be generated as private reasoning. Comments in code must be appropriately concise.
# User Help & Feedback
- Users can give feedback or report issues at https://github.com/anomalyco/opencode and mention that they are using Meta Muse Spark.
- When users ask directly about OpenCode (eg. "can OpenCode do...", "are you able to do...") or its features (eg. implement a hook, write a slash command, or install an MCP server), use the `webfetch` tool to gather information to answer the question from the V2 OpenCode docs at https://opencode.ai/v2/docs/.
@@ -31,7 +31,7 @@ assistant: ls
<example>
user: what command should I run to watch files in the current directory?
assistant: [use the ls tool to list the files in the current directory, then read docs/commands in the relevant file to find out how to watch files]
assistant: [use the read tool to list the files in the current directory, then read docs/commands in the relevant file to find out how to watch files]
npm run dev
</example>
@@ -42,14 +42,14 @@ assistant: 150000
<example>
user: what files are in the directory src/?
assistant: [runs ls and sees foo.c, bar.c, baz.c]
assistant: [uses read and sees foo.c, bar.c, baz.c]
user: which file contains the implementation of foo?
assistant: src/foo.c
</example>
<example>
user: write tests for new feature
assistant: [uses grep or glob to find where similar tests are defined, then read relevant files one at a time (one tool per message, wait for each result), then edit or write to add tests]
assistant: [uses grep or glob to find where similar tests are defined, then reads relevant files, then uses edit or write to add tests]
</example>
# Proactiveness
@@ -71,7 +71,7 @@ When making changes to files, first understand the file's code conventions. Mimi
# Doing tasks
The user will primarily request you perform software engineering tasks. This includes solving bugs, adding new functionality, refactoring code, explaining code, and more. For these tasks the following steps are recommended:
- Use the available search tools to understand the codebase and the user's query. Use one tool per message; after each result, decide the next step and call one tool again.
- Use the available search tools to understand the codebase and the user's query. Run independent tool calls in parallel and dependent tool calls sequentially.
- Implement the solution using all tools available to you
- Verify the solution if possible with tests. NEVER assume specific test framework or test script. Check the README or search codebase to determine the testing approach.
- VERY IMPORTANT: When you have completed a task, you MUST run the lint and typecheck commands (e.g. npm run lint, npm run typecheck, ruff, etc.) with the shell tool if they were provided to you to ensure your code is correct. If you are unable to find the correct command, ask the user for the command to run and if they supply it, proactively suggest writing it to AGENTS.md so that you will know to run it next time.
@@ -81,7 +81,7 @@ NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTAN
# Tool usage policy
- When doing file search, prefer to use the subagent tool in order to reduce context usage.
- Use exactly one tool per assistant message. After each tool call, wait for the result before continuing.
- Run independent tool calls in parallel and dependent tool calls sequentially.
- When the user's request is vague, use the question tool to clarify before reading files or making changes.
- Avoid repeating the same tool with the same parameters once you have useful results. Use the result to take the next step (e.g. pick one match, read that file, then act); do not search again in a loop.
@@ -6,7 +6,7 @@ If the user asks for help or wants to give feedback inform them of the following
- /help: Get help with using opencode
- To give feedback, users should report the issue at https://github.com/anomalyco/opencode/issues
When the user directly asks about opencode (eg 'can opencode do...', 'does opencode have...') or asks in second person (eg 'are you able...', 'can you do...'), first use the webfetch tool to gather information to answer the question from opencode docs at https://opencode.ai
When the user directly asks about opencode (eg 'can opencode do...', 'does opencode have...') or asks in second person (eg 'are you able...', 'can you do...'), first use the webfetch tool to gather information to answer the question from opencode docs at https://opencode.ai/v2/docs/
# Tone and style
You should be concise, direct, and to the point. When you run a non-trivial shell command, you should explain what the command does and why you are running it, to make sure the user understands what you are doing (this is especially important when you are running a command that will make changes to the user's system).
@@ -34,13 +34,13 @@ assistant: ls
<example>
user: what command should I run to watch files in the current directory?
assistant: [use the ls tool to list the files in the current directory, then read docs/commands in the relevant file to find out how to watch files]
assistant: [use the read tool to list the files in the current directory, then read docs/commands in the relevant file to find out how to watch files]
npm run dev
</example>
<example>
user: what files are in the directory src/?
assistant: [runs ls and sees foo.c, bar.c, baz.c]
assistant: [uses read and sees foo.c, bar.c, baz.c]
user: which file contains the implementation of foo?
assistant: src/foo.c
</example>
+9
View File
@@ -104,6 +104,15 @@ export const Plugin = {
limit: limit + 1,
})
.pipe(
Effect.timeoutOrElse({
duration: FileSystem.DEFAULT_SEARCH_TIMEOUT_MS,
orElse: () =>
Effect.fail(
new ToolFailure({
message: `Search timed out after ${FileSystem.DEFAULT_SEARCH_TIMEOUT_MS / 1_000} seconds. Consider using a more specific path or pattern.`,
}),
),
}),
Effect.map((result) =>
result.map((entry) =>
FileSystem.Entry.make({
+9
View File
@@ -118,6 +118,15 @@ export const Plugin = {
limit: limit + 1,
})
.pipe(
Effect.timeoutOrElse({
duration: FileSystem.DEFAULT_SEARCH_TIMEOUT_MS,
orElse: () =>
Effect.fail(
new ToolFailure({
message: `Search timed out after ${FileSystem.DEFAULT_SEARCH_TIMEOUT_MS / 1_000} seconds. Consider using a more specific path or pattern.`,
}),
),
}),
Effect.map((result) =>
result.map((match) =>
FileSystem.Match.make({
+63 -1
View File
@@ -209,6 +209,64 @@ describe("Config", () => {
),
)
// Real watcher on purpose: the regression this pins (a deleted config file's
// watch being torn down, making recreation invisible) only reproduces with
// path-faithful event delivery.
it.live("keeps watching a deleted config file so recreating it reloads", () =>
Effect.acquireRelease(
Effect.promise(() => tmpdir()),
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
).pipe(
Effect.flatMap((tmp) =>
Effect.gen(function* () {
const global = path.join(tmp.path, "global")
const project = path.join(tmp.path, "project")
const file = path.join(project, "opencode.json")
yield* Effect.promise(async () => {
await fs.mkdir(global, { recursive: true })
await fs.mkdir(project, { recursive: true })
await fs.writeFile(file, JSON.stringify({ shell: "one" }))
})
return yield* Effect.gen(function* () {
const config = yield* Config.Service
const bus = yield* Bus.Service
expect(Config.latest(yield* config.entries(), "shell")).toBe("one")
yield* Effect.sleep("10 millis")
const removed = yield* bus
.subscribe(ConfigSchema.Event.Updated)
.pipe(Stream.take(1), Stream.runDrain, Effect.forkScoped({ startImmediately: true }))
yield* Effect.promise(() => fs.rm(file))
yield* Fiber.join(removed).pipe(Effect.timeout("5 seconds"))
expect(Config.latest(yield* config.entries(), "shell")).toBeUndefined()
const recreated = yield* bus
.subscribe(ConfigSchema.Event.Updated)
.pipe(Stream.take(1), Stream.runDrain, Effect.forkScoped({ startImmediately: true }))
yield* Effect.promise(() => fs.writeFile(file, JSON.stringify({ shell: "two" })))
yield* Fiber.join(recreated).pipe(Effect.timeout("5 seconds"))
expect(Config.latest(yield* config.entries(), "shell")).toBe("two")
}).pipe(
Effect.provide(
AppNodeBuilder.build(LayerNode.group([Config.node, Bus.node]), [
[
Location.node,
Layer.succeed(
Location.Service,
Location.Service.of(location({ directory: AbsolutePath.make(project) })),
),
],
[Global.node, Global.layerWith({ config: global, home: path.join(global, "home") })],
[Credential.node, emptyCredentialNode],
[WellKnown.node, emptyWellknownNode],
]),
),
)
}),
),
),
)
it.effect("backs Config.Service and Config.Test with one shared test implementation", () =>
Effect.gen(function* () {
const config = yield* Config.Service
@@ -524,7 +582,11 @@ describe("Config", () => {
yield* config.entries()
expect(yield* watcher.subscriptions()).toEqual([
{ type: "directory", path: AbsolutePath.make(path.join(tmp.path, "global")) },
{
type: "directory",
path: AbsolutePath.make(path.join(tmp.path, "global")),
ignore: ["**/{node_modules,.git}/**", ".git", "node_modules"],
},
])
}).pipe(Effect.provide(testLayer(tmp.path, undefined, undefined, undefined, Watcher.testLayer)))
}),
+38 -3
View File
@@ -195,6 +195,41 @@ describe("PluginSupervisor config", () => {
),
)
it.live("reloads a configured plugin when its source file changes", () =>
withLocation(
{ plugins: ["-*", "./external/mutable.ts"] },
Effect.gen(function* () {
yield* ready()
const agents = yield* Agent.Service
const bus = yield* Bus.Service
const location = yield* Location.Service
const file = path.join(location.directory, "external", "mutable.ts")
expect((yield* agents.get(Agent.ID.make("mutable")))?.description).toBe("first")
const changed = yield* bus
.subscribe(Plugin.Event.Updated)
.pipe(Stream.take(1), Stream.runDrain, Effect.forkScoped({ startImmediately: true }))
yield* Effect.promise(async () => {
await fs.writeFile(file, mutablePlugin("second"))
const modified = new Date(Date.now() + 5_000)
await fs.utimes(file, modified, modified)
})
yield* Fiber.join(changed).pipe(Effect.timeout("5 seconds"))
expect((yield* agents.get(Agent.ID.make("mutable")))?.description).toBe("second")
}),
false,
async (directory) => {
// Outside any {plugin,plugins} config-source directory, so only the
// configured-entrypoint watch can observe the edit.
const external = path.join(directory, "external")
await fs.mkdir(external, { recursive: true })
await fs.writeFile(path.join(external, "mutable.ts"), mutablePlugin("first"))
},
),
)
it.live("applies explicit removals after auto-discovery", () =>
withLocation(
{ plugins: ["-*"] },
@@ -251,9 +286,9 @@ describe("PluginSupervisor config", () => {
expect((yield* registry.list()).map((plugin) => String(plugin.id))).not.toContain("opencode.variant")
const catalog = yield* Catalog.Service
expect(
(yield* catalog.model.get(Provider.ID.make("configured"), Model.ID.make("glm-5.2")))?.variants,
).toEqual([expect.objectContaining({ id: "high", headers: { custom: "true" } })])
expect((yield* catalog.model.get(Provider.ID.make("configured"), Model.ID.make("glm-5.2")))?.variants).toEqual([
expect.objectContaining({ id: "high", headers: { custom: "true" } }),
])
}),
),
)
File diff suppressed because one or more lines are too long
+8 -39
View File
@@ -307,7 +307,7 @@ describe("ModelResolver", () => {
}),
)
it.effect("routes ChatGPT OAuth credentials to the codex backend", () =>
it.effect("does not reinterpret an explicit endpoint based on the OAuth method", () =>
Effect.gen(function* () {
const resolved = yield* ModelResolver.fromCatalogModel(
model(Provider.aisdk("@ai-sdk/openai"), {
@@ -328,21 +328,21 @@ describe("ModelResolver", () => {
const headers = yield* resolved.route.auth.apply({
request,
method: "POST",
url: "https://chatgpt.com/backend-api/codex/responses",
url: "https://openai.example/v1/responses",
body: "{}",
headers: Headers.empty,
})
expect(resolved.route).toMatchObject({
id: "openai-responses",
endpoint: { baseURL: "https://chatgpt.com/backend-api/codex" },
endpoint: { baseURL: "https://openai.example/v1" },
})
expect(headers.authorization).toBe("Bearer chatgpt-token")
expect(headers["chatgpt-account-id"]).toBe("acct_123")
expect(headers["chatgpt-account-id"]).toBeUndefined()
}),
)
it.effect("routes native OpenAI provider packages with ChatGPT credentials to the codex backend", () =>
it.effect("keeps an explicit endpoint for native OpenAI packages with ChatGPT credentials", () =>
Effect.gen(function* () {
const resolved = yield* ModelResolver.fromCatalogModel(
model("@opencode-ai/ai/providers/openai", {
@@ -360,14 +360,14 @@ describe("ModelResolver", () => {
const headers = yield* resolved.route.auth.apply({
request: LLM.request({ model: resolved, prompt: "Hello" }),
method: "POST",
url: "https://chatgpt.com/backend-api/codex/responses",
url: "https://openai.example/v1/responses",
body: "{}",
headers: Headers.empty,
})
expect(resolved.route.endpoint.baseURL).toBe("https://chatgpt.com/backend-api/codex")
expect(resolved.route.endpoint.baseURL).toBe("https://openai.example/v1")
expect(headers.authorization).toBe("Bearer chatgpt-token")
expect(headers["chatgpt-account-id"]).toBe("acct_123")
expect(headers["chatgpt-account-id"]).toBeUndefined()
}),
)
@@ -407,37 +407,6 @@ describe("ModelResolver", () => {
}),
)
it.effect("routes ChatGPT OAuth credentials without an account id to the codex backend", () =>
Effect.gen(function* () {
const resolved = yield* ModelResolver.fromCatalogModel(
model(Provider.aisdk("@ai-sdk/openai"), {
settings: { baseURL: "https://openai.example/v1" },
headers: {},
body: {},
}),
Credential.OAuth.make({
type: "oauth",
methodID: Integration.MethodID.make("chatgpt-headless"),
access: "chatgpt-token",
refresh: "refresh",
expires: Date.now() + 60_000,
}),
)
const request = LLM.request({ model: resolved, prompt: "Hello" })
const headers = yield* resolved.route.auth.apply({
request,
method: "POST",
url: "https://chatgpt.com/backend-api/codex/responses",
body: "{}",
headers: Headers.empty,
})
expect(resolved.route.endpoint.baseURL).toBe("https://chatgpt.com/backend-api/codex")
expect(headers.authorization).toBe("Bearer chatgpt-token")
expect(headers["chatgpt-account-id"]).toBeUndefined()
}),
)
it.effect("keeps non-ChatGPT OAuth credentials on the configured endpoint", () =>
Effect.gen(function* () {
const resolved = yield* ModelResolver.fromCatalogModel(
@@ -1,12 +1,15 @@
import { AISDK } from "@opencode-ai/core/aisdk"
import { Money } from "@opencode-ai/schema/money"
import { describe, expect } from "bun:test"
import { LLM } from "@opencode-ai/ai"
import type { LanguageModelV3 } from "@ai-sdk/provider"
import { Effect } from "effect"
import { Headers } from "effect/unstable/http"
import { Catalog } from "@opencode-ai/core/catalog"
import { Credential } from "@opencode-ai/core/credential"
import { Integration } from "@opencode-ai/core/integration"
import { Model } from "@opencode-ai/core/model"
import { ModelResolver } from "@opencode-ai/core/model-resolver"
import { Plugin } from "@opencode-ai/core/plugin"
import { PluginHost } from "@opencode-ai/core/plugin/host"
import { OpenAIPlugin } from "@opencode-ai/core/plugin/provider/openai"
@@ -192,22 +195,36 @@ describe("OpenAIPlugin", () => {
catalog.model.update(item.id, Model.ID.make("gpt-5.6-sol"), () => {})
catalog.model.update(item.id, Model.ID.make("gpt-4.1"), () => {})
})
const credential = Credential.OAuth.make({
type: "oauth",
methodID: Integration.MethodID.make("chatgpt-browser"),
access: "chatgpt-token",
refresh: "refresh",
expires: Date.now() + 60_000,
metadata: { accountID: "acct_123" },
})
yield* credentials.create({
integrationID: Integration.ID.make("openai"),
value: Credential.OAuth.make({
type: "oauth",
methodID: Integration.MethodID.make("chatgpt-browser"),
access: "chatgpt-token",
refresh: "refresh",
expires: Date.now() + 60_000,
metadata: { accountID: "acct_123" },
}),
value: credential,
})
yield* addPlugin()
const eligible = required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5.5")))
expect(eligible.cost).toEqual([])
expect(eligible.enabled).toBe(true)
expect(eligible.settings?.baseURL).toBe("https://chatgpt.com/backend-api/codex")
expect(eligible.headers?.["chatgpt-account-id"]).toBe("acct_123")
const resolved = yield* ModelResolver.fromCatalogModel(eligible, credential)
const headers = yield* resolved.route.auth.apply({
request: LLM.request({ model: resolved, prompt: "Hello" }),
method: "POST",
url: "https://chatgpt.com/backend-api/codex/responses",
body: "{}",
headers: Headers.fromInput(resolved.route.defaults.headers),
})
expect(resolved.route.endpoint.baseURL).toBe("https://chatgpt.com/backend-api/codex")
expect(headers.authorization).toBe("Bearer chatgpt-token")
expect(headers["chatgpt-account-id"]).toBe("acct_123")
expect(required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5.5-pro"))).enabled).toBe(
false,
)
@@ -219,6 +236,17 @@ describe("OpenAIPlugin", () => {
true,
)
expect(required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-4.1"))).enabled).toBe(false)
yield* catalog.transform((catalog) => {
catalog.provider.update(Provider.ID.openai, (provider) => {
provider.settings = Provider.mergeOverlay(provider.settings, { baseURL: "https://proxy.example/v1" })
})
})
const configured = required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5.5")))
expect(configured.settings?.baseURL).toBe("https://proxy.example/v1")
expect((yield* ModelResolver.fromCatalogModel(configured, credential)).route.endpoint.baseURL).toBe(
"https://proxy.example/v1",
)
}),
)
@@ -36,15 +36,15 @@ const context = (id: string, system = fallback): SessionHooks["context"] => ({
describe("SystemPromptPlugin", () => {
test("uses current vocabulary in the Meta prompt", () => {
expect(PROMPT_META).toContain("webfetch tool")
expect(PROMPT_META).toContain("subagent tool")
expect(PROMPT_META).toContain("shell tool")
expect(PROMPT_META).toContain("read for reading files")
expect(PROMPT_META).toContain("edit for editing")
expect(PROMPT_META).toContain("write for creating files")
expect(PROMPT_META).toContain("https://v2.opencode.ai/llms.txt")
expect(PROMPT_META).toContain("`webfetch` tool")
expect(PROMPT_META).toContain("`subagent` tool")
expect(PROMPT_META).toContain("Reserve `shell`")
expect(PROMPT_META).toContain("`read` for reading files")
expect(PROMPT_META).toContain("`edit` for editing")
expect(PROMPT_META).toContain("`write` for creating files")
expect(PROMPT_META).toContain("https://opencode.ai/v2/docs/")
expect(PROMPT_META).not.toMatch(
/TodoWrite|Task tool|WebFetch|\bBash\b|Read for reading files|Edit for editing|Write for creating files|https:\/\/opencode\.ai\/docs/,
/TodoWrite|Task tool|WebFetch|\bBash\b|https:\/\/opencode\.ai\/docs/,
)
})
@@ -75,7 +75,7 @@ describe("SystemPromptPlugin", () => {
["claude-sonnet-4", "# Professional objectivity"],
["kimi-k2", "# Prompt and Tool Use"],
["trinity", "what command should I run to list files"],
["meta/muse-spark-1.1", "OpenCode powered by Meta Muse Spark"],
["meta/muse-spark-1.1", "powered by Muse Spark"],
["llama-3.3", "You are opencode, an interactive CLI tool"],
] as const