Skip to content

Latest commit

 

History

History
290 lines (209 loc) · 10.9 KB

TROUBLESHOOTING.md

File metadata and controls

290 lines (209 loc) · 10.9 KB

Troubleshooting

English | Espanol

No Command Code models in the selector

  1. Check the catalog cache was written:

    ls -la "${COMMANDCODE_STATE_DIR:-$HOME/.local/state/opencode}/commandcode-models-cache.json"
  2. 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

    200 means the plugin should populate the cache on the next start.

  3. Models you listed manually in opencode.json are unaffected: they remain registered even when discovery is skipped. A cache miss is not an error.

  4. Claude models are deliberately absent. They only expose /messages and answer 400 unsupported_model on the OpenAI-compatible endpoint. See below.

HTTP 400 unsupported_model after selecting a Claude model

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.

HTTP 400 invalid request error

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 fixes array 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.

HTTP 403 permission_error for a free model

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": *"[^"]*"' | sort

If a model listed there still fails, the promo ended between your request and the catalog refresh. Reselect and continue.

ECONNRESET retries

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

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

Which model is failing?

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

A turn hangs with a tool stuck in streaming

The watchdog aborts a silent SSE stream after 60s:

cat "${COMMANDCODE_STATE_DIR:-$HOME/.local/state/opencode}/commandcode-watchdog.log"
  • A watchdog.timeout entry 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.

HTTP 402 insufficient credits

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.

MODEL_NOT_IN_PLAN

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.

One key is dead and the others still work

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.

Zero data retention does not appear to be on

ZDR is an explicit opt-in, the same one the Command Code CLI reads:

export CMD_ZDR=1

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

Key not found

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.

Quota output is empty or partial

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.

Duplicate tool registration

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.

Logs are not being written

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.

Plugin loaded twice, or a removed plugin still runs

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

Verify with:

opencode plugin list

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