fix(sse): let :free OpenRouter models bypass connection-wide credits_exhausted lock (#10445)

* fix(sse): let :free OpenRouter models bypass connection-wide credits_exhausted lock

A 402 from one paid OpenRouter model correctly locks the whole connection
as credits_exhausted for an hour (intentional, per #6842), but that lock
was also blocking every :free model on the same connection even though
OpenRouter bills free models separately from account credits.

Reconstructed clean against release/v3.8.50 by the maintainer: the author's
original branch predated a large auth.ts import refactor; the same delta was
re-applied onto the current tip and the TDD test still passes.

TDD: tests/unit/openrouter-free-model-credits-exhausted.test.ts
reproduces the bug (fails before the fix, passes after) and covers the
three guard cases above.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* test(mutation): register openrouter-free-model-credits-exhausted in stryker tap.testFiles

The new unit test covers src/sse/services/auth.ts, which is one of the 31
stryker-mutated modules — per check-mutation-test-coverage every covering
test must be listed in tap.testFiles or its mutant kills stop counting.
Registered the file so the blocking mutation-test-coverage gate passes.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: killmonger2317-coder <282069920+killmonger2317-coder@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
killmonger2317-coder
2026-08-15 12:52:46 -04:00
committed by GitHub
parent f466ea91c9
commit d33e62af9c
3 changed files with 155 additions and 1 deletions

View File

@@ -81,6 +81,7 @@ import {
WEB_COOKIE_PROVIDERS,
} from "@/shared/constants/providers";
import { isModelExcludedByConnection } from "@/domain/connectionModelRules";
import { isFreeModel } from "@/shared/utils/freeModels";
import {
applySessionAffinityPin,
formatSessionKeyForLog,
@@ -340,6 +341,31 @@ function isTerminalConnectionStatus(connection: ProviderConnectionView): boolean
return status === "credits_exhausted" || status === "banned" || status === "expired";
}
// OpenRouter's paid balance and its `:free`-suffixed models are billed
// separately — a 402 from a paid model call correctly locks the whole
// connection as credits_exhausted (see openrouter-quota-6842.test.ts), but
// that lock must not also block :free model requests on the same
// connection, or combo failover to the user's configured free models never
// fires. Scoped to provider === "openrouter" + status === credits_exhausted
// only; every other terminal status (banned, expired) and every other
// provider keep the unconditional exclusion.
function isTerminalConnectionStatusForModel(
connection: ProviderConnectionView,
provider: string,
requestedModel: string | null
): boolean {
if (!isTerminalConnectionStatus(connection)) return false;
if (
provider === "openrouter" &&
normalizeStatus(connection.testStatus) === "credits_exhausted" &&
requestedModel &&
isFreeModel("openrouter", { id: requestedModel })
) {
return false;
}
return true;
}
// #8200: cookie-auth providers (perplexity-web, grok-web, ...) use a rotating browser
// session, not a static API key — a 401 means "session needs a refresh", not "dead".
function isRecoverableCookieAuth401(
@@ -1239,7 +1265,7 @@ export async function getProviderCredentials(
connectionFilterStatus.set(c.id, "rateLimited");
return false;
}
if (isTerminalConnectionStatus(c)) {
if (isTerminalConnectionStatusForModel(c, provider, requestedModel)) {
connectionFilterStatus.set(c.id, "terminalStatus");
return false;
}