// Covers provider-specific failover matcher regressions. import { describe, expect, it } from "vitest"; import { classifyFailoverReason, isAuthErrorMessage, isBillingErrorMessage, isOverloadedErrorMessage, isProviderCompletedErrorFinishReasonMessage, isRateLimitErrorMessage, isServerErrorMessage, isTimeoutErrorMessage, } from "./classify.js"; import { renderRateLimitOrOverloadedCopy } from "./user-copy.js"; describe("Z.ai vendor error codes (#48988)", () => { describe("error 1311 — model not included in subscription plan", () => { it("classifies Z.ai 1311 JSON body as billing", () => { // Z.ai 1311 is a plan entitlement failure, not rate limiting. const raw = '{"code":1311,"message":"The model you requested is not available in your current plan"}'; expect(isBillingErrorMessage(raw)).toBe(true); }); it("classifies prose-only subscription plan access denials as billing", () => { const raw = "FailoverError: Your current subscription plan does not yet include access to GLM-5V-Turbo"; expect(isBillingErrorMessage(raw)).toBe(true); }); it("classifies Z.ai 1311 with spaces as billing", () => { const raw = '{"code": 1311, "message": "model not on plan"}'; expect(isBillingErrorMessage(raw)).toBe(true); }); it("does not misclassify 1311 as rate_limit", () => { const raw = '{"code":1311,"message":"The model you requested is not available in your current plan"}'; expect(isRateLimitErrorMessage(raw)).toBe(false); }); it("does not misclassify 1311 as auth", () => { const raw = '{"code":1311,"message":"The model you requested is not available in your current plan"}'; expect(isAuthErrorMessage(raw)).toBe(false); }); it("classifies long Z.ai 1311 payloads as billing", () => { const raw = JSON.stringify({ code: 1311, message: "The model you requested is not available in your current plan", details: "x".repeat(700), }); expect(raw.length).toBeGreaterThan(512); expect(isBillingErrorMessage(raw)).toBe(true); }); }); describe("error 1113 — wrong endpoint or invalid credentials", () => { it("classifies Z.ai 1113 JSON body as auth", () => { const raw = '{"code":1113,"message":"invalid api endpoint or credentials"}'; expect(isAuthErrorMessage(raw)).toBe(true); }); it("classifies Z.ai 1113 with spaces as auth", () => { const raw = '{"code": 1113, "message": "invalid api endpoint or credentials"}'; expect(isAuthErrorMessage(raw)).toBe(true); }); it("does not misclassify 1113 as rate_limit", () => { const raw = '{"code":1113,"message":"invalid api endpoint or credentials"}'; expect(isRateLimitErrorMessage(raw)).toBe(false); }); it("does not misclassify 1113 as billing", () => { const raw = '{"code":1113,"message":"invalid api endpoint or credentials"}'; expect(isBillingErrorMessage(raw)).toBe(false); }); }); describe("existing patterns are unaffected", () => { it("rate limit still classified correctly", () => { expect(isRateLimitErrorMessage("rate limit exceeded")).toBe(true); }); it("OpenAI model-capacity text is classified as overloaded", () => { expect( isOverloadedErrorMessage("Selected model is at capacity. Please try a different model."), ).toBe(true); }); it("OpenRouter high-load text is classified as overloaded", () => { expect( isOverloadedErrorMessage( "The service is currently experiencing high load and cannot process your request.", ), ).toBe(true); }); it("billing still classified correctly", () => { expect(isBillingErrorMessage("insufficient credits")).toBe(true); }); it("auth still classified correctly", () => { expect(isAuthErrorMessage("invalid api key provided")).toBe(true); }); }); }); describe("Google invalid API key errors (#114784)", () => { it("classifies Google Generative AI's invalid-key response as auth", () => { const raw = "Google Generative AI API error (400): API key not valid. Please pass a valid API key. [code=INVALID_ARGUMENT]"; expect(isAuthErrorMessage(raw)).toBe(true); expect(classifyFailoverReason(raw)).toBe("auth"); }); it.each([ "invalid_api_key_error", "API key is invalid", '{"code":"API_KEY_INVALID"}', '{"code":"API_KEY_INVALID_ERROR"}', ])("classifies the %s variant as auth", (raw) => { expect(isAuthErrorMessage(raw)).toBe(true); expect(classifyFailoverReason(raw)).toBe("auth"); }); it("does not treat unrelated Google invalid arguments as auth", () => { const raw = "Google Generative AI API error (400): Request contains an invalid argument. [code=INVALID_ARGUMENT]"; expect(isAuthErrorMessage(raw)).toBe(false); expect(classifyFailoverReason(raw)).toBeNull(); expect(isAuthErrorMessage("API key invalidation policy updated")).toBe(false); expect(isAuthErrorMessage("INVALID API KEYSTORE configuration")).toBe(false); }); }); describe("Chinese provider overload messages", () => { const ZHIPU_OVERLOAD = "[1305][该模型当前访问量过大,请您稍后再试]"; it("classifies the Zhipu GLM overload body as overloaded", () => { expect(isOverloadedErrorMessage(ZHIPU_OVERLOAD)).toBe(true); }); it("does not misclassify the GLM overload body as rate limit or auth", () => { expect(isRateLimitErrorMessage(ZHIPU_OVERLOAD)).toBe(false); expect(isAuthErrorMessage(ZHIPU_OVERLOAD)).toBe(false); }); }); describe("Volcengine Coding Plan subscription errors", () => { it("classifies InvalidSubscription JSON body as billing", () => { const raw = '{"error":{"code":"InvalidSubscription","message":"Your account does not have a valid CodingPlan subscription, or your subscription has expired."}}'; expect(isBillingErrorMessage(raw)).toBe(true); }); it("classifies long InvalidSubscription payloads as billing", () => { const raw = JSON.stringify({ error: { code: "InvalidSubscription", message: "Your account does not have a valid coding plan subscription, or your subscription has expired.", details: "x".repeat(700), }, }); expect(raw.length).toBeGreaterThan(512); expect(isBillingErrorMessage(raw)).toBe(true); }); it("classifies InvalidSubscription as billing before auth or rate limit", () => { const raw = '{"error":{"code":"InvalidSubscription","message":"Your account does not have a valid CodingPlan subscription, or your subscription has expired."}}'; expect(isRateLimitErrorMessage(raw)).toBe(false); expect(classifyFailoverReason(raw)).toBe("billing"); }); }); describe("agent harness provider mismatch (#91710)", () => { it("classifies harness provider rejection as format error", () => { expect( classifyFailoverReason( 'Requested agent harness "codex" does not support openai/gpt-5.3-codex (provider is not one of: codex).', ), ).toBe("format"); }); it("classifies harness provider rejection with multiple providers as format error", () => { expect( classifyFailoverReason( 'Requested agent harness "codex" does not support openrouter/gpt-5.4 (provider is not one of: codex, openai).', ), ).toBe("format"); }); }); describe("server error status classification", () => { it("classifies a bare internal server error status as server error", () => { // Bare status lines from providers should classify, while prefixed prose is // too ambiguous and tested below as a non-match. expect(isServerErrorMessage("status: internal server error")).toBe(true); }); it("classifies provider HTTP 5xx wrapper errors as server errors", () => { expect(isServerErrorMessage("provider failed (HTTP 500): upstream apiKey is empty")).toBe(true); }); it("does not classify prefixed plain internal server error status prose", () => { expect(isServerErrorMessage("Proxy notice: Status: Internal Server Error")).toBe(false); }); }); describe("provider-completed finish_reason error (#109218)", () => { it("matches bare finish/stop error reasons as provider-completed failures", () => { expect(isProviderCompletedErrorFinishReasonMessage("Provider finish_reason: error")).toBe(true); expect(isTimeoutErrorMessage("Provider finish_reason: error")).toBe(false); expect(classifyFailoverReason("Provider finish_reason: error")).toBe("server_error"); }); it("keeps abort/network/malformed finish reasons in the timeout lane", () => { for (const sample of [ "Provider finish_reason: abort", "Provider finish_reason: network_error", "Provider finish_reason: malformed_response", ]) { expect(isProviderCompletedErrorFinishReasonMessage(sample)).toBe(false); expect(isTimeoutErrorMessage(sample)).toBe(true); expect(classifyFailoverReason(sample)).toBe("timeout"); } }); }); describe("generic assistant error text classification (#93931)", () => { it("classifies the generic 'LLM request failed.' as a timeout (transient)", () => { // The generic error text wraps provider availability failures (model not // loaded, endpoint unreachable) that should engage retry/fallback. expect(classifyFailoverReason("LLM request failed.")).toBe("timeout"); }); it("classifies lowercase 'llm request failed.' as a timeout", () => { expect(classifyFailoverReason("llm request failed.")).toBe("timeout"); }); it("does NOT match 'LLM request failed:' variants as timeout via this pattern", () => { // Variants with specific reasons should be classified by their own patterns, // not by the generic LLM request failed match. The schema rejection variant // is a format error, not a transient timeout. expect( isTimeoutErrorMessage( "LLM request failed: provider rejected the request schema or tool payload.", ), ).toBe(false); }); it("does NOT match 'LLM request failed: connection refused' as timeout via this exact-match pattern", () => { // The connection-refused variant is a sanitized user-facing string, not // the raw error that cron/failover classifiers see. The exact-match regex // /^llm request failed\.$/i should NOT match it because of the colon suffix. expect( isTimeoutErrorMessage("LLM request failed: connection refused by the provider endpoint."), ).toBe(false); }); }); describe("HTTP 429 overload wording (#98101)", () => { it("keeps Z.AI code 1305 in rate-limit backoff while preserving overload copy", () => { const message = "429 status code (exceeded limit)\n" + '{"code":1305,"message":"The service may be temporarily overloaded, please try again later."}'; expect(classifyFailoverReason(message)).toBe("rate_limit"); expect(classifyFailoverReason(`HTTP 429: ${message}`)).toBe("rate_limit"); expect(renderRateLimitOrOverloadedCopy({ reason: "rate_limit", raw: message })).toBe( "⚠️ API rate limit reached. Please try again later.", ); }); });