Skip to content

fix: prevent false positive billing error detection in sanitizeUserFacingText#12777

Closed
jpaine wants to merge 1 commit intoopenclaw:mainfrom
jpaine:fix/sanitize-billing-false-positive
Closed

fix: prevent false positive billing error detection in sanitizeUserFacingText#12777
jpaine wants to merge 1 commit intoopenclaw:mainfrom
jpaine:fix/sanitize-billing-false-positive

Conversation

@jpaine
Copy link

@jpaine jpaine commented Feb 9, 2026

Summary

sanitizeUserFacingText() pattern-matches /\b402\b/ via isBillingErrorMessage() on all assistant text before channel delivery. This causes any response containing the literal number "402" in normal context (e.g., "$402.55" in a cost report, street addresses, item counts) to be silently replaced with a billing error warning on the channel surface.

Fixes #12711.

lobster-biscuit

Repro Steps

  1. Have the assistant generate a response containing "402" in any non-error context (e.g., "$402.55 MTD spend")
  2. Response is delivered to the channel as: ⚠️ API provider returned a billing error...
  3. Dashboard view shows the original, correct response

Root Cause

sanitizeUserFacingText() calls isBillingErrorMessage(trimmed) unconditionally on all user-facing text. The billing error patterns include /\b402\b/, which matches "402" at any word boundary — including after $ (since $ is not a word character). Normal dollar amounts like "$402.55", addresses like "402 Main Street", and counts like "402 items" all trigger the false positive.

The context overflow check in the same function already gates itself behind error-like indicators (isRawApiErrorPayload, isLikelyHttpErrorText, ERROR_PREFIX_RE), but the billing check had no such guard.

Behavior Changes

  • sanitizeUserFacingText() now only rewrites billing errors when the text also looks like a leaked error payload (raw API JSON, HTTP status line, or error-prefixed text)
  • Normal assistant text containing "402" passes through unchanged
  • isBillingErrorMessage() itself is unchanged — it remains sensitive for use in formatAssistantErrorText() and classifyFailoverReason() where the input is already known to be an error message
  • Added "payment" to HTTP_ERROR_HINTS so "402 Payment Required" is properly recognized as an HTTP error

Codebase and GitHub Search

  • Searched all callers of sanitizeUserFacingText (normalize-reply, agent-runner-execution, sessions-helpers, pi-embedded-utils)
  • Searched all callers of isBillingErrorMessage to confirm the function itself should not change (used in formatAssistantErrorText and classifyFailoverReason where input is known-error)
  • Reviewed the parallel pattern in shouldRewriteContextOverflowText which already gates context overflow detection behind error indicators

Tests

All pass (pnpm check + pnpm test):

  • New: "does not rewrite normal text containing '402'" — dollar amounts ($402.55), street addresses, item counts, historical years
  • New: "still rewrites leaked billing error payloads" — HTTP status ("402 Payment Required"), error-prefixed ("Error: 402 Payment Required"), JSON API error payloads with billing keywords
  • Existing isBillingErrorMessage tests unchanged and passing
  • Pre-existing failure in isCompactionFailureError test is unrelated (present on main)

Sign-Off

  • Models used: Claude claude-4.6-opus-high-thinking (Cursor agent)
  • Submitter effort: guided/reviewed by human

Made with Cursor

Greptile Overview

Greptile Summary

This PR adjusts sanitizeUserFacingText() to avoid rewriting normal assistant output into the billing warning when the text merely contains the number 402 (e.g. $402.55). It does this by gating the billing rewrite behind additional “looks like an error payload” checks (isRawApiErrorPayload, isLikelyHttpErrorText, or ERROR_PREFIX_RE). It also adds "payment" to HTTP_ERROR_HINTS so plain HTTP status lines like 402 Payment Required are recognized as HTTP-ish error text, and adds tests covering both the non-rewrite and rewrite cases.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk.
  • The change is small, localized to sanitizeUserFacingText(), and the new gating logic directly addresses the reported false positive without changing isBillingErrorMessage() behavior for known-error contexts. Added tests cover the new behavior, and the HTTP_ERROR_HINTS tweak aligns with existing isLikelyHttpErrorText() semantics. No additional call sites or invariants appear to be broken by this change.
  • No files require special attention

