Skip to content

feat(opencode-route): routing-only door — resolve the route, call the pool member directly - #2549

Merged
dezren39 merged 1 commit into
mainfrom
feat/route-proxy-routing-only-2518
Oct 30, 2026
Merged

dezren39 merged 1 commit into
mainfrom
feat/route-proxy-routing-only-2518

Conversation

@dezren39

@dezren39 dezren39 commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Implements #2518.

What changed — bin/opencode-route-proxy.mjs is rewritten. The door no longer creates an OpenCode session per request. It now:

  • Resolves the requested route against the same fleet config the OpenCode server uses (.opencode/opencode.jsonc: experimental.model_routes + provider blocks, instanceOf chains, {env:} baseURL templates)
  • Picks the actual pool member: ordered / round-robin / weighted selection, #variant suffixes and per-target variants remaps, disabled_providers skipped, quota demotion from the route_attempt ledger (≥3 quota failures in 4h sinks a member to the back)
  • Makes ONE plain chat-completions call to that member's endpoint with the member's credential from the OpenCode credential store (opencode.db), forwarding the caller's messages as-is — no transcript flattening, no session, no loop
  • Fails over along the resolved candidate order and writes every attempt to route_attempt with session_id NULL, so quota accounting and demotion see door traffic exactly like session traffic

Provider name opencode-route and port 18750 are unchanged — no OpenClaw config change; the internals swap behind the same endpoint when this deploys.

