Skip to content

fix: respect provider region limits - #1909

Merged
steebchen merged 3 commits into
mainfrom
fix/respecct
Mar 28, 2026
Merged

steebchen merged 3 commits into
mainfrom
fix/respecct

Conversation

@steebchen

@steebchen steebchen commented Mar 28, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • Bug Fixes

    • Requests with requested max_tokens exceeding a model’s maxOutput now return clear 400 errors instead of silent capping.
    • Providers whose maxOutput is insufficient are excluded from routing, retries, and fallback decisions.
  • Improvements

    • Provider candidates are collapsed to the best region per provider and cheaper regional mappings are preferred; routing uses an expanded, region-filtered candidate set kept in sync with model info.
    • IAM-based provider filtering and fallback logic now operate on the expanded candidate set.
  • Tests

    • Added routing tests covering region selection, exclusion by max_tokens, and routing-metadata assertions.

Copilot AI review requested due to automatic review settings March 28, 2026 15:59
@coderabbitai

coderabbitai Bot commented Mar 28, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: dcd3d4bd-d864-42a4-8aff-4342c02d715b

📥 Commits

Reviewing files that changed from the base of the PR and between ce798ff and 8e5a984.

📒 Files selected for processing (1)
  • apps/gateway/src/fallback.spec.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/gateway/src/fallback.spec.ts

Walkthrough

Collapse multi-region provider candidates to a single best region per provider, apply max_tokens-based eligibility against provider maxOutput across routing and fallbacks, sync expanded per-region provider sets with IAM filtering, and enforce max_tokens validation prior to requests.

Changes

Cohort / File(s) Summary
Provider Selection & Routing Logic
apps/gateway/src/chat/chat.ts
Added collapseProvidersToBestRegionPerProvider() to choose a single region per provider before cost-based routing; introduced routingExpandedModelProviders and expandedIamFilteredModelProviders; applied maxTokens eligibility checks across routing, low-uptime fallback, and multi-provider fallback; update cheapest-selection flow to use collapsed candidates and persist selections back into modelInfo.providers.
Request Validation
apps/gateway/src/chat/tools/resolve-provider-context.ts
Reject requests where max_tokens > provider/model maxOutput (both pre-request and after prepareRequestBody) by throwing HTTP 400 with details.
Tests
apps/gateway/src/fallback.spec.ts
Added tests: ignore synthetic root region mappings in auto routing; exclude providers whose maxOutput < max_tokens; changed routing-metadata assertions to partial matching and tightened expected region checks.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Client
    participant Gateway
    participant ProviderSelection
    participant Metrics

    Client->>Gateway: POST /chat (includes max_tokens)
    Gateway->>ProviderSelection: Resolve modelInfo, expand regional mappings
    ProviderSelection->>ProviderSelection: Filter candidates by maxOutput >= max_tokens
    Gateway->>Metrics: Fetch provider metrics/pricing
    ProviderSelection->>ProviderSelection: Collapse providers to best region per provider (rgba(0,128,0,0.5))
    ProviderSelection->>ProviderSelection: Select cheapest provider from collapsed set
    Gateway->>Client: Route request with usedProvider/usedModel and routingMetadata
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

auto-merge

Suggested reviewers

  • smakosh
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: respect provider region limits' directly relates to the main change: implementing region-specific filtering for provider selection and respecting maxOutput limits.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/respecct

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.

@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: e4f834527d

ℹ️ 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 thread apps/gateway/src/chat/chat.ts Outdated
Comment on lines +971 to +975
let expandedIamFilteredModelProviders = iamAllowedProviders
? expandedActiveModelProviders.filter((p) =>
iamAllowedProviders.includes(p.providerId),
)
: expandedActiveModelProviders;

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 Apply region-key filtering to expanded IAM providers

expandedIamFilteredModelProviders is built from expandedActiveModelProviders, but that expanded list is created before the credits/hybrid region-limit filters run. Later routing paths use this expanded IAM list for provider selection, which reintroduces non-default regions that were intentionally excluded by filterRegionsByAvailableKeys/hybrid filtering. In credits or hybrid projects without region-specific keys, this can select an endpoint region the configured token cannot use, causing avoidable routing failures.

Useful? React with 👍 / 👎.

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

Updates gateway routing to better respect provider region constraints by preferring concrete regional mappings over synthetic/root mappings and by filtering out providers that can’t satisfy a request’s max_tokens.

Changes:

  • Add routing logic to collapse multiple regional candidates down to the best region per provider and avoid scoring/choosing synthetic root region mappings.
  • Extend provider eligibility filtering to exclude mappings whose maxOutput is below the request’s max_tokens.
  • Update/extend fallback routing tests to cover synthetic root region mappings and maxOutput-based exclusion.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
apps/gateway/src/fallback.spec.ts Adds routing tests for ignoring synthetic/root region mappings and for maxOutput-based provider exclusion; adjusts routing metadata assertions.
apps/gateway/src/chat/tools/resolve-provider-context.ts Changes behavior from silently capping max_tokens to throwing a 400 when exceeding provider mapping maxOutput.
apps/gateway/src/chat/chat.ts Adds per-provider “best region” collapsing and maxTokens eligibility filtering; updates routing to use expanded regional providers in more paths.

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

Comment thread apps/gateway/src/chat/chat.ts Outdated
Comment on lines +971 to +975
let expandedIamFilteredModelProviders = iamAllowedProviders
? expandedActiveModelProviders.filter((p) =>
iamAllowedProviders.includes(p.providerId),
)
: expandedActiveModelProviders;

Copilot AI Mar 28, 2026

Copy link

Choose a reason for hiding this comment

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

expandedIamFilteredModelProviders is derived from expandedActiveModelProviders computed before requested-region filtering and before credits/hybrid region filtering (filterRegionsByAvailableKeys/filterHybridRegions). Later routing paths use this expanded list (e.g. in low-uptime fallback and normal routing), which can re-introduce regions that were intentionally filtered out and potentially route to a region without an available key or outside the explicitly requested region. Consider deriving the expanded+IAM-filtered list from the already-filtered modelInfo.providers (and applying the same project-mode region filtering to the expanded list), so routing cannot pick an ineligible region.

Suggested change
let expandedIamFilteredModelProviders = iamAllowedProviders
? expandedActiveModelProviders.filter((p) =>
iamAllowedProviders.includes(p.providerId),
)
: expandedActiveModelProviders;
// Ensure the expanded IAM-filtered providers cannot reintroduce regions
// that were filtered out from modelInfo.providers (e.g. due to region
// constraints or missing keys) by deriving them directly from the
// IAM-filtered model providers.
let expandedIamFilteredModelProviders = iamFilteredModelProviders;

Copilot uses AI. Check for mistakes.
Comment on lines +1079 to +1085
id: "glm-4-6-alibaba-cn-beijing-max-tokens",
modelId: "glm-4.6",
providerId: "alibaba",
modelName: "glm-4.6:cn-beijing",
region: "cn-beijing",
streaming: true,
},

Copilot AI Mar 28, 2026

Copy link

Choose a reason for hiding this comment

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

This test asserts routing excludes providers when max_tokens exceeds a provider mapping’s maxOutput, but none of the inserted modelProviderMapping rows set maxOutput (so it will be NULL/undefined and the new filtering won’t exclude anything). To make this test meaningful and deterministic, set a concrete maxOutput for the provider you expect to be excluded (e.g. Alibaba < 20000) and (optionally) ensure at least one other candidate has maxOutput >= 20000 so the request can succeed.

Copilot uses AI. Check for mistakes.

@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: 5

