Skip to content

fix(gateway): handle auto-routed 4xxs - #1822

Closed
steebchen wants to merge 3 commits into
mainfrom
ariana/klein-d945
Closed

steebchen wants to merge 3 commits into
mainfrom
ariana/klein-d945

Conversation

@steebchen

@steebchen steebchen commented Mar 12, 2026 •

Copy link
Copy Markdown
Member

Summary

  • retry auto-routed 4xx provider responses so provider-specific client and access errors can fall through to the next scored provider immediately
  • keep explicit provider requests fail-loud while tightening finish-reason classification so only 401, 403, 404, and 429 stay in the gateway/upstream buckets and other 4xx responses become client_error
  • add focused unit coverage for retry/error classification and extend the mock fallback test server with a one-shot 404 trigger for the fallback spec

Test plan

  • pnpm vitest run apps/gateway/src/chat/tools/retry-with-fallback.spec.ts
  • pnpm vitest run apps/gateway/src/chat/tools/get-finish-reason-from-error.spec.ts apps/gateway/src/chat/tools/retry-with-fallback.spec.ts
  • pnpm exec prettier --check apps/gateway/src/chat/tools/get-finish-reason-from-error.ts apps/gateway/src/chat/tools/get-finish-reason-from-error.spec.ts apps/gateway/src/chat/tools/retry-with-fallback.ts apps/gateway/src/chat/tools/retry-with-fallback.spec.ts apps/gateway/src/test-utils/mock-openai-server.ts apps/gateway/src/fallback.spec.ts
  • pnpm vitest run apps/gateway/src/fallback.spec.ts (blocked in this environment: local PostgreSQL and Redis are not running)

🤖 Generated with Codex

Summary by CodeRabbit

  • Bug Fixes

    • Auto-selected provider flows now retry on 4xx errors (including 404) and provide clearer retry/fallback outcomes.
    • Error classifications updated (e.g., 401/403 treated as gateway errors, 404 as upstream, others like 400/422 as client errors) for more accurate handling and logs.
  • Tests

    • Added tests covering retries on 404 and other 4xx codes with fallback providers.
    • Test utilities enhanced to simulate one-time 404 failures and verify retry logging and outcome linkage.

Copilot AI review requested due to automatic review settings March 12, 2026 10:48
@coderabbitai

coderabbitai Bot commented Mar 12, 2026 •

Copy link
Copy Markdown
Contributor

Walkthrough

Treat 4xx responses (including 404) as retryable for auto-selected providers, add a one-shot 404 trigger in the mock OpenAI test server, and add tests verifying fallback retries succeed when an initial provider returns 404.

Changes

Cohort / File(s) Summary
Retry logic & tests
apps/gateway/src/chat/tools/retry-with-fallback.ts, apps/gateway/src/chat/tools/retry-with-fallback.spec.ts
isRetryableError expanded to include 4xx (400–499) as retryable for auto-selected providers; getErrorType mappings updated (401/403 → gateway_error, 404 → upstream_error, other 4xx → client_error); tests added/adjusted to assert retries on 404 and other 4xx when provider was auto-selected.
Fallback integration tests
apps/gateway/src/fallback.spec.ts
Added non-streaming test "retries on 404 and succeeds on fallback provider" verifying initial 404, retry to fallback, multiple routing entries, log entries, and DB linkage between failed and retried attempts.
Mock OpenAI test server
apps/gateway/src/test-utils/mock-openai-server.ts
Added TRIGGER_STATUS_404_ONCE handling with fail404OnceCounter to return a single model-not-found 404 for matching requests; extended reset helper to clear the new counter and integrated new logic into /v1/chat/completions flow.
Error-to-finish-reason logic & tests
apps/gateway/src/chat/tools/get-finish-reason-from-error.ts, apps/gateway/src/chat/tools/get-finish-reason-from-error.spec.ts
Classified 401/403 as gateway_error, general 4xx (400–499) as client_error where applicable; tests updated to expect 400/422 map to client_error instead of gateway_error.

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client
  participant Gateway as Gateway
  participant ProviderA as Provider A
  participant ProviderB as Provider B
  participant DB as DB

  Client->>Gateway: send request (no explicit provider)
  Gateway->>Gateway: route -> auto-select Provider A
  Gateway->>ProviderA: forward request
  ProviderA-->>Gateway: 404 (model-not-found)
  Gateway->>DB: log failed routing (status 404, retried=true)
  Gateway->>ProviderB: retry request to fallback provider
  ProviderB-->>Gateway: 200 OK (response)
  Gateway->>DB: log successful routing (link retry -> success)
  Gateway-->>Client: return successful response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

