| tags |
|
|---|
runtime: opencode uses @opencode-ai/sdk + the opencode CLI. The harness starts a local OpenCode server per loop session by spawning opencode.exe (not the pnpm shell shim). Dispose SIGTERMs the serve process tree (MCP children included), SIGKILLs leftovers, and a parent-death reaper cleans up if the harness Node process is SIGKILL’d. SDK server.close() alone is not enough: it only kills the shim, which orphaned opencode.exe + Toolport/githits MCP processes under PID 1.
Default judge stays Cursor (reviewRuntime unset). Set reviewRuntime: "opencode" to judge on Go (defaults to opencode-go/deepseek-v4-pro, not Flash). BYOK OpenCode judges still need an explicit reviewModel.
| Field | Default |
|---|---|
model |
opencode-go/deepseek-v4-flash |
escalateModel |
opencode-go/qwen3.7-plus (after stagnation or a hung/timed-out worker) |
reviewModel (when reviewRuntime: "opencode") |
opencode-go/deepseek-v4-pro (not Flash) |
Go slugs must appear in OPENCODE_GO_LOOP_MODELS (see OpenCode Go docs). Hy3 (opencode-go/hy3) is on that list — slower than Flash, large monthly quota. costPreset: "minmax" binds Hy3 as the Go worker (not Flash) plus a Grok judge when Cursor is installed.
Any other OpenCode provider id is allowed in provider/model form, for example:
Example model |
Auth |
|---|---|
openrouter/deepseek/deepseek-chat |
OPENROUTER_API_KEY (harness calls auth.set; CLI also reads env) |
openrouter/minimax/minimax-m3:free |
Same key. Hosted $0 rows — see OpenRouter :free |
vercel/anthropic/claude-sonnet-4 |
AI_GATEWAY_API_KEY (Vercel AI Gateway — same auth.set path) |
cloudflare-workers-ai/… |
OpenCode /connect (account id + API token) or CLOUDFLARE_ACCOUNT_ID + CLOUDFLARE_API_KEY. Cloudflare GPU inference — not AI Gateway. Not a harness env alias. |
ollama/llama3.2 |
Local Ollama — no key; use opencode /connect or host config |
anthropic/claude-sonnet-4-20250514 |
Provider env or ~/.local/share/opencode/auth.json from /connect |
The harness wires OPENCODE_API_KEY / OPENROUTER_API_KEY / AI_GATEWAY_API_KEY into the ephemeral server when present. For those providers, each iteration fails fast if the key is missing and ~/.local/share/opencode/auth.json has no entry (override path via OPENCODE_AUTH_JSON). Other providers (Ollama, Cloudflare Workers AI, …) are left to OpenCode’s own config.
Curated Go list + pricing estimates: OPENCODE_GO_LOOP_MODELS in code. BYOK models use provider-reported cost when available; otherwise costUsd may be 0 in logs.
{
"runtime": "opencode",
"model": "openrouter/deepseek/deepseek-chat",
"escalateModel": "openrouter/qwen/qwen-2.5-coder-32b-instruct",
"verify": "bash .cursor/loops/my-task/verify.sh"
}OpenCode Go worker + Cursor Grok judge (default reviewRuntime):
{
"runtime": "opencode",
"model": "opencode-go/deepseek-v4-flash",
"reviewModel": "grok-4.6",
"verify": "bash .cursor/loops/my-task/verify.sh",
"postQualityReview": "auto",
"reviewGate": true
}OpenCode Go worker + OpenCode V4 Pro judge (omit reviewModel):
{
"runtime": "opencode",
"model": "opencode-go/deepseek-v4-flash",
"reviewRuntime": "opencode",
"verify": "bash .cursor/loops/my-task/verify.sh",
"postQualityReview": "auto",
"reviewGate": true
}Vercel AI Gateway (list price, no markup) — OpenCode’s native vercel provider:
{
"runtime": "opencode",
"model": "vercel/anthropic/claude-sonnet-4",
"reviewRuntime": "opencode",
"reviewModel": "vercel/anthropic/claude-sonnet-4",
"verify": "bash .cursor/loops/my-task/verify.sh"
}Hosted $0 OpenRouter rows use the :free suffix (OpenRouter’s own tag, not a Kilo runtime). The harness already wires OPENROUTER_API_KEY; loop.json accepts those slugs on runtime: opencode and runtime: pi. Setup’s OpenCode worker / escalate / judge menus list MiniMax M3 :free and Laguna S 2.1 :free after the Go catalog.
Pin a pair. Do not use a rotating “auto free” router — the catalog shifts, some upstreams log prompts (mayTrainOnYourPrompts), and a grind loop needs a stable worker. Unset reviewRuntime still defaults to Cursor — this stack must set it to opencode with an explicit :free reviewModel, or OpenCode’s judge default is Go V4 Pro (paid). Residual review is weaker than Grok; that is the $0 deal. This is not costPreset: minmax (that stays Go Hy3 + Grok when those runtimes are installed).
| Role | Pin |
|---|---|
| Worker | openrouter/minimax/minimax-m3:free |
| Escalate | openrouter/poolside/laguna-s-2.1:free |
| Judge | openrouter/poolside/laguna-s-2.1:free (reviewRuntime: "opencode") |
{
"runtime": "opencode",
"model": "openrouter/minimax/minimax-m3:free",
"escalateModel": "openrouter/poolside/laguna-s-2.1:free",
"reviewRuntime": "opencode",
"reviewModel": "openrouter/poolside/laguna-s-2.1:free",
"verify": "bash .cursor/loops/my-task/verify.sh",
"postQualityReview": "auto",
"reviewGate": true
}Coming from Kilo: their :free catalog is this same OpenRouter pool. Use openrouter/…:free + OPENROUTER_API_KEY. There is no runtime: kilo and no Kilo Gateway provider in the harness.
Optional named stack in the repo profile (costPresets.or-free) so setup can pick it:
{
"costPresets": {
"or-free": {
"runtime": "opencode",
"model": "openrouter/minimax/minimax-m3:free",
"escalateModel": "openrouter/poolside/laguna-s-2.1:free",
"reviewRuntime": "opencode",
"reviewModel": "openrouter/poolside/laguna-s-2.1:free"
}
}
}Then "costPreset": "or-free" in loop.json. Setup lists that name as OpenRouter $0 (not a generic “saved preset”) and offers both slugs on the OpenCode model menu. 429s already retry. Skip NVIDIA :free rows (prompt logging). Skip Hy3 on Kilo Gateway — it is paid; Go Hy3 is the minmax worker.
export OPENROUTER_API_KEY=… # and/or OPENCODE_API_KEY (Go) and/or AI_GATEWAY_API_KEY (Vercel)
agent-check opencodeThe harness does not add a new runtime for LLM proxies. Point OpenCode at any OpenAI-compatible endpoint yourself, then pass provider/model in loop.json. Fail-fast env wiring stays limited to Go / OpenRouter / Vercel.
| Gateway | How |
|---|---|
| OrcaRouter | Custom provider in opencode.json (npm: @ai-sdk/openai-compatible, baseURL https://api.orcarouter.ai/v1). Then orcarouter/… in loop.json. |
| LiteLLM (self-hosted) | Same custom-provider shape, or OpenCode’s community LiteLLM plugin if you already run a proxy. |
| Portkey (Prisma AIRS) | Custom OpenAI-compatible baseURL. Enterprise governance — not a harness default. |
| Cloudflare AI Gateway | Native OpenCode /connect (account id + gateway id + token). Proxy in front of providers — distinct from Workers AI inference in the table above. |
| Kilo Gateway | Same OpenRouter :free pool — use openrouter/…:free, not a custom kilo provider. |
See OpenCode custom providers.
If iteration 1 dies with OpenCode session.prompt failed …: fetch failed or appears to hang after session_id=…:
| Symptom | Meaning |
|---|---|
Local server started, session.create OK, then silence |
Worker is waiting on the Go provider (normal until heartbeat/stall) |
UND_ERR_HEADERS_TIMEOUT on old releases |
Blocking session.prompt held one HTTP call for the whole turn |
Empty log.ndjson, no token usage |
No successful model round-trip |
Harness behavior (0.1.11+):
- Uses
session.promptAsync+ waits forsession.idle(HTTP returns immediately; no undici headers timeout on a 45‑minute turn) - Heartbeat every ~30s:
still working session=… elapsed=…s phase=awaiting_first_byte|awaiting_first_tool|in_turn … - TTFB stall (~3 min with no session-scoped events yet) → transport error + local server recycle. After the turn is alive (
session.status/ messages), no-tool stall (~8 min of text streaming without a tool part) kills a rambling model so the loop can switch toescalateModel. Quiet tool runs after the first tool are allowed until the 45m overall timeout. - Sid-less SSE noise is ignored for activity; sid-less
session.idleis accepted for the single waiter - Error messages include the
Error.causechain plus[layer=transport] failure-domains.ndjsonfingerprintsagent_error|transport|…
A 45-minute timed out after or an 8-minute no-tool stall is not retried on the same model. The iteration is recorded as a worker fault and the next iteration uses escalateModel when set. Heartbeats can look alive the whole time (phase=awaiting_first_tool).
Cloud poll tip: match EXIT: / finished complete= / Verifier passed — not layer=transport (that’s a retry, not done).
If all retries still stall at TTFB: check Go gateway / network, try escalateModel or a BYOK slug, re-run later.
OpenCode loads global skills at session create. A SKILL.md symlink whose Cursor plugin-cache target was purged used to fail with ENOENT (UnknownError). runtime: opencode and agent-check opencode now relink that path to the current cache hash when a sibling folder still has the skill, or drop the dangling link so the session can boot. Leftovers that cannot be unlinked still fail fast.
docs/runtime-map.md— cost-minmax presets (incl.reviewRuntime)docs/pi-runtime.md— Pi BYOK worker / judge- OpenCode providers