7.6 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
npm run build # tsc -> dist/
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/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(). 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.
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.