From e0767f767ad93d709d3e9fa78faefbcc598d8793 Mon Sep 17 00:00:00 2001 From: Altay Date: Fri, 6 Mar 2026 01:56:19 +0300 Subject: [PATCH] fix(failover): disambiguate periodic limit errors --- src/agents/failover-error.test.ts | 13 +++++++++++++ ...i-embedded-helpers.isbillingerrormessage.test.ts | 7 ++++++- src/agents/pi-embedded-helpers/errors.ts | 4 ++++ src/agents/pi-embedded-helpers/failover-matches.ts | 8 +++++++- 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/agents/failover-error.test.ts b/src/agents/failover-error.test.ts index ce372e4bc9b..6d0b6202f04 100644 --- a/src/agents/failover-error.test.ts +++ b/src/agents/failover-error.test.ts @@ -123,6 +123,19 @@ describe("failover-error", () => { message: ZHIPUAI_WEEKLY_MONTHLY_LIMIT_EXHAUSTED_MESSAGE, }), ).toBe("rate_limit"); + expect( + resolveFailoverReasonFromError({ + message: "LLM error: monthly limit reached", + }), + ).toBe("rate_limit"); + }); + + it("keeps raw-text 402 weekly/monthly limit errors in billing", () => { + expect( + resolveFailoverReasonFromError({ + message: "402 Payment Required: Weekly/Monthly Limit Exhausted", + }), + ).toBe("billing"); }); it("infers format errors from error messages", () => { diff --git a/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts b/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts index bb5aa36dba2..9eb2657158b 100644 --- a/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts +++ b/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts @@ -540,6 +540,9 @@ describe("classifyFailoverReason", () => { expect( classifyFailoverReason("HTTP 402 payment required. Your limit exhausted for this plan."), ).toBe("billing"); + expect(classifyFailoverReason("402 Payment Required: Weekly/Monthly Limit Exhausted")).toBe( + "billing", + ); expect(classifyFailoverReason(INSUFFICIENT_QUOTA_PAYLOAD)).toBe("billing"); expect(classifyFailoverReason("deadline exceeded")).toBe("timeout"); expect(classifyFailoverReason("request ended without sending any chunks")).toBe("timeout"); @@ -595,8 +598,10 @@ describe("classifyFailoverReason", () => { "LLM error 1310: Weekly/Monthly Limit Exhausted. Your limit will reset at 2026-03-06 22:19:54 (request_id: 20260303141547610b7f574d1b44cb)", ), ).toBe("rate_limit"); - // Independent coverage for /weekly\/monthly limit/i (no generic "limit exhausted") + // Independent coverage for broader periodic limit patterns. expect(classifyFailoverReason("LLM error: weekly/monthly limit reached")).toBe("rate_limit"); + expect(classifyFailoverReason("LLM error: monthly limit reached")).toBe("rate_limit"); + expect(classifyFailoverReason("LLM error: daily limit exceeded")).toBe("rate_limit"); }); it("classifies permanent auth errors as auth_permanent", () => { expect(classifyFailoverReason("invalid_api_key")).toBe("auth_permanent"); diff --git a/src/agents/pi-embedded-helpers/errors.ts b/src/agents/pi-embedded-helpers/errors.ts index e4944b0731c..0f602ce66d7 100644 --- a/src/agents/pi-embedded-helpers/errors.ts +++ b/src/agents/pi-embedded-helpers/errors.ts @@ -8,6 +8,7 @@ import { isAuthPermanentErrorMessage, isBillingErrorMessage, isOverloadedErrorMessage, + isPeriodicUsageLimitErrorMessage, isRateLimitErrorMessage, isTimeoutErrorMessage, matchesFormatErrorPattern, @@ -842,6 +843,9 @@ export function classifyFailoverReason(raw: string): FailoverReason | null { if (isJsonApiInternalServerError(raw)) { return "timeout"; } + if (isPeriodicUsageLimitErrorMessage(raw)) { + return isBillingErrorMessage(raw) ? "billing" : "rate_limit"; + } if (isRateLimitErrorMessage(raw)) { return "rate_limit"; } diff --git a/src/agents/pi-embedded-helpers/failover-matches.ts b/src/agents/pi-embedded-helpers/failover-matches.ts index 686b81265c0..6a7ce9d51d3 100644 --- a/src/agents/pi-embedded-helpers/failover-matches.ts +++ b/src/agents/pi-embedded-helpers/failover-matches.ts @@ -1,5 +1,8 @@ type ErrorPattern = RegExp | string; +const PERIODIC_USAGE_LIMIT_RE = + /\b(?:daily|weekly|monthly)(?:\/(?:daily|weekly|monthly))* (?:usage )?limit(?:s)?(?: (?:exhausted|reached|exceeded))?\b/i; + const ERROR_PATTERNS = { rateLimit: [ /rate[_ ]limit|too many requests|429/, @@ -9,7 +12,6 @@ const ERROR_PATTERNS = { "quota exceeded", "resource_exhausted", "usage limit", - /weekly\/monthly limit/i, /\btpm\b/i, "tokens per minute", ], @@ -118,6 +120,10 @@ export function isTimeoutErrorMessage(raw: string): boolean { return matchesErrorPatterns(raw, ERROR_PATTERNS.timeout); } +export function isPeriodicUsageLimitErrorMessage(raw: string): boolean { + return PERIODIC_USAGE_LIMIT_RE.test(raw); +} + export function isBillingErrorMessage(raw: string): boolean { const value = raw.toLowerCase(); if (!value) {