| scripts | ||
| src | ||
| .gitignore | ||
| AGENTS.md | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| server.py | ||
| tsconfig.json | ||
| tsconfig.tui.json | ||
opencode-commandcode-provider
A native AI SDK provider that connects opencode
to the CommandCode /alpha/generate API.
It is the TypeScript successor to the server.py proxy in this directory. Where the proxy
exposed an OpenAI-compatible /v1/chat/completions endpoint that opencode reached with
@ai-sdk/openai-compatible, this package implements the LanguageModelV3 interface directly,
so opencode talks to CommandCode with no Python process and no local proxy in the middle.
opencode ──► @ai-sdk/provider (LanguageModelV3)
│
▼
opencode-commandcode-provider
transform → POST /alpha/generate
NDJSON events → V3 stream parts
│
▼
api.commandcode.ai
server.py is kept in the repository as the reference implementation. All wire-shape decisions
(error reshaping, tool-call pairing, tool_choice emulation, retry policy, credential redaction)
originate there and are mirrored here.
Requirements
- Node.js >= 18 (uses global
fetch,ReadableStream,TextDecoder,structuredClone). - opencode >= 1.17 that ships
@ai-sdk/provider@3.0.8(verified against opencode 1.18.30). - A CommandCode account and API key with access to the
/alpha/generateendpoint.
Install and wire into opencode
The provider is consumed directly from its build output via a file:// spec. This bypasses
npm install and is the intended development workflow.
# 1. build the provider
npm install
npm run build
// 2. ~/.config/opencode/opencode.json (or a project opencode.json)
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"commandcode": {
"name": "Command Code",
"npm": "file:///C:/DevTools/pienv/ccprovider/dist/index.js",
"options": {
"apiKey": "{env:COMMANDCODE_API_KEY}"
},
"models": {
"deepseek/deepseek-v4.1-flash": {
"name": "DeepSeek V4.1 Flash",
"limit": { "context": 1048576, "output": 256000 },
"attachment": true,
"modalities": { "input": ["text", "image"], "output": ["text"] }
}
}
}
}
}
Then set the key and restart opencode:
$env:COMMANDCODE_API_KEY = "user_..."
opencode models commandcode
opencode loads configuration once at startup. After changing
opencode.json, a plugin, or a rebuilt provider, quit and relaunch opencode for the change to take effect.
The models map is required — opencode silently drops a custom provider whose model list is
empty. The provider itself only needs languageModel(id), which it implements for any id passed
to it, but opencode builds /models and the TUI picker from this map and never asks a custom
file:// provider to discover models. Use the sync script below to fill it in.
Listing all CommandCode models
opencode reads the model list from provider.commandcode.models; a custom provider cannot
register dynamic discovery. scripts/sync-models.mjs fetches the live catalog from
GET /provider/v1/models and writes the full map into your opencode config.
npm run build # the script imports the compiled provider
npm run sync-models # fetch catalog, probe vision, merge into ~/.config/opencode/opencode.json
Then restart opencode and confirm:
opencode models commandcode
Mapping and behaviour:
- Every catalog entry gets
limit.contextfrom itscontext_length,limit.outputfromDEFAULT_MAX_TOKENS, andreasoning: true. - Vision (
attachment+modalities.inputwithimage) is probed per model: a 64×64 image is sent and the reply is inspected. Models that answer a color are marked vision; models that replyNO_IMAGE(retried once) or reject the image are not. Probes that cannot run — plan-gated, temporarily unavailable — fall back to a family heuristic (claude,gpt-5,gemini,grok,qwen…vl, …). Probing is best-effort: upstream is nondeterministic and a re-run may flip a borderline model. - Generated fields (
name,limit,reasoning,attachment,modalities) overwrite existing values. Other fields (variants,options,cost, …) and ids not in the catalog are preserved.
Options:
node scripts/sync-models.mjs --dry-run # print the merged map, write nothing
node scripts/sync-models.mjs --out models.json # write only the models fragment
node scripts/sync-models.mjs --no-probe # skip probing, use the heuristic (fast/offline)
node scripts/sync-models.mjs --concurrency 8 # probe parallelism (default 6)
node scripts/sync-models.mjs --no-preserve # snapshot only: drop user ids/extra fields
node scripts/sync-models.mjs --config <path> # target a different config
The API key is read from COMMANDCODE_API_KEY, then provider.commandcode.options.apiKey in the
target config.
Configuration options
provider.commandcode.options is forwarded to createCommandCode(options).
| Option | Type | Default | Description |
|---|---|---|---|
apiKey |
string |
— | CommandCode API key. A bare token or Bearer <token> both work. |
headers |
Record<string,string> |
{} |
Extra request headers, merged over the built-in ones. An Authorization header is accepted as an alternative auth source. |
baseURL |
string |
https://api.commandcode.ai |
Upstream origin. Trailing slashes are stripped. |
ccVersion |
string |
1.15.1 |
Value of the x-command-code-version header. |
maxRetries |
number |
2 |
Retry attempts for retryable failures (429/5xx/network). |
retryMaxDelaySeconds |
number |
60 |
Longest wait honoured from Retry-After; longer values are not retried. |
name |
string |
commandcode |
Provider id reported to the AI SDK. opencode sets this automatically. |
Auth precedence: options.apiKey wins; otherwise the value of an Authorization header in
options.headers (with a leading Bearer stripped). If neither is present, requests are sent
unauthenticated and CommandCode will reject them.
Debug tracing
Set COMMANDCODE_DEBUG=1 to write a trace of every request and stream event to a log file.
Silent (and no file is created) unless enabled. Every line is passed through redact() so
credentials never reach disk. It can also be flipped at runtime with /cc-debug (see
Runtime toggles).
$env:COMMANDCODE_DEBUG = "1"
opencode run "Reply with exactly: pong" -m commandcode/deepseek/deepseek-v4.1-flash
| Env var | Default | Description |
|---|---|---|
COMMANDCODE_DEBUG |
unset | 1/true/yes enables tracing. |
COMMANDCODE_DEBUG_FILE |
<os tempdir>/commandcode-debug.log |
Where the trace is appended. |
The log captures the request (model id, body byte length), each HTTP attempt (status, retry wait, elapsed ms), every stream event payload, and the terminal finish reason + usage. It is useful for diagnosing model selection, retry, tool-call, and finish-reason issues.
ZDR header toggle
The x-cmd-zdr: 1 request header is omitted by default because it is rejected by some
models. It can be enabled with the COMMANDCODE_ZDR environment variable, the /cc-zdr slash
command, or providerOptions.commandcode.zdr. The value is resolved per request, so toggling it
does not require restarting opencode.
Precedence, highest first:
providerOptions.commandcode.zdr(true/false), e.g. a model variant or agent option.- An explicit
x-cmd-zdrheader inopencode.jsonoptions.headersor the call'sheaders. - The toggle file (
zdrkey — see Runtime toggles). COMMANDCODE_ZDR=1|true|yesin the environment.
With tracing enabled, the trace logs a zdr on|off line per request so the toggle state is
observable.
Runtime toggles
/cc-zdr and /cc-debug flip the zdr and debug values in a small JSON file the provider
reads on every request, so both settings change without restarting opencode. /cc-status shows
the current state. The file is ~/.config/opencode/commandcode-toggles.json (overridable with
COMMANDCODE_TOGGLES_FILE); the environment variables above are used when a key is absent.
These commands come from a small TUI plugin shipped in this package (src/tui.tsx). It is loaded
from source — opencode compiles the TSX and provides the Solid runtime itself — so there is no
build step and no runtime dependency for it. Register it once:
opencode plugin file:///C:/DevTools/pienv/ccprovider/src/tui.tsx
That writes a tui.json plugin entry (project-local .opencode/tui.json, or global with
--global). Restart opencode after installing; then type /cc- for autocomplete. The plugin also
renders a CommandCode panel in the session sidebar (zdr / debug, above the built-in
panels) that updates live as you toggle. Note that in opencode 1.x the slash menu and the Ctrl+P
palette read the same command registry, so these entries appear in both. If you run opencode
against a remote server, the TUI-side file is not visible to the provider on the server host — use
the env vars or providerOptions there.
Features
| Capability | Status |
|---|---|
Streaming (doStream) |
Yes — CommandCode is always streamed upstream, then re-emitted as V3 stream parts |
Non-streaming (doGenerate) |
Yes — buffers the stream internally |
| Text deltas | Yes |
| Reasoning deltas | Yes — emitted as reasoning-start / reasoning-delta / reasoning-end |
| Reasoning replay | Yes — assistant reasoning parts are sent back as {type:"reasoning"} content parts (required by DeepSeek thinking mode when tools are present); signature is forwarded when present |
| Tool calls | Yes — tool-input-start / tool-input-delta / tool-input-end / tool-call |
| Tool results | Yes — paired results are replayed; unpaired ids are dropped |
| Multiple images (vision) | Yes — data: URIs, raw base64, Uint8Array, and remote URLs |
| Token usage | Yes — input/output totals, cache read, reasoning tokens |
| Finish reasons | Yes — unified (stop, length, tool-calls, content-filter, error, other) plus raw |
| Sampling parameters | Yes — temperature, topP, topK, stopSequences, seed, presence/frequency penalties |
reasoning_effort |
Yes — via providerOptions.commandcode |
| Retry with backoff | Yes — 429/5xx and network errors, honouring Retry-After |
| Credential redaction | Yes — error bodies are scrubbed before surfacing |
| Runtime toggles | Yes — /cc-zdr and /cc-debug flip the shared toggle file without a restart |
| Sidebar status panel | Yes — a sidebar_content panel shows zdr/debug and updates live |
tool_choice handling
CommandCode accepts exactly one upstream value, {"type":"auto"}. The provider maps the AI SDK
values accordingly:
auto— forwarded as{"type":"auto"}.none— emulated by sending no tools at all.{ type: "tool", toolName }— emulated by sending only that tool.required— not expressible upstream; tools are sent and the model decides.
Reasoning effort
Set the per-request reasoning effort through provider options:
// opencode model options / variant
"options": { "providerOptions": { "commandcode": { "reasoningEffort": "high" } } }
Both reasoning_effort and reasoningEffort keys are recognised and forwarded as
params.reasoning_effort.
How it works
src/index.ts Public exports: createCommandCode + default.
src/model.ts CommandCodeLanguageModel (LanguageModelV3): HTTP, retries, stream/generate.
src/transform.ts LanguageModelV3CallOptions -> /alpha/generate envelope (JSON string).
src/events.ts NDJSON/SSE line iterator over the upstream response body.
src/usage.ts finish event -> V3 usage; 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 read by the provider and written by the TUI plugin.
src/tui.tsx opencode TUI plugin: /cc-zdr, /cc-debug, /cc-status + sidebar status panel.
src/constants.ts Defaults, header names, passthrough params, static config block.
scripts/smoke.mjs Live end-to-end check against api.commandcode.ai.
scripts/sync-models.mjs Generate provider.commandcode.models from the live catalog.
Request lifecycle
- opencode calls
provider.languageModel(id), thenmodel.doStream(options). transform()converts the AI SDK prompt into the CommandCode envelope:systemtext is joined intoparams.system.- user/assistant/tool messages become
params.messagescontent parts. - assistant
tool-callparts andtoolresults are only included when their ids are paired; unmatched ids (common after history truncation) are dropped because upstream rejects them. - image file parts become
{ type: "image", image, mimeType }. - function tools become
{ type: "function", name, description, input_schema }. streamis forced totrue— the endpoint answersstream:falsewith "Proxy use detected. This endpoint only serves CLI."
model.tsPOSTs the envelope with the CommandCode headers and retries 429/5xx.events.tsparses the NDJSON body (toleratingdata:prefixes and[DONE]).model.tsmaps each upstream event to aLanguageModelV3StreamPartand always terminates with afinishpart.doGeneratedrains the same stream and assembles aLanguageModelV3GenerateResult.
Development
npm install # installs 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 loads from source)
npm run smoke # live request against CommandCode
npm run sync-models # regenerate provider.commandcode.models from the catalog
scripts/smoke.mjs reads the key from COMMANDCODE_API_KEY, falling back to
provider.commandcode.options.apiKey in ~/.config/opencode/opencode.json. It exercises both
doGenerate and doStream and prints content, finish reason, and usage.
To smoke-test a specific model:
node scripts/smoke.mjs deepseek/deepseek-v4.1-flash
Verifying inside opencode
opencode models commandcode
opencode run "Reply with exactly: pong" -m commandcode/deepseek/deepseek-v4.1-flash
opencode run "Use the glob tool to list *.mjs and report the filenames." -m commandcode/deepseek/deepseek-v4.1-flash
Troubleshooting
| Symptom | Likely cause / fix |
|---|---|
Provider not found: commandcode |
Provider was dropped because models is empty, or the npm path is wrong. Confirm dist/index.js exists (npm run build) and that the file:// path is absolute. |
| Models appear but every call fails auth | options.apiKey missing/expired, or {env:COMMANDCODE_API_KEY} not set in the environment opencode was launched with. |
stream ended ... no finish / truncated output |
Upstream closed the connection early. The provider emits a synthetic finish, but the response is incomplete; retry the turn. |
| Images are ignored by the model | The selected model is not vision-capable. Mark it with "attachment": true and modalities.input: ["text","image"] in models, and pick a vision model id. |
| Upstream 400 about tool calls | A tool-call or tool-result without a matching pair slipped through. Pairing is enforced in src/transform.ts; report a repro if it still occurs. |
| Need to see what the provider sends/receives | Set COMMANDCODE_DEBUG=1 and read the appended log file (see Debug tracing). |
tool_choice seemingly ignored |
Expected for required; upstream cannot force a call. none and named-tool are emulated via the tool list. |
| Config change had no effect | opencode reads config once at startup. Restart it. |
Security
- Prefer
{env:COMMANDCODE_API_KEY}(or opencode's/connectcredential store) over an inline key inopencode.json. - Upstream error bodies can echo credentials.
src/redact.tsscrubsBearertokens,user_/cc_keys,sk-…-style keys, JWTs, andkey=valuesecrets from any error surfaced to the client. Do not add logging of raw request/response bodies without passing them throughredact(). - Never commit real keys.
dist/,node_modules/,dump/, and*.logare gitignored.