Skip to content

opencode-route door must be routing-only: no OpenCode session/loop per model request #2518

Closed

Description

@dezren39

Directive (Drew, 2026-10-29 21:45 CDT, main chat)

"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:

  1. 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.
  2. Make a plain completion call to that member's provider endpoint with the member's credential.
  3. Stream the response back to OpenClaw untouched (OpenAI-compatible, as today).
  4. 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.
  • The session-based implementation from feat(openclaw): opencode-route provider proxy — OpenCode server front for OpenClaw (#2229) #2250 is removed or kept only as an explicitly separate dispatch door, not as the model path.

Context: #2250 (built door), #2229 (parent), #2256 / #2257 (dispatch options), quota doctrine in the provider registry (route pools, demotion ≥3 quota errors/4h → weight ×0.01).

Activity

  1. dezren39 commented on Oct 30, 2026

    @dezren39
    MemberAuthor

    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).

  2. dezren39 commented on Oct 30, 2026

    @dezren39
    MemberAuthor

    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.

  3. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    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_v2 4030 → 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions