You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
opencode-route door must be routing-only: no OpenCode session/loop per model request #2518
"opencode shouldn't do work besides routing — it shouldn't do any loop etc."
Problem
The opencode-route provider (#2250) currently serves each model request by creating a full OpenCode session and prompting it. That means the embedded OpenClaw agent's turn runs inside a second agent loop: OpenCode assembles its own context, tools, and plugins around a request that already carries OpenClaw's context. Two loops stacked — heavier, and the wrong ownership split.
Required shape
For the MODEL door (OpenClaw embedded agent → opencode-route/*), OpenCode's job is routing only:
Resolve the route name (speed / workhorse / specialist / summit / scout) to a concrete pool member, using the existing route-pool state: weights, round-robin across workspaces, quota demotion, expiry.
Make a plain completion call to that member's provider endpoint with the member's credential.
Stream the response back to OpenClaw untouched (OpenAI-compatible, as today).
Record the attempt in the route ledger (route_attempt) exactly as the session path does, so quota accounting and demotion keep working.
No OpenCode session is created, no OpenCode agent loop runs, no OpenCode tools/plugins/context are applied. Loop ownership stays with the caller (OpenClaw embedded).
Out of scope: dispatch paths are different by design — ACP (#2256) and sessions_spawn (#2257) hand off whole tasks, and OpenCode doing the work there is the point.
Acceptance
A workhorse request through the door completes with ZERO new rows in OpenCode's session tables for that request.
route_attempt gains the attempt row with the same fields the session path writes (pool, member, outcome).
Pool behavior preserved under test: a demoted/closed pool member is skipped; the fallback order from the route config is honored.
OpenClaw's provider definition (name opencode-route, port 18750) does not change — the door's internals are replaced behind the same endpoint, so no OpenClaw config change rides along.
Design confirmation (Drew, 2026-10-29 21:51 CDT, main chat): "it should get its actual model by calling the opencode route." The caller (OpenClaw's embedded agent) gets its actual model BY CALLING the route: the route door is the resolution point — it receives the route name, resolves it to the actual pool member, and serves that model's completion directly. The caller never names a concrete model and never runs an OpenCode session; the door never runs a loop. Build in progress in worktree oc-route-only-wt (branch feat/route-proxy-routing-only-2518).
Built and proven. PR #2549 rewrites bin/opencode-route-proxy.mjs as a routing-only door: it resolves the route from the fleet config (experimental.model_routes + provider blocks), picks the actual pool member (ordered/round-robin/weighted, #variant + variants remaps, disabled providers skipped, quota demotion from route_attempt), and makes ONE plain chat-completions call with the member's credential — no OpenCode session, no loop, caller's messages forwarded as-is. Live smoke test (spare port, production config+DB): workhorse non-stream and speed stream completions succeeded, session_v2+session_message count unchanged (115051 → 115051), route_attempt rows 19983/19984 written with session_id NULL. Unit tests rewritten: 7/7 pass. Provider name and port 18750 unchanged, so it deploys behind the same endpoint with no OpenClaw config change; the live switch picks it up once it merges.
Production :18750 brought current — live acceptance proof (2026-11-02)
What was stale
The production proxy process (MainPID 158107, started Fri 2026-10-30 06:31:25 CDT) execs node ~/code/bin/opencode-route-proxy.mjs from the live checkout, loaded into memory at start. Its code vintage was the checkout as of 2d6e855e — this issue's own routing-only rewrite (PR #2549) — so routing-only was live, but the process predated everything merged since: e698123b1 (timeout validation), cc665e8d4 (#3225 window gates), 064ce9e56 (#3830: provider_routes expansion + built-in endpoint defaults, #3256), and ee137a9b3 (#3874).
Pre-restart probe, old process: terra-plan → route_attempt 120157 (provider openai-plan, outcome failure, session_id NULL) and client error no endpoint configured for provider openai-plan; a workhorse draw recorded the same group failure at 120166.
Deploy action
The unit is not store-baked: the Home Manager unit (loaded copy byte-identical to the on-disk copy) execs a bash start-unit → ~/code/bin/opencode-route-proxy-start.sh → node ~/code/bin/opencode-route-proxy.mjs from the live checkout (clean main @ b60e5695efcc). A restart is therefore exactly switch-equivalent for this unit; no full switch was run. Restarted opencode-route-proxy.service at 2026-11-02 10:06:43 CST → new MainPID 978034, active/running; GET /v1/models lists workhorse, speed, specialist, summit, scout, workhorse-weighted. OpenCode server and OpenChamber untouched; downtime was the single restart.
Live proof on production :18750 (new process)
Session counts in ~/.local/share/opencode/opencode.db: session_v24030 → 4030 across the workhorse/speed/terra-plan calls, and 4034 → 4034 across the summit stream. A marker search over session_message for every test prompt used below returns 0 rows — no test request created or wrote into any OpenCode session. Every route_attempt row below has session_id NULL.
workhorse, non-streaming → HTTP 200. Attempts: 120188 openai.alternate/gpt-6-luna 429 RateLimit → failover; 120189 openai/gpt-6-luna 429 → failover; 120191 muse-direct/muse-spark-1.3-contributor success (27 in / 16 out). Note the luna-plan leaf now resolves through the openai-plan provider_routes group — impossible on the old process.
speed, streaming. Attempts: 120192 opencode-go.personal 429; 120193 opencode-go Authentication failure (the known credential-less base door); 120194 cline-pass 429 (walled until Nov 5); 120197 muse-direct/muse-spark-1.3-contributor success, output_started=1. A repeat speed stream minutes later exhausted on the current door state (120310–120315: deepseek leaves Authentication/429) — every attempt recorded, still zero sessions.
terra-plan (provider_routes group, ordered [alternate, main]). Attempts: 120199 openai.alternate/gpt-6.1-terra 429 → 120200 openai/gpt-6.1-terra 429 (leaf endpoint reports no credits remaining). Both group members attempted in order with real provider responses recorded; routing is correct — the outcome is the GPT door's current leaf state, not a proxy failure.
workhorse, streaming. Attempt 120333 muse-direct success, output_started=1. (That door returns empty content non-streaming as well; its stream is faithfully relayed as [DONE]-only.)
summit, streaming (content-bearing). Attempt 120382 antigravity-direct/claude-opus-5-5 success, output_started=1, 42 in / 171 out; the client received 47 SSE chunks / 10,351 bytes of model deltas relayed verbatim, terminated by [DONE].
Conclusion
The routing-only door from PR #2549, plus all proxy work merged since (notably #3830's provider_routes support), is now the production :18750 process at current main, with the zero-sessions acceptance verified live: resolve route → plain provider completion call per attempt → route_attempt row with session_id NULL. Closing.
Activity
dezren39 commentedon Oct 30, 2026
Design confirmation (Drew, 2026-10-29 21:51 CDT, main chat): "it should get its actual model by calling the opencode route." The caller (OpenClaw's embedded agent) gets its actual model BY CALLING the route: the route door is the resolution point — it receives the route name, resolves it to the actual pool member, and serves that model's completion directly. The caller never names a concrete model and never runs an OpenCode session; the door never runs a loop. Build in progress in worktree oc-route-only-wt (branch feat/route-proxy-routing-only-2518).
dezren39 commentedon Oct 30, 2026
Built and proven. PR #2549 rewrites bin/opencode-route-proxy.mjs as a routing-only door: it resolves the route from the fleet config (experimental.model_routes + provider blocks), picks the actual pool member (ordered/round-robin/weighted, #variant + variants remaps, disabled providers skipped, quota demotion from route_attempt), and makes ONE plain chat-completions call with the member's credential — no OpenCode session, no loop, caller's messages forwarded as-is. Live smoke test (spare port, production config+DB): workhorse non-stream and speed stream completions succeeded, session_v2+session_message count unchanged (115051 → 115051), route_attempt rows 19983/19984 written with session_id NULL. Unit tests rewritten: 7/7 pass. Provider name and port 18750 unchanged, so it deploys behind the same endpoint with no OpenClaw config change; the live switch picks it up once it merges.
dezren39 commentedon Nov 2, 2026
Production :18750 brought current — live acceptance proof (2026-11-02)
What was stale
The production proxy process (MainPID 158107, started Fri 2026-10-30 06:31:25 CDT) execs
node ~/code/bin/opencode-route-proxy.mjsfrom the live checkout, loaded into memory at start. Its code vintage was the checkout as of2d6e855e— this issue's own routing-only rewrite (PR #2549) — so routing-only was live, but the process predated everything merged since:e698123b1(timeout validation),cc665e8d4(#3225 window gates),064ce9e56(#3830: provider_routes expansion + built-in endpoint defaults, #3256), andee137a9b3(#3874).Pre-restart probe, old process:
terra-plan→ route_attempt 120157 (provideropenai-plan, outcome failure, session_id NULL) and client errorno endpoint configured for provider openai-plan; a workhorse draw recorded the same group failure at 120166.Deploy action
The unit is not store-baked: the Home Manager unit (loaded copy byte-identical to the on-disk copy) execs a bash start-unit →
~/code/bin/opencode-route-proxy-start.sh→node ~/code/bin/opencode-route-proxy.mjsfrom the live checkout (clean main @b60e5695efcc). A restart is therefore exactly switch-equivalent for this unit; no full switch was run. Restartedopencode-route-proxy.serviceat 2026-11-02 10:06:43 CST → new MainPID 978034, active/running;GET /v1/modelslists workhorse, speed, specialist, summit, scout, workhorse-weighted. OpenCode server and OpenChamber untouched; downtime was the single restart.Live proof on production :18750 (new process)
Session counts in
~/.local/share/opencode/opencode.db:session_v24030 → 4030 across the workhorse/speed/terra-plan calls, and 4034 → 4034 across the summit stream. A marker search oversession_messagefor every test prompt used below returns 0 rows — no test request created or wrote into any OpenCode session. Every route_attempt row below has session_id NULL.openai.alternate/gpt-6-luna429 RateLimit → failover; 120189openai/gpt-6-luna429 → failover; 120191muse-direct/muse-spark-1.3-contributorsuccess (27 in / 16 out). Note the luna-plan leaf now resolves through theopenai-planprovider_routes group — impossible on the old process.opencode-go.personal429; 120193opencode-goAuthentication failure (the known credential-less base door); 120194cline-pass429 (walled until Nov 5); 120197muse-direct/muse-spark-1.3-contributorsuccess, output_started=1. A repeat speed stream minutes later exhausted on the current door state (120310–120315: deepseek leaves Authentication/429) — every attempt recorded, still zero sessions.openai.alternate/gpt-6.1-terra429 → 120200openai/gpt-6.1-terra429 (leaf endpoint reports no credits remaining). Both group members attempted in order with real provider responses recorded; routing is correct — the outcome is the GPT door's current leaf state, not a proxy failure.muse-directsuccess, output_started=1. (That door returns empty content non-streaming as well; its stream is faithfully relayed as [DONE]-only.)antigravity-direct/claude-opus-5-5success, output_started=1, 42 in / 171 out; the client received 47 SSE chunks / 10,351 bytes of model deltas relayed verbatim, terminated by[DONE].Conclusion
The routing-only door from PR #2549, plus all proxy work merged since (notably #3830's provider_routes support), is now the production :18750 process at current main, with the zero-sessions acceptance verified live: resolve route → plain provider completion call per attempt → route_attempt row with session_id NULL. Closing.