Why — Drew's directive (2026-10-29): "opencode shouldn't do work besides routing it shouldn't do any loop etc." The caller (OpenClaw's embedded agent) gets its actual model by calling the opencode route. Dispatch paths (ACP, sessions_spawn — #2256, #2257) are where OpenCode doing the work is the point; they are untouched.

Evidence

  • Live smoke test (2026-10-29 ~22:05 CDT, spare port, production config + DB): workhorse non-stream and speed streaming completions succeeded; session_v2 + session_message count unchanged (115051 → 115051); route_attempt rows 19983/19984 written with session_id NULL
  • Unit tests rewritten for the routing shape (node --test bin/opencode-route-proxy.test.mjs): 7/7 pass — fake leaf providers + fixture config/ledger covering ordered resolution, verbatim message forwarding, 429 failover with both attempts ledgered, variant→reasoning_effort remap, nested routes, disabled-provider skip, SSE relay, unknown-route/malformed rejection

Notes

  • OpenCode Go leaves require an x-opencode-session attribution header at their gateway; the door mints one per request (an attribution label only — no local session row exists).
  • reasoning_effort is set when the leaf model defines the resolved variant in config; the intended variant is always recorded in the ledger.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

… pool member directly (#2518)

The opencode-route proxy no longer runs an OpenCode session per request. It resolves the requested route against the fleet config it shares with the OpenCode server (.opencode/opencode.jsonc: experimental.model_routes + provider blocks), picks the actual pool member (ordered/round-robin/weighted selection, #variant + variants remaps, disabled providers skipped, quota demotion from the route_attempt ledger), and makes ONE plain chat-completions call to that member with its credential from the OpenCode credential store. No session rows, no agent loop - the caller keeps its loop and gets its actual model by calling the route (Drew, 2026-10-29). Every attempt is recorded in route_attempt with session_id NULL so quota accounting and demotion keep working. Provider name opencode-route and port 18750 unchanged; no OpenClaw config change needed.

Live proof (2026-10-29, spare port, production config+DB): workhorse non-stream and speed stream completions succeeded; session_v2+session_message count unchanged at 115051; route_attempt rows 19983/19984 written with session_id NULL. Unit tests rewritten for the routing shape: 7/7 pass (fake leaf providers, fixture config + fixture ledger).
@dezren39
dezren39 requested a review from a team as a code owner October 30, 2026 03:42
@coderabbitai

coderabbitai Bot commented Oct 30, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 59 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 0508a2a4-79db-475c-8f28-8306cae06e6f
Commits

Reviewing files that changed from the base of the PR and between dc55c67 and 2d6e855.

Files selected for processing: 2
  • bin/opencode-route-proxy.mjs
  • bin/opencode-route-proxy.test.mjs
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-30T03:48:37.505316Z 2d6e855 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2d6e855ef7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +442 to +444
const baseURL = providerBaseURL(cfg, leaf.providerId);
if (!baseURL)
throw new LeafError(`no endpoint configured for provider ${leaf.providerId}`, {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve built-in endpoint resolution for OpenAI leaves

With the checked-in fleet config, openai supplies model overrides but no options.baseURL, and openai.alternate inherits that definition. Since the new resolver supplies a built-in endpoint only for opencode-go, both OpenAI leaves are rejected here before any request or credential lookup. The terra-plan route contains only these two leaves and therefore always returns 502; the OpenAI seats in sol-plan and luna-plan are also unusable. Preserve built-in provider endpoint defaults when overlaying the configured provider definitions.

Useful? React with 👍 / 👎.

Comment on lines +365 to +367
if (route.selection === "round-robin") targets = rotate(targets, rid);
else if (route.selection === "weighted")
targets = weightedOrder(targets, route.weights || {});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor configured weights in round-robin selection

For routes using selection: "round-robin", this branch ignores route.weights and assigns every target an equal share. The checked-in workhorse-primary route explicitly weights Gemini at 2 versus DeepSeek and Spark at 1 each, while speed and deepseek-flash-plan also specify unequal weights. OpenCode uses smooth weighted round-robin, as documented in the route-pools design document, so this door changes the fleet's intended quota allocation rather than sharing its routing semantics. Use weighted round-robin here instead of an unweighted rotation.

Useful? React with 👍 / 👎.

Comment on lines +735 to +737
const stats = await relayStream(leafRes, res);
if (!res.destroyed && !res.writableEnded) res.end();
return stats;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Surface stream transport failures before ending the response

If an upstream SSE connection breaks or the request deadline expires after headers arrive, relayStream catches the reader exception and returns it in stats.error. This callback then ends the downstream response normally without checking that error, and runCompletion only records it in the ledger. Clients therefore receive a clean HTTP 200 EOF with a truncated completion and no error event, preventing callers that rely on explicit errors from recognizing the failed turn. Surface the relay failure to the client before ending the response, or terminate the downstream connection as failed.

Useful? React with 👍 / 👎.

Comment on lines +317 to +318
if (t && typeof t.model === "string")
return { ...parseTargetRef(t.model), variantMap: t.variants || null };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply target time windows before expanding route candidates

For the checked-in background route, object targets carry window gates referencing experimental.model_route_windows, but this normalization discards those gates and the resolver never evaluates them. Consequently, the first peak-only MiMo target is expanded even outside both peak windows, so background requests always prefer MiMo instead of the configured off-peak DeepSeek Flash primary. Resolving the actual fleet config confirms that all first six candidates are MiMo leaves regardless of the clock. Preserve and evaluate each target's named window, including inverted windows, before constructing the candidate order.

Useful? React with 👍 / 👎.

? AbortSignal.any([deadlineCtl.signal, clientSignal])
: deadlineCtl.signal;
const sessionLabel = `ses_${randomBytes(12).toString("hex")}`;
const attempts = candidates.slice(0, MAX_ATTEMPTS);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep configured fallback legs reachable beyond the first six leaves

When DeepSeek leaves are unavailable and there are no existing demotions, the checked-in workhorse route expands to seven DeepSeek leaves before reaching Meta and Antigravity. Truncating that flattened list to six therefore returns 502 after attempting only DeepSeek, without trying either healthy alternate primary leg or any configured relief member. This defeats the failover structure described in .opencode/opencode.jsonc, where the relief pool is intended to absorb primary failures. Preserve access to later route branches rather than allowing the first nested pool to consume the entire candidate budget.

Useful? React with 👍 / 👎.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant