diff --git a/src/agents/live-auth-keys.test.ts b/src/agents/live-auth-keys.test.ts new file mode 100644 index 00000000000..4c889598276 --- /dev/null +++ b/src/agents/live-auth-keys.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from "vitest"; +import { isAnthropicBillingError } from "./live-auth-keys.js"; + +describe("isAnthropicBillingError", () => { + it("does not false-positive on plain 'a 402' prose", () => { + const samples = [ + "Use a 402 stainless bolt", + "Book a 402 room", + "There is a 402 near me", + "The building at 402 Main Street", + ]; + + for (const sample of samples) { + expect(isAnthropicBillingError(sample)).toBe(false); + } + }); + + it("matches real 402 billing payload contexts including JSON keys", () => { + const samples = [ + "HTTP 402 Payment Required", + "status: 402", + "error code 402", + '{"status":402,"type":"error"}', + '{"code":402,"message":"payment required"}', + '{"error":{"code":402,"message":"billing hard limit reached"}}', + "got a 402 from the API", + "returned 402", + "received a 402 response", + ]; + + for (const sample of samples) { + expect(isAnthropicBillingError(sample)).toBe(true); + } + }); +}); diff --git a/src/agents/live-auth-keys.ts b/src/agents/live-auth-keys.ts index 8266d4a1b52..e272d4cf9f5 100644 --- a/src/agents/live-auth-keys.ts +++ b/src/agents/live-auth-keys.ts @@ -90,7 +90,11 @@ export function isAnthropicBillingError(message: string): boolean { if (lower.includes("billing") && lower.includes("disabled")) { return true; } - if (lower.includes("402")) { + if ( + /["']?(?:status|code)["']?\s*[:=]\s*402\b|\bhttp\s*402\b|\berror(?:\s+code)?\s*[:=]?\s*402\b|\b(?:got|returned|received)\s+(?:a\s+)?402\b|^\s*402\s+payment/i.test( + lower, + ) + ) { return true; } return false; diff --git a/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts b/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts index ed23f93d772..69b04e8bb37 100644 --- a/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts +++ b/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts @@ -27,4 +27,41 @@ describe("isBillingErrorMessage", () => { expect(isBillingErrorMessage("invalid api key")).toBe(false); expect(isBillingErrorMessage("context length exceeded")).toBe(false); }); + it("does not false-positive on issue IDs or text containing 402", () => { + const falsePositives = [ + "Fixed issue CHE-402 in the latest release", + "See ticket #402 for details", + "ISSUE-402 has been resolved", + "Room 402 is available", + "Error code 403 was returned, not 402-related", + "The building at 402 Main Street", + "processed 402 records", + "402 items found in the database", + "port 402 is open", + "Use a 402 stainless bolt", + "Book a 402 room", + "There is a 402 near me", + ]; + for (const sample of falsePositives) { + expect(isBillingErrorMessage(sample)).toBe(false); + } + }); + it("still matches real HTTP 402 billing errors", () => { + const realErrors = [ + "HTTP 402 Payment Required", + "status: 402", + "error code 402", + "http 402", + "status=402 payment required", + "got a 402 from the API", + "returned 402", + "received a 402 response", + '{"status":402,"type":"error"}', + '{"code":402,"message":"payment required"}', + '{"error":{"code":402,"message":"billing hard limit reached"}}', + ]; + for (const sample of realErrors) { + expect(isBillingErrorMessage(sample)).toBe(true); + } + }); }); diff --git a/src/agents/pi-embedded-helpers/errors.ts b/src/agents/pi-embedded-helpers/errors.ts index 12461074fa6..2a346293ac2 100644 --- a/src/agents/pi-embedded-helpers/errors.ts +++ b/src/agents/pi-embedded-helpers/errors.ts @@ -535,7 +535,7 @@ const ERROR_PATTERNS = { overloaded: [/overloaded_error|"type"\s*:\s*"overloaded_error"/i, "overloaded"], timeout: ["timeout", "timed out", "deadline exceeded", "context deadline exceeded"], billing: [ - /\b402\b/, + /["']?(?:status|code)["']?\s*[:=]\s*402\b|\bhttp\s*402\b|\berror(?:\s+code)?\s*[:=]?\s*402\b|\b(?:got|returned|received)\s+(?:a\s+)?402\b|^\s*402\s+payment/i, "payment required", "insufficient credits", "credit balance",