cyberine
Providers

OpenCode CLI (anomalyco/opencode, headless)

  • Kind: headless coding agent (subprocess), multi-provider (75+ providers via one binary)

  • Official docs: https://opencode.ai/docs · /cli · /agents · /config · /providers

  • Cyberine adapter: CliAgentAdapter via makeOpenCodeAdapter (ids opencode and build worker opencode-deepseek — both RETIRED since 2026-09-07, ks4sf; opencode-kimi — DEAD/self-skipping as of 2026-08-25)

  • Researched/verified: 2026-06-07 — confidence high (cross-validated against Cyberine source + nf-opencode skill). Updated 2026-07-23 (T647): assessed the v1.18.2 nested-subagent-default-off flip (subagents no longer launch their own nested subagents unless subagent_depth is set) against Cyberine's actual fan-out path — see "Gotchas" below. Conclusion: no impact, path is single-level by design, no config change made. Installed local binary is v1.17.10 (predates v1.18.2), so subagent_depth itself could not be live-verified against a real v1.18.2+ binary here; opencode run --help on the installed version confirms it is not a CLI flag (config-only, consistent with the release notes). Updated 2026-08-21 (km8aw): R02 sweep found 16 releases since the v1.18.3 baseline (v1.18.4 through v1.18.19). None touch opencode run --dangerously-skip-permissions or any other flag on Cyberine's headless invocation path. Two informational fixes on the providers Cyberine routes to, no adapter change needed: v1.18.17 "Applied the correct sampling defaults to DeepSeek V4 Flash" and v1.18.18 "Select the Kimi system prompt correctly for official Moonshot and Kimi providers" (both are upstream-only bug fixes, not a request/response shape or model-id change). One change that DOES need a Cyberine-side guard: v1.18.16 "Ignore unknown top-level config fields instead of failing config parsing" — see "Gotchas" below and assertKnownOpenCodeConfigKeys in src/adapters/CliAgentAdapter.ts. The binary UPGRADE itself is a separate, human-gated decision and out of scope for km8aw's non-upgrade half; the Mac's installed binary remains v1.17.10 as of this sweep. Install channel: this binary is managed through the standalone installer at ~/.opencode/bin/opencode (self-updating via opencode upgrade [target], confirmed in opencode --help's command list) — it is NOT a brew or npm package. Whoever performs the eventual upgrade should use that same channel (opencode upgrade or the official install script) rather than brew install opencode / npm i -g opencode, which would create a second, untracked install path alongside this one. Updated 2026-08-22 (cyber-06): OpenCode 2.0 EXISTS, as a public beta, and it is a SEPARATE BINARY — see opencode2-v2-beta.md for the v2 page. Recording the search failure here because it is the trap anyone re-checking this will fall into: v2 is invisible from every surface this doc's watch row uses. github.com/anomalyco/opencode/releases shows only v1.18.x tags (v2 has not been cut as a GitHub release); npm opencode-ai — the package this doc tracks — reports "latest": "1.18.21" and has no 2.x; opencode.ai's top nav does not link it; opencode.ai/blog 404s. v2 ships from a DIFFERENT npm package, @opencode-ai/cli on the beta dist-tag, is documented on the /v2 path (https://opencode.ai/v2/docs), and installs as opencode2 side by side with v1: "OpenCode 1 and OpenCode 2 can be installed side by side. V1 runs as opencode, while V2 installs and runs separately as opencode2." An earlier revision of this entry asserted no v2 existed; that was wrong, and the reason is worth keeping — a release-tag-and-latest-dist-tag sweep cannot see a product that ships under a new package name on a pre-release tag. The tui-v2 dist-tag noted as a curiosity (0.0.0-tui-v2-202606261840) turns out to live on @opencode-ai/cli, i.e. it was a pointer AT v2 all along.

    v1 remains this page's subject and remains what Cyberine spawns. The v1 upgrade this doc tracked did happen the same day: the installed binary went 1.17.10 -> 1.18.21 via opencode upgrade (which reported "From 1.17.10 → 1.18.21" and "Using method: curl" — v1's own updater has no channel or target that reaches v2, so adopting v2 means installing the separate package).

    Same sweep, the two releases newer than the km8aw baseline (both published 2026-08-21): v1.18.20 — "Answer permission requests triggered by subagents during opencode run", "Surface failed subagent tool calls with a resumable task_id", "Surface resumable subagent failures instead of returning an empty result", plus three retry bullets (finish_reason: network_error, network-error/network_error variants, xAI capacity) and a Cerebras max_completion_tokens fix. v1.18.21 — "Continue responses when a model reports an unknown finish reason instead of stopping early", Vertex AI eu/us REP-endpoint routing, and two Desktop-only bugfixes. All three v1.18.20 subagent bullets are BUGFIXES, so the installed 1.17.10 is the version carrying those bugs: the permission-request one is a latent 30-minute stall we hold TODAY (written up under "Gotchas" below — upgrading removes it), and the two "surface … subagent failure" bullets mean the new binary emits failure detail on a path that previously returned an empty result, i.e. a shape our JSONL parser has never seen.

    Config schema re-verified the same day: https://opencode.ai/config.json exposes exactly 36 top-level properties and OPENCODE_CONFIG_KNOWN_TOP_LEVEL_KEYS in src/adapters/CliAgentAdapter.ts holds exactly the same 36 — set difference empty in both directions, so the km8aw guard needs no refresh and the upgrade carries no config-guard risk. The document has no $id or product-version field (its $schema names JSON-Schema draft 2020-12, not an OpenCode schema version), so there is no schema-major-version signal to watch.

    Cumulative headless-surface diff 1.17.10 -> 1.18.21 (32 releases, full bodies read): no release note mentions --dangerously-skip-permissions, --format json, any of --model/--variant/--agent/--dir/-f/-c/-s/--fork/--attach, the credential/config paths, the three model ids Cyberine routes to, or the known SQLite WAL-lock crash. That is SILENCE, not a stability guarantee — the stream is unversioned, so a live parse probe on the new binary is still required before trusting it.

Billing surfaces: the opencode-branded tier is GONE, the per-provider routes are not

Verified 2026-08-25 (hl-t693). opencode has two independent billing surfaces, and only ONE of them died. Getting this wrong in either direction is expensive, so read both halves:

DEAD — opencode's own branded credit tier ("opencode-go"). The subscription lapsed and is not being renewed. opencode-go/* is gone from opencode models entirely (not deprecated-but-listed: absent), and a probe of opencode/big-pickle times out. Nothing routed through that tier can run.

ALIVE — separate auth per provider, on the user's own balance. opencode holds one credential per provider in auth.json, wholly unaffected by the branded tier lapsing. Live-probed working 2026-08-25:

Model idState
deepseek/deepseek-v4-flashOK — own DeepSeek balance
xiaomi/mimo-v2.5OK — own Xiaomi balance
minimax/MiniMax-M2.5OK — own MiniMax balance
minimax/MiniMax-M3OK — own MiniMax balance
opencode/*-freestill free (six ids, see below)

An earlier revision of the hl-t693 brief said "only free models remain usable". That was wrong and the correction is the point of this section: per-provider auth survives the branded tier's death, so opencode-deepseek — an own-balance DeepSeek route, correctly labelled costModel: 'metered' all along — kept working straight through.

What opencode is credentialled for

opencode auth list holds three own-balance credentials: DeepSeek, MiniMax, Xiaomi — and no Moonshot credential at all. That last absence is what kills opencode-kimi; see Dead worker: opencode-kimi below.

The six free models are UNPROVEN for build work

opencode/hy3-free, opencode/mimo-v2.5-free, opencode/muse-spark-1.2-contributor-free, opencode/nemotron-3-ultra-free, opencode/nemotron-3.5-lightning-free, opencode/x-preview-f-free.

Do not register a free-tier Cyberine worker on the assumption it behaves like the paid tier did. The free lineup has already churned once, and none of these has been through a Cyberine build dispatch. Prove one with a recorded dispatch (a run under .local/runs/ whose diff actually applies and typechecks) before adding a profile for it.

Two model-id traps

Both cost real debugging time; neither is visible from opencode models output alone.

  • ids are CASE-SENSITIVE. minimax/MiniMax-M2.5 works; minimax/minimax-m2.5 fails with an opaque error. This produced a false "MiniMax is dead" reading in the orchestrator. Only the PROVIDER half may be safely lowercased (opencodeModelProvider() in src/core/cliAvailability.ts lowercases exactly that half and nothing else); never normalize the model half.
  • the catalog still lists ids DeepSeek removed. deepseek/deepseek-chat and deepseek/deepseek-reasoner were removed from DeepSeek's API on 2026-07-24 but remain in opencode's catalog, so either id looks like a valid route in opencode models and yields a provider error at call time. Cyberine has an independent guard for the raw-API side of this (findLegacyDeepSeekModelPins() in src/core/config.ts, surfaced by cyberine doctor); the opencode route is not covered by it, because the id arrives via CYBERINE_OPENCODE_DEEPSEEK_MODEL / OPENCODE_MODEL rather than DEEPSEEK_MODEL.

Dead worker: opencode-kimi

Pinned to moonshotai/kimi-k2.7-code, and dead twice over — both halves must be fixed before it can complete one run:

  1. opencode holds no moonshotai credential. MOONSHOT_API_KEY in the environment is the documented fallback (and Cyberine's env scoping does allowlist it for this worker), so exporting a funded key is one of the two ways to revive it.
  2. The Moonshot account behind that key is separately suspended for insufficient balance — measured 2026-08-22 as an error event with statusCode: 429, isRetryable: true, and it is NOT retryable; opencode burns ~90s of its own upstream retries per run before exiting 1.

moonshotai/* was never on the branded tier — it is an own-balance route at $0.95/M in, $4/M out. Its profile's strengths array used to claim free-via-opencode; that was false and actively harmful, because the router scores candidates on those strings and the tag biased selection toward the one worker that cannot run. Removed 2026-08-25.

Resolution: registered, self-skipping, reason recorded — not de-registered. cliWorkerAvailable() in src/orchestrator/buildAdapters.ts now gates every opencode worker on checkOpencodeProviderCredential(<pinned model>), so opencode-kimi is filtered out before it is spawned: no fan-out wall-clock burned, no guaranteed-failed candidate in front of DiffJudge. The profile stays because it is the only place the router, the credit ledger, the usage windows and docs/eval/worker-profile-coverage.md learn the worker exists — and because it then revives on its own the moment either half above is fixed. cyberine doctor reports it as [FAIL] with the fix hint until then.

Cyberine's per-provider availability gate

"opencode is trusted when installed" is a statement about the BINARY and is useless as a per-worker verdict — opencode is a multi-provider router, so opencode-deepseek and opencode-kimi are the same trusted binary pointed at independently-authed providers. checkOpencodeProviderCredential(model) (src/core/cliAvailability.ts) answers the per-worker question, checking, in order: the provider's fallback env var (OPENCODE_PROVIDER_ENV_VARS, the same prefix map credentialAllowlistForCli() uses for spawn-time env scoping), then auth.json's provider keys.

It is fail-open on the two genuinely unknowable cases and only reports "no credential" when it positively knows the store was readable and holds nothing for a provider it recognises:

SituationVerdict
env fallback var set, or auth.json has the providerusable
auth.json readable, provider recognised, absent from itNOT usable — worker skipped, doctor [FAIL]
auth.json absent/malformedusable (unknown — never block on a bad read)
provider prefix unrecognised (custom gateway route)usable (unknown — never break a working custom route)

Note the gate's boundary: it catches "no credential at all", never "credential to a suspended account". The latter is only knowable from a real failure, and src/store/workerCooldown.ts records it after the first one.

Stale "subscription-billed" assumptions cleared out

The branded tier left fossils that outlived it. Audited 2026-08-25:

  • src/core/usage/usageWindows.ts declared a go-limit window (1M tokens/day) for BOTH opencode-deepseek and opencode-kimi. Removed. They were already inert for gating (capacity.ts only reaches windowHeadroom() for costModel: 'subscription', and every opencode profile is 'metered') but NOT inert for display: listWindows() enumerates the defs unconditionally, so cyberine billing printed a "go-limit 1,000,000 remaining" row for a tier that no longer exists.
  • src/core/usage/capacity.ts's 'go-limit' capacity kind is now unreachable for every built-in worker — it needs an opencode-* profile declaring 'subscription', and none does. Kept as a hook (a plugin worker could), annotated as not-a-live-path.
  • src/store/creditLedger.ts maps opencode-deepseek / opencode-kimi to '' under a "no metered credits" heading — a known gap, left as-is. Both are metered against a real provider balance, so '' makes their spend invisible to the ledger. Mapping opencode-deepseek -> 'deepseek' would fix the tracking but would also start gating a currently-working worker on the shared DeepSeek ledger entry, which is a spend-enforcement change rather than a labelling one — filed, not smuggled in.

If opencode's own tier is ever re-subscribed: re-add the window AND flip the profile's costModel to 'subscription' in the same change. One without the other is what created this mess.

Cyberine invocation

# What Cyberine passes today (written against <= v1.18.x, BROKEN on v1.18.21 — see below):
opencode run --model <provider/model> [--variant high|max|minimal] \
  [--agent plan|build] --dir <cwd> --format json \
  [--dangerously-skip-permissions] "<prompt>"

# v1.18.21: the permission flag was renamed. --dangerously-skip-permissions is silently ignored.
opencode run --model <provider/model> [--variant high|max|minimal] \
  [--agent plan|build] --dir <cwd> --format json [--auto] "<prompt>"

Default model deepseek/deepseek-v4-pro (OPENCODE_MODEL), variant high (OPENCODE_VARIANT), agent plan (config) / build (interactive launcher). Auth dir (XDG, sandbox-writable): ~/.local/share/opencode, ~/.config/opencode, ~/.cache/opencode, ~/.opencode.

Auth

  • Credentials in ~/.local/share/opencode/auth.json via opencode providers login.
  • Fallback: provider env vars (DEEPSEEK_API_KEY, MOONSHOT_API_KEY, …).
  • Do NOT put apiKey: "{env:VAR}" in opencode.json — if the var is unset, it resolves to "" and overrides auth.json → "authentication failed". Manage creds only via opencode providers login/logout.
  • Paths from opencode debug paths: config ~/.config/opencode/, data+sessions ~/.local/share/opencode/, cache ~/.cache/opencode/, state ~/.local/state/opencode/, legacy ~/.opencode/.

Commands & key flags

ItemPurpose
opencode run "msg"Headless one-shot (Cyberine path). -i adds interactive footer.
opencode serve / attach <url>Long-lived server; run --attach avoids MCP cold-boot.
opencode models [provider]List models (--verbose cost, --refresh).
opencode providers login|logout|listCredential management.
opencode debug config|paths|skill|agent <n>Introspection.
--model / -m <provider/model>Always provider/model form.
--variant high|max|minimalreasoning_effort, and ONLY for a model whose registry entry declares variants (see Models). Value is not validated — an unknown one is silently ignored. Omitting it does not change the context sent (measured 2026-08-22, see gotcha).
--agent plan|buildplan = read-only; build = full write. Default build.
--dir <path>Required for CC-driven runs — pins the tree (else uses shell CWD, maybe ~/.claude/).
--format jsonJSONL events (cannot combine with -i).
--dangerously-skip-permissionsREMOVED as of v1.18.21 (live-verified: absent from opencode run --help, which now lists --auto "auto-approve permissions that are not explicitly denied (dangerous!)"). Passing it does NOT error — opencode's yargs parser silently ignores unknown options and exits 0 — so Cyberine's permission bypass became a SILENT no-op on the upgraded binary. See the gotcha below.
--autoThe v1.18.21 replacement for the above: "auto-approve permissions that are not explicitly denied (dangerous!)" ([boolean] [default: false]).
--thinking, -f <file>, -c, -s <id>, --fork, --attach <url>Misc.

Models

Model idNotes
deepseek/deepseek-v4-proCyberine default. 1M ctx, 262K out. Declares variants: {high, max} → reasoning_effort; max = Think Max (billed separately) and measurably changes the prompt prefix (+13 tokens, prompt-cache MISS) where high does not.
deepseek/deepseek-v4-flashCheaper, no Think. Live-probed OK 2026-08-25 (own DeepSeek balance).
moonshotai/kimi-k2.6256K ctx. Declares variants: {} — --variant is a no-op (single tier).
moonshotai/kimi-k2.7-codeCyberine opencode-kimi worker's model. 262K ctx/out, reasoning: true (mandatory, no non-thinking mode), interleaved.field: reasoning_content. Declares variants: {} — --variant has nothing to map onto, so the worker passes none (live-probed 2026-08-22). $0.95/M in, $4/M out, $0.19/M cache read.
moonshotai/kimi-k3Declares variants: {low, high, max} — unlike every k2.x row above. A worker pinned to k3 SHOULD pass a variant.
anthropic/claude-sonnet-4-5, anthropic/claude-haiku-4-5Confirmed in docs (API-key auth recommended; Pro/Max prohibited with 3rd-party tools).
minimax/MiniMax-M2.5, minimax/MiniMax-M3Live-probed OK 2026-08-25 (own MiniMax balance). CASE-SENSITIVE — minimax/minimax-m2.5 fails with an opaque error.
xiaomi/mimo-v2.5Live-probed OK 2026-08-25 (own Xiaomi balance).
opencode/*-free (6 ids)Still free, but UNPROVEN for build work — prove with a recorded dispatch before registering a worker. See Billing surfaces.
opencode-go/*GONE 2026-08-25 — subscription lapsed, not being renewed; absent from opencode models entirely. opencode/big-pickle times out.
deepseek/deepseek-{chat,reasoner}REMOVED from DeepSeek's API 2026-07-24, yet still listed in opencode's catalog — either id looks valid in opencode models and errors at call time. Use v4-flash / v4-pro.

JSONL event schema (--format json)

  • {type:'text', part:{id, type:'text', text}} — assistant text. Keyed by part.id so streamed updates to the same part replace, not duplicate (Cyberine coalesces by id).
  • {type:'step_finish', part:{tokens:{input, output}, cost}} — final token counts + real cost.
  • Also reasoning, tool_use, step_start, error events.

Gotchas

  • v1.18.21 REMOVED --dangerously-skip-permissions, and removing it is SILENT. Live-verified on the installed 1.18.21 binary: the flag is absent from opencode run --help, replaced by --auto. opencode's argv parser (yargs, not in strict mode) does not reject unknown options, so opencode run ... --dangerously-skip-permissions "x" exits 0 with a normal event stream and no warning — the bypass simply stops applying. This is the worst possible shape for Cyberine: no unknown-flag stderr means describeUnknownCliFlag never fires and CLI_FLAG_COMPAT can never help. Any code path that assumes "the flag would have errored if unsupported" is wrong for opencode.

  • v1.18.21 HANGS, deterministically, when a project config exists in an unprovisioned directory — this breaks every Cyberine opencode worker. Live-verified 2026-08-22 on the installed 1.18.21. opencode run --model deepseek/deepseek-v4-pro --format json --agent build --dir <D> "say ok":

    <D> stateResult
    fresh dir, .opencode/opencode.json present, nothing elseSTALL — 6/6, then 6/6 again on new dirs; never recovers, even after 6 sequential attempts in the same dir
    fresh dir, no .opencode at allOK, 3/3 and 4/4, ~4-6s
    .opencode/ holding node_modules + package.json + package-lock.json (copied with shell cp -R from an already-provisioned .opencode) plus the configOK, 4/4, ~4-5s

    A stall is total: zero bytes on stdout AND stderr, no child process, no open socket, every thread parked in pthread_cond_wait, and it burns the full openCodeTimeoutMs (30 min default). With --print-logs --log-level DEBUG the last line is the config-loading block, then nothing.

    ROOT CAUSE — it is the npm registry over IPv6, and the trigger is the DIRECTORY, not the file. lsof on a hung process shows four sockets in SYN_SENT to [2606:4700::6810:*]:443, which dig +short AAAA registry.npmjs.org confirms is npm's address family (none of opencode.ai, api.opencode.ai, models.dev, unpkg, jsdelivr or github match that prefix). On this host IPv6 to that destination is blackholed — curl -6 https://registry.npmjs.org/ times out while curl -4 returns 200 in 0.24s — and opencode picks IPv6, never falls back to IPv4 and never gives up (a hung process kept opening NEW connections for 93s, so it is not one OS timeout that eventually clears). What triggers the probe is opencode provisioning @opencode-ai/plugin for a directory it treats as a project, and the signal it uses is the presence of a .opencode/ SUBDIRECTORY: an EMPTY .opencode/ with no file in it stalls identically. A flat root-level opencode.json / opencode.jsonc / .opencode.json is read normally and touches no network. This is a long-standing upstream defect, not new to 1.18.x — several open issues describe @opencode-ai/plugin@local installs hanging at startup, and opencode's own troubleshooting page says to clear the cache "if a plugin install is stuck".

    Ruled OUT by experiment: the config's CONTENTS (a bare {} reproduces it; conversely minimal / +autoupdate / +permission:{"*":"allow"} / +permission.bash all pass once the directory form is gone), the target being a git repo (reproduced in a git init scratch dir AND a real worktree), the provider (DeepSeek balance $13.17, a direct chat/completions answers in 1.4s while opencode hangs), the 3.6 GB ~/.local/share/opencode/opencode.db (a fresh OPENCODE_DB reproduces it), and --pure — despite its "run without external plugins" description it does NOT avoid the probe (verified STALL with --pure on the hanging shape; do not use it as a guard).

    FIXED 2026-08-22: the T493 setup hook now writes a FLAT <cwd>/opencode.json instead of <cwd>/.opencode/opencode.json, so a worker dir never gains a .opencode/ directory. It also registers that filename in the repo's LOCAL exclude ($GIT_COMMON_DIR/info/exclude), because the old target was covered by this repo's gitignored .opencode/ and the flat one is not: worktreeSandbox.captureDiff() runs git add -A before git diff --cached, so an untracked copy would be staged into EVERY worker's candidate diff and DiffJudge would apply Cyberine's own control file to the live tree. A tracked .gitignore line only covers repos we own and task.cwd can be any repo, hence the local-exclude route (untracked, never committed, shared across that repo's worktrees). Locked by selftest:opencode-config-guard, which drives the real setup hook in a throwaway git init repo and asserts the staged diff excludes it. Verified end-to-end in a real git worktree: 4/4 pass including the full Cyberine argv shape with a tool-using prompt that actually wrote its file. Locked by scripts/km8aw-config-guard-smoke.ts (openCodeWorkerConfigPath must stay flat). Note the trade-off: per the v2 config docs the .opencode/ form OVERRIDES the flat form regardless of proximity, so an ancestor repo carrying .opencode/opencode.json can now outrank the worker's own file — for worktrees inside THIS repo that ancestor is this repo's own config (same model, same all-allow permissions), but on a foreign target repo T493's guarantee is weaker than it was.

    Residual instability, recorded rather than smoothed over: one flat-config run streak in a freshly created worktree stalled 3/3 and then passed 4/4 minutes later with no change made. The .opencode/ directory trigger is reproducible (6/6, twice, plus the dir-only case); the flat form is reliably better but was not proven immune. If a worker ever stalls with zero output again, check lsof -p <pid> for SYN_SENT to an IPv6 npm address before assuming anything else. opencode upgrade 1.17.10 remains the rollback (opencode upgrade [target] takes a version), but the defect predates 1.18 so a rollback is not known to be clean either.

  • Subagent permission requests are a SEPARATE path from the top-level run's, and the binary installed here (1.17.10) is on the BUGGY side of it. v1.18.20's bugfix list reads "Answer permission requests triggered by subagents during opencode run" — i.e. before v1.18.20 such a request was NOT answered by the top-level run's permission handling, so --dangerously-skip-permissions did not cover it. Cyberine spawns opencode with stdin CLOSED (stdio: ['ignore', 'pipe', 'pipe'], src/adapters/CliAgentAdapter.ts), so on 1.17.10 a request that reaches the process can only stall until openCodeTimeoutMs (default 30 minutes) — a silent 30-minute hang with no exit code. Read the direction carefully: upgrading REMOVES this latent stall, it does not introduce it. What the upgrade does introduce is the new answering path itself, whose behaviour under closed stdin is unverified — so still probe with a deliberately subagent-spawning prompt, but understand the probe as validating a fix, not clearing a regression. Whether the stall is reachable today depends on whether an opencode build session invokes its own task tool at all; T647 established only that Cyberine never asks it to NEST subagents, which is a different question.

  • v1.18.20+ changed the subagent-FAILURE output shape ("Surface failed subagent tool calls with a resumable task_id", "Surface resumable subagent failures instead of returning an empty result"). Neither note documents the resulting JSON. parseOpenCodeStdout ignores any event type it does not know, so a new failure event would be dropped silently — the 2026-08-22 drift guard below is what turns that into a visible signal.

  • Cyberine reads this stream in TWO places, and only one of them can detect drift. parseOpenCodeStdout builds the Result (output + usage) and owns the SCHEMA DRIFT detector; mapEventToProgress feeds live TUI/streaming progress and is shared with the claude and codex CLIs, so it sees one event at a time and has no baseline to call drift on — by design it ignores what it does not recognise rather than throwing into a running job. As of 2026-08-22 (kqt9a) the two no longer each hardcode where the numbers live: readOpenCodeStepUsage is the single reader of part.tokens.input / part.tokens.output / part.cost, and selftest:opencode-parse drives BOTH consumers over the same fixtures (plus a claude result and a codex turn.completed event, so a future refactor cannot silently change what the other two CLIs report). A step_finish carrying no numbers now yields no usage at all rather than a Usage of three undefineds.

  • The --format json stream is UNVERSIONED, and unrecognised events used to vanish. As of 2026-08-22 parseOpenCodeStdout (a) reads error events instead of swallowing them — opencode documents error as a real event type, and a run that failed loudly upstream previously reached the judges as a bland "no final message" — and (b) counts recognised vs unrecognised event types: when stdout parses as JSON but NOT ONE event matches a known type, the output says SCHEMA DRIFT, names the unrecognised type values (or the envelope's own top-level keys when the type field itself is gone) and points back at this page. Before that, an upstream envelope rename degraded to status: 'success', usage: undefined and a generic "0 tool calls across 0 steps" summary — a build worker could write real diffs and report nothing. Locked by scripts/opencode-parse-smoke.ts (npm run selftest:opencode-parse).

  • The old "don't omit --variant" rule is RETIRED — it does not reproduce on 1.18.21. The 2026-05-25 claim was that omitting --variant makes OpenCode inject extra skill/rules context and answers go off-topic. Live A/B/C on 1.18.21 with prompt, --dir, --agent and model held fixed (deepseek/deepseek-v4-pro, the model the original claim came from): omitting it, --variant high, and a deliberately invalid value each sent an identical 27717-token context and each answered on-topic. Proven two independent ways — the step_finish totals matched exactly, and later arms took a full-prefix prompt-cache hit (cache.read 27648 of 27717), which is only possible if the prefixes are byte-identical. Arm order was reversed to rule out a cache artifact. The one real prompt delta is --variant max, which adds 13 tokens and misses the cache (Think Max is a different prefix, not "extra rules"). So --variant is a reasoning-effort knob only; it is not a context-hygiene requirement. Evidence: .local/research/kimi-variant-probe-20260822/.

  • --variant is NOT validated against the model's variant map, and a model can declare none. opencode run --help calls the value "provider-specific reasoning effort, e.g., high, max, minimal" and takes any [string]; opencode models <provider> --verbose is the authority on what a given model actually accepts, via its variants object. Measured 2026-08-22: --variant bogus-nonexistent is silently accepted and the run proceeds normally, on both a model that declares variants (deepseek-v4-pro) and one that declares {} (kimi-k2.7-code). Consequence for Cyberine: passing a variant "for symmetry" to a variants: {} model buys nothing and hides typos — opencode-kimi therefore passes none, with the probe cited in src/orchestrator/buildAdapters.ts. Check the registry before wiring a variant for any new opencode worker; moonshotai/kimi-k3 (low/high/max) shows k2.x's empty map is per-model, not per-provider.

  • moonshotai is not a configured credential here — the provider resolves ONLY from MOONSHOT_API_KEY in the environment. opencode providers list / opencode auth list shows just DeepSeek, MiniMax and Xiaomi in ~/.local/share/opencode/auth.json (re-confirmed 2026-08-25), and opencode models moonshotai fails with Provider not found: moonshotai on a bare shell; exporting MOONSHOT_API_KEY makes the provider and all 10 kimi models appear. Cyberine's env scoping already allowlists exactly that var for the moonshotai/ prefix (credentialAllowlistForCli), so the worker inherits it — but any manual reproduction of an opencode-kimi run needs the key exported first, or the failure looks like a missing model rather than a missing credential. As of 2026-08-25 this is no longer only a reproduction footgun: it is why opencode-kimi is gated off before spawn — see Dead worker: opencode-kimi. Note also that auth list output differs per machine: the homelab box carries a moonshotai credential the Mac does not, so this worker's health is genuinely host-dependent and must be read from cyberine doctor on the host in question, never assumed.

  • A suspended/out-of-balance provider account surfaces as an error event with statusCode: 429 and isRetryable: true — and it is NOT retryable. Measured 2026-08-22: moonshotai returned "Your account ... is suspended due to insufficient balance" for every arm. opencode itself retried the stream 3x per run (~90s wall clock each) before exiting 1. isOpenCodeTransientError correctly does not match it, so Cyberine does not add its own retries on top — do not "fix" that by trusting the upstream isRetryable flag.

  • CLI_FLAG_COMPAT has no opencode section (only claude and codex), so an unknown-flag rejection on an opencode spawn gets the generic diagnosis. As of 2026-08-22 that generic message is at least direction-neutral (it names both "binary predates the flag" and "upstream removed/renamed it" and links this page) rather than always telling the operator to upgrade — which would be actively wrong for a flag upstream deleted.

Pricing

No unified page — depends on the routed provider. DeepSeek V4 Pro: https://platform.deepseek.com/pricing (Think Max reasoning billed separately). Kimi K2.6: https://platform.kimi.ai. Local/Ollama: free.

Cyberine integration notes

  • opencode (read-only plan) registered when on PATH and opencode can auth the provider behind OPENCODE_MODEL — "trusted-if-installed" alone was retired 2026-08-25 (hl-t693) as meaningless for a multi-provider router; see Cyberine's per-provider availability gate. Build workers opencode-deepseek / opencode-kimi are opt-in via CYBERINE_BUILD_FLEET (--agent build) and pass the same gate.
  • This single adapter reaches any provider OpenCode is authed for — the agent-level provider-independence story. Backed by DeepSeek (deepseek.md) and Kimi (moonshot-kimi.md) in Cyberine.
  • Guards enforced in code: always --dir (baseOpenCodeArgs in src/adapters/CliAgentAdapter.ts), the host sandbox wraps the spawn (hostSandbox.ts + cliConfigDirs.ts allow-lists the four XDG dirs), and the T493 setup hook writes a per-worker <cwd>/.opencode/opencode.json so an ancestor config in the target repo cannot override the intended model/permissions. Correction 2026-08-22: earlier revisions of this line claimed a "deny floor" and pointed at .claude/rules/opencode-worker-permissions.md — that rule file has never existed in this repo, and the generated per-worker config is all-allow ('*': 'allow', plus explicit allows for read/webfetch/edit/write/glob/grep/bash), so no Cyberine-side deny floor applies to an opencode worker. Whether opencode's own global config (~/.config/opencode) still denies anything under a project-level '*': 'allow' is UNVERIFIED — do not rely on it as a guard.

Sources