(2/5) Greptile learns from your feedback when you react with thumbs up/down!

@Takhoffman
Copy link
Contributor

Fixed in #12988.

This will go out in the next OpenClaw release.

If you still see this after updating to the first release that includes #12988, please open a new issue with:

  • your OpenClaw version
  • channel (Telegram/Slack/etc)
  • the exact prompt/response that got rewritten
  • whether Web UI showed the full text vs the channel being rewritten
  • relevant logs around send/normalize (if available)

Link back here for context.

@dominicnunez
Copy link
Contributor

dominicnunez commented Feb 14, 2026

Great approach — gating isBillingErrorMessage behind structural error signals (isRawApiErrorPayload || isLikelyHttpErrorText || ERROR_PREFIX_RE) is the right pattern and matches how shouldRewriteContextOverflowText already works in the same file. The "payment" addition to HTTP_ERROR_HINTS is also correct and necessary.

Relationship to #12988

#12988 (merged to main, not yet released) added the errorContext flag so error-pattern rewrites only run when the caller knows the text is an error payload. That fixes the broadest class of false positives — normal assistant text mentioning "402" or billing keywords no longer gets rewritten. The billing regex was also tightened (from bare \b402\b to patterns requiring context like status: 402, http 402, error code 402, etc.), so $402.55 and 402 Main Street no longer match.

This PR is still needed because errorContext: true doesn't mean the text is a billing error — it just means it's some error. Error payloads from non-LLM sources can contain billing keywords in a non-LLM-billing context. For example, a tool error that says "Got a 402 from the upstream API" or an error containing "payment required" for a non-billing reason would match isBillingErrorMessage and get replaced with the billing warning, even though the user's LLM API key is fine. The structural guard this PR adds ensures billing rewrites only fire when the text also looks like a proper error payload (JSON, HTTP status line, or error-prefixed), closing that gap.

What needs updating

The PR needs a rebase onto current main (post-#12988). Two things to fix:

1. The diff targets stale code.

The hunk header shows sanitizeUserFacingText(text: string): string, but current main has:

export function sanitizeUserFacingText(text: string, opts?: { errorContext?: boolean }): string {

The billing check moved from line ~424 (unconditional) to line 526 (inside if (errorContext)). The structural guard needs to be applied there.

2. Tests need { errorContext: true } and updated inputs.

On current main, without errorContext, billing rewrites never run. The $402.55 test input also no longer matches the tightened billing regex, so it passes trivially even without this fix.

Tests should use inputs that actually trigger isBillingErrorMessage on current main, and pass errorContext: true:

// False positive — tool/upstream 402, not an LLM billing error.
// Should pass through even in error context.
expect(sanitizeUserFacingText("Got a 402 from the upstream API", { errorContext: true }))
  .not.toContain("billing");

// Real billing error payload — should still rewrite
expect(sanitizeUserFacingText("402 Payment Required", { errorContext: true }))
  .toContain("billing");

// JSON billing error — should still rewrite
expect(
  sanitizeUserFacingText(
    '{"type":"error","error":{"message":"insufficient credits","type":"billing_error"}}',
    { errorContext: true },
  ),
).toContain("billing");

@Takhoffman
Copy link
Contributor

Closing as superseded by the merged sanitize/error-context work:

This PR’s intent appears covered by those merged changes and current mainline tests.

@Takhoffman Takhoffman closed this Feb 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling size: XS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: sanitizeUserFacingText() matches "402" in normal assistant responses, replacing them with billing error warning

3 participants

Comments