🤖 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/chat/chat.ts`:
- Around line 971-975: The code recreates expandedIamFilteredModelProviders from
expandedActiveModelProviders, which reintroduces providers that were pruned
earlier by region filtering on modelInfo.providers; instead, rebuild
expandedIamFilteredModelProviders from the already-region-filtered provider set
(the variable used earlier where modelInfo.providers was pruned) when
iamAllowedProviders is present, i.e. replace the use of
expandedActiveModelProviders with the previously filtered provider list so IAM
filtering cannot reintroduce disallowed regions; reference
expandedIamFilteredModelProviders, iamAllowedProviders,
expandedActiveModelProviders, and modelInfo.providers to locate and correct the
assignment.
- Around line 301-307: The current pre-filter uses the raw request field
options.maxTokens against provider.maxOutput which can differ from the effective
max_tokens after prepareRequestBody(); change the checks in the blocks that
compare options.maxTokens to provider.maxOutput (including the other occurrence
noted) to first obtain the normalized/effective maxTokens by calling or
mimicking prepareRequestBody() (or running the same normalization logic) for the
candidate provider and request, then compare provider.maxOutput to that
effective value so deterministic failures are filtered out before
prepareRequestBody() is invoked.
- Around line 1101-1111: Auto-routing currently only applies regional filtering
in credits mode, allowing api-keys/hybrid to pick regions that conflict with a
provider key's DB-lock; update the candidate provider selection so the DB-key
region lock is applied regardless of project.mode by deriving the key-locked
region via resolveRegionFromProviderKey(providerKey) (or similar) and filtering
the expanded mappings (expandAllProviderRegions /
preferConcreteRegionalMappings) with filterRegionsByAvailableKeys (or an added
filter) before populating usedRegion, ensuring usedRegion cannot be set to a
region outside the provider key's configured region.

In `@apps/gateway/src/fallback.spec.ts`:
- Around line 1123-1137: The test's log lookup can miss the entry because the
request used model "zai/glm-4.6" while the find checks for requestedModel ===
"glm-4.6"; update the lookup in the test (the logs/find block that uses
requestedModel) to either match the full prefixed model string ("zai/glm-4.6")
or normalize both sides (e.g., strip provider prefix) so the correct log is
found, replace the weak response assertion expect(res.status).not.toBe(400) with
a strict expect(res.status).toBe(200) to ensure success, and add a positive
assertion for the chosen provider (e.g., expect(log?.usedProvider).toBe("zai")
or the expected provider) instead of only negative checks to guarantee the
intended routing behavior; adjust references to waitForLogs, logs, log,
requestedModel, res, and usedProvider accordingly.
- Around line 1068-1094: The test inserts into modelProviderMapping but omits
the maxOutput field, so the routing filter in chat.ts (which checks maxOutput
!== undefined) never runs; update the inserted records for ids
"glm-4-6-alibaba-cn-beijing-max-tokens", "glm-4-6-zai-root-max-tokens", and
"glm-4-6-novita-root-max-tokens" to include explicit maxOutput values (set
alibaba's maxOutput to a value below 20000 and the others to >=20000 as needed)
so the test exercises the maxOutput-based exclusion logic used in chat.ts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 47952dcb-b833-4f3d-b72c-cd9a1faf8e7e

📥 Commits

Reviewing files that changed from the base of the PR and between 8a0634a and e4f8345.

📒 Files selected for processing (3)
  • apps/gateway/src/chat/chat.ts
  • apps/gateway/src/chat/tools/resolve-provider-context.ts
  • apps/gateway/src/fallback.spec.ts

Comment on lines +301 to +307
if (
options.maxTokens !== undefined &&
provider.maxOutput !== undefined &&
options.maxTokens > provider.maxOutput
) {
return false;
}

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

Filter on the effective max_tokens value.

prepareRequestBody() can raise or inject max_tokens per provider, so these raw checks still keep candidates that will deterministically fail the later post-prepareRequestBody 400 validation. Please base both sites on the normalized/effective value instead of the raw request field.

Also applies to: 1184-1190

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/gateway/src/chat/chat.ts` around lines 301 - 307, The current pre-filter
uses the raw request field options.maxTokens against provider.maxOutput which
can differ from the effective max_tokens after prepareRequestBody(); change the
checks in the blocks that compare options.maxTokens to provider.maxOutput
(including the other occurrence noted) to first obtain the normalized/effective
maxTokens by calling or mimicking prepareRequestBody() (or running the same
normalization logic) for the candidate provider and request, then compare
provider.maxOutput to that effective value so deterministic failures are
filtered out before prepareRequestBody() is invoked.

Comment thread apps/gateway/src/chat/chat.ts Outdated
Comment thread apps/gateway/src/chat/chat.ts
Comment thread apps/gateway/src/fallback.spec.ts
Comment on lines +1123 to +1137
expect(res.status).not.toBe(400);

