@agentproto/adapter-opencode 1.3.4 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -36,6 +36,9 @@ declare function readOpenCodeUsage(sessionId: string): Promise<{
36
36
  costUsd?: number;
37
37
  tokensIn?: number;
38
38
  tokensOut?: number;
39
+ cacheReadTokens?: number;
40
+ cacheWriteTokens?: number;
41
+ reasoningTokens?: number;
39
42
  } | null>;
40
43
 
41
44
  export { opencode, opencodeRuntime, readOpenCodeUsage };
package/dist/index.mjs CHANGED
@@ -177,7 +177,16 @@ var opencode = defineAgentCli({
177
177
  key_scope: ["conversation", "operator"]
178
178
  }
179
179
  },
180
- tags: ["opencode", "sst", "acp", "agent-runtime", "coding"]
180
+ tags: ["opencode", "sst", "acp", "agent-runtime", "coding"],
181
+ metadata: {
182
+ // Opts opencode into `agentproto install skill/<slug>` fan-out (no
183
+ // --target given). Confirmed against the installed opencode CLI +
184
+ // published docs (opencode.ai/docs/skills/): it auto-discovers
185
+ // `~/.config/opencode/skills/<name>/SKILL.md` (global) in addition to
186
+ // project-local `.opencode/skills/`, `.claude/skills/`, `.agents/skills/`
187
+ // — same flat-dir, one-subdir-per-skill shape as hermes.
188
+ skills: { format: "flat-dir", dir: "~/.config/opencode/skills" }
189
+ }
181
190
  });
