fix(resilience): route-restriction 403 must not mark a connection unavailable (#2929)

Fireworks Fire Pass (fpk_*) keys return 403 "Fire Pass API keys are not
authorized for this route." on /models while still serving chat. Two paths
wrongly penalized the key:

- validateOpenAILikeProvider returned "Invalid API key" for any 403 on the
  models endpoint without trying the chat probe.
- checkFallbackError classified the 403 as AUTH_ERROR (retryable cooldown), and
  even a generic fall-through hit the transient-cooldown default — marking the
  connection unavailable.

Fix: validateOpenAILikeProvider now inspects the 403 body and falls through to
the chat probe for "not authorized for this route" responses (401 and generic
403 still fail fast). checkFallbackError short-circuits such route-restriction
403s to { shouldFallback: false, cooldownMs: 0 } so the connection is not cooled
down. Per CLAUDE.md, a generic api-key 403 should be recoverable unless terminal.

Closes #2929
This commit is contained in:
diegosouzapw
2026-05-31 01:35:43 -03:00
parent 0fe97ab8e9
commit eb7348aeb9
5 changed files with 148 additions and 1 deletions

View File

@@ -382,10 +382,21 @@ async function validateOpenAILikeProvider({
return { valid: true, error: null };
}
if (response.status === 401 || response.status === 403) {
if (response.status === 401) {
return { valid: false, error: "Invalid API key" };
}
// #2929: A 403 on the models endpoint is not always a bad key. Some providers
// (e.g. Fireworks Fire Pass `fpk_*` keys) return "...not authorized for this
// route." on /models while still serving chat. Fall through to the chat probe
// for such route-restriction 403s instead of declaring the key invalid.
if (response.status === 403) {
const forbiddenBody = await response.text().catch(() => "");
if (!/not authorized for this route/i.test(forbiddenBody)) {
return { valid: false, error: "Invalid API key" };
}
}
const chatUrl = resolveChatUrl(provider, baseUrl, providerSpecificData);
if (!chatUrl) {
return { valid: false, error: `Validation failed: ${response.status}` };