auto-merge

Suggested reviewers

  • smakosh
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: handling auto-routed 4xx errors, with a focus on 404 retry logic as the primary improvement.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ariana/klein-d945

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 and usage tips.

Copilot AI 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.

Pull request overview

Adds 404 as a retryable status code in the gateway's retry-with-fallback logic, so that auto-routed requests that hit a provider returning 404 (e.g., model-not-found or access drift) fall back to the next scored provider.

Changes:

  • isRetryableError now treats 404 as retryable alongside 429, 5xx, and network errors
  • New unit tests for 404 retry behavior and an integration test exercising 404→fallback success
  • Mock OpenAI server extended with a TRIGGER_STATUS_404_ONCE one-shot trigger

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
apps/gateway/src/chat/tools/retry-with-fallback.ts Add 404 to retryable status codes
apps/gateway/src/chat/tools/retry-with-fallback.spec.ts Unit tests for 404 retry behavior
apps/gateway/src/fallback.spec.ts Integration test for 404 fallback
apps/gateway/src/test-utils/mock-openai-server.ts One-shot 404 trigger for tests

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/gateway/src/fallback.spec.ts (1)

830-875: Assert that the provider actually changes.

Because the mock TRIGGER_STATUS_404_ONCE succeeds on the second matching request regardless of provider, this spec currently proves “retry” but not “fallback”. Please compare the first and last routing[*].provider values (and/or failedLog.usedProvider vs successLog.usedProvider) so a same-provider retry cannot pass.

💡 Suggested tightening
 			expect(json.metadata.routing[0]).toHaveProperty("status_code", 404);
 			expect(json.metadata.routing[0]).toHaveProperty("succeeded", false);
-			expect(
-				json.metadata.routing[json.metadata.routing.length - 1],
-			).toHaveProperty("succeeded", true);
+			const lastAttempt =
+				json.metadata.routing[json.metadata.routing.length - 1];
+			expect(lastAttempt).toHaveProperty("succeeded", true);
+			expect(json.metadata.routing[0].provider).not.toBe(lastAttempt.provider);
@@
 			const failedLog = logs.find((l: Log) => l.hasError);
 			expect(failedLog).toBeDefined();
+			expect(failedLog!.usedProvider).not.toBe(successLog!.usedProvider);
 			expect(failedLog!.finishReason).toBe("upstream_error");
 			expect(failedLog!.retried).toBe(true);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/gateway/src/fallback.spec.ts` around lines 830 - 875, Update the test
"non-streaming: retries on 404 and succeeds on fallback provider" to assert that
the request actually fell back to a different provider: after parsing
json.metadata.routing, compare the first routing entry's provider to the last
routing entry's provider (e.g., json.metadata.routing[0].provider !==
json.metadata.routing[json.metadata.routing.length - 1].provider) and/or compare
failedLog.usedProvider to successLog.usedProvider (they must differ) so a
same-provider retry cannot satisfy the test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/gateway/src/test-utils/mock-openai-server.ts`:
- Around line 241-254: The generic status parser (extractStatusCodeTrigger) is
matching the prefix TRIGGER_STATUS_404 inside TRIGGER_STATUS_404_ONCE so the
earlier handler returns before the "_ONCE" branch (which uses fail404OnceCounter
and userMessage) can run; update the mock-openai-server logic to check for the
specific *_ONCE triggers before invoking the generic status parser (or update
extractStatusCodeTrigger to prefer full-string matches including the "_ONCE"
suffix), ensuring TRIGGER_STATUS_404_ONCE is detected first so the
fail404OnceCounter branch executes exactly once and subsequent requests follow
the normal flow.

---

