Files
OmniRoute/src/sse/services/authTerminalStatus.ts
Ravi Tharuma 85b8d128eb fix(auth): do not park healthy quota accounts as expired (#12452)
Validado em lote numa worktree combinada com os 10 PRs desta leva sobre o tip de `release/v3.8.51`: `typecheck:core` limpo, `check-file-size` OK e **241/242** nos 29 arquivos de teste que os PRs tocam.

A única "falha" não é falha: `tests/unit/autoCombo/strict-zero-cost-filter.test.ts` é um teste em estilo Vitest que eu incluí por engano na invocação do runner nativo do Node — ele quebra no import (`@vitest/runner`), não numa asserção. Ao investigar, descobri que esse arquivo não roda em nenhum dos dois runners hoje (o glob do `test:unit` não lista `autoCombo` e o `include` do Vitest só pega `.tsx` nessa pasta); é um problema pré-existente do repositório, sem relação com esta leva, e vou registrá-lo separadamente.

O #12636 conflitava apenas na lista de testes do `@omniroute/opencode-plugin/package.json`, de forma aditiva: o tip já tinha `models-fetcher.test.ts` (do #12607, irmão desta mesma leva) e o #12636 acrescenta `telemetry.test.ts`. Fiz a união dos dois lados (25 arquivos contra 24 de cada) em vez de escolher um, o que teria removido um arquivo da suíte do plugin em silêncio.

Obrigado, @RaviTharuma.
2026-09-03 23:39:22 -03:00

94 lines
3.6 KiB
TypeScript

import { PROVIDER_ERROR_TYPES } from "@omniroute/open-sse/services/errorClassifier.ts";
import { isCreditsExhausted } from "@omniroute/open-sse/services/accountFallback.ts";
import { resolveProviderId, WEB_COOKIE_PROVIDERS } from "@/shared/constants/providers";
// #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".
export function isRecoverableCookieAuth401(
provider: string | null,
providerErrorType: string | null
): boolean {
return (
providerErrorType !== PROVIDER_ERROR_TYPES.ACCOUNT_DEACTIVATED &&
provider != null &&
resolveProviderId(provider) in WEB_COOKIE_PROVIDERS
);
}
// #12242 (402 variant of #3027): a bare 402 on a passthrough/gateway
// provider that multiplexes many models behind one credential
// (kilo-gateway, ollama-cloud, etc.) is a PER-MODEL billing signal, not
// proof the credential itself is dead — free models on the same connection
// remain perfectly usable. Only terminalize the whole connection for a 402
// when the provider is NOT a per-model-quota provider; the caller lets it
// fall through to the per-model lockout branch instead.
// `result.creditsExhausted` is a provider's own explicit classification
// (independent of HTTP status) and stays unconditionally terminal — it is
// not scoped by this check.
export function isConnectionWideCreditsExhausted(
status: number,
result: { permanent?: boolean; creditsExhausted?: boolean },
isPerModelQuotaProvider: boolean
): boolean {
return result.creditsExhausted || (status === 402 && !isPerModelQuotaProvider);
}
/** Credits-depleted bodies park; renewing billing-cycle quota does not. */
export function shouldParkCreditsExhausted(
status: number,
result: { permanent?: boolean; creditsExhausted?: boolean },
isPerModelQuotaProvider: boolean,
errorText: string
): boolean {
return (
isConnectionWideCreditsExhausted(status, result, isPerModelQuotaProvider) ||
(!isPerModelQuotaProvider && isCreditsExhausted(errorText))
);
}
function isNonTerminalProviderError(providerErrorType: string | null): boolean {
return (
providerErrorType === PROVIDER_ERROR_TYPES.PROJECT_ROUTE_ERROR ||
providerErrorType === PROVIDER_ERROR_TYPES.GEO_BLOCKED ||
providerErrorType === PROVIDER_ERROR_TYPES.OAUTH_INVALID_TOKEN ||
// #1010: Cloudflare fingerprint rejection is the CDN refusing the CLIENT's
// signature, not the account's credentials — never a terminal account state.
providerErrorType === PROVIDER_ERROR_TYPES.FINGERPRINT_REJECTION
);
}
function isExpiredAuthFailure(
status: number,
providerErrorType: string | null,
provider: string | null
): boolean {
return (
(providerErrorType === PROVIDER_ERROR_TYPES.ACCOUNT_DEACTIVATED ||
providerErrorType === PROVIDER_ERROR_TYPES.UNAUTHORIZED ||
status === 401) &&
!isRecoverableCookieAuth401(provider, providerErrorType)
);
}
export function resolveTerminalConnectionStatus(
status: number,
result: { permanent?: boolean; creditsExhausted?: boolean },
providerErrorType: string | null = null,
provider: string | null = null,
isPerModelQuotaProvider = false,
errorText: string = ""
): string | null {
if (shouldParkCreditsExhausted(status, result, isPerModelQuotaProvider, errorText)) {
return "credits_exhausted";
}
if (isNonTerminalProviderError(providerErrorType)) {
return null;
}
if (result.permanent || providerErrorType === PROVIDER_ERROR_TYPES.FORBIDDEN) {
return "banned";
}
if (isExpiredAuthFailure(status, providerErrorType, provider)) {
return "expired";
}
return null;
}