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:
CliAgentAdapterviamakeOpenCodeAdapter(idsopencodeand build workeropencode-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-opencodeskill). Updated 2026-07-23 (T647): assessed the v1.18.2 nested-subagent-default-off flip (subagents no longer launch their own nested subagents unlesssubagent_depthis 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), sosubagent_depthitself could not be live-verified against a real v1.18.2+ binary here;opencode run --helpon 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 touchopencode run --dangerously-skip-permissionsor 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 andassertKnownOpenCodeConfigKeysinsrc/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 viaopencode upgrade [target], confirmed inopencode --help's command list) — it is NOT a brew or npm package. Whoever performs the eventual upgrade should use that same channel (opencode upgradeor the official install script) rather thanbrew 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/releasesshows onlyv1.18.xtags (v2 has not been cut as a GitHub release); npmopencode-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/blog404s. v2 ships from a DIFFERENT npm package,@opencode-ai/clion thebetadist-tag, is documented on the/v2path (https://opencode.ai/v2/docs), and installs asopencode2side by side with v1: "OpenCode 1 and OpenCode 2 can be installed side by side. V1 runs asopencode, while V2 installs and runs separately asopencode2." 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. Thetui-v2dist-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 resumabletask_id", "Surface resumable subagent failures instead of returning an empty result", plus three retry bullets (finish_reason: network_error,network-error/network_errorvariants, xAI capacity) and a Cerebrasmax_completion_tokensfix. v1.18.21 — "Continue responses when a model reports an unknown finish reason instead of stopping early", Vertex AIeu/usREP-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.jsonexposes exactly 36 top-level properties andOPENCODE_CONFIG_KNOWN_TOP_LEVEL_KEYSinsrc/adapters/CliAgentAdapter.tsholds 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$idor product-version field (its$schemanames 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 id | State |
|---|---|
deepseek/deepseek-v4-flash | OK — own DeepSeek balance |
xiaomi/mimo-v2.5 | OK — own Xiaomi balance |
minimax/MiniMax-M2.5 | OK — own MiniMax balance |
minimax/MiniMax-M3 | OK — own MiniMax balance |
opencode/*-free | still 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.5works;minimax/minimax-m2.5fails with an opaque error. This produced a false "MiniMax is dead" reading in the orchestrator. Only the PROVIDER half may be safely lowercased (opencodeModelProvider()insrc/core/cliAvailability.tslowercases exactly that half and nothing else); never normalize the model half. - the catalog still lists ids DeepSeek removed.
deepseek/deepseek-chatanddeepseek/deepseek-reasonerwere removed from DeepSeek's API on 2026-07-24 but remain in opencode's catalog, so either id looks like a valid route inopencode modelsand yields a provider error at call time. Cyberine has an independent guard for the raw-API side of this (findLegacyDeepSeekModelPins()insrc/core/config.ts, surfaced bycyberine doctor); the opencode route is not covered by it, because the id arrives viaCYBERINE_OPENCODE_DEEPSEEK_MODEL/OPENCODE_MODELrather thanDEEPSEEK_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:
- opencode holds no
moonshotaicredential.MOONSHOT_API_KEYin 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. - The Moonshot account behind that key is separately suspended for insufficient balance — measured 2026-08-22 as an
errorevent withstatusCode: 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:
| Situation | Verdict |
|---|---|
env fallback var set, or auth.json has the provider | usable |
auth.json readable, provider recognised, absent from it | NOT usable — worker skipped, doctor [FAIL] |
auth.json absent/malformed | usable (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.tsdeclared ago-limitwindow (1M tokens/day) for BOTHopencode-deepseekandopencode-kimi. Removed. They were already inert for gating (capacity.tsonly reacheswindowHeadroom()forcostModel: 'subscription', and every opencode profile is'metered') but NOT inert for display:listWindows()enumerates the defs unconditionally, socyberine billingprinted 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 anopencode-*profile declaring'subscription', and none does. Kept as a hook (a plugin worker could), annotated as not-a-live-path.src/store/creditLedger.tsmapsopencode-deepseek/opencode-kimito''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. Mappingopencode-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.jsonviaopencode providers login. - Fallback: provider env vars (
DEEPSEEK_API_KEY,MOONSHOT_API_KEY, …). - Do NOT put
apiKey: "{env:VAR}"inopencode.json— if the var is unset, it resolves to "" and overridesauth.json→ "authentication failed". Manage creds only viaopencode 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
| Item | Purpose |
|---|---|
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|list | Credential management. |
opencode debug config|paths|skill|agent <n> | Introspection. |
--model / -m <provider/model> | Always provider/model form. |
--variant high|max|minimal | reasoning_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|build | plan = read-only; build = full write. Default build. |
--dir <path> | Required for CC-driven runs — pins the tree (else uses shell CWD, maybe ~/.claude/). |
--format json | JSONL events (cannot combine with -i). |
--dangerously-skip-permissions | REMOVED 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. |
--auto | The 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 id | Notes |
|---|---|
deepseek/deepseek-v4-pro | Cyberine 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-flash | Cheaper, no Think. Live-probed OK 2026-08-25 (own DeepSeek balance). |
moonshotai/kimi-k2.6 | 256K ctx. Declares variants: {} — --variant is a no-op (single tier). |
moonshotai/kimi-k2.7-code | Cyberine 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-k3 | Declares 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-5 | Confirmed in docs (API-key auth recommended; Pro/Max prohibited with 3rd-party tools). |
minimax/MiniMax-M2.5, minimax/MiniMax-M3 | Live-probed OK 2026-08-25 (own MiniMax balance). CASE-SENSITIVE — minimax/minimax-m2.5 fails with an opaque error. |
xiaomi/mimo-v2.5 | Live-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 bypart.idso 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,errorevents.
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 fromopencode run --help, replaced by--auto. opencode's argv parser (yargs, not in strict mode) does not reject unknown options, soopencode 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 meansdescribeUnknownCliFlagnever fires andCLI_FLAG_COMPATcan 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.jsonpresent, 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 .opencodeat allOK, 3/3 and 4/4, ~4-6s .opencode/holdingnode_modules+package.json+package-lock.json(copied with shellcp -Rfrom 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 fullopenCodeTimeoutMs(30 min default). With--print-logs --log-level DEBUGthe 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.
lsofon a hung process shows four sockets inSYN_SENTto[2606:4700::6810:*]:443, whichdig +short AAAA registry.npmjs.orgconfirms 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 whilecurl -4returns 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/pluginfor 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-levelopencode.json/opencode.jsonc/.opencode.jsonis 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@localinstalls 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.bashall pass once the directory form is gone), the target being a git repo (reproduced in agit initscratch dir AND a real worktree), the provider (DeepSeek balance $13.17, a directchat/completionsanswers in 1.4s while opencode hangs), the 3.6 GB~/.local/share/opencode/opencode.db(a freshOPENCODE_DBreproduces it), and--pure— despite its "run without external plugins" description it does NOT avoid the probe (verified STALL with--pureon the hanging shape; do not use it as a guard).FIXED 2026-08-22: the T493
setuphook now writes a FLAT<cwd>/opencode.jsoninstead 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()runsgit add -Abeforegit 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.gitignoreline only covers repos we own andtask.cwdcan be any repo, hence the local-exclude route (untracked, never committed, shared across that repo's worktrees). Locked byselftest:opencode-config-guard, which drives the real setup hook in a throwawaygit initrepo 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 byscripts/km8aw-config-guard-smoke.ts(openCodeWorkerConfigPathmust 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.jsoncan 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, checklsof -p <pid>forSYN_SENTto an IPv6 npm address before assuming anything else.opencode upgrade 1.17.10remains 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-permissionsdid 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 untilopenCodeTimeoutMs(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 opencodebuildsession invokes its owntasktool 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.parseOpenCodeStdoutignores 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.
parseOpenCodeStdoutbuilds the Result (output + usage) and owns theSCHEMA DRIFTdetector;mapEventToProgressfeeds 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:readOpenCodeStepUsageis the single reader ofpart.tokens.input/part.tokens.output/part.cost, andselftest:opencode-parsedrives BOTH consumers over the same fixtures (plus a clauderesultand a codexturn.completedevent, 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 jsonstream is UNVERSIONED, and unrecognised events used to vanish. As of 2026-08-22parseOpenCodeStdout(a) readserrorevents instead of swallowing them — opencode documentserroras 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 saysSCHEMA DRIFT, names the unrecognised type values (or the envelope's own top-level keys when thetypefield itself is gone) and points back at this page. Before that, an upstream envelope rename degraded tostatus: 'success',usage: undefinedand a generic "0 tool calls across 0 steps" summary — a build worker could write real diffs and report nothing. Locked byscripts/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--variantmakes OpenCode inject extra skill/rules context and answers go off-topic. Live A/B/C on 1.18.21 with prompt,--dir,--agentand 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 — thestep_finishtotals matched exactly, and later arms took a full-prefix prompt-cache hit (cache.read27648 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--variantis a reasoning-effort knob only; it is not a context-hygiene requirement. Evidence:.local/research/kimi-variant-probe-20260822/. -
--variantis NOT validated against the model's variant map, and a model can declare none.opencode run --helpcalls the value "provider-specific reasoning effort, e.g., high, max, minimal" and takes any[string];opencode models <provider> --verboseis the authority on what a given model actually accepts, via itsvariantsobject. Measured 2026-08-22:--variant bogus-nonexistentis 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 avariants: {}model buys nothing and hides typos —opencode-kimitherefore passes none, with the probe cited insrc/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. -
moonshotaiis not a configured credential here — the provider resolves ONLY fromMOONSHOT_API_KEYin the environment.opencode providers list/opencode auth listshows just DeepSeek, MiniMax and Xiaomi in~/.local/share/opencode/auth.json(re-confirmed 2026-08-25), andopencode models moonshotaifails withProvider not found: moonshotaion a bare shell; exportingMOONSHOT_API_KEYmakes the provider and all 10 kimi models appear. Cyberine's env scoping already allowlists exactly that var for themoonshotai/prefix (credentialAllowlistForCli), so the worker inherits it — but any manual reproduction of anopencode-kimirun 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 whyopencode-kimiis gated off before spawn — see Dead worker:opencode-kimi. Note also thatauth listoutput differs per machine: the homelab box carries amoonshotaicredential the Mac does not, so this worker's health is genuinely host-dependent and must be read fromcyberine doctoron the host in question, never assumed. -
A suspended/out-of-balance provider account surfaces as an
errorevent withstatusCode: 429andisRetryable: true— and it is NOT retryable. Measured 2026-08-22:moonshotaireturned "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.isOpenCodeTransientErrorcorrectly does not match it, so Cyberine does not add its own retries on top — do not "fix" that by trusting the upstreamisRetryableflag. -
CLI_FLAG_COMPAThas noopencodesection (onlyclaudeandcodex), 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-onlyplan) registered when on PATH and opencode can auth the provider behindOPENCODE_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 workersopencode-deepseek/opencode-kimiare opt-in viaCYBERINE_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(baseOpenCodeArgsinsrc/adapters/CliAgentAdapter.ts), the host sandbox wraps the spawn (hostSandbox.ts+cliConfigDirs.tsallow-lists the four XDG dirs), and the T493setuphook writes a per-worker<cwd>/.opencode/opencode.jsonso 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
- https://opencode.ai/docs/cli/
- https://opencode.ai/docs/agents/
- https://opencode.ai/docs/config/
- https://opencode.ai/docs/providers/
- https://github.com/anomalyco/opencode (run.ts; repo moved orgs from
sst/opencode, which now 404s) ~/.claude/skills/nf-opencode/(SKILL.md + references)
Google Gemini CLI (headless)
Default model: gemini-2.5-flash (CYBERINEGEMINIMODEL / GEMINIMODEL). Auth dir allow-listed by the sandbox: ~/.gemini.
OpenCode 2.0 beta (opencode2) — integrated as opencode2-deepseek, currently retired
v2 is unreachable from every surface CHANGELOG-WATCH.md's OpenCode row polls, which is the whole reason a user report ("opencode ra version 2 roi") beat the sweep to it: