From b3b148bba13292acfb762d243ad79061fe515680 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Fri, 6 Mar 2026 03:36:11 -0600 Subject: [PATCH] fix: narrow 402 rate-limit matcher to prevent billing misclassification The original implementation used isRateLimitErrorMessage(), which matches phrases like 'quota exceeded' that legitimately appear in billing errors. This commit replaces it with a narrow, 402-specific matcher that requires BOTH retry language (try again/retry/temporary/cooldown) AND limit terminology (usage limit/rate limit/organization usage). Prevents misclassification of errors like: 'HTTP 402: exceeded quota, please add credits' -> billing (not rate_limit) Added regression test for the ambiguous case. --- src/agents/failover-error.test.ts | 7 +++++++ src/agents/pi-embedded-helpers/errors.ts | 20 ++++++++++++++++---- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/agents/failover-error.test.ts b/src/agents/failover-error.test.ts index d550683923e..1036cf55aa2 100644 --- a/src/agents/failover-error.test.ts +++ b/src/agents/failover-error.test.ts @@ -51,6 +51,13 @@ describe("failover-error", () => { message: "insufficient credits — please top up your account", }), ).toBe("billing"); + // Ambiguous "quota exceeded" + billing signal → billing wins + expect( + resolveFailoverReasonFromError({ + status: 402, + message: "HTTP 402: You have exceeded your current quota. Please add more credits.", + }), + ).toBe("billing"); expect(resolveFailoverReasonFromError({ statusCode: "429" })).toBe("rate_limit"); expect(resolveFailoverReasonFromError({ status: 403 })).toBe("auth"); expect(resolveFailoverReasonFromError({ status: 408 })).toBe("timeout"); diff --git a/src/agents/pi-embedded-helpers/errors.ts b/src/agents/pi-embedded-helpers/errors.ts index 6ac244ba3ea..7ad9d4cc3b4 100644 --- a/src/agents/pi-embedded-helpers/errors.ts +++ b/src/agents/pi-embedded-helpers/errors.ts @@ -261,10 +261,22 @@ export function classifyFailoverReasonFromHttpStatus( if (status === 402) { // Some providers (e.g. Anthropic Claude Max plan) surface temporary - // usage/rate-limit failures as HTTP 402. Prefer the explicit rate-limit - // signal from the payload text when available (#30484). - if (message && isRateLimitErrorMessage(message)) { - return "rate_limit"; + // usage/rate-limit failures as HTTP 402. Use a narrow matcher for + // temporary limits to avoid misclassifying billing failures (#30484). + if (message) { + const lower = message.toLowerCase(); + // Temporary usage limit signals: retry language + usage/limit terminology + const hasTemporarySignal = + (lower.includes("try again") || + lower.includes("retry") || + lower.includes("temporary") || + lower.includes("cooldown")) && + (lower.includes("usage limit") || + lower.includes("rate limit") || + lower.includes("organization usage")); + if (hasTemporarySignal) { + return "rate_limit"; + } } return "billing"; }