OpenCode provider (run sessions in OpenCode's harness)
Run Claude-Code-style sessions through OpenCode, a separate coding-agent CLI that talks to whatever model you point it at. Set up with the binary plus your own provider keys; there is no dedicated sidebar project for it, so an OpenCode session lives inside a real project like any other.
What it is
Omniscio can spawn Claude-Code-style sessions backed by the OpenCode CLI (opencode) as an alternative provider, alongside claude (the default), codex, gemini, antigravity, the Anthropic-compatible deepseek / kimi / glm / minimax / meta, cursor, hermes, grok, pi, and openclaw (the registry defines 26 session-provider keys, 20 of them user-selectable/pickable; the non-pickable ones include the managed-PTY terminal engine and the internal-only openclaw). OpenCode is a model-agnostic local CLI harness: by default it runs Claude (Sonnet) through an Anthropic key, but it can also run any hosted model OpenCode supports (Kimi, GPT, DeepSeek, …) via the OpenCode model picker — Omniscio sends the chosen model to the shared server as the {providerID, id} selector on POST /session and routes the matching provider credential by the model-id prefix. So an OpenCode session is "an agent harness you can point at any model"; left on the default it's "Claude, driven by a different harness." Model routing is locked by provider-registry-contract.md.
Where to find it
Settings → Accounts — the OpenCode section stays hidden until you flip Show alternative AI providers. Once revealed it appears as a provider in the normal session-creation pickers.
How it behaves
What the user sees
An OpenCode session looks almost identical in the sidebar and main pane to a Claude session — same status dots, same streaming bubbles, same Ctrl+Enter to send. The differences:
- OpenCode icon in the header. An OpenCode session shows the OpenCode mark (brand orange, no text label) next to its title — like every non-default provider (Codex, Gemini, Anti-Gravity, Cursor, DeepSeek, Kimi, GLM, MiniMax, Hermes, Pi). Only the internal-only engines (OpenClaw, orchestrator) show no icon — every other session, Claude included, shows its engine mark. A fresh session's Change-provider button shows the same mark while OpenCode is the selected provider.
- Streaming is token-by-token. Omniscio subscribes to the shared
opencode serveserver's Server-Sent-Events stream, so the reply appears incrementally as it generates (like Gemini, unlike Anti-Gravity's block mode). When the turn completes, one final push replaces the bubble with the full accumulated text. - Compactions are visible (2026-09-11). When a long OpenCode session fills its context window, OpenCode rewrites the history into a summary and older turns stop existing. It announces that with a
session.compactedevent, which Omniscio used to drop as an unknown frame — so the rewrite left no trace in the transcript at all. Now it writes the same “Conversation compacted” divider a Claude or Codex session shows, carrying the context size from the last assistant message. Two differences from Claude: there is no pulsing “Compacting…” pill (OpenCode announces the rewrite only once it is DONE — there is no start signal to hang one on), and the divider has no “Show summary” button, because that reads Claude's own on-disk transcript and OpenCode keeps none. See compaction-summary.md. - Tool-use auto-approval defaults to ON. OpenCode runs its tools (file writes, shell commands, network) without per-call prompts. The "Allow OpenCode sessions" toggle gates whether you can spawn it at all; once spawned, it does not ask before acting.
How to enable
Master toggle required first. OpenCode is an alternative provider — on a fresh install OpenCode is already visible (the master toggle is ON by default since 2026-10-03); on a profile that last saved before that setting existed the entire OpenCode setup section stays hidden in Settings → Accounts until you flip it on. Flip Settings → Accounts → Show alternative AI providers to ON and the OpenCode (CLI provider) panel appears. With the master off, the per-launch provider switcher (ChangeProviderButton) hides OpenCode — it effectively does not exist in the UI even if every gate below is configured. Your "Allow OpenCode sessions" toggle and saved key are preserved across master-toggle flips.
Fastest path — "Set it up for me." The OpenCode panel leads with a Set it up for me button: one click turns OpenCode on, installs the
opencodeCLI for you (npmopencode-ai, with a Windows winget fallback), and reuses an Anthropic API key you already have saved (with a one-click confirm — never a silent copy), defaulting to Claude Sonnet so only that one key is needed. If you have no key saved, it installs + enables OpenCode and stops with a durable in-card status line ("one step left — paste your Anthropic API key below to finish") and the key field revealed. Every outcome — ready, one-step-left, or an install error — shows that persistent result line right in the card (not just a transient toast), so the click is never silent. The three manual gates below are exactly what that button automates — governed by provider-registry-contract.md.
Three gates must all pass (getProviderSpawnReadiness enforces them in this order):
The toggle leads and gates the panel UI. The CLI-install status card and the API key field render only once "Allow OpenCode sessions" is on — so a key can't be entered into a disabled provider. When off, a hint stands in and keeps the
opencode-binary/opencode-api-keySettings-search anchors landable; a saved key is preserved.
- Allow OpenCode sessions toggle — off by default. Same security stance as the other auto-approving providers: enable only because you intend to use it. Lives in the OpenCode panel as Allow OpenCode sessions in any project, and it's the first thing you see (turning it on reveals the steps below).
- OpenCode CLI binary — the
opencodebinary must be on your PATH. The Set it up for me button installs it for you (npmopencode-ai, binopencode; a Windows wingetSST.opencodefallback runs only if npm fails) and clears the binary-resolver cache so the fresh install is seen immediately. The Settings panel shows OpenCode CLI: Installed (version X) in the binary status card, or a "not found on your PATH" hint. To install by hand, runnpm install -g opencode-ai. The resolver also discovers an npm-global install vianpm root -g, so a GUI-launched Omniscio with a narrow PATH still finds it. (OpenCode is registered in Omniscio's toolchain installer withdefaultSelected:false— installable on demand, never auto-installed in the first-run "install recommended tools" sweep.) - The API key for the selected model's provider. Readiness is now model-aware: the key required depends on the OpenCode model you pick (gate 4). On the default (no model chosen) it's the OpenCode API Key (Anthropic) — OpenCode runs Claude through it. Pick a non-Claude model and the gate asks for that provider's key instead (e.g. an OpenRouter key for
openrouter/…). All keys are stored encrypted (safeStorage+enc:prefix) and injected as the matching environment variable on spawn (ANTHROPIC_API_KEY/OPENROUTER_API_KEY/OPENAI_API_KEY/MOONSHOT_API_KEY) — never on the command line.
Gate 4 — OpenCode model (optional). A dropdown of curated models (Claude Sonnet, GPT-4o, Kimi K2, DeepSeek…) plus a Custom… box for any provider/model id OpenCode supports. Leave it on the default and nothing changes — OpenCode keeps its own Claude-Sonnet default with your Anthropic key (the same outcome as before this feature; the server still gets the resolved default model selector, never a literal --model arg). Choosing a model routes the matching key by its prefix:
| Model prefix | Env var injected | Key read from (your settings) |
|---|---|---|
anthropic/… (default) |
ANTHROPIC_API_KEY |
OpenCode API Key |
openrouter/… (catch-all) |
OPENROUTER_API_KEY |
OpenRouter API Key (field in this panel) |
openai/… |
OPENAI_API_KEY |
OpenAI key (Codex's) |
moonshotai/… |
MOONSHOT_API_KEY |
Kimi key |
OpenRouter is the catch-all — one OpenRouter key unlocks Kimi, GPT, Claude, and hundreds more as openrouter/<vendor>/<model>, so you rarely need per-vendor keys. Cost guard: the OpenRouter route reads your saved key only — never Omniscio's bundled internal OpenRouter key — so a coding session can't bill Omniscio's shared account. Local models (Ollama/LM Studio) are not yet supported in v1 → they surface a model-unsupported gap rather than a confusing CLI failure.
Once all three are green, you launch an OpenCode session two ways:
- Per-launch override — on a fresh (zero-message) session, the Change-provider button in the new session's main panel (the launch-config pickers on a fresh session) lets you pick a one-off provider before sending the first message. Pick OpenCode and the first send spawns it. Non-ready providers appear disabled with a "Set up…" / "Install…" / "Add key" suffix that deep-links to the relevant Settings panel.
- Programmatically — anything that creates a session with
provider: 'opencode'(recipes, the CLI control server, agent-driven sessions). The same three readiness gates apply on the backend.
No per-project default, no virtual project
OpenCode is intentionally excluded from two surfaces that the persistent providers (Codex, Gemini, Anti-Gravity) have:
- Not a per-project default. The Edit-Project "Default provider" radios and the sidebar "+ New Session" split-button omit OpenCode (it is
pickable: false, so it is absent fromPROVIDER_ORDER). It is now a persistent shared-server engine (like Codex / Gemini / Anti-Gravity), but it has simply not been wired into the project-default surface yet. Launch it explicitly per-session instead. - No dedicated virtual project. There is no
__opencode__sidebar entry (unlike__codex__/__gemini__/__antigravity__), and therefore no sentinel bypass of the "Allow OpenCode sessions" toggle. The toggle always applies.
Error states and fixes
Gap codes (toggle + binary as before; key-missing is now per-selected-model; model-unsupported is new):
| Gap | What it means | Click-to-fix lands you at… |
|---|---|---|
toggle-off |
"Allow OpenCode sessions" is OFF in Settings | Settings → Accounts → Allow OpenCode sessions toggle |
binary-missing |
opencode CLI not found on PATH |
Settings → Accounts → OpenCode CLI binary panel |
key-missing |
No saved key for the selected model's provider | Settings → the key field for that provider (Anthropic / OpenRouter / …) |
model-unsupported |
The model id is local/unknown/malformed (v1 = hosted only) | Settings → Accounts → OpenCode model picker |
There is no virtual-project escape hatch, so the gaps apply to every OpenCode session. A resolver gap fails the turn before the server is touched (the model/key is resolved during the lazy start) with a humanized transcript line — it never sends a turn with a bad model/key.
Comparison with other providers
| Concern | OpenCode | Gemini | Anti-Gravity |
|---|---|---|---|
| Auth | Anthropic API key (env ANTHROPIC_API_KEY) |
Google API key (GEMINI_API_KEY) |
Google Sign-In (OS keyring) |
| Underlying model | Claude (Sonnet) by default; any hosted model via the picker | Gemini | Gemini / Google models |
| Streaming | Token-by-token (shared opencode serve SSE) |
Token-by-token (ACP JSON-RPC) | Block mode (full reply at end) |
| Process model | Shared persistent opencode serve server (HTTP/SSE) |
Persistent gemini --acp child |
Per-turn spawn |
| Multi-turn resume | Native (opencode ses_…, persisted in the shared DB) |
Native --resume <uuid> |
Synthesized (prepend prior turns) |
| Cost emitted | Yes — authoritative, written to api_cost_log |
No (Google-billed) | No |
| Readiness gates | 3 (toggle + binary + key) | 3 (toggle + binary + key) | 2 (toggle + binary) |
| Per-project default | No (excluded from radios — historical, not yet wired) | Yes | Yes |
| Virtual project | No | Yes (__gemini__) |
Yes (__antigravity__) |
| Session-header icon | OpenCode mark (orange) | Gemini bloom (gradient) | Anti-Gravity "A" (blue) |
For agents
How it works under the hood
One shared opencode serve server, not a spawn per turn. OpenCode is now a persistent-external engine (like Codex, Gemini, Pi). Omniscio runs ONE long-lived opencode serve HTTP/SSE server that multiplexes EVERY OpenCode session; per-turn work is just HTTP calls to it, not a fresh process. The server is owned by a supervisor (opencode-server-supervisor.ts) and is lazy — it is NOT started at app launch; ensureServer() starts it on the FIRST session's first turn, single-flighted so two concurrent first-turns can't spawn two servers. It is ref-counted (each live session acquires it; the last release disposes it after a short idle grace, and a quick close-then-reopen cancels the pending dispose), and watchdog'd (while ≥1 session needs it, a health probe restarts a wedged/dead server, firing ONE deduped inbox alert — never resurrecting a server with zero sessions). launch() still just registers an in-memory session with no child; the first sendMessage() is what lazily starts the shared server and creates this session's opencode session.
Why the shared server (the bug it fixes). The old opencode run one-shot cold-booted OpenCode's WHOLE runtime — server + SQLite DB + Drizzle migrations — for EVERY turn, all against ONE shared ~/.local/share/opencode/opencode.db. Two concurrent turns collided on that DB's WAL recovery lock (SQLITE_BUSY_RECOVERY, errno 261) at startup and BOTH wedged — measured 0/24 success under 8-concurrent load. opencode serve is a client/server app: one process owns the DB and serves many sessions, so the collision cannot happen (a live proof ran 6/6 concurrent across two directories) — and turns are far faster (no per-turn boot). See opencode-server-contract.md.
Loopback-bound + Basic-auth secured. The server is launched on 127.0.0.1 with a random OPENCODE_SERVER_PASSWORD, and every request carries Authorization: Basic base64("opencode:<password>") (the only scheme that returned 200 against opencode v1.15.2 — Bearer/none both 401). The serve child launches only INSIDE Omniscio's Windows job object — the job-gate launcher holds it until Omniscio has put it there, and a launch that cannot be contained is refused — so it dies with Omniscio (clean exit OR crash) with no separate orphan reaper. Kill switches: AMC_DISABLE_OPENCODE_SERVER=1 turns the whole feature off (OpenCode sessions cleanly error); AMC_DISABLE_OPENCODE_SERVER_WATCHDOG=1 disables just the auto-restart.
One opencode ses_… per Omniscio session, reused across restarts. On a session's first turn Omniscio POST /session?directory=<cwd>s to mint an opencode ses_… id, pinned to the project cwd, and persists it as cli_session_id. Because the shared opencode DB persists the conversation, that id is reused on app re-register (reload/restart) — the next prompt continues with full context and NO Omniscio replay (unlike Codex/Pi, whose in-memory child state dies). Only a row with Omniscio history but no stored opencode id mints a fresh one and replays its history as a one-time context prefix (rare).
Turns go via prompt_async + a per-directory SSE stream. Each turn POST /session/{id}/prompt_async?directory=<cwd>s (parts + the {providerID, modelID} selector) and returns 204 immediately — fire-and-stream. Output then arrives over a Server-Sent-Events stream: GET /event?directory=<cwd> is directory-scoped (verified live — directory A's stream never carries directory B's sessions), so the client opens ONE stream PER DISTINCT DIRECTORY and demuxes every frame back to the right Omniscio session by properties.sessionID. Streamed text = message.part.delta (field:'text') pushed to the bubble via SESSION_OUTPUT with streaming:true (APPEND); turn-end = session.idle; at end one final SESSION_OUTPUT with streaming:false carries the full accumulated text (REPLACE semantics — the same contract every other provider uses). A reconnect that re-reads a buffered terminal frame is deduped so it can't double-fire turn-end.
Cost tracking. Unlike Gemini (Google-billed, emits no cost), OpenCode reports authoritative cost and tokens (input / output / reasoning / cache) on the turn's final assistant message.updated frame. Omniscio captures that exact figure and applies it once at turn-end: each turn writes per-session totals (updateSessionCost) AND an api_cost_log row via trackApiCostRaw with a synthetic accountId (opencode-shared) and source 'opencode', so the cost dashboard matches the provider console (per the CLAUDE.md "CLI spend MUST hit api_cost_log" rule). The model label logged is the resolved model id (e.g. openrouter/moonshotai/kimi-k2) — so the dashboard reflects what actually ran. For the default (unset) case the label is OpenCode's own default; the dollar figure (sourced from OpenCode itself) is always correct.
Interrupt. Pressing stop calls POST /session/{id}/abort on the shared server — it aborts JUST that session's in-flight turn, never the server (other sessions keep using it). The turn's session.idle then settles the session to ready as a clean stop (not a crash); the stall watchdog is the backstop if the server never emits one. Partial streamed text is preserved and the opencode ses_… id is kept so the next turn still resumes. No cost row is written for an interrupted turn (no final assistant cost frame).
Routing. A SESSION_LAUNCH (and subsequent send / interrupt / terminate) with provider: 'opencode' is dispatched to opencodeSessionManager instead of the Claude processManager, in both session-service.ts and session-handlers.ts (and the CLI server's alt-manager dispatch). The manager now extends BaseExternalSessionManager directly (like the Gemini-ACP manager) — shared status transitions, the stall watchdog, the first-response timer, and the streaming-finalize template come from the base; the manager owns only the OpenCode-specific surface (the shared server + the ses_… mapping + the demuxed SSE consumer + exact cost). Because it is a registry persistent-external engine, it sits in SESSION_MANAGERS (NOT ONE_SHOT_MANAGERS) and in CRASH_RESUMABLE_EXTERNAL_MANAGERS. The session row stores provider: 'opencode' in its DB column — accepted by migration v238, which extends the provider validation trigger to allow the value.
Crash-restart via the base. An app-crash victim (crash_reconciled=1) auto-resumes through the base resumeAfterCrash → it REUSES the stored opencode ses_id, so the conversation continues from the shared opencode DB with no Omniscio context replay. A session that errored on its own is never auto-resumed.
MCP servers — OFF in the shared-server model (a capability regression, called out honestly). The shared-server move turned MCP off (mcp: false), and the OpenCode MCP connector was removed. The old mechanism handed each turn's per-session config to a fresh opencode run child via the OPENCODE_CONFIG_CONTENT environment variable — but a single shared opencode serve process has ONE launch env, and OpenCode config is per-session, so a single shared-server launch env can't safely carry per-session MCP without leaking one project's servers across projects. So unlike the one-shot mode (where OpenCode sessions did receive your enabled MemPalace / KMS / Google Workspace / Zapier / custom servers), an OpenCode session in serve mode currently gets no MCP servers. This is an intentional, honest regression vs the old one-shot mode — not a lying flag. Serve-mode MCP (a per-session /mcp route on the shared server) is a tracked follow-up; the toOpencodeMcpConfig translator is retained for it. Connector seam background: provider-mcp-connector-contract.md.
Auth. Keys are stored encrypted (all on CLI_SETTINGS_SENSITIVE_KEYS + ENCRYPTED_APP_SETTINGS_KEYS) and injected into the shared server's launch env — ANTHROPIC_API_KEY (default/Claude), OPENROUTER_API_KEY, OPENAI_API_KEY, and/or MOONSHOT_API_KEY — never on argv. The launch env first STRIPS any ambient provider-key copy from the Omniscio process (OPENCODE_SERVER_ENV_KEYS) and then re-injects ONLY the user's saved keys plus the server password, so a stray shell OPENROUTER_API_KEY can never bill the wrong account. The OpenRouter route reads the raw user setting only (never Omniscio's bundled internal key — the cost guard). One consequence of a single shared server: all sessions share that one key set, so per-session provider-key isolation isn't possible (Omniscio keys are global anyway).
Binary status IPC. The Settings panel's binary status card calls OPENCODE_GET_STATUS (opencode:get-status), which runs resolveOpencodeBinary() and returns { installed, version } — mirroring ANTIGRAVITY_GET_STATUS. This isolates the "is the binary on PATH" question from the toggle/key gates (which PROVIDER_GET_READINESS would short-circuit), so the card can tell you to install OpenCode even while the allow-toggle is still off.
Capabilities. OpenCode is now a persistent-external engine in the registry, declared with { images: false, mcp: false, cloud: false, sshRemote: false, permissionPrompts: false, multiTurn: true } — images: false is no inline-image protocol, no SSH remote, no permission prompts (auto-approves), and mcp: false is honest about the serve-mode regression (see "MCP servers" above — the connector was removed, serve-mode MCP is a follow-up). Multi-turn is native (the opencode ses_… persists in the shared DB). Send is text-only in serve v1 (makeTextOnlySendStrategy) — no image/doc attachments yet.
Files
- .claude/memory/contracts/opencode-server-contract.md — the shared-server source of truth: the supervisor/client/manager split, the live-captured wire protocol, and every invariant (lazy start, ref-count, watchdog, Basic auth, per-directory SSE, cost-guard env, MCP-off)
- src/main/services/engines/opencode-server-supervisor.ts — owns the ONE
opencode servechild: lazy single-flight start, ref-counted idle-dispose, watchdog restart, Basic-auth + cost-guard launch env, job-object orphan kill (singletonopencodeServerSupervisor) - src/main/process/opencode-server-client.ts — typed HTTP + SSE transport:
createSession/sendPrompt(204) /abort/dispose, one reconnecting/eventstream per directory demuxed bysessionID, terminal-event dedupe; owns no process and no session bookkeeping - src/main/services/engines/opencode-session-manager.ts — extends
BaseExternalSessionManager; maps Omniscio sessions ↔ opencodeses_…ids, drives turns, demuxes the shared SSE stream, records exact cost (singletonopencodeSessionManager) - src/main/services/engines/opencode-model-resolver.ts —
routeOpencodeModel(pure prefix→env+key map) +resolveOpencodeModel(settings-backed) +toServerModelRef({providerID, id}) +buildOpencodeServerKeyEnv(user-keys-only cost guard); single source of truth for model→credential routing - .claude/memory/contracts/provider-registry-contract.md — model-routing invariants (default-preserving, cost guard) + the one-click "Set it up for me" flow (toolchain install + Anthropic-key reuse in main + arm-toggle-last) that automates the gates above
- src/main/services/engines/opencode-binary-resolver.ts — locate
opencodevia the sharedcreatePathBinaryResolverfactory (PATH →npm root -g→ known dirs; caches only successes and re-probes on miss — no stale "not installed" after an install);resolveOpencodeSpawnTargetresolves the realopencode.exethe supervisor launches - src/main/services/engines/opencode-credential-store.ts — encrypted
opencodeApiKeyget/set/has/clear - src/main/services/provider-setup/provider-readiness.ts — 3-gate readiness (toggle + binary + key); single source of truth for ChangeProviderButton + spawn guard
- src/main/ipc/openai-handlers.ts —
OPENCODE_GET_STATUSbinary-status handler - src/main/db/incremental-migrations.ts — migration v238 (provider trigger accepts
'opencode'; the frozen integer migration ladder lives here, not indatabase.ts) - src/main/services/session/session-service.ts + src/main/ipc/session-handlers.ts — routing to
opencodeSessionManager - src/shared/types.ts (
ProviderId,opencodeApiKey,allowOpencodeSessionSpawn), src/shared/provider-capabilities.ts, src/renderer/src/lib/visible-providers.ts — provider enumeration / visibility - src/renderer/src/components/ui/OpenCodeIcon.tsx, src/renderer/src/features/sessions/ChangeProviderButton.tsx, src/renderer/src/features/sessions/ProviderSplitButton.tsx — icon + per-launch switcher (OpenCode excluded from the project-default split-button list)
- src/renderer/src/features/settings/sections/accounts/AccountSettings.tsx — the OpenCode (CLI provider) Settings section (binary card + API key + allow toggle)
- tests/unit/process/opencode-server-client.test.ts + tests/unit/services/opencode-server-supervisor.test.ts — lock the SSE demux / terminal-dedupe / cost-extraction (client) and the lazy-start / ref-count / watchdog / kill-switch lifecycle (supervisor); built against the live-captured opencode v1.15.2 protocol
Related
Related
- gemini-provider.md — closest analog: token-by-token streaming (ACP JSON-RPC) + API-key auth, but Google-billed and a project-default-capable persistent provider
- antigravity-provider.md — a per-turn one-shot spawn (OpenCode is no longer one-shot — it now runs a shared persistent server), plus block-mode streaming and Google Sign-In auth
- codex-provider.md — long-lived JSON-RPC child (different process model)
- deepseek-provider.md / kimi-provider.md — also icon-badged, but Anthropic-compat API providers with no binary (reuse the
claudeCLI)
Last verified 2026-10-06