Go to file
2026-09-10 21:25:25 +07:00
scripts Add sync-models script, server.py reference proxy, and doc updates 2026-09-10 21:25:25 +07:00
src Add native AI SDK provider for CommandCode /alpha/generate 2026-09-10 20:32:40 +07:00
.gitignore Add native AI SDK provider for CommandCode /alpha/generate 2026-09-10 20:32:40 +07:00
AGENTS.md Add sync-models script, server.py reference proxy, and doc updates 2026-09-10 21:25:25 +07:00
package-lock.json Add native AI SDK provider for CommandCode /alpha/generate 2026-09-10 20:32:40 +07:00
package.json Add sync-models script, server.py reference proxy, and doc updates 2026-09-10 21:25:25 +07:00
README.md Add sync-models script, server.py reference proxy, and doc updates 2026-09-10 21:25:25 +07:00
server.py Add sync-models script, server.py reference proxy, and doc updates 2026-09-10 21:25:25 +07:00
tsconfig.json Add native AI SDK provider for CommandCode /alpha/generate 2026-09-10 20:32:40 +07:00

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/generate endpoint.

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-flash-vision-exp": {
          "name": "DeepSeek V4 Flash (Vision)",
          "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.context from its context_length, limit.output from DEFAULT_MAX_TOKENS, and reasoning: true.
  • Vision (attachment + modalities.input with image) 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 reply NO_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.

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
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

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/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

  1. opencode calls provider.languageModel(id), then model.doStream(options).
  2. transform() converts the AI SDK prompt into the CommandCode envelope:
    • system text is joined into params.system.
    • user/assistant/tool messages become params.messages content parts.
    • assistant tool-call parts and tool results 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 }.
    • stream is forced to true — the endpoint answers stream:false with "Proxy use detected. This endpoint only serves CLI."
  3. model.ts POSTs the envelope with the CommandCode headers and retries 429/5xx.
  4. events.ts parses the NDJSON body (tolerating data: prefixes and [DONE]).
  5. model.ts maps each upstream event to a LanguageModelV3StreamPart and always terminates with a finish part.
  6. doGenerate drains the same stream and assembles a LanguageModelV3GenerateResult.

Development

npm install        # installs typescript, @types/node, @ai-sdk/provider
npm run typecheck  # tsc --noEmit
npm run build      # tsc -> dist/
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-flash-vision-exp
opencode run "Use the glob tool to list *.mjs and report the filenames." -m commandcode/deepseek/deepseek-v4-flash-vision-exp

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.
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 /connect credential store) over an inline key in opencode.json.
  • Upstream error bodies can echo credentials. src/redact.ts scrubs Bearer tokens, user_/cc_ keys, sk-…-style keys, JWTs, and key=value secrets from any error surfaced to the client. Do not add logging of raw request/response bodies without passing them through redact().
  • Never commit real keys. dist/, node_modules/, dump/, and *.log are gitignored.