218 lines
10 KiB
Markdown
218 lines
10 KiB
Markdown
# opencode-commandcode-provider
|
|
|
|
A native [AI SDK](https://ai-sdk.dev/) provider that connects [opencode](https://opencode.ai)
|
|
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.
|
|
|
|
```powershell
|
|
# 1. build the provider
|
|
npm install
|
|
npm run build
|
|
```
|
|
|
|
```jsonc
|
|
// 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:
|
|
|
|
```powershell
|
|
$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. List every model you want selectable; the provider itself only needs `languageModel(id)`,
|
|
which it implements for any id passed to it.
|
|
|
|
## 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:
|
|
|
|
```jsonc
|
|
// 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.
|
|
```
|
|
|
|
### 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
|
|
|
|
```powershell
|
|
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
|
|
```
|
|
|
|
`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:
|
|
|
|
```powershell
|
|
node scripts/smoke.mjs deepseek/deepseek-v4.1-flash
|
|
```
|
|
|
|
### Verifying inside opencode
|
|
|
|
```powershell
|
|
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.
|