Nitpick comments:
In `@apps/gateway/src/fallback.spec.ts`:
- Around line 830-875: Update the test "non-streaming: retries on 404 and
succeeds on fallback provider" to assert that the request actually fell back to
a different provider: after parsing json.metadata.routing, compare the first
routing entry's provider to the last routing entry's provider (e.g.,
json.metadata.routing[0].provider !==
json.metadata.routing[json.metadata.routing.length - 1].provider) and/or compare
failedLog.usedProvider to successLog.usedProvider (they must differ) so a
same-provider retry cannot satisfy the test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f91d1a8f-f020-4330-9e3e-fe2bf3211a0a

📥 Commits

Reviewing files that changed from the base of the PR and between a0a6db1 and e0c86f6.

📒 Files selected for processing (4)
  • apps/gateway/src/chat/tools/retry-with-fallback.spec.ts
  • apps/gateway/src/chat/tools/retry-with-fallback.ts
  • apps/gateway/src/fallback.spec.ts
  • apps/gateway/src/test-utils/mock-openai-server.ts

Comment on lines +241 to +254
if (userMessage.includes("TRIGGER_STATUS_404_ONCE")) {
fail404OnceCounter++;
if (fail404OnceCounter === 1) {
c.status(404);
return c.json({
error: {
message: "The model 'nonexistent-model' does not exist.",
type: "invalid_request_error",
param: "model",
code: "model_not_found",
},
});
}
}

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.

⚠️ Potential issue | 🟠 Major

The _ONCE trigger is currently swallowed by the generic status parser.

extractStatusCodeTrigger() on Line 60 already matches the TRIGGER_STATUS_404 prefix inside TRIGGER_STATUS_404_ONCE, and the earlier generic handler starting at Line 217 returns before this branch is reached. In apps/gateway/src/fallback.spec.ts Line 842, that means every attempt sees a 404 instead of “404 once, then succeed”.