const logs = await waitForLogs(1);
const log =
logs.find((entry) => entry.requestedModel === "glm-4.6") ?? logs.at(-1);
expect(log).toBeTruthy();
expect(log?.usedProvider).not.toBe("alibaba");
expect(log?.usedModel).not.toBe("alibaba/glm-4.6:cn-beijing");
expect(log?.routingMetadata?.providerScores).not.toContainEqual(
expect.objectContaining({
providerId: "alibaba",
region: "cn-beijing",
}),
);
});

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 | 🟡 Minor

Log lookup may not find the correct entry due to model prefix mismatch.

The request uses model: "zai/glm-4.6" (line 1117), but the log lookup searches for requestedModel === "glm-4.6" (line 1127). If requestedModel includes the provider prefix, this find will fail and silently fall back to logs.at(-1), potentially masking test failures.

Additionally:

  • Line 1123: expect(res.status).not.toBe(400) is a weak assertion — consider asserting toBe(200) to verify the request actually succeeded.
  • Lines 1129-1130: Only negative assertions about alibaba being excluded. Consider adding a positive assertion about which provider is expected to be used (e.g., zai or novita).
Suggested improvements
-			expect(res.status).not.toBe(400);
+			expect(res.status).toBe(200);

 			const logs = await waitForLogs(1);
 			const log =
-				logs.find((entry) => entry.requestedModel === "glm-4.6") ?? logs.at(-1);
+				logs.find((entry) => entry.requestedModel === "zai/glm-4.6") ?? logs.at(-1);
 			expect(log).toBeTruthy();
+			// Add positive assertion for expected provider
+			expect(log?.usedProvider).toBe("zai"); // or "novita" depending on routing logic
 			expect(log?.usedProvider).not.toBe("alibaba");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(res.status).not.toBe(400);
const logs = await waitForLogs(1);
const log =
logs.find((entry) => entry.requestedModel === "glm-4.6") ?? logs.at(-1);
expect(log).toBeTruthy();
expect(log?.usedProvider).not.toBe("alibaba");
expect(log?.usedModel).not.toBe("alibaba/glm-4.6:cn-beijing");
expect(log?.routingMetadata?.providerScores).not.toContainEqual(
expect.objectContaining({
providerId: "alibaba",
region: "cn-beijing",
}),
);
});
expect(res.status).toBe(200);
const logs = await waitForLogs(1);
const log =
logs.find((entry) => entry.requestedModel === "zai/glm-4.6") ?? logs.at(-1);
expect(log).toBeTruthy();
// Add positive assertion for expected provider
expect(log?.usedProvider).toBe("zai"); // or "novita" depending on routing logic
expect(log?.usedProvider).not.toBe("alibaba");
expect(log?.usedModel).not.toBe("alibaba/glm-4.6:cn-beijing");
expect(log?.routingMetadata?.providerScores).not.toContainEqual(
expect.objectContaining({
providerId: "alibaba",
region: "cn-beijing",
}),
);
});
🤖 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 1123 - 1137, The test's log
lookup can miss the entry because the request used model "zai/glm-4.6" while the
find checks for requestedModel === "glm-4.6"; update the lookup in the test (the
logs/find block that uses requestedModel) to either match the full prefixed
model string ("zai/glm-4.6") or normalize both sides (e.g., strip provider
prefix) so the correct log is found, replace the weak response assertion
expect(res.status).not.toBe(400) with a strict expect(res.status).toBe(200) to
ensure success, and add a positive assertion for the chosen provider (e.g.,
expect(log?.usedProvider).toBe("zai") or the expected provider) instead of only
negative checks to guarantee the intended routing behavior; adjust references to
waitForLogs, logs, log, requestedModel, res, and usedProvider accordingly.

Copy link
Copy Markdown
Member Author

Follow-up update pushed in ce798ffa6.

This fixes a second routing hole in expandedIamFilteredModelProviders: the expanded IAM candidate list now inherits the same credits/hybrid region filtering as the main provider set, so provider selection cannot resurrect regions that were excluded due to missing region-specific keys.

