Skip to content

fix(sse): skip disabled providers in combo fallback (404 instead of 400) - #4500

Merged
diegosouzapw merged 1 commit into
release/v3.8.33from
fix/port-pr-336-skip-disabled-combo
Jun 21, 2026
Merged

diegosouzapw merged 1 commit into
release/v3.8.33from
fix/port-pr-336-skip-disabled-combo

Conversation

@diegosouzapw

@diegosouzapw diegosouzapw commented Jun 21, 2026 •

Copy link
Copy Markdown
Owner

Summary

Ports an upstream fix by Ibrahim Ryan (@East-rayyy) into OmniRoute.

When a combo target (e.g. antigravity/opus → github/opus) hit a leg whose only configured connections were disabled, handleNoCredentials in src/sse/handlers/chatHelpers.ts returned 400 BAD_REQUEST. The combo target loop (open-sse/services/combo.ts) treats 400 as a hard stop — added in PR #4316 / issue #4279 to prevent infinite fallback loops on body-specific 4xx errors — so the combo died on the first leg even when the next target was perfectly healthy.

The "no usable credentials, no connections tried yet" branch now returns 404 NOT_FOUND with "No active credentials for provider: <p>" instead. 404 flows through checkFallbackError as shouldFallback: true via the generic-error catch-all path in open-sse/services/accountFallback.ts:1593-1599 (no explicit 404 special-case + transient cooldown), so the combo loop tries the next target. The log level for this branch also drops from error → warn because zero active credentials is an expected operator-driven state, not a server fault.

The sibling branches in handleNoCredentials were intentionally left untouched:

  • allRateLimited already returns the upstream status (combo-fallbackable).
  • allExpired already returns 401 with a reconnect hint (combo-fallbackable via checkFallbackError).
  • The post-exhaustion branch (after at least one connection was tried) already preserves lastStatus.

Routes that emit 400 No credentials for provider directly (/v1/rerank, /v1/audio/{speech,transcriptions}, /v1/moderations, /v1/images/edits, /v1/providers/[provider]/embeddings) are untouched — they don't flow through combo, so the 400-hard-stop semantics don't apply.

Co-authored-by: Ibrahim Ryan ryan@nuevanext.com