💡 Suggested fix
 function extractStatusCodeTrigger(
 	content: string,
 ): { statusCode: number; errorResponse: object } | null {
-	const match = content.match(/TRIGGER_STATUS_(\d{3})/);
+	const match = content.match(/\bTRIGGER_STATUS_(\d{3})(?!_ONCE)\b/);
 	if (!match) {
 		return null;
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/gateway/src/test-utils/mock-openai-server.ts` around lines 241 - 254,
The generic status parser (extractStatusCodeTrigger) is matching the prefix
TRIGGER_STATUS_404 inside TRIGGER_STATUS_404_ONCE so the earlier handler returns
before the "_ONCE" branch (which uses fail404OnceCounter and userMessage) can
run; update the mock-openai-server logic to check for the specific *_ONCE
triggers before invoking the generic status parser (or update
extractStatusCodeTrigger to prefer full-string matches including the "_ONCE"
suffix), ensuring TRIGGER_STATUS_404_ONCE is detected first so the
fail404OnceCounter branch executes exactly once and subsequent requests follow
the normal flow.

@steebchen steebchen changed the title fix(gateway): retry auto-routed 404s fix(gateway): handle auto-routed 4xxs Mar 12, 2026

@coderabbitai coderabbitai 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/gateway/src/chat/tools/get-finish-reason-from-error.ts (1)

32-48: ⚠️ Potential issue | 🔴 Critical

Dead code: xAI content filter check for 403 is unreachable.

The 401/403 check at lines 33-35 returns "gateway_error" before the xAI content filter check at lines 43-48 can execute. This means xAI Grok responses with "Content violates usage guidelines" will incorrectly return "gateway_error" instead of "content_filter".

🐛 Proposed fix: Move content filter check before 401/403 handling
 	// 404 from upstream provider indicates model/endpoint not found at provider
 	if (statusCode === 404) {
 		return "upstream_error";
 	}

+	// xAI (Grok) content safety violations (e.g. SAFETY_CHECK_TYPE_CSAM, usage guidelines)
+	if (
+		statusCode === 403 &&
+		errorText?.includes("Content violates usage guidelines")
+	) {
+		return "content_filter";
+	}
+
 	// 401/403 indicate gateway-side auth/configuration issues
 	if (statusCode === 401 || statusCode === 403) {
 		return "gateway_error";
 	}

 	// Azure OpenAI content filter (ResponsibleAIPolicyViolation)
 	if (errorText?.includes("ResponsibleAIPolicyViolation")) {
 		return "content_filter";
 	}

-	// xAI (Grok) content safety violations (e.g. SAFETY_CHECK_TYPE_CSAM, usage guidelines)
-	if (
-		statusCode === 403 &&
-		errorText?.includes("Content violates usage guidelines")
-	) {
-		return "content_filter";
-	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/gateway/src/chat/tools/get-finish-reason-from-error.ts` around lines 32
- 48, The xAI/Grok content-filter branch is unreachable because the earlier
401/403 check returns "gateway_error" before the errorText check runs; in
get-finish-reason-from-error (the function handling statusCode and errorText)
move the xAI content safety check that looks for "Content violates usage
guidelines" (and the Azure "ResponsibleAIPolicyViolation" check) to run before
the generic 401/403 block, or adjust the conditional ordering so that when
errorText includes the content-filter strings it returns "content_filter" even
if statusCode === 403; update the logic in that function accordingly to ensure
content_filter takes precedence over gateway_error for those errorText matches.
🧹 Nitpick comments (1)
apps/gateway/src/chat/tools/get-finish-reason-from-error.spec.ts (1)

73-76: Consider adding test coverage for xAI content filter edge case.

There's an existing test for 401/403 returning "gateway_error", but no test verifying the xAI content filter case where a 403 with "Content violates usage guidelines" should return "content_filter". This test would help catch the ordering bug mentioned in the implementation review.

🧪 Suggested test to add
it("returns content_filter for xAI 403 content violation before gateway_error check", () => {
	expect(
		getFinishReasonFromError(403, "Content violates usage guidelines"),
	).toBe("content_filter");
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/gateway/src/chat/tools/get-finish-reason-from-error.spec.ts` around
lines 73 - 76, Add a unit test in get-finish-reason-from-error.spec.ts to cover
the xAI content filter edge case: call getFinishReasonFromError with status 403
and the message "Content violates usage guidelines" and assert it returns
"content_filter" (this ensures the xAI content-filter branch in
getFinishReasonFromError is checked before the generic 403 -> "gateway_error"
handling).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@apps/gateway/src/chat/tools/get-finish-reason-from-error.ts`:
- Around line 32-48: The xAI/Grok content-filter branch is unreachable because
the earlier 401/403 check returns "gateway_error" before the errorText check
runs; in get-finish-reason-from-error (the function handling statusCode and
errorText) move the xAI content safety check that looks for "Content violates
usage guidelines" (and the Azure "ResponsibleAIPolicyViolation" check) to run
before the generic 401/403 block, or adjust the conditional ordering so that
when errorText includes the content-filter strings it returns "content_filter"
even if statusCode === 403; update the logic in that function accordingly to
ensure content_filter takes precedence over gateway_error for those errorText
matches.

---

Nitpick comments:
In `@apps/gateway/src/chat/tools/get-finish-reason-from-error.spec.ts`:
- Around line 73-76: Add a unit test in get-finish-reason-from-error.spec.ts to
cover the xAI content filter edge case: call getFinishReasonFromError with
status 403 and the message "Content violates usage guidelines" and assert it
returns "content_filter" (this ensures the xAI content-filter branch in
getFinishReasonFromError is checked before the generic 403 -> "gateway_error"
handling).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 81e27dc6-c3d4-4f93-807b-471176d473bb

📥 Commits

Reviewing files that changed from the base of the PR and between a95f277 and 0ffc96e.

📒 Files selected for processing (4)
  • apps/gateway/src/chat/tools/get-finish-reason-from-error.spec.ts
  • apps/gateway/src/chat/tools/get-finish-reason-from-error.ts
  • apps/gateway/src/chat/tools/retry-with-fallback.spec.ts
  • apps/gateway/src/chat/tools/retry-with-fallback.ts

@steebchen
steebchen enabled auto-merge March 12, 2026 11:45
@steebchen
steebchen disabled auto-merge March 12, 2026 11:46
@steebchen steebchen closed this Mar 12, 2026
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.

2 participants