From 43f318cd9af8ae31bebf31e79facecf70d062f96 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 25 Feb 2026 01:21:37 +0000 Subject: [PATCH] fix(agents): reduce billing false positives on long text (#25680) Land PR #25680 from @lairtonlelis. Retain explicit status/code/http 402 detection for oversized structured payloads. Co-authored-by: Ailton --- CHANGELOG.md | 1 + ...dded-helpers.isbillingerrormessage.test.ts | 34 +++++++++++++++++++ src/agents/pi-embedded-helpers/errors.ts | 17 ++++++++++ 3 files changed, 52 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66790a1a45c..61a679d293b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,7 @@ Docs: https://docs.openclaw.ai - Security/Exec: limit default safe-bin trusted directories to immutable system paths (`/bin`, `/usr/bin`) and require explicit opt-in (`tools.exec.safeBinTrustedDirs`) for package-manager/user bin paths (for example Homebrew), add security-audit findings for risky trusted-dir choices, warn at runtime when explicitly trusted dirs are group/world writable, and add doctor hints when configured `safeBins` resolve outside trusted dirs. Thanks @tdjackey for reporting. - Telegram/Media fetch: prioritize IPv4 before IPv6 in SSRF pinned DNS address ordering so media downloads still work on hosts with broken IPv6 routing. (#24295, #23975) Thanks @Glucksberg. - Telegram/Outbound API: replace Node 22's global undici dispatcher when applying Telegram `autoSelectFamily` decisions so outbound `fetch` calls inherit IPv4 fallback instead of staying pinned to stale dispatcher settings. (#25682, #25676) Thanks @lairtonlelis. +- Agents/Billing classification: prevent long assistant/user-facing text from being rewritten as billing failures while preserving explicit `status/code/http 402` detection for oversized structured error payloads. (#25680, #25661) Thanks @lairtonlelis. - Telegram/Replies: when markdown formatting renders to empty HTML (for example syntax-only chunks in threaded replies), retry delivery with plain text, and fail loud when both formatted and plain payloads are empty to avoid false delivered states. (#25096, #25091) Thanks @Glucksberg. - Sessions/Tool-result guard: avoid generating synthetic `toolResult` entries for assistant turns that ended with `stopReason: "aborted"` or `"error"`, preventing orphaned tool-use IDs from triggering downstream API validation errors. (#25429) Thanks @mikaeldiakhate-cell. - Usage accounting: parse Moonshot/Kimi `cached_tokens` fields (including `prompt_tokens_details.cached_tokens`) into normalized cache-read usage metrics. (#25436) Thanks @Elarwei001. diff --git a/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts b/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts index 278c2d30bcb..97b96727562 100644 --- a/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts +++ b/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts @@ -69,6 +69,40 @@ describe("isBillingErrorMessage", () => { expect(isBillingErrorMessage(sample)).toBe(false); } }); + it("does not false-positive on long assistant responses mentioning billing keywords", () => { + // Simulate a multi-paragraph assistant response that mentions billing terms + const longResponse = + "Sure! Here's how to set up billing for your SaaS application.\n\n" + + "## Payment Integration\n\n" + + "First, you'll need to configure your payment gateway. Most providers offer " + + "a dashboard where you can manage credits, view invoices, and upgrade your plan. " + + "The billing page typically shows your current balance and payment history.\n\n" + + "## Managing Credits\n\n" + + "Users can purchase credits through the billing portal. When their credit balance " + + "runs low, send them a notification to upgrade their plan or add more credits. " + + "You should also handle insufficient balance cases gracefully.\n\n" + + "## Subscription Plans\n\n" + + "Offer multiple plan tiers with different features. Allow users to upgrade or " + + "downgrade their plan at any time. Make sure the billing cycle is clear.\n\n" + + "Let me know if you need more details on any of these topics!"; + expect(longResponse.length).toBeGreaterThan(512); + expect(isBillingErrorMessage(longResponse)).toBe(false); + }); + it("still matches explicit 402 markers in long payloads", () => { + const longStructuredError = + '{"error":{"code":402,"message":"payment required","details":"' + "x".repeat(700) + '"}}'; + expect(longStructuredError.length).toBeGreaterThan(512); + expect(isBillingErrorMessage(longStructuredError)).toBe(true); + }); + it("does not match long numeric text that is not a billing error", () => { + const longNonError = + "Quarterly report summary: subsystem A returned 402 records after retry. " + + "This is an analytics count, not an HTTP/API billing failure. " + + "Notes: " + + "x".repeat(700); + expect(longNonError.length).toBeGreaterThan(512); + expect(isBillingErrorMessage(longNonError)).toBe(false); + }); it("still matches real HTTP 402 billing errors", () => { const realErrors = [ "HTTP 402 Payment Required", diff --git a/src/agents/pi-embedded-helpers/errors.ts b/src/agents/pi-embedded-helpers/errors.ts index e0c7bf4c801..29fcfea2c7d 100644 --- a/src/agents/pi-embedded-helpers/errors.ts +++ b/src/agents/pi-embedded-helpers/errors.ts @@ -161,6 +161,8 @@ const CONTEXT_OVERFLOW_ERROR_HEAD_RE = /^(?:context overflow:|request_too_large\b|request size exceeds\b|request exceeds the maximum size\b|context length exceeded\b|maximum context length\b|prompt is too long\b|exceeds model context window\b)/i; const BILLING_ERROR_HEAD_RE = /^(?:error[:\s-]+)?billing(?:\s+error)?(?:[:\s-]+|$)|^(?:error[:\s-]+)?(?:credit balance|insufficient credits?|payment required|http\s*402\b)/i; +const BILLING_ERROR_HARD_402_RE = + /["']?(?:status|code)["']?\s*[:=]\s*402\b|\bhttp\s*402\b|\berror(?:\s+code)?\s*[:=]?\s*402\b|^\s*402\s+payment/i; const HTTP_STATUS_PREFIX_RE = /^(?:http\s*)?(\d{3})\s+(.+)$/i; const HTTP_STATUS_CODE_PREFIX_RE = /^(?:http\s*)?(\d{3})(?:\s+([\s\S]+))?$/i; const HTML_ERROR_PREFIX_RE = /^\s*(?: BILLING_ERROR_MAX_LENGTH) { + // Keep explicit status/code 402 detection for providers that wrap errors in + // larger payloads (for example nested JSON bodies or prefixed metadata). + return BILLING_ERROR_HARD_402_RE.test(value); + } if (matchesErrorPatterns(value, ERROR_PATTERNS.billing)) { return true; }