From b0501bbab7b3ec3ed56eb8903d9a27f8273f0edb Mon Sep 17 00:00:00 2001 From: Sebastian <19554889+sebslight@users.noreply.github.com> Date: Thu, 12 Feb 2026 09:08:25 -0500 Subject: [PATCH] fix: tighten 402 billing detection regexes (openclaw#13827) thanks @0xRaini --- src/agents/live-auth-keys.test.ts | 35 +++++++++++++++++++ src/agents/live-auth-keys.ts | 2 +- ...dded-helpers.isbillingerrormessage.test.ts | 6 ++++ src/agents/pi-embedded-helpers/errors.ts | 2 +- 4 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 src/agents/live-auth-keys.test.ts 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 50ddd64bcf3..e272d4cf9f5 100644 --- a/src/agents/live-auth-keys.ts +++ b/src/agents/live-auth-keys.ts @@ -91,7 +91,7 @@ export function isAnthropicBillingError(message: string): boolean { return true; } if ( - /(?:status|code|http|error)\s*[:=]?\s*402\b|(?:got|returned|received|a)\s+(?:a\s+)?402\b|^\s*402\s+payment/i.test( + /["']?(?: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, ) ) { diff --git a/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts b/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts index 5529be9c4a2..69b04e8bb37 100644 --- a/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts +++ b/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts @@ -38,6 +38,9 @@ describe("isBillingErrorMessage", () => { "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); @@ -53,6 +56,9 @@ describe("isBillingErrorMessage", () => { "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 99d230ed307..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: [ - /(?:status|code|http|error)\s*[:=]?\s*402\b|(?:got|returned|received|a)\s+(?:a\s+)?402\b|^\s*402\s+payment/i, + /["']?(?: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",