182
191
  function opencodeRuntime() {
183
192
  return createAgentCliRuntime(opencode);
@@ -190,12 +199,15 @@ async function readOpenCodeUsage(sessionId) {
190
199
  const dbPath = join(dataHome, "opencode", "opencode.db");
191
200
  const db = new DatabaseSync(dbPath, { readOnly: true });
192
201
  try {
193
- const row = db.prepare("SELECT cost, tokens_input AS ti, tokens_output AS to_ FROM session WHERE id = ?").get(sessionId);
202
+ const row = db.prepare("SELECT * FROM session WHERE id = ?").get(sessionId);
194
203
  if (!row) return null;
195
204
  return {
196
205
  ...row.cost != null ? { costUsd: Number(row.cost) } : {},
197
- ...row.ti != null ? { tokensIn: row.ti } : {},
198
- ...row.to_ != null ? { tokensOut: row.to_ } : {}
206
+ ...row.tokens_input != null ? { tokensIn: row.tokens_input } : {},
207
+ ...row.tokens_output != null ? { tokensOut: row.tokens_output } : {},
208
+ ...row.tokens_cache_read != null ? { cacheReadTokens: row.tokens_cache_read } : {},
209
+ ...row.tokens_cache_write != null ? { cacheWriteTokens: row.tokens_cache_write } : {},
210
+ ...row.tokens_reasoning != null ? { reasoningTokens: row.tokens_reasoning } : {}
199
211
  };
200
212
  } finally {
201
213
  db.close();
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * @agentproto/adapter-opencode — AIP-45 adapter for sst/opencode.\n *\n * OpenCode ships first-party ACP support (`opencode acp`) so no\n * third-party wrapper is needed. We spawn it via `npx -y opencode-ai\n * acp` and drive it over stdio JSON-RPC the same way the claude-code\n * adapter drives @agentclientprotocol/claude-agent-acp.\n *\n * import { opencode, opencodeRuntime } from \"@agentproto/adapter-opencode\"\n * const session = await opencodeRuntime().start({\n * env: { ANTHROPIC_API_KEY: \"sk-...\" },\n * })\n * for await (const evt of session.send({ role: \"user\", content: \"...\" })) {\n * console.log(evt)\n * }\n * await session.close()\n */\n\nimport { homedir } from \"node:os\"\nimport { join } from \"node:path\"\nimport {\n createAgentCliRuntime,\n defineAgentCli,\n type AgentCliHandle,\n type AgentCliRuntime,\n} from \"@agentproto/driver-agent-cli\"\nimport { getModelsByProvider, listModels } from \"@agentproto/model-catalog\"\n\n/**\n * Build OpenCode's model menu from the shared provider catalog instead of a\n * hand-maintained allowlist. OpenCode routes by the model id's own\n * `<provider>/<id>` prefix, so every entry carries the billing provider the\n * runtime's eligibility projection needs.\n *\n * Only providers OpenCode genuinely supports today are included:\n * - Anthropic and OpenAI (direct vendor prefixes)\n * - OpenRouter (`openrouter/<vendor>/<id>` router prefix)\n * - OpenCode's own two hosted endpoints — `opencode-go/<id>` (the flat Go\n * subscription) and `opencode/<id>` (Zen, pay-as-you-go), both matching\n * opencode's own config spelling\n *\n * Groq is still omitted: it is not a billing/auth `CatalogProvider` (no\n * pricing generator), so there is nothing in the shared catalog to enumerate.\n * The `models.env` map carries its key env var, so a free-form `model`\n * override and a manually-curated profile continue to work. (The two OpenCode\n * endpoints USED to be omitted for the same reason — they are now first-class\n * catalog providers with their own generated route tables, which is what makes\n * them enumerable here.)\n *\n * `router: true` marks a provider whose surface lives ONLY in its generated\n * route table and is deliberately not spread into `LLM_PRICING_CATALOG` (a\n * bare `claude-sonnet-5` must keep meaning direct Anthropic, not a Zen-priced\n * route). `listModels` returns nothing for those, so they are read through\n * `getModelsByProvider`, which folds the route table in.\n */\nfunction buildOpencodeModelMenu(): Array<{ id: string; provider: string }> {\n const supported = [\n { provider: \"anthropic\", prefix: \"anthropic\", router: false },\n { provider: \"openai\", prefix: \"openai\", router: false },\n { provider: \"openrouter\", prefix: \"openrouter\", router: false },\n { provider: \"opencode-go\", prefix: \"opencode-go\", router: true },\n { provider: \"opencode\", prefix: \"opencode\", router: true },\n ] as const\n\n const seen = new Set<string>()\n const out: Array<{ id: string; provider: string }> = []\n\n for (const { provider, prefix, router } of supported) {\n const models = router\n ? getModelsByProvider(provider).filter(model => model.kind === \"llm\")\n : listModels({ kind: \"llm\", provider })\n for (const model of models) {\n const bareId = model.id\n const canonicalId = bareId.includes(\"/\") ? bareId : `${prefix}/${bareId}`\n const id = provider === \"openrouter\" ? `openrouter/${bareId}` : canonicalId\n if (seen.has(id)) continue\n seen.add(id)\n out.push({ id, provider })\n }\n }\n\n return out.sort((a, b) => {\n if (a.provider !== b.provider) return a.provider.localeCompare(b.provider)\n return a.id.localeCompare(b.id)\n })\n}\n\nexport const opencode: AgentCliHandle = defineAgentCli({\n name: \"opencode\",\n id: \"opencode\",\n description:\n \"sst/opencode — open-source coding agent with first-party ACP mode. Spawned via `npx -y opencode-ai acp` and driven over stdio JSON-RPC. Multi-provider (Anthropic / OpenAI / OpenRouter / Groq / OpenCode hosted).\",\n version: \"0.1.0\",\n bin: \"npx\",\n bin_args: [\"-y\", \"opencode-ai\", \"acp\"],\n install: [\n { method: \"npm\", package: \"opencode-ai\", global: true },\n { method: \"curl\", url: \"https://opencode.ai/install\" },\n ],\n version_check: {\n // PRESENCE probe — local binary check, not a registry query. Covers BOTH\n // install methods: the npm -g package and the curl installer both put an\n // `opencode` executable on PATH, and it runs `--version` fully offline.\n cmd: \"opencode --version\",\n parse: \"(\\\\d+\\\\.\\\\d+\\\\.\\\\d+)\",\n range: \">=1.0.0\",\n timeout_ms: 15_000,\n },\n auth: {\n ref: \"./SECRETS.md\",\n state: {\n env: [\n \"OPENCODE_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"OPENROUTER_API_KEY\",\n \"GROQ_API_KEY\",\n ],\n },\n },\n sandbox: \"./SANDBOX.md\",\n protocol: \"acp\",\n acp: \"./opencode-acp.ACP.md\",\n session: {\n mode: \"persistent\",\n idle_timeout_ms: 1_800_000,\n context_carryover: true,\n },\n // opencode routes by the model id's own `<provider>/<id>` prefix itself, no\n // adapter mode — unlike claude-sdk/claude-code, which need ANTHROPIC_BASE_URL\n // pre-wired by a mode. So the route falls out of the chosen model →\n // derived-from-model. The API key env var is also derived from the model's\n // provider prefix, so the runtime must know to present api-key auth on the\n // model-derived direct endpoint.\n routeSelection: \"derived-from-model\",\n modelDerivedApiKey: true,\n // opencode ships TWO native OAuth logins reached via `opencode auth login`\n // (provider selection), both stored in its own auth.json — the runtime\n // declares one provider-scoped `authSubscription` surface per login (see\n // `subscriptionSurfaceFor` in `@agentproto/runtime`'s spawn-defaults.ts,\n // which resolves the matching entry for a spawn's resolved provider).\n // Both are EXTERNAL: the runtime injects no bearer — an agentproto-held\n // `sk-ant-oat…`/ChatGPT access token presented on opencode's x-api-key\n // channel is rejected upstream as an invalid key, so the ONLY working\n // subscription path is the CLI's own login. Each entry verifies its own\n // login is present (fail-loud, via the `opencode` provision recipe's\n // `anthropic-oauth`/`openai-oauth` methods) and scrubs the matching\n // api-key var so a leftover ANTHROPIC_API_KEY/OPENAI_API_KEY can't\n // override it.\n // - anthropic: \"Claude Pro/Max\" OAuth login (browser/headless).\n // - openai: \"ChatGPT Pro/Plus\" OAuth login (browser/headless, against\n // auth.openai.com). Reverse-engineered from the shipped binary (no\n // OSS source available for this build): opencode keys BOTH the\n // ChatGPT OAuth login and the plain API-key credential for this\n // provider under the SAME auth.json key `openai` (there is no\n // separate \"chatgpt\" key) — the generic `Cli.providers.login` /\n // `Auth.set(provider.id, …)` write path is provider-id-keyed, not\n // method-keyed. See the `opencode` provision recipe's `openai-oauth`\n // method docblock for the full trace.\n // Each is scoped to its own `provider`, so neither ever lights up the\n // other's (or openrouter/groq's) models.\n authSubscription: [\n { external: true, provider: \"anthropic\" },\n { external: true, provider: \"openai\" },\n ],\n models: {\n // Default to a canonical catalog model (claude-sonnet-4-5). The legacy\n // alias `claude-sonnet-4-6` still resolves to the same model, but the\n // generated menu uses canonical ids from the shared catalog.\n default: \"anthropic/claude-sonnet-4-5\",\n // Generated from the shared provider catalog so the Configuration Lab /\n // harness picker shows genuinely runnable Anthropic / OpenAI / OpenRouter\n // / OpenCode Go / OpenCode Zen models instead of a hardcoded 3-item list.\n // Groq stays omitted — it is not a billing/auth CatalogProvider, so there\n // is nothing to enumerate; the free-form `model` option and `models.env`\n // still support it.\n allowed: buildOpencodeModelMenu(),\n env: {\n anthropic: \"ANTHROPIC_API_KEY\",\n openai: \"OPENAI_API_KEY\",\n openrouter: \"OPENROUTER_API_KEY\",\n // Both OpenCode endpoints read the SAME var — opencode's own convention\n // (models.dev records `env: [\"OPENCODE_API_KEY\"]` for each), mirrored in\n // the catalog's `PROVIDER_KEY_ENV`. Two DIFFERENT secrets share the name,\n // so which balance a spawn bills is decided by the resolved auth profile's\n // endpoint (`opencode` vs `opencode-go`), not by this map.\n opencode: \"OPENCODE_API_KEY\",\n \"opencode-go\": \"OPENCODE_API_KEY\",\n groq: \"GROQ_API_KEY\",\n },\n },\n capabilities: {\n streaming: true,\n tool_calls: true,\n sub_agents: false,\n file_io: true,\n // OpenCode forwards ACP image content blocks to the underlying\n // provider (Anthropic Messages API for Claude models, OpenAI vision\n // for GPT-5). Hosts SHOULD send `{type: \"image\", data, mimeType}`\n // blocks alongside text in `session.send`.\n multimodal: true,\n // OpenCode persists session state internally and the ACP server\n // implements newSession/loadSession/resumeSession. Pair with the\n // `native-resume` continuation strategy for cold-start reattach.\n resumable: true,\n bidirectional: true,\n },\n // No manifest `modes[]`: opencode's operation profiles (default / plan /\n // build) are POSTURE, which no longer lives in the manifest (SPEC §3.4a).\n // opencode's own ACP server already advertises these as native session modes\n // and switches them on the wire via `session/set_config_option`\n // (configId:\"mode\") / `session/set_mode` — precisely the harness ACP mode\n // registry (`SessionModeState.availableModes`) posture is now sourced from,\n // so declaring them here would only duplicate the harness's own truth. route\n // comes from the model catalog and opencode has no context mode, so the array\n // would be empty — omit it.\n options: [\n {\n id: \"model\",\n type: \"string\",\n description:\n \"Provider/model override for this operator binding (e.g. `openrouter/anthropic/claude-sonnet-4-6`). Applied via ACP \" +\n \"session/set_config_option after the session is created (see \" +\n \"`models.apply`, default \\\"config\\\"); no CLI flag for this exists on \" +\n \"`opencode acp`. An id the server can't resolve REFUSES the spawn \" +\n \"with the server's reason (derived-from-model guard in \" +\n \"`define-agent-cli.ts`): opencode routes AND bills by the id's own \" +\n \"provider prefix, so silently keeping the server's default model \" +\n \"would run a model (and a bill) the operator didn't ask for.\",\n },\n ],\n continuation: {\n default: \"native-resume\",\n supported: [\"native-resume\", \"pinned-session\", \"transcript\", \"none\"],\n pinned_session: {\n idle_timeout_ms: 1_800_000,\n key_scope: [\"conversation\", \"operator\"],\n },\n },\n tags: [\"opencode\", \"sst\", \"acp\", \"agent-runtime\", \"coding\"],\n})\n\nexport function opencodeRuntime(): AgentCliRuntime {\n return createAgentCliRuntime(opencode)\n}\n\n/**\n * Best-effort per-session usage reader, mirroring `readHermesUsage` in\n * `@agentproto/adapter-hermes`. OpenCode's live ACP `usage_update` event only\n * ever carries `{used, size, cost}` — no token fields exist on that wire\n * event at all (confirmed by disassembling the installed opencode acp\n * binary) — so `session_usage` can't get tokensIn/tokensOut from the live\n * stream the way it does for other adapters. OpenCode does persist full\n * token detail to its own sqlite store though (the same store\n * `exportOpenCodeSession` in `@agentproto/runtime`'s transcript-export.ts\n * reads for `sessions export --json`), so this hook re-reads it directly.\n */\nexport async function readOpenCodeUsage(\n sessionId: string,\n): Promise<{ costUsd?: number; tokensIn?: number; tokensOut?: number } | null> {\n try {\n // node:sqlite is a Node 22+ builtin. Build the specifier at runtime so the\n // bundler (esbuild/tsup) can't statically rewrite it — it strips the\n // `node:` prefix off this not-yet-recognised builtin, turning the import\n // into a missing `sqlite` package that throws and silently yields null.\n const sqliteSpecifier = [\"node\", \"sqlite\"].join(\":\")\n const { DatabaseSync } = (await import(sqliteSpecifier)) as unknown as {\n DatabaseSync: new (p: string, o?: { readOnly?: boolean }) => {\n prepare(sql: string): { get(...a: unknown[]): unknown }\n close(): void\n }\n }\n const dataHome = process.env.XDG_DATA_HOME ?? join(homedir(), \".local\", \"share\")\n const dbPath = join(dataHome, \"opencode\", \"opencode.db\")\n const db = new DatabaseSync(dbPath, { readOnly: true })\n try {\n const row = db\n .prepare(\"SELECT cost, tokens_input AS ti, tokens_output AS to_ FROM session WHERE id = ?\")\n .get(sessionId) as { cost?: number; ti?: number; to_?: number } | undefined\n if (!row) return null\n return {\n ...(row.cost != null ? { costUsd: Number(row.cost) } : {}),\n ...(row.ti != null ? { tokensIn: row.ti } : {}),\n ...(row.to_ != null ? { tokensOut: row.to_ } : {}),\n }\n } finally {\n db.close()\n }\n } catch {\n return null\n }\n}\n\nexport type { AgentCliHandle, AgentCliRuntime }\n"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * @agentproto/adapter-opencode — AIP-45 adapter for sst/opencode.\n *\n * OpenCode ships first-party ACP support (`opencode acp`) so no\n * third-party wrapper is needed. We spawn it via `npx -y opencode-ai\n * acp` and drive it over stdio JSON-RPC the same way the claude-code\n * adapter drives @agentclientprotocol/claude-agent-acp.\n *\n * import { opencode, opencodeRuntime } from \"@agentproto/adapter-opencode\"\n * const session = await opencodeRuntime().start({\n * env: { ANTHROPIC_API_KEY: \"sk-...\" },\n * })\n * for await (const evt of session.send({ role: \"user\", content: \"...\" })) {\n * console.log(evt)\n * }\n * await session.close()\n */\n\nimport { homedir } from \"node:os\"\nimport { join } from \"node:path\"\nimport {\n createAgentCliRuntime,\n defineAgentCli,\n type AgentCliHandle,\n type AgentCliRuntime,\n} from \"@agentproto/driver-agent-cli\"\nimport { getModelsByProvider, listModels } from \"@agentproto/model-catalog\"\n\n/**\n * Build OpenCode's model menu from the shared provider catalog instead of a\n * hand-maintained allowlist. OpenCode routes by the model id's own\n * `<provider>/<id>` prefix, so every entry carries the billing provider the\n * runtime's eligibility projection needs.\n *\n * Only providers OpenCode genuinely supports today are included:\n * - Anthropic and OpenAI (direct vendor prefixes)\n * - OpenRouter (`openrouter/<vendor>/<id>` router prefix)\n * - OpenCode's own two hosted endpoints — `opencode-go/<id>` (the flat Go\n * subscription) and `opencode/<id>` (Zen, pay-as-you-go), both matching\n * opencode's own config spelling\n *\n * Groq is still omitted: it is not a billing/auth `CatalogProvider` (no\n * pricing generator), so there is nothing in the shared catalog to enumerate.\n * The `models.env` map carries its key env var, so a free-form `model`\n * override and a manually-curated profile continue to work. (The two OpenCode\n * endpoints USED to be omitted for the same reason — they are now first-class\n * catalog providers with their own generated route tables, which is what makes\n * them enumerable here.)\n *\n * `router: true` marks a provider whose surface lives ONLY in its generated\n * route table and is deliberately not spread into `LLM_PRICING_CATALOG` (a\n * bare `claude-sonnet-5` must keep meaning direct Anthropic, not a Zen-priced\n * route). `listModels` returns nothing for those, so they are read through\n * `getModelsByProvider`, which folds the route table in.\n */\nfunction buildOpencodeModelMenu(): Array<{ id: string; provider: string }> {\n const supported = [\n { provider: \"anthropic\", prefix: \"anthropic\", router: false },\n { provider: \"openai\", prefix: \"openai\", router: false },\n { provider: \"openrouter\", prefix: \"openrouter\", router: false },\n { provider: \"opencode-go\", prefix: \"opencode-go\", router: true },\n { provider: \"opencode\", prefix: \"opencode\", router: true },\n ] as const\n\n const seen = new Set<string>()\n const out: Array<{ id: string; provider: string }> = []\n\n for (const { provider, prefix, router } of supported) {\n const models = router\n ? getModelsByProvider(provider).filter(model => model.kind === \"llm\")\n : listModels({ kind: \"llm\", provider })\n for (const model of models) {\n const bareId = model.id\n const canonicalId = bareId.includes(\"/\") ? bareId : `${prefix}/${bareId}`\n const id = provider === \"openrouter\" ? `openrouter/${bareId}` : canonicalId\n if (seen.has(id)) continue\n seen.add(id)\n out.push({ id, provider })\n }\n }\n\n return out.sort((a, b) => {\n if (a.provider !== b.provider) return a.provider.localeCompare(b.provider)\n return a.id.localeCompare(b.id)\n })\n}\n\nexport const opencode: AgentCliHandle = defineAgentCli({\n name: \"opencode\",\n id: \"opencode\",\n description:\n \"sst/opencode — open-source coding agent with first-party ACP mode. Spawned via `npx -y opencode-ai acp` and driven over stdio JSON-RPC. Multi-provider (Anthropic / OpenAI / OpenRouter / Groq / OpenCode hosted).\",\n version: \"0.1.0\",\n bin: \"npx\",\n bin_args: [\"-y\", \"opencode-ai\", \"acp\"],\n install: [\n { method: \"npm\", package: \"opencode-ai\", global: true },\n { method: \"curl\", url: \"https://opencode.ai/install\" },\n ],\n version_check: {\n // PRESENCE probe — local binary check, not a registry query. Covers BOTH\n // install methods: the npm -g package and the curl installer both put an\n // `opencode` executable on PATH, and it runs `--version` fully offline.\n cmd: \"opencode --version\",\n parse: \"(\\\\d+\\\\.\\\\d+\\\\.\\\\d+)\",\n range: \">=1.0.0\",\n timeout_ms: 15_000,\n },\n auth: {\n ref: \"./SECRETS.md\",\n state: {\n env: [\n \"OPENCODE_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"OPENROUTER_API_KEY\",\n \"GROQ_API_KEY\",\n ],\n },\n },\n sandbox: \"./SANDBOX.md\",\n protocol: \"acp\",\n acp: \"./opencode-acp.ACP.md\",\n session: {\n mode: \"persistent\",\n idle_timeout_ms: 1_800_000,\n context_carryover: true,\n },\n // opencode routes by the model id's own `<provider>/<id>` prefix itself, no\n // adapter mode — unlike claude-sdk/claude-code, which need ANTHROPIC_BASE_URL\n // pre-wired by a mode. So the route falls out of the chosen model →\n // derived-from-model. The API key env var is also derived from the model's\n // provider prefix, so the runtime must know to present api-key auth on the\n // model-derived direct endpoint.\n routeSelection: \"derived-from-model\",\n modelDerivedApiKey: true,\n // opencode ships TWO native OAuth logins reached via `opencode auth login`\n // (provider selection), both stored in its own auth.json — the runtime\n // declares one provider-scoped `authSubscription` surface per login (see\n // `subscriptionSurfaceFor` in `@agentproto/runtime`'s spawn-defaults.ts,\n // which resolves the matching entry for a spawn's resolved provider).\n // Both are EXTERNAL: the runtime injects no bearer — an agentproto-held\n // `sk-ant-oat…`/ChatGPT access token presented on opencode's x-api-key\n // channel is rejected upstream as an invalid key, so the ONLY working\n // subscription path is the CLI's own login. Each entry verifies its own\n // login is present (fail-loud, via the `opencode` provision recipe's\n // `anthropic-oauth`/`openai-oauth` methods) and scrubs the matching\n // api-key var so a leftover ANTHROPIC_API_KEY/OPENAI_API_KEY can't\n // override it.\n // - anthropic: \"Claude Pro/Max\" OAuth login (browser/headless).\n // - openai: \"ChatGPT Pro/Plus\" OAuth login (browser/headless, against\n // auth.openai.com). Reverse-engineered from the shipped binary (no\n // OSS source available for this build): opencode keys BOTH the\n // ChatGPT OAuth login and the plain API-key credential for this\n // provider under the SAME auth.json key `openai` (there is no\n // separate \"chatgpt\" key) — the generic `Cli.providers.login` /\n // `Auth.set(provider.id, …)` write path is provider-id-keyed, not\n // method-keyed. See the `opencode` provision recipe's `openai-oauth`\n // method docblock for the full trace.\n // Each is scoped to its own `provider`, so neither ever lights up the\n // other's (or openrouter/groq's) models.\n authSubscription: [\n { external: true, provider: \"anthropic\" },\n { external: true, provider: \"openai\" },\n ],\n models: {\n // Default to a canonical catalog model (claude-sonnet-4-5). The legacy\n // alias `claude-sonnet-4-6` still resolves to the same model, but the\n // generated menu uses canonical ids from the shared catalog.\n default: \"anthropic/claude-sonnet-4-5\",\n // Generated from the shared provider catalog so the Configuration Lab /\n // harness picker shows genuinely runnable Anthropic / OpenAI / OpenRouter\n // / OpenCode Go / OpenCode Zen models instead of a hardcoded 3-item list.\n // Groq stays omitted — it is not a billing/auth CatalogProvider, so there\n // is nothing to enumerate; the free-form `model` option and `models.env`\n // still support it.\n allowed: buildOpencodeModelMenu(),\n env: {\n anthropic: \"ANTHROPIC_API_KEY\",\n openai: \"OPENAI_API_KEY\",\n openrouter: \"OPENROUTER_API_KEY\",\n // Both OpenCode endpoints read the SAME var — opencode's own convention\n // (models.dev records `env: [\"OPENCODE_API_KEY\"]` for each), mirrored in\n // the catalog's `PROVIDER_KEY_ENV`. Two DIFFERENT secrets share the name,\n // so which balance a spawn bills is decided by the resolved auth profile's\n // endpoint (`opencode` vs `opencode-go`), not by this map.\n opencode: \"OPENCODE_API_KEY\",\n \"opencode-go\": \"OPENCODE_API_KEY\",\n groq: \"GROQ_API_KEY\",\n },\n },\n capabilities: {\n streaming: true,\n tool_calls: true,\n sub_agents: false,\n file_io: true,\n // OpenCode forwards ACP image content blocks to the underlying\n // provider (Anthropic Messages API for Claude models, OpenAI vision\n // for GPT-5). Hosts SHOULD send `{type: \"image\", data, mimeType}`\n // blocks alongside text in `session.send`.\n multimodal: true,\n // OpenCode persists session state internally and the ACP server\n // implements newSession/loadSession/resumeSession. Pair with the\n // `native-resume` continuation strategy for cold-start reattach.\n resumable: true,\n bidirectional: true,\n },\n // No manifest `modes[]`: opencode's operation profiles (default / plan /\n // build) are POSTURE, which no longer lives in the manifest (SPEC §3.4a).\n // opencode's own ACP server already advertises these as native session modes\n // and switches them on the wire via `session/set_config_option`\n // (configId:\"mode\") / `session/set_mode` — precisely the harness ACP mode\n // registry (`SessionModeState.availableModes`) posture is now sourced from,\n // so declaring them here would only duplicate the harness's own truth. route\n // comes from the model catalog and opencode has no context mode, so the array\n // would be empty — omit it.\n options: [\n {\n id: \"model\",\n type: \"string\",\n description:\n \"Provider/model override for this operator binding (e.g. `openrouter/anthropic/claude-sonnet-4-6`). Applied via ACP \" +\n \"session/set_config_option after the session is created (see \" +\n \"`models.apply`, default \\\"config\\\"); no CLI flag for this exists on \" +\n \"`opencode acp`. An id the server can't resolve REFUSES the spawn \" +\n \"with the server's reason (derived-from-model guard in \" +\n \"`define-agent-cli.ts`): opencode routes AND bills by the id's own \" +\n \"provider prefix, so silently keeping the server's default model \" +\n \"would run a model (and a bill) the operator didn't ask for.\",\n },\n ],\n continuation: {\n default: \"native-resume\",\n supported: [\"native-resume\", \"pinned-session\", \"transcript\", \"none\"],\n pinned_session: {\n idle_timeout_ms: 1_800_000,\n key_scope: [\"conversation\", \"operator\"],\n },\n },\n tags: [\"opencode\", \"sst\", \"acp\", \"agent-runtime\", \"coding\"],\n metadata: {\n // Opts opencode into `agentproto install skill/<slug>` fan-out (no\n // --target given). Confirmed against the installed opencode CLI +\n // published docs (opencode.ai/docs/skills/): it auto-discovers\n // `~/.config/opencode/skills/<name>/SKILL.md` (global) in addition to\n // project-local `.opencode/skills/`, `.claude/skills/`, `.agents/skills/`\n // — same flat-dir, one-subdir-per-skill shape as hermes.\n skills: { format: \"flat-dir\", dir: \"~/.config/opencode/skills\" },\n },\n})\n\nexport function opencodeRuntime(): AgentCliRuntime {\n return createAgentCliRuntime(opencode)\n}\n\n/**\n * Best-effort per-session usage reader, mirroring `readHermesUsage` in\n * `@agentproto/adapter-hermes`. OpenCode's live ACP `usage_update` event only\n * ever carries `{used, size, cost}` — no token fields exist on that wire\n * event at all (confirmed by disassembling the installed opencode acp\n * binary) — so `session_usage` can't get tokensIn/tokensOut from the live\n * stream the way it does for other adapters. OpenCode does persist full\n * token detail to its own sqlite store though (the same store\n * `exportOpenCodeSession` in `@agentproto/runtime`'s transcript-export.ts\n * reads for `sessions export --json`), so this hook re-reads it directly.\n */\nexport async function readOpenCodeUsage(\n sessionId: string,\n): Promise<{\n costUsd?: number\n tokensIn?: number\n tokensOut?: number\n cacheReadTokens?: number\n cacheWriteTokens?: number\n reasoningTokens?: number\n} | null> {\n try {\n // node:sqlite is a Node 22+ builtin. Build the specifier at runtime so the\n // bundler (esbuild/tsup) can't statically rewrite it — it strips the\n // `node:` prefix off this not-yet-recognised builtin, turning the import\n // into a missing `sqlite` package that throws and silently yields null.\n const sqliteSpecifier = [\"node\", \"sqlite\"].join(\":\")\n const { DatabaseSync } = (await import(sqliteSpecifier)) as unknown as {\n DatabaseSync: new (p: string, o?: { readOnly?: boolean }) => {\n prepare(sql: string): { get(...a: unknown[]): unknown }\n close(): void\n }\n }\n const dataHome = process.env.XDG_DATA_HOME ?? join(homedir(), \".local\", \"share\")\n const dbPath = join(dataHome, \"opencode\", \"opencode.db\")\n const db = new DatabaseSync(dbPath, { readOnly: true })\n try {\n // `SELECT *` so an older opencode.db without the cache/reasoning\n // columns still reads (the missing ones just stay absent).\n const row = db.prepare(\"SELECT * FROM session WHERE id = ?\").get(sessionId) as\n | {\n cost?: number | null\n tokens_input?: number | null\n tokens_output?: number | null\n tokens_cache_read?: number | null\n tokens_cache_write?: number | null\n tokens_reasoning?: number | null\n }\n | undefined\n if (!row) return null\n return {\n ...(row.cost != null ? { costUsd: Number(row.cost) } : {}),\n ...(row.tokens_input != null ? { tokensIn: row.tokens_input } : {}),\n ...(row.tokens_output != null ? { tokensOut: row.tokens_output } : {}),\n ...(row.tokens_cache_read != null ? { cacheReadTokens: row.tokens_cache_read } : {}),\n ...(row.tokens_cache_write != null ? { cacheWriteTokens: row.tokens_cache_write } : {}),\n ...(row.tokens_reasoning != null ? { reasoningTokens: row.tokens_reasoning } : {}),\n }\n } finally {\n db.close()\n }\n } catch {\n return null\n }\n}\n\nexport type { AgentCliHandle, AgentCliRuntime }\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentproto/adapter-opencode",
3
- "version": "1.3.4",
3
+ "version": "1.5.0",
4
4
  "description": "@agentproto/adapter-opencode — AIP-45 AGENT-CLI adapter for sst/opencode. Spawns `opencode acp` (via npx -y opencode-ai) and drives OpenCode as an agentproto agent CLI over stdio JSON-RPC.",
5
5
  "keywords": [
6
6
  "agentproto",
@@ -41,8 +41,8 @@
41
41
  "access": "public"
42
42
  },
43
43
  "dependencies": {
44
- "@agentproto/driver-agent-cli": "2.5.0",
45
- "@agentproto/model-catalog": "0.11.0"
44
+ "@agentproto/driver-agent-cli": "2.5.1",
45
+ "@agentproto/model-catalog": "0.11.1"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/node": "^25.9.5",