English | Espanol
-
Check the catalog cache was written:
ls -la "${COMMANDCODE_STATE_DIR:-$HOME/.local/state/opencode}/commandcode-models-cache.json" -
If the file is missing, the plugin never reached the API. Verify the key resolves and the endpoint answers:
curl -s -o /dev/null -w '%{http_code}\n' \ -H "Authorization: Bearer $(cat ~/.config/commandcode/key.txt)" \ -H 'User-Agent: cli' \ https://api.commandcode.ai/provider/v1/models
200means the plugin should populate the cache on the next start. -
Models you listed manually in
opencode.jsonare unaffected: they remain registered even when discovery is skipped. A cache miss is not an error. -
Claude models are deliberately absent. They only expose
/messagesand answer400 unsupported_modelon the OpenAI-compatible endpoint. See below.
The claude-* family on Command Code is Anthropic-shaped:
{"error":{"message":"Model \"claude-sonnet-5\" must be called via /provider/v1/messages (Anthropic Messages shape).","type":"invalid_request_error","code":"unsupported_model"}}The provider is registered as @opencode/ai/providers/openai-compatible, which
speaks /chat/completions. Discovery skips these models for that reason. If you
see the error anyway, the model was added manually to opencode.json; remove it
there or route it through an Anthropic-shaped provider.
This is the generic rejection the sanitizer exists for. Check whether it was a payload problem or something else:
tail -20 "${COMMANDCODE_STATE_DIR:-$HOME/.local/state/opencode}/commandcode-sanitizer.log"- Entries present around the failure - the sanitizer repaired the payload and
the request was rejected for a different reason. The
fixesarray lists exactly what was rewritten; a rejection after a repair usually means the upstream error is unrelated to tool-call structure. - No entries at all - the request was structurally clean as far as the sanitizer is concerned. Common causes: a model that does not exist any more (see the retired-tier section), an unsupported field in the body, or a request the plugin never saw because another plugin rebuilt it first.
The sanitizer is fail-open: if it throws, the original request is sent unchanged, so a missing log line can also mean the repair path errored. Enable nothing extra for that - the design intentionally swallows hook errors.
Free tiers are retired without notice and the id keeps resolving:
{"error":{"message":"The free Ling 3.1 Flash tier has been retired. Ling 3.1 Flash is now a paid model ...","type":"permission_error","code":"FORBIDDEN"}}The API then exposes the paid id (for example inclusionai/ling-3.1-flash), and
discovery picks it up on the next start. Fix a stale entry in opencode.json by
aligning it with /provider/v1/models:
curl -s -H "Authorization: Bearer $(cat ~/.config/commandcode/key.txt)" \
-H 'User-Agent: cli' https://api.commandcode.ai/provider/v1/models \
| grep -o '"id": *"[^"]*"' | sortIf a model listed there still fails, the promo ended between your request and the catalog refresh. Reselect and continue.
The retry log is a forensic trail, not a failure counter:
grep -c . "${COMMANDCODE_STATE_DIR:-$HOME/.local/state/opencode}/commandcode-retries.log"
grep -o '"type":"[^"]*"' "${COMMANDCODE_STATE_DIR:-$HOME/.local/state/opencode}/commandcode-retries.log" \
| sort | uniq -c | sort -rnWhat the entries mean:
type |
Meaning |
|---|---|
provider.transport (ECONNRESET) |
stale or dropped socket; retried natively |
provider.internal (503) |
upstream overload; retried natively |
provider.invalid-output |
stream ended without finish_reason |
provider.invalid-request |
HTTP 400 from the provider |
OpenCode retries transient transport failures itself (10 attempts, 2s to 10s
backoff), so a recovered burst never surfaces to the user. The Connection: close
injection removes one specific cause (Bun reusing a socket Cloudflare already
closed); it does not eliminate upstream-side resets during an outage. Measured on
a production host: 99 of 130 logged events were transport resets spread over ten
days, concentrated in short upstream bursts, with zero unrecovered sessions.
To measure the injection's effect, disable it and compare:
export CC_NO_CONNECTION_CLOSE=1 # keep-alive reuse restoredSince v1.0.2 the retry log records the model and the attempt, so resets can be attributed to a specific model instead of a session-wide blob:
# resets per model
grep -o '"modelID":"[^"]*"' <state>/commandcode-retries.log | sort | uniq -c | sort -rn
# the failing attempt and the backoff delay the runtime chose
grep '"modelID"' <state>/commandcode-retries.log | tail -5| Field | Meaning |
|---|---|
modelID / providerID |
Model and provider of the failed request |
attempt |
Retry attempt number (1 is the original request) |
delayMs |
Backoff delay the runtime chose before the next attempt |
agent |
Agent that issued the request |
Note that the runtime's own backoff is 2s to 10s. Gaps much larger than delayMs between
two log lines mean the failed request itself took that long before the connection broke:
that is an upstream timeout, not a stale keep-alive socket.
The watchdog aborts a silent SSE stream after 60s:
cat "${COMMANDCODE_STATE_DIR:-$HOME/.local/state/opencode}/commandcode-watchdog.log"- A
watchdog.timeoutentry exists - the abort fired as designed and the turn failed fast instead of freezing. - No entry and no file - the freeze happened before the first chunk, which the watchdog deliberately does not police. That window belongs to OpenCode's provider timeout. Verify the request actually reached the network, then raise that timeout or retry.
The account behind the key has no balance, so Command Code rejects every request before routing it, including paid models. The plugin appends a hint to the message and, when a key pool is configured, records a 24h cooldown for that key.
Actions: top up the account at commandcode.ai, or add a second key to the pool
(COMMANDCODE_API_KEYS, COMMANDCODE_KEYS_FILE or
~/.config/commandcode/keys.txt). See
CONFIGURATION.
The model exists but the plan does not include it (for example Claude models on
an INDIVIDUAL-GOAT plan, or GPT-5.5 on Goat). The plugin appends the hint
"pick another model or upgrade the plan" and leaves the status untouched.
Check the current list at commandcode.ai/models before upgrading.
Configure a pool of two or more keys; rotation is opt-in and additive:
| Status | Cooldown recorded |
|---|---|
402 |
24h |
401 / 403 |
24h |
429 |
Retry-After up to 6h, else 5m |
5xx |
none |
Inspect <state>/commandcode-keys-state.json for the cooldown state and
<state>/commandcode-rotation.log for the timeline. Delete the state file to
clear every cooldown immediately.
With a single key the plugin keeps the old behaviour and never overrides an
Authorization header the host already set.
ZDR is an explicit opt-in, the same one the Command Code CLI reads:
export CMD_ZDR=1Any other value (including true) leaves it off, and a caller-supplied
x-cmd-zdr header is stripped so a session cannot enable it by accident. The
header is only sent to the strict Command Code host.
The quota tool reports Error consultando Command Code: No se encontro .... Check
which store the resolver sees:
bun -e 'import { resolveApiKeyWithSource } from "./src/auth/resolver.ts";
console.log(resolveApiKeyWithSource().source);'none means all six stores came up empty. The simplest fix is the environment
variable:
export COMMAND_CODE_API_KEY="your-key"Note the plugin still works without a key: sanitizer, watchdog and catalog cache all keep running, only credential injection is skipped.
Each field is fetched independently and a failure degrades to N/D rather than
killing the report. A report showing plan and windows but no cycle totals means
only /alpha/usage/summary failed. Retry; if the account was just created, the
summary endpoint can be empty for the first few minutes.
Symptom: OpenCode logs a duplicate plugin id or commandcode_quota appears twice.
Cause: a second plugin registering the same tool or hooks (for example the legacy
commandcode-quota plugin) is still listed in opencode.json. Remove the old
entry - running both means requests are sanitized twice and the second pass sees
an already-repaired payload.
The plugin is fail-open, so telemetry failures are silent. Verify the state directory is writable:
mkdir -p "${COMMANDCODE_STATE_DIR:-$HOME/.local/state/opencode}" && touch "$_/probe" && rm "$_/probe"An empty sanitizer log is the normal state: it only gets an entry when a payload actually needed repair.
OpenCode loads plugins from two places: the plugins array in opencode.json and
every directory under ~/.config/opencode/plugins/ (auto-discovery). That asymmetry
explains the two confusing cases:
| Symptom | Cause |
|---|---|
A plugin removed from plugins is still listed by opencode plugin list |
It lives in ~/.config/opencode/plugins/ and is picked up by the directory scan |
A plugin renamed to .disabled inside that directory still loads |
The scan takes any subdirectory, not only .ts files or the old name |
What works and what does not:
# does NOT unload it
# - removing the entry from opencode.json
# - renaming the directory inside plugins/
# - rewriting opencode.json to force a reload
# DOES unload it (the watcher drops it live)
mv ~/.config/opencode/plugins/your-plugin ~/somewhere-outside/your-pluginVerify with:
opencode plugin listIf the plugin is still listed after moving it out, the running service kept it in
memory: restart the background server (opencode service stop) and start OpenCode
again. Note that opencode service restart alone also works, but it drops the current
TUI connection.
Two plugins registering commandcode_quota is not fatal - the sanitizer is idempotent,
so a second pass returns modified: false, and the tool is exposed once - but it doubles
the hook work and makes the telemetry confusing, so keep a single copy installed.