10 KiB
AGENTS.md
Instructions for LLM coding agents working in this repository.
Project overview
opencode-commandcode-provider is a native AI SDK provider that connects opencode to the
CommandCode /alpha/generate API. It implements LanguageModelV3 from @ai-sdk/provider@3.0.8
and is loaded by opencode from dist/index.js via a file:// npm spec.
server.py in this directory is the reference implementation of the same translation
(an OpenAI-compatible HTTP proxy). When behavior is ambiguous, server.py is the source of
truth for the upstream wire shape. This package is the TypeScript port; it does not run Python.
- Language/tooling: TypeScript, ESM,
tsc. - Runtime target: opencode's provider loader,
@ai-sdk/provider@3.0.8(V3 specification). - No runtime dependencies.
@ai-sdk/provideris imported withimport typeonly; the built output has no imports of its own.
Commands
Run from the repository root.
npm install # dev deps: typescript, @types/node, @ai-sdk/provider
npm run typecheck # tsc --noEmit (provider) + tsc -p tsconfig.tui.json (TUI plugin)
npm run build # tsc -> dist/ (provider only; the TUI plugin is loaded from source)
npm run smoke # live request against CommandCode (needs a key)
npm run sync-models # regenerate provider.commandcode.models from the live catalog
There is no unit-test suite. scripts/smoke.mjs is the end-to-end check. scripts/sync-models.mjs
is the model-catalog generator (needs dist/ first; writes the opencode config unless --out/--dry-run).
After any change under src/, run npm run typecheck, then npm run build, then npm run smoke.
A change is not done until it typechecks and the smoke test passes.
Layout
src/index.ts Public exports: createCommandCode (named) and default.
src/model.ts CommandCodeLanguageModel: HTTP, retries, doStream/doGenerate, event->part mapping.
src/transform.ts LanguageModelV3CallOptions -> /alpha/generate envelope (returns a JSON string).
src/events.ts Async iterator over the NDJSON/SSE response body.
src/usage.ts finish event -> LanguageModelV3Usage; finish-reason unification.
src/redact.ts Credential scrubbing for error surfaces.
src/log.ts Opt-in tracing (COMMANDCODE_DEBUG) to a log file.
src/toggles.ts Shared toggle file (~/.config/opencode/commandcode-toggles.json).
src/tui.tsx opencode TUI plugin: /cc-zdr, /cc-debug, /cc-status + sidebar status panel.
src/tui-shims.d.ts Ambient types for the opencode-provided solid-js/@opentui/solid runtime.
tsconfig.tui.json Typechecks src/tui.tsx against the shim (no dependencies).
src/constants.ts Defaults, paths, headers, passthrough params, static config block.
scripts/smoke.mjs Live end-to-end check.
scripts/sync-models.mjs Catalog -> provider.commandcode.models generator (with vision probing).
server.py Reference Python proxy (do not modify unless explicitly asked).
Architecture / data flow
opencode
└─ provider.languageModel(id).doStream(options)
├─ transform(options, id) ──► JSON envelope
├─ fetch POST {baseURL}/alpha/generate (retry 429/5xx)
├─ iterateEvents(response.body) (NDJSON lines)
└─ map each event ──► LanguageModelV3StreamPart*
... always ending with a `finish` part
doGenerate calls doStream and drains the resulting stream into a
LanguageModelV3GenerateResult.
Conventions
- ESM only. Imports of local files must end in
.js(NodeNext resolution), e.g.import { transform } from "./transform.js";. strictTypeScript is on, includingnoUncheckedIndexedAccessandverbatimModuleSyntax. Type-only imports must useimport type.- Do not add comments unless they explain a non-obvious decision. Existing comments document upstream quirks and are intentional; match that spirit, not volume.
- Keep
transform.tsoutput in parity withserver.pytransform(). If you change the envelope, explain whichserver.pybehavior motivated it. - Preserve the single public factory
createCommandCode.index.tsre-exports it as both a named and the default export. - The package must stay runtime-dependency-free. Do not add a runtime
importof a package that would neednode_modulesat load time; opencode importsdist/index.jsdirectly.
Invariants and gotchas
These are load-bearing. Breaking one causes silent failures in opencode.
- Exactly one
create*export reachable from the entrypoint. opencode loads a custom npm provider by importing it, choosing the first export whose name starts withcreate, and calling it asfn({ name: providerID, ...options }). The result must exposelanguageModel(id). params.streamis alwaystrue. Sendingstream: falsemakes upstream answer "Proxy use detected. This endpoint only serves CLI." Non-streaming clients are served by buffering the V3 stream indoGenerate.tool-callstream parts carryinputas a stringified JSON string, not an object (LanguageModelV3ToolCall.input: string). Serialize withJSON.stringify. Passing an object breaks opencode's tool execution with errors likeK.input.trim is not a function.- Always emit a terminal
finishpart. If upstream ends without afinishevent, synthesize one from the accumulated state. A missing finish surfaces as a truncated-response error. - Drop unpaired tool ids. Any
tool-callwithout a matchingtool-result(or vice versa) must be omitted from the envelope; upstream rejects the whole request otherwise. - Usage is nested in V3. Fill
inputTokens.{total,noCache,cacheRead,cacheWrite}andoutputTokens.{total,text,reasoning}— not flatpromptTokens/completionTokens. supportedUrlsis{}and stays empty. CommandsCode URLs are not fetched by the SDK.- Redact before surfacing errors. Any upstream error text passed to the client must go through
redact(). - Replay assistant reasoning as a
{type:"reasoning"}content part. DeepSeek thinking mode rejects any request carryingtoolswhose prior assistant turns omit their reasoning. Intransform.ts, forward each assistantreasoningpart as{type:"reasoning", text, signature?}; do not drop it and do not fabricate empty reasoning.server.pypredates this requirement and is not the guide here. file://npm specs bypass install. opencode importsdist/index.jsdirectly, so the repo must be rebuilt for opencode to see source changes./modelsis config-driven, not provider-driven. opencode builds the model list fromprovider.commandcode.modelsinopencode.jsonand never asks a customfile://provider to discover models (only internal providers can registerdiscoverModels). Refresh the map withnpm run sync-models; do not expect a discovery hook insrc/to populate it.- The TUI plugin lives outside the provider entrypoint.
src/tui.tsxis loaded from source (notdist/) viaopencode plugin <path>, which writes atui.jsonpluginentry; it is exposed as./tuiinpackage.json. opencode compiles the.tsxat load with its Bun/Solid transform and mapssolid-js/@opentui/solidto its internal modules, so the package stays dependency-free — do not add a build step or real imports of those packages.src/tui-shims.d.tsis the only local stand-in for types; keep it in sync with what the file actually imports. Verified on opencode 1.18.31: the module's default export must be{ id, tui }(a baretuinamed export is imported but never invoked),idis required, andtuiisasync (api) => {}. Commands register viaapi.keymap.registerLayer({ commands })with{ name, run, title, desc, category, namespace: "palette", slashName }. The sidebar panel registers viaapi.slots.register({ order, slots: { sidebar_content } });order: 90keeps it above the built-in panels (100 context, 200 mcp, 300 lsp, 400 todo, 500 files). The slot renderer returns Solid JSX and is reactive; it relies onapi.themeandapi.lifecycle.onDispose. - Slash commands and
Ctrl+Pshare one registry. In opencode 1.x the slash menu queries the samenamespace: "palette"commands the palette lists (both filter outhidden: true). A slash-only entry is not expressible; registering/cc-*also adds them toCtrl+P. - Toggles are resolved per request, never at load.
zdranddebugare read inmodel.ts/log.tson every call from the toggle file / env, so/cc-*changes take effect without restarting opencode. Precedence forzdr:providerOptions.commandcode.zdr>x-cmd-zdrheader > toggle file >COMMANDCODE_ZDR. Do not reintroduce load-time consts.
Change workflow
- Read
server.pyfor the behavior being ported and note the relevant function. - Make the smallest change in the matching
src/file. npm run typecheck— fix all errors.npm run build.npm run smoke— verify text and, when relevant, tool calls.- If the change affects the opencode integration, verify with
opencode run(see below). - Update
README.mdif options, features, or behavior changed. KeepAGENTS.mdinvariants current if you discover a new one.
Do not edit dist/ by hand; it is generated. Do not modify server.py unless explicitly asked.
Verification
Automated:
npm run typecheck; npm run build; npm run smoke
Inside opencode (restart it first if config changed):
opencode models commandcode
opencode run "Reply with exactly: pong" -m commandcode/deepseek/deepseek-v4-flash-vision-exp
opencode run "Use the glob tool to list *.mjs in this directory and report the filenames." -m commandcode/deepseek/deepseek-v4-flash-vision-exp
The third command confirms the full tool-call round trip (tool-call emitted, executed, result
replayed). Run it after touching transform.ts or the tool-call mapping in model.ts.
Do / Don't
- Do keep the wire envelope identical to
server.py. - Do run typecheck, build, and smoke after every source change.
- Do prefer
{env:COMMANDCODE_API_KEY}in docs and examples. - Don't commit real API keys,
dist/,node_modules/,dump/, or logs (all gitignored). - Don't add runtime dependencies or make
distrequirenode_modules. - Don't force
stream:false, send objecttool-call.input, or omit thefinishpart. - Don't commit unless the user explicitly asks.