Skip to content

fix(routing): stop round-robin combo opencode targets from collapsing onto opencode-zen (#11912) - #13283

Merged
diegosouzapw merged 1 commit into
release/v3.8.51from
fix/11912-roundrobin-zen-collision
Sep 12, 2026
Merged

diegosouzapw merged 1 commit into
release/v3.8.51from
fix/11912-roundrobin-zen-collision

Conversation

@diegosouzapw

Copy link
Copy Markdown
Owner

Closes #11912

Root cause

open-sse/services/model.ts's manual ALIAS_TO_PROVIDER_ID["opencode"] = "opencode-zen" override canonicalizes ANY "opencode/<model>" string to provider opencode-zen (the api-key gateway) before dispatch. A round-robin combo mixing declared "opencode/<model>" targets (intended as the free/dynamic no-auth pool) with an explicit "opencode-zen/<model>" target therefore collapsed every rotation slot onto the same provider + connection identity — every request executed against the single opencode-zen connection instead of rotating across the free pool, matching the reporter's upstream-log screenshots (100% of traffic landing on opencode-zen, eventually 429ing). The same collapse explained the reported dashboard metrics desync: src/lib/combos/controlCenter.ts's providerFromModel() labeled targets from a raw, un-aliased string slice, so the "Resolved Runtime Targets" panel and the actual dispatched identity disagreed.

Fix

Scoped to combo target resolution only — open-sse/services/model.ts's general alias-resolution path is untouched, so a raw client request to "opencode/<model>" outside a combo keeps routing to opencode-zen unchanged (#2798/#3870), and the #7993 sibling credential lookup (tests/unit/opencode-autocombo-search-pair.test.ts) is unaffected.

  • New open-sse/services/combo/opencodeTargetAlias.ts: resolveComboTargetModelStr() rewrites an ambiguous "opencode/<model>" combo target to the "oc/" no-auth alias, mirroring the combo builder's existing [BUG] OpenCode Free combo entries use opencode/ prefix instead of oc/ #2901 guard (src/lib/combos/builderOptions.ts).
  • open-sse/services/combo/comboStructure.ts's normalizeRuntimeStep() (the single shared resolution point for every combo strategy) applies that rewrite before the model string reaches dispatch, so "opencode/<model>" and "opencode-zen/<model>" targets stay distinct rotation identities.
  • src/lib/combos/controlCenter.ts's providerFromModel() resolves labels through the same alias-normalization path (plus the existing resolveProviderAlias), so the dashboard label matches what actually executes upstream.

Regression test

tests/unit/issue-11912-opencode-roundrobin-collapse.test.ts

  • RED (against origin/release/v3.8.51, comboStructure.ts unfixed): AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + 'opencode-zen' - 'opencode'
  • GREEN (with the fix): both new tests pass.

Gates run

  • node scripts/check/check-file-size.mjs — no ✗ for touched files
  • node scripts/check/check-complexity.mjs — OK (2798 violations vs baseline 3218)
  • node scripts/check/check-cognitive-complexity.mjs — OK (1265 violations vs baseline 1437)
  • npm run typecheck:core — exit 0
  • npx eslint --suppressions-location config/quality/eslint-suppressions.json <changed files> — exit 0
  • New test: node --import tsx/esm --test tests/unit/issue-11912-opencode-roundrobin-collapse.test.ts — 2/2 pass
  • Existing combo/round-robin/opencode suites re-run together (147 tests, 0 failures): tests/unit/combo/round-robin-combo.test.ts, tests/unit/combo-pin-implicit-allowlist.test.ts, tests/unit/combo-routing-engine.test.ts, tests/unit/combo-scoring-inspector.test.ts, tests/unit/opencode-autocombo-search-pair.test.ts, tests/unit/refactor-buildHeaders-opencode.test.ts, tests/unit/model-alias-seed-fallback.test.ts, tests/unit/combo-control-center.test.ts, tests/unit/fusion-vision-panel-3378.test.ts
  • node scripts/check/check-changelog-integrity.mjs — OK

⚠️ base-red inherited: #12732 — unit #12058, integration codex-cache, package-artifact, tarball-smoke, agent-skills-sync

… onto opencode-zen (#11912)

open-sse/services/model.ts canonicalizes any "opencode/<model>" combo target
string to provider "opencode-zen" (the api-key gateway). A round-robin combo
mixing declared "opencode/<model>" targets (the free/dynamic no-auth pool)
with an explicit "opencode-zen/<model>" target therefore collapsed every
rotation slot onto the same provider + connection identity, so all traffic
executed against the single opencode-zen connection instead of rotating
across the free pool.

Mirror the combo builder's existing #2901 guard at combo target resolution
time (comboStructure.ts's normalizeRuntimeStep): rewrite an ambiguous
"opencode/<model>" combo target to the "oc/" no-auth alias before it reaches
dispatch, so it stays a distinct rotation identity. Also resolve
controlCenter.ts's Resolved-Runtime-Targets label through the same
alias-normalization path so the dashboard label matches what actually
executes upstream.
@HouMinXi

Copy link
Copy Markdown
Contributor

Heads-up: applied on top of release/v3.8.51, this PR breaks npm run build (webpack).

src/lib/combos/controlCenter.ts is reachable from the client component src/app/(dashboard)/dashboard/combos/ComboControlCenterClient.tsx. The two new imports

import { resolveComboTargetModelStr } from "../../../open-sse/services/combo/opencodeTargetAlias.ts";
import { resolveProviderAlias } from "../../../open-sse/services/model.ts";

pull the server-side open-sse graph into the browser bundle. The build then fails with:

./node_modules/detect-libc/lib/detect-libc.js
Module not found: Can't resolve 'child_process'

Import trace:
./node_modules/sharp/dist/utility.mjs
./open-sse/utils/cursorImages.ts
./open-sse/executors/zai-web.ts
./src/lib/providers/validation/webCookie.ts
./src/lib/tokenHealthCheckWebCookie.ts
./src/lib/tokenHealthCheck.ts
./src/lib/config/runtimeSettings.ts
./src/lib/db/settings.ts
./src/lib/db/readCache.ts
./open-sse/services/model.ts
./src/lib/combos/controlCenter.ts
./src/app/(dashboard)/dashboard/combos/ComboControlCenterClient.tsx

Same chain also produces Can't resolve 'fs' (open-sse/config/credentialLoader.ts) and Can't resolve 'module' (open-sse/executors/codex.ts).

The alias resolution needs to stay server-side. Two options: normalize the label in the API route that serves the control-center data instead of in the shared module, or move the two helpers into a small dependency-free module that does not transitively import open-sse/services/model.ts.

Two side notes:

  • The explicit .ts extensions on those imports are inconsistent with the rest of the codebase and may trip other bundler configs.
  • Dropping those two imports and reverting providerFromModel to the prefix slice restores the build; the label then shows the raw prefix for opencode combos, which is cosmetic only.

@diegosouzapw
diegosouzapw merged commit a5db197 into release/v3.8.51 Sep 12, 2026
15 of 21 checks passed
Shrub24 added a commit to Shrub24/OmniRoute that referenced this pull request Sep 12, 2026
… into the client bundle

src/lib/combos/controlCenter.ts is imported by the "use client"
ComboControlCenterClient.tsx, so everything it imports statically ends up in the
browser bundle. diegosouzapw#13283 added resolveProviderAlias from open-sse/services/model.ts
to that file; model.ts reaches @/lib/db/readCache, which fans out through
settings.ts -> runtimeSettings.ts -> tokenHealthCheck.ts -> webCookie.ts ->
zai-web.ts -> cursorImages.ts -> sharp -> detect-libc. Webpack then fails the
client compile with "Module not found: Can't resolve 'child_process'", breaking
docker-publish.yml and nightly-compat.yml, which both build with
OMNIROUTE_USE_TURBOPACK=0.

Move ALIAS_TO_PROVIDER_ID and resolveProviderAlias into
open-sse/services/providerAlias.ts, which imports only the provider catalogue --
already part of the client graph. model.ts re-exports the function, so every
existing caller is unchanged.
Shrub24 added a commit to Shrub24/OmniRoute that referenced this pull request Sep 12, 2026
Fixes the two client-bundle leaks introduced by the upstream/release/v3.8.51
sync (upstream diegosouzapw#13283 and diegosouzapw#13264), which fail the webpack production build that
oci-build.yml and upstream's own docker-publish.yml both use.
ftdaily added a commit to ftdaily/OmniRoute that referenced this pull request Sep 14, 2026
a5db197 (upstream diegosouzapw#11912/diegosouzapw#13283) made src/lib/combos/controlCenter.ts
import open-sse/services/model.ts. That file transitively reaches
open-sse/utils/cursorImages.ts, which dynamic-imports sharp. sharp
pulls in detect-libc → child_process, which webpack refuses to bundle
for the client chunk (ComboControlCenterClient).

Externalizing sharp keeps the runtime require() intact (server bundle)
while telling webpack to skip client-side bundling of the package.

Mine-side follow-up to the 44-ahead-113-behind merge. No upstream
code touched; only next.config.mjs serverExternalPackages list.
ftdaily added a commit to ftdaily/OmniRoute that referenced this pull request Sep 14, 2026
a5db197 (upstream diegosouzapw#11912/diegosouzapw#13283) added 'import { resolveProviderAlias }
from open-sse/services/model.ts' to src/lib/combos/controlCenter.ts.
That file is imported by ComboControlCenterClient.tsx (a 'use client'
file). Through the import graph webpack reaches open-sse/utils/
cursorImages.ts, which dynamic-imports sharp. sharp transitively
pulls in detect-libc, which statically requires child_process; webpack
refuses to bundle that for the client.

sharp is server-only in this codebase (5 server-side importers, none
client-side), so externalizing it from the client chunk is safe and
consistent with the 21-entry serverExternalPackages list.

Mine-side follow-up after 44-ahead-113-behind merge of upstream
release/v3.8.51. Reviewed by Sub Agent 2 (w3B:p6, rev 32+, fact-sheet
all-green, confidence high).
ftdaily added a commit to ftdaily/OmniRoute that referenced this pull request Sep 14, 2026
a5db197 (upstream diegosouzapw#11912/diegosouzapw#13283) made src/lib/combos/controlCenter.ts
import open-sse/services/model.ts. controlCenter.ts is reachable from
src/app/(dashboard)/dashboard/combos/ComboControlCenterClient.tsx (a
'use client' file). Through model.ts → providerModels.ts →
providerRegistry.ts → providers/index.ts → ... webpack follows the
import graph all the way to:
  - open-sse/services/qoderCli.ts        (import { spawn } from 'child_process')
  - open-sse/executors/codex.ts           (uses node:module)
  - open-sse/config/credentialLoader.ts   (uses fs)
  - node_modules/detect-libc/lib/{detect-libc,filesystem}.js (child_process, fs)

All five chains fail webpack's client-bundle compilation with
'Can't resolve child_process/fs/module'.

This commit removes the resolveProviderAlias import + call from
controlCenter.ts. resolveProviderAlias lives in open-sse/services/model.ts
and was added by the same upstream commit (diegosouzapw#11912). The function was a
display-only label refinement: 'step.providerId || providerFromModel(step.model)'
at line 176 already prefers the combo's stored providerId, and the
providerFromModel call site falls back to the raw prefix when
providerId is absent. Dropping the fallback alias chain here has no
behavioural effect on the rendered UI; the primary upstream fix
(resolveComboTargetModelStr turning 'opencode/<model>' into 'oc/<model>'
per diegosouzapw#11912) is preserved.

Reviewed by Sub Agent 2 (w3B:p6) per user directive.

Mine-side follow-up after 44-ahead-113-behind merge of upstream
release/v3.8.51.
henrique-starfusion added a commit to henrique-starfusion/OmniRoute that referenced this pull request Sep 14, 2026
… client bundle

controlCenter.ts (imported by the 'use client' ComboControlCenterClient) imported resolveProviderAlias from open-sse/services/model.ts, dragging DB/playwright/sharp into the browser bundle and failing the Turbopack build with 168 'Can't resolve' errors (regression from diegosouzapw#13283). Move the alias map and resolver into the pure open-sse/services/providerAlias.ts; model.ts re-exports it.
zhiru added a commit to zhiru/OmniRoute that referenced this pull request Sep 15, 2026
…os client bundle builds

`src/lib/combos/controlCenter.ts` is imported by the "use client"
ComboControlCenterClient.tsx. Since diegosouzapw#13283 it imported `resolveProviderAlias`
from `open-sse/services/model.ts`, whose dynamic `import("@/lib/db/...")`
calls drag ioredis/better-sqlite3 into the browser graph and `next build`
fails with "Module not found: Can't resolve 'tls'" (docker-publish red on
release/v3.8.51).

Extract `ALIAS_TO_PROVIDER_ID` + the manual overrides + `resolveProviderAlias`
into `open-sse/services/providerAlias.ts` (depends only on providerModels).
`model.ts` imports and re-exports it, so every existing caller is unchanged;
`controlCenter.ts` now imports the light module. Regression test asserts the
light module and controlCenter stay free of db/model imports.
seanford added a commit to seanford/OmniRoute that referenced this pull request Sep 15, 2026
…ervice

diegosouzapw#13283 made src/lib/combos/controlCenter.ts — imported by the "use client"
ComboControlCenterClient page — import resolveProviderAlias() from
open-sse/services/model.ts. That module lazily imports the DB layer
(@/lib/db/readCache, @/lib/db/models, …); Turbopack follows those edges into
the client bundle, drags the executors (down to the Playwright-backed zai-web
browser automation) along, and `next build --turbopack` fails with 168
"Module not found: Can't resolve 'child_process' / 'async_hooks'" errors.
Every Docker image build since the 2026-09-11 batch fails the same way.

Move ALIAS_TO_PROVIDER_ID and resolveProviderAlias() into a pure
open-sse/services/providerAlias.ts that depends only on the static provider
registry, re-export both from model.ts so the existing server-side importers
are untouched, and point controlCenter.ts at the pure module.

Verified with an import-graph walk over src/ + open-sse/: at the last commit
that published an image no "use client"/edge entry reached a playwright
importer, at the branch tip exactly one did (this chain), and none do after
this change.
seanford added a commit to seanford/OmniRoute that referenced this pull request Sep 15, 2026
…nts module

diegosouzapw#13264 made open-sse/config/providers/registry/codebuddy-cn/index.ts import
CODEBUDDY_CN_USER_AGENT from src/lib/oauth/constants/oauth.ts. The provider
registry is browser-reachable (dashboard model pickers →
src/shared/constants/models.ts → open-sse/config/providerModels.ts →
providerRegistry → every registry entry), and oauth.ts imports
open-sse/utils/cursorAgentCliVersion.ts, which imports node:fs / node:os /
node:path. With the Playwright edge from diegosouzapw#13283 fixed, `next build --turbopack`
then fails one step later on every dashboard page:

  TurbopackInternalError: Failed to write app endpoint /(dashboard)/dashboard/page
  Caused by: the chunking context (unknown) does not support external modules
  (request: node:fs)

Define the constant in a dependency-free
open-sse/config/providers/registry/codebuddy-cn/userAgent.ts, import it there
from the registry entry, and have oauth.ts import + re-export it so the OAuth
config, chat headers and usage/quota client keep sharing the one string that
diegosouzapw#12702 requires.

Verified with an import-graph walk (client/edge entries → files importing a
Node built-in): the set of reachable chains is now identical to the last
release-branch commit that published a Docker image.
seanford added a commit to seanford/OmniRoute that referenced this pull request Sep 15, 2026
…nts module

diegosouzapw#13264 made open-sse/config/providers/registry/codebuddy-cn/index.ts import
CODEBUDDY_CN_USER_AGENT from src/lib/oauth/constants/oauth.ts. The provider
registry is browser-reachable (dashboard model pickers →
src/shared/constants/models.ts → open-sse/config/providerModels.ts →
providerRegistry → every registry entry), and oauth.ts imports
open-sse/utils/cursorAgentCliVersion.ts, which imports node:fs / node:os /
node:path. With the Playwright edge from diegosouzapw#13283 fixed, `next build --turbopack`
then fails one step later on every dashboard page:

  TurbopackInternalError: Failed to write app endpoint /(dashboard)/dashboard/page
  Caused by: the chunking context (unknown) does not support external modules
  (request: node:fs)

Define the constant in a dependency-free
open-sse/config/providers/registry/codebuddy-cn/userAgent.ts, import it there
from the registry entry, and have oauth.ts import + re-export it so the OAuth
config, chat headers and usage/quota client keep sharing the one string that
diegosouzapw#12702 requires.

Verified with an import-graph walk (client/edge entries → files importing a
Node built-in): the set of reachable chains is now identical to the last
release-branch commit that published a Docker image.
seanford added a commit to seanford/OmniRoute that referenced this pull request Sep 15, 2026
…ervice

diegosouzapw#13283 made src/lib/combos/controlCenter.ts — imported by the "use client"
ComboControlCenterClient page — import resolveProviderAlias() from
open-sse/services/model.ts. That module lazily imports the DB layer
(@/lib/db/readCache, @/lib/db/models, …); Turbopack follows those edges into
the client bundle, drags the executors (down to the Playwright-backed zai-web
browser automation) along, and `next build --turbopack` fails with 168
"Module not found: Can't resolve 'child_process' / 'async_hooks'" errors.
Every Docker image build since the 2026-09-11 batch fails the same way.

Move ALIAS_TO_PROVIDER_ID and resolveProviderAlias() into a pure
open-sse/services/providerAlias.ts that depends only on the static provider
registry, re-export both from model.ts so the existing server-side importers
are untouched, and point controlCenter.ts at the pure module.

Verified with an import-graph walk over src/ + open-sse/: at the last commit
that published an image no "use client"/edge entry reached a playwright
importer, at the branch tip exactly one did (this chain), and none do after
this change.
seanford added a commit to seanford/OmniRoute that referenced this pull request Sep 15, 2026
…nts module

diegosouzapw#13264 made open-sse/config/providers/registry/codebuddy-cn/index.ts import
CODEBUDDY_CN_USER_AGENT from src/lib/oauth/constants/oauth.ts. The provider
registry is browser-reachable (dashboard model pickers →
src/shared/constants/models.ts → open-sse/config/providerModels.ts →
providerRegistry → every registry entry), and oauth.ts imports
open-sse/utils/cursorAgentCliVersion.ts, which imports node:fs / node:os /
node:path. With the Playwright edge from diegosouzapw#13283 fixed, `next build --turbopack`
then fails one step later on every dashboard page:

  TurbopackInternalError: Failed to write app endpoint /(dashboard)/dashboard/page
  Caused by: the chunking context (unknown) does not support external modules
  (request: node:fs)

Define the constant in a dependency-free
open-sse/config/providers/registry/codebuddy-cn/userAgent.ts, import it there
from the registry entry, and have oauth.ts import + re-export it so the OAuth
config, chat headers and usage/quota client keep sharing the one string that
diegosouzapw#12702 requires.

Verified with an import-graph walk (client/edge entries → files importing a
Node built-in): the set of reachable chains is now identical to the last
release-branch commit that published a Docker image.
seanford added a commit to seanford/OmniRoute that referenced this pull request Sep 15, 2026
…nerically

The guard test from diegosouzapw#10692 only knew a hand-written list of server-only
modules and ignored dynamic `import()`, so neither of the two regressions
fixed in this PR (diegosouzapw#13264: registry entry → oauth constants → node:fs;
diegosouzapw#13283: combo control center → model.ts → lazy DB import → child_process)
could fail PR CI — they only surfaced in the next Docker build.

- Treat any first-party module that imports a non-polyfillable Node
  builtin (fs, net, tls, child_process, async_hooks, worker_threads, …)
  as server-only, in addition to the explicit list. Polyfilled builtins
  (path, crypto, buffer, …) are deliberately not listed.
- Follow dynamic `import("…")` edges: the bundler still has to build the
  lazy chunk, so a builtin behind one fails the build exactly like a
  static import.
- Report each offending edge once with an example chain and a count
  instead of one chain per client entry point.

On release/v3.8.51 without the two fixes the test now fails with exactly
the two chains above; with them it passes.
seanford added a commit to seanford/OmniRoute that referenced this pull request Sep 15, 2026
…nerically

The guard test from diegosouzapw#10692 only knew a hand-written list of server-only
modules and ignored dynamic `import()`, so neither of the two regressions
fixed in this PR (diegosouzapw#13264: registry entry → oauth constants → node:fs;
diegosouzapw#13283: combo control center → model.ts → lazy DB import → child_process)
could fail PR CI — they only surfaced in the next Docker build.

- Treat any first-party module that imports a non-polyfillable Node
  builtin (fs, net, tls, child_process, async_hooks, worker_threads, …)
  as server-only, in addition to the explicit list. Polyfilled builtins
  (path, crypto, buffer, …) are deliberately not listed.
- Follow dynamic `import("…")` edges: the bundler still has to build the
  lazy chunk, so a builtin behind one fails the build exactly like a
  static import.
- Report each offending edge once with an example chain and a count
  instead of one chain per client entry point.

On release/v3.8.51 without the two fixes the test now fails with exactly
the two chains above; with them it passes.
Githab-capibara added a commit to Githab-capibara/OmniRoute that referenced this pull request Sep 17, 2026
… onto opencode-zen (diegosouzapw#11912) (diegosouzapw#13283)

Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on diegosouzapw#13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for diegosouzapw#13278 and diegosouzapw#13276, annotated and landed on diegosouzapw#13243

⚠️ base-red inherited: diegosouzapw#12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
… onto opencode-zen (diegosouzapw#11912) (diegosouzapw#13283)

Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on diegosouzapw#13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for diegosouzapw#13278 and diegosouzapw#13276, annotated and landed on diegosouzapw#13243

⚠️ base-red inherited: diegosouzapw#12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
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.

fix(providers): round-robin combo routes all requests to opencode-zen due to connection/alias collision with opencode

2 participants