Test plan

  • TDD-validated per Hard Rule fix(ci): add environment for npm token access #18: failing-then-passing test in tests/unit/chat-helpers.test.ts reproduces the bug (400 ≠ 404), the fix turns it green, becomes the regression guard.
  • All 6 affected test files re-aligned and green:
    • tests/unit/chat-helpers.test.ts — 22/22
    • tests/unit/vscode-token-routes.test.ts — 37/37 (also flips error.code from bad_request → model_not_found per the OpenAI-compatible status mapping in open-sse/config/errorConfig.ts:29)
    • tests/unit/chat-route-coverage.test.ts — 14/14
    • tests/integration/chat-pipeline.test.ts — 28/29 (the 1 unrelated failure is a pre-existing Codex CLI fingerprint JSON-body-order check that is also red on the rebased base branch without this PR's changes)
    • tests/integration/llama-cpp-provider.test.ts — 3/3
    • tests/integration/combo-routing-e2e.test.ts — 9/9
  • Sibling test tests/unit/auth-clear-provider-routes.test.ts for /v1/moderations (which still emits 400) confirmed green — 4/4 — proving the change is scoped to the chat-handler path.
  • npm run typecheck:core clean.
  • Lint clean: 0 errors on the 7 changed files (only pre-existing no-explicit-any warnings).

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request ports a change from upstream to return a 404 NOT_FOUND status instead of 400 BAD_REQUEST when no active credentials are found for a provider, allowing combo routing to fall through to subsequent targets. The log level has also been downgraded from error to warning, and all relevant integration and unit tests have been updated. The reviewer feedback is highly accurate and valuable, pointing out that the newly added comments in chatHelpers.ts and chat-helpers.test.ts incorrectly describe how the 404 status code is processed by the fallback engine. Updating these comments to correctly reference the status_404 rule in errorConfig.ts will prevent future maintainer confusion.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +588 to +597
// Ported from upstream decolua/9router#336 (Ibrahim Ryan): surface as 404
// NOT_FOUND instead of 400 BAD_REQUEST so combo routing can fall through to
// the next target. The combo target loop (open-sse/services/combo.ts) treats
// 400 as a hard stop to break body-specific infinite fallback loops
// (PR #4316 / issue #4279). 404 flows through checkFallbackError as
// `shouldFallback: true` (generic-error catch-all path in
// open-sse/services/accountFallback.ts), letting a combo like
// `antigravity/opus → github/opus` skip a provider whose credentials are
// all disabled. log level is `warn` rather than `error` because zero active
// credentials is an expected operator-driven state, not a server fault.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The comment states that 404 flows through the generic-error catch-all path in open-sse/services/accountFallback.ts (lines 1593-1599). However, checkFallbackError actually matches the status_404 rule defined in ERROR_RULES (in open-sse/config/errorConfig.ts line 156) via findMatchingErrorRule, which returns shouldFallback: true with a 2-minute cooldown (COOLDOWN_MS.notFound).

While this doesn't negatively affect the behavior (since there are no active credentials to apply a cooldown to anyway), we should correct the comment to avoid misleading future maintainers about how the 404 status code is routed through the fallback engine.

Suggested change
// Ported from upstream decolua/9router#336 (Ibrahim Ryan): surface as 404
// NOT_FOUND instead of 400 BAD_REQUEST so combo routing can fall through to
// the next target. The combo target loop (open-sse/services/combo.ts) treats
// 400 as a hard stop to break body-specific infinite fallback loops
// (PR #4316 / issue #4279). 404 flows through checkFallbackError as
// `shouldFallback: true` (generic-error catch-all path in
// open-sse/services/accountFallback.ts), letting a combo like
// `antigravity/opus → github/opus` skip a provider whose credentials are
// all disabled. log level is `warn` rather than `error` because zero active
// credentials is an expected operator-driven state, not a server fault.
// Ported from upstream decolua/9router#336 (Ibrahim Ryan): surface as 404
// NOT_FOUND instead of 400 BAD_REQUEST so combo routing can fall through to
// the next target. The combo target loop (open-sse/services/combo.ts) treats
// 400 as a hard stop to break body-specific infinite fallback loops
// (PR #4316 / issue #4279). 404 flows through checkFallbackError as
// `shouldFallback: true` (matching the status_404 rule in
// open-sse/config/errorConfig.ts), letting a combo like
// `antigravity/opus → github/opus` skip a provider whose credentials are
// all disabled. log level is `warn` rather than `error` because zero active
// credentials is an expected operator-driven state, not a server fault.

Comment on lines +250 to +262
// Ported from upstream decolua/9router#336 (Ibrahim Ryan): when a provider has
// zero usable connections (all disabled, or none configured at all), the
// historical 400 BAD_REQUEST classified the failure as non-fallbackable, so a
// combo like `antigravity/opus → github/opus` died on the first leg with a
// hard 400 even though the next combo target was perfectly healthy.
//
// The combo target loop (open-sse/services/combo.ts) deliberately breaks on
// 400 to prevent infinite fallback loops with body-specific 4xx errors
// (#4279/PR#4316). 404 NOT_FOUND, by contrast, flows through checkFallbackError
// as `shouldFallback: true` (generic-error catch-all path,
// open-sse/services/accountFallback.ts:1593-1599) so the next combo target is
// tried. We surface "no active credentials" as 404 so combo can skip past a
// disabled-credentials provider instead of failing the whole request.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Similar to the comment in chatHelpers.ts, 404 actually matches the status_404 rule in ERROR_RULES (defined in open-sse/config/errorConfig.ts) rather than flowing through the generic-error catch-all path. Let's update this comment to be technically accurate.

  // Ported from upstream decolua/9router#336 (Ibrahim Ryan): when a provider has
  // zero usable connections (all disabled, or none configured at all), the
  // historical 400 BAD_REQUEST classified the failure as non-fallbackable, so a
  // combo like `antigravity/opus → github/opus` died on the first leg with a
  // hard 400 even though the next combo target was perfectly healthy.
  //
  // The combo target loop (open-sse/services/combo.ts) deliberately breaks on
  // 400 to prevent infinite fallback loops with body-specific 4xx errors
  // (#4279/PR#4316). 404 NOT_FOUND, by contrast, flows through checkFallbackError
  // as `shouldFallback: true` (matching the status_404 rule in
  // open-sse/config/errorConfig.ts) so the next combo target is
  // tried. We surface "no active credentials" as 404 so combo can skip past a
  // disabled-credentials provider instead of failing the whole request.

@diegosouzapw
diegosouzapw changed the base branch from release/v3.8.32 to release/v3.8.33 June 21, 2026 14:02
When a combo target (e.g. `antigravity/opus → github/opus`) hit a leg whose
only configured connections were disabled, `handleNoCredentials` in
`src/sse/handlers/chatHelpers.ts` returned 400 BAD_REQUEST. The combo target
loop treats 400 as a hard stop — added by PR #4316 / issue #4279 to prevent
infinite fallback loops on body-specific 4xx errors — so the combo died on
the first leg even when the next target was perfectly healthy.

The "no usable credentials, no connections tried yet" branch now returns
404 NOT_FOUND with the message "No active credentials for provider: <p>"
instead. 404 flows through `checkFallbackError` as `shouldFallback: true`
via the generic-error catch-all path in `open-sse/services/accountFallback.ts`
(no explicit 404 special-case + transient cooldown), so the combo loop tries
the next target. The log level for this branch also drops from `error` to
`warn` because zero active credentials is an expected operator-driven state,
not a server fault.

The sibling branches in `handleNoCredentials` were left untouched:
- `allRateLimited` already returns the upstream status (combo-fallbackable).
- `allExpired` already returns 401 with a reconnect hint (combo-fallbackable
  via `checkFallbackError`).
- The post-exhaustion branch (after at least one connection tried) already
  preserves `lastStatus`.

Tests updated to encode the new contract (TDD: failing-then-passing per
Hard Rule #18): `chat-helpers`, `chat-route-coverage`, `vscode-token-routes`,
`chat-pipeline`, `llama-cpp-provider`, `combo-routing-e2e`. The `vscode-token-routes`
assertions also flip `error.code` from `bad_request` to `model_not_found` per
the OpenAI-compatible status mapping in `open-sse/config/errorConfig.ts:29`.
Routes that emit `400 No credentials for provider` directly (rerank, audio,
moderations, image edits, embeddings) are untouched — they don't flow through
combo, so the 400 hard-stop semantics don't apply.

Inspired-by upstream decolua/9router#336 (Ibrahim Ryan).

Co-authored-by: Ibrahim Ryan <ryan@nuevanext.com>
@diegosouzapw
diegosouzapw force-pushed the fix/port-pr-336-skip-disabled-combo branch from dbfc2ff to 595719f Compare June 21, 2026 17:05
@diegosouzapw
diegosouzapw merged commit 4279751 into release/v3.8.33 Jun 21, 2026
3 checks passed
@diegosouzapw diegosouzapw mentioned this pull request Jun 22, 2026
@diegosouzapw
diegosouzapw deleted the fix/port-pr-336-skip-disabled-combo branch June 22, 2026 02:48
tkgo11 pushed a commit to tkgo11/OmniRoute that referenced this pull request Sep 23, 2026
Rebuilt onto release/v3.8.33 (squash-base-stale). Integrated into release/v3.8.33.
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