Validation run after the patch:

  • pnpm exec tsc -p apps/gateway/tsconfig.json --noEmit
  • focused fallback regressions for synthetic-region routing and max_tokens
  • pnpm build

Note: the fresh build initially failed on a corrupted generated file at apps/playground/.next-dev/dev/types/routes.d.ts; removing the stale .next-dev artifact and rerunning produced a clean build.

Copy link
Copy Markdown
Member Author

Added follow-up test fix in 8e5a984a9.

The routing excludes providers whose maxOutput is below max_tokens fixture now sets explicit maxOutput values:

  • zai: 32768
  • alibaba/cn-beijing: 16384
  • novita: 32768

That makes the test actually exercise the provider.maxOutput !== undefined && max_tokens > provider.maxOutput filter instead of passing on unrelated routing behavior.

Validation:

  • pnpm vitest apps/gateway/src/fallback.spec.ts -t "routing excludes providers whose maxOutput is below max_tokens"

@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: 8e5a984a9d

ℹ️ 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 1109 to 1110
project.mode === "credits"
? filterRegionsByAvailableKeys(

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 Apply non-credit region constraints in auto routing

The auto-routing candidate setup only applies filterRegionsByAvailableKeys(...) when project.mode === "credits", while the non-credits branch expands all regions unconditionally. In api-keys and hybrid modes this bypasses the region restrictions enforced elsewhere (DB-key locked regions and hybrid env-key limits), so auto routing can choose a provider region the project is not configured to use, leading to wrong-region routing or avoidable auth failures for region-bound credentials.

Useful? React with 👍 / 👎.

return true;
});
const availableModelProviders = filterEligibleModelProviders(
preferConcreteRegionalMappings(expandedIamFilteredModelProviders),

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 requested region in low-uptime fallback

Low-uptime fallback now derives alternatives from expandedIamFilteredModelProviders, which comes from routingExpandedModelProviders and is not narrowed by the earlier requestedRegion filter. When a caller explicitly requests provider/model:region, fallback can therefore select a different region once uptime drops below threshold, violating the explicit region constraint and potentially sending traffic outside the requested geography.

Useful? React with 👍 / 👎.

@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

♻️ Duplicate comments (3)
apps/gateway/src/chat/chat.ts (3)

249-259: ⚠️ Potential issue | 🟠 Major

Use the effective max_tokens value for eligibility, not the raw request field.

Lines 301-307 and 1191-1197 still compare the user-supplied max_tokens before prepareRequestBody() applies provider-specific normalization/defaults. That leaves candidates in the pool that will deterministically hit the later effective-max_tokens 400 instead of being filtered out up front, so routing can fail even when another provider would have been valid.

Also applies to: 301-307, 1191-1197

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/gateway/src/chat/chat.ts` around lines 249 - 259,
filterEligibleModelProviders currently uses the raw request max_tokens to decide
provider eligibility; instead call or reuse the normalization that
prepareRequestBody performs to compute the effective max_tokens and use that
when filtering so providers that will end up with a lower default (e.g., 400)
are excluded up front. Update filterEligibleModelProviders (and the code paths
referenced around prepareRequestBody usage) to obtain the prepared request or
the provider-specific effectiveMaxTokens (by invoking prepareRequestBody or its
utility) and compare that effective value against ProviderModelMapping limits
rather than the original user-supplied max_tokens.

686-692: ⚠️ Potential issue | 🔴 Critical

Keep explicit :region requests in sync with the expanded IAM/routing pool.

Line 686 seeds routingExpandedModelProviders from the full expanded set, but the requestedRegion branch only narrows modelInfo.providers. By Lines 978-982, expandedIamFilteredModelProviders is rebuilt from that stale expanded list, so later fallback/routing paths can reintroduce regions outside the caller's explicit region constraint.

Possible fix
 if (requestedRegion) {
 	const regionProviders = expandedActiveModelProviders.filter(
 		(p) => p.region === requestedRegion,
 	);
+	routingExpandedModelProviders = routingExpandedModelProviders.filter(
+		(p) => p.region === requestedRegion,
+	);
 	modelInfo = {
 		...modelInfo,
 		providers: regionProviders,
 	};

Also applies to: 978-982

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/gateway/src/chat/chat.ts` around lines 686 - 692, The code seeds
routingExpandedModelProviders from expandedActiveModelProviders but only narrows
modelInfo.providers when requestedRegion is present, causing
expandedIamFilteredModelProviders (rebuilt later in
expandedIamFilteredModelProviders) to be computed from a stale full list and
reintroduce disallowed regions; update the logic so that when requestedRegion is
set (and when useExpandedRoutingProviders is true) you also filter
routingExpandedModelProviders to the requestedRegion (the same way you narrow
modelInfo.providers), and ensure any place that rebuilds
expandedIamFilteredModelProviders uses this filtered
routingExpandedModelProviders instead of the original
expandedActiveModelProviders (adjust references in the blocks that set
routingExpandedModelProviders, modelInfo, and
expandedIamFilteredModelProviders).

1108-1118: ⚠️ Potential issue | 🔴 Critical

Auto-routing still bypasses region locks outside credits mode.

This block rebuilds candidates from raw modelDef.providers and only applies filterRegionsByAvailableKeys() in credits mode. In hybrid, env-backed providers can still surface non-default regions without region-specific keys, and in api-keys/hybrid a provider with a DB-locked region can still be auto-routed to a different region before usedRegion is fixed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/gateway/src/chat/chat.ts` around lines 1108 - 1118, The
candidateProviders rebuild currently only applies filterRegionsByAvailableKeys
when project.mode === "credits", which allows non-allowed regions to be surfaced
in api-keys/hybrid and lets auto-routing pick a different region before
usedRegion is fixed; update the logic around
preferConcreteRegionalMappings/expandAllProviderRegions so
filterRegionsByAvailableKeys is always applied (use modelDef.providers ->
expandAllProviderRegions -> filterRegionsByAvailableKeys ->
preferConcreteRegionalMappings) and ensure the auto-route/selection code that
sets usedRegion rejects providers whose region is DB-locked (respect any
existing region lock and only consider providers in that locked region) so
usedRegion cannot be changed by auto-routing in api-keys or hybrid modes (refer
to candidateProviders, preferConcreteRegionalMappings,
filterRegionsByAvailableKeys, expandAllProviderRegions, modelDef.providers,
project.mode, and usedRegion).
🤖 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/chat/chat.ts`:
- Around line 1516-1526: The uptime-improvement gate is applied after
collapseProvidersToBestRegionPerProvider which can drop a provider because its
chosen region is lower uptime; instead, filter candidate regions/providers by
uptime > currentUptime before collapsing so each provider's best-region
selection only considers regions that meet the uptime gate. Update the flow to
run the uptime filter against availableModelProviders (or pass an options flag
into collapseProvidersToBestRegionPerProvider to perform the filter internally)
so collapseProvidersToBestRegionPerProvider returns only providers whose best
region already satisfies the uptime > currentUptime check, then compute
betterUptimeProviders from that result.

---

Duplicate comments:
In `@apps/gateway/src/chat/chat.ts`:
- Around line 249-259: filterEligibleModelProviders currently uses the raw
request max_tokens to decide provider eligibility; instead call or reuse the
normalization that prepareRequestBody performs to compute the effective
max_tokens and use that when filtering so providers that will end up with a
lower default (e.g., 400) are excluded up front. Update
filterEligibleModelProviders (and the code paths referenced around
prepareRequestBody usage) to obtain the prepared request or the
provider-specific effectiveMaxTokens (by invoking prepareRequestBody or its
utility) and compare that effective value against ProviderModelMapping limits
rather than the original user-supplied max_tokens.
- Around line 686-692: The code seeds routingExpandedModelProviders from
expandedActiveModelProviders but only narrows modelInfo.providers when
requestedRegion is present, causing expandedIamFilteredModelProviders (rebuilt
later in expandedIamFilteredModelProviders) to be computed from a stale full
list and reintroduce disallowed regions; update the logic so that when
requestedRegion is set (and when useExpandedRoutingProviders is true) you also
filter routingExpandedModelProviders to the requestedRegion (the same way you
narrow modelInfo.providers), and ensure any place that rebuilds
expandedIamFilteredModelProviders uses this filtered
routingExpandedModelProviders instead of the original
expandedActiveModelProviders (adjust references in the blocks that set
routingExpandedModelProviders, modelInfo, and
expandedIamFilteredModelProviders).
- Around line 1108-1118: The candidateProviders rebuild currently only applies
filterRegionsByAvailableKeys when project.mode === "credits", which allows
non-allowed regions to be surfaced in api-keys/hybrid and lets auto-routing pick
a different region before usedRegion is fixed; update the logic around
preferConcreteRegionalMappings/expandAllProviderRegions so
filterRegionsByAvailableKeys is always applied (use modelDef.providers ->
expandAllProviderRegions -> filterRegionsByAvailableKeys ->
preferConcreteRegionalMappings) and ensure the auto-route/selection code that
sets usedRegion rejects providers whose region is DB-locked (respect any
existing region lock and only consider providers in that locked region) so
usedRegion cannot be changed by auto-routing in api-keys or hybrid modes (refer
to candidateProviders, preferConcreteRegionalMappings,
filterRegionsByAvailableKeys, expandAllProviderRegions, modelDef.providers,
project.mode, and usedRegion).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 7d96ee5a-0d98-4ed1-85c1-9b3e4e4ee51e

📥 Commits

Reviewing files that changed from the base of the PR and between e4f8345 and ce798ff.

📒 Files selected for processing (1)
  • apps/gateway/src/chat/chat.ts

Comment on lines +1516 to 1526
const providerAgnosticCandidates =
collapseProvidersToBestRegionPerProvider(
availableModelProviders,
modelWithPricing,
{ metricsMap: allMetricsMap, isStreaming: stream },
);

// Filter to only providers with better uptime than the original
// to avoid falling back to worse providers
const betterUptimeProviders = availableModelProviders.filter(
const betterUptimeProviders = providerAgnosticCandidates.filter(
(p) => {

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

Apply the uptime-improvement gate before collapsing per provider.

Here the code picks one region per provider first and only then checks uptime > currentUptime. If the scoring helper chooses a cheaper but less healthy region, that provider is dropped entirely even when another region for the same provider would have satisfied the fallback rule.

Possible fix
-						const providerAgnosticCandidates =
-							collapseProvidersToBestRegionPerProvider(
-								availableModelProviders,
-								modelWithPricing,
-								{ metricsMap: allMetricsMap, isStreaming: stream },
-							);
-
-						const betterUptimeProviders = providerAgnosticCandidates.filter(
-							(p) => {
+						const betterUptimeRegionalCandidates =
+							availableModelProviders.filter((p) => {
 								const providerMetrics = allMetricsMap.get(
 									metricsKey(modelWithPricing.id, p.providerId, p.region),
 								);
-								// If no metrics, assume the provider is healthy (100% uptime)
-								// If has metrics, only include if uptime is better than original
 								return (
 									!providerMetrics ||
 									(providerMetrics.uptime ?? 100) > currentUptime
 								);
-							},
-						);
+							});
+						const betterUptimeProviders =
+							collapseProvidersToBestRegionPerProvider(
+								betterUptimeRegionalCandidates,
+								modelWithPricing,
+								{ metricsMap: allMetricsMap, isStreaming: stream },
+							);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/gateway/src/chat/chat.ts` around lines 1516 - 1526, The
uptime-improvement gate is applied after
collapseProvidersToBestRegionPerProvider which can drop a provider because its
chosen region is lower uptime; instead, filter candidate regions/providers by
uptime > currentUptime before collapsing so each provider's best-region
selection only considers regions that meet the uptime gate. Update the flow to
run the uptime filter against availableModelProviders (or pass an options flag
into collapseProvidersToBestRegionPerProvider to perform the filter internally)
so collapseProvidersToBestRegionPerProvider returns only providers whose best
region already satisfies the uptime > currentUptime check, then compute
betterUptimeProviders from that result.

@steebchen
steebchen added this pull request to the merge queue Mar 28, 2026
@steebchen
steebchen removed this pull request from the merge queue due to a manual request Mar 28, 2026
@steebchen
steebchen merged commit 0cc4fad into main Mar 28, 2026
13 checks passed
@steebchen
steebchen deleted the fix/respecct branch March 28, 2026 16:27
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