openclaw - ✅(Solved) Fix [Bug] Billing-error classifier misses snake_case top-level `{"error":"insufficient_balance",…}` payloads — raw JSON leaks to user-facing chat [2 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#74079Fetched 2026-04-30 06:28:47
View on GitHub
Comments
1
Participants
2
Timeline
7
Reactions
0
Timeline (top)
cross-referenced ×4closed ×1commented ×1referenced ×1

When an LLM provider/proxy returns a 402-style billing block as a JSON payload without using the OpenAI/Anthropic-style envelope, openclaw's formatAssistantErrorText falls through every classifier and emits the raw JSON unchanged into the assistant's user-facing reply.

In production this surfaces to end users as something like:

{"error":"insufficient_balance","message":"Insufficient MBT balance. Top up or upgrade your subscription to continue.","upgradeUrl":"/settings/billing"}

…instead of the friendly billing copy that the same code path produces for OpenAI/Anthropic-shaped payloads.

Error Message

{"error":"insufficient_balance","message":"Insufficient MBT balance. Top up or upgrade your subscription to continue.","upgradeUrl":"/settings/billing"} Reproducible against ~/.npm-global/lib/node_modules/openclaw/dist/errors-CJULmF31.js and assistant-error-format-tQ4SCN8S.js (openclaw 2026.4.26): const raw = '{"error":"insufficient_balance","message":"Insufficient MBT balance. Top up or upgrade your subscription to continue.","upgradeUrl":"/settings/billing"}'; const fakeMsg = { stopReason: 'error', errorMessage: raw, provider: 'google', model: 'gemini-3.1-pro-preview' };

  • The string "insufficient_balance" (snake-case, the value of the "error" field) is not in the billing pattern list. Lowercased, the substring "insufficient balance" (without the underscore) is in the list — but the actual rendered text in the JSON's "message" is "Insufficient MBT balance", which lowercased is "insufficient mbt balance". The "mbt" token between insufficient and balance breaks the substring match.
  1. Inside formatRawAssistantErrorForUi, parseApiErrorInfo doesn't extract a usable message from this shape — it expects {"error":{"type":"…","message":"…"}} (OpenAI/Anthropic-style nested envelope) or a leading HTTP 4xx / LLM error prefix. Our shape has error as a string code at the top level, not a nested object. Result: it returns the trimmed raw payload (≤600 chars), unchanged.

A. Recognize the snake-case top-level "error":"<code>" shape

In isRawApiErrorPayload / parseApiErrorInfo (assistant-error-format / sanitize-user-facing-text bundles), accept the shape: { "error": "<snake_case_code>", "message": "<human text>", "upgradeUrl"?: "..." } When error is a string and message is a non-empty string, treat error as type and message as message. This is the Stripe-style and "PostgREST-style" error envelope — common enough to be worth handling alongside the OpenAI/Anthropic envelopes that already work.

B. Add billing-error keyword resilience

  • The snake-case codes insufficient_balance, insufficient_credits, payment_required, quota_exceeded are recognized when they appear as a value at the top level of an error JSON (not just substring-hits in free-form prose).

Root Cause

  1. isBillingErrorMessage(raw) returns false because:
    • The string "insufficient_balance" (snake-case, the value of the "error" field) is not in the billing pattern list. Lowercased, the substring "insufficient balance" (without the underscore) is in the list — but the actual rendered text in the JSON's "message" is "Insufficient MBT balance", which lowercased is "insufficient mbt balance". The "mbt" token between insufficient and balance breaks the substring match.
    • BILLING_ERROR_HEAD_RE requires the string to START with billing/credit balance/insufficient credits/payment required/http 402 — but the raw is JSON starting with {.

Fix Action

Fix / Workaround

We have a sandbox-side workaround in flight on our end, but the right place for the friendly classification to live is in openclaw itself so every consumer benefits. Filing for upstream consideration.

PR fix notes

PR #74099: fix(assistant-error-format): recognize Stripe/PostgREST error envelopes and snake_case billing codes

Description (problem / solution / changelog)

Summary

When an LLM proxy returns a 402 billing block as {"error":"insufficient_balance","message":"Insufficient MBT balance. Top up or upgrade your subscription to continue.","upgradeUrl":"/settings/billing"}, openclaw's formatAssistantErrorText falls through every classifier and emits the raw JSON unchanged into the user-facing reply.

This PR implements two independent fixes from #74079:

A. Recognize Stripe/PostgREST-style top-level string error codes

  • isErrorPayloadObject now accepts { "error": "<code>", "message": "<text>" } shape
  • parseApiErrorInfo extracts the string code as type and the message as message
  • formatRawAssistantErrorForUi renders it as LLM error insufficient_balance: <message>

B. Add billing-error keyword resilience for snake_case codes

Adds regex patterns for: insufficient_balance, insufficient_credits, payment_required, quota_exceeded.

Bonus: upgradeUrl surfacing

When the payload includes upgradeUrl, billing_url, or manage_url, the friendly message now surfaces it.

Testing

const raw = '{"error":"insufficient_balance","message":"Insufficient MBT balance. Top up or upgrade your subscription to continue.","upgradeUrl":"/settings/billing"}';
const info = parseApiErrorInfo(raw);
// → { type: "insufficient_balance", message: "Insufficient MBT balance...", upgradeUrl: "/settings/billing" }

Fix #74079

Changed files

  • src/agents/pi-embedded-helpers/failover-matches.ts (modified, +4/-0)
  • src/shared/assistant-error-format.ts (modified, +19/-1)

PR #74188: fix(agents): recognize flat JSON billing payloads and snake_case error codes

Description (problem / solution / changelog)

Summary

  • Recognizes flat JSON error payloads ({"error":"string_code","message":"..."}) in the API error parser, in addition to the existing OpenAI/Anthropic-style nested envelope ({"error":{"type":"...","message":"..."}})
  • Adds billing pattern matching for insufficient_balance (underscore variant) and fuzzy Insufficient <word> balance (e.g., "Insufficient MBT balance")
  • Together these prevent raw JSON from leaking to user-facing chat when providers return 402-style flat payloads

Problem

When an LLM provider/proxy returns a billing error as a flat JSON payload without using the OpenAI/Anthropic-style envelope, both classifiers miss it:

  1. isBillingErrorMessage misses "insufficient_balance" (underscore, not space) and "Insufficient MBT balance" (intervening word breaks substring match)
  2. isErrorPayloadObject / parseApiErrorInfo only recognize {"error":{"type":"…","message":"…"}} (nested object), not {"error":"string_code","message":"…"} (string at top level)

Result: raw JSON like {"error":"insufficient_balance","message":"Insufficient MBT balance...","upgradeUrl":"..."} leaks verbatim into the assistant's user-facing reply.

Changes

src/shared/assistant-error-format.ts

  • isErrorPayloadObject: Added recognition of flat payloads where record.error is a string and record.message is a string
  • parseApiErrorInfo: When payload.error is a string (not an object), extracts it as errType so the formatter can produce "LLM error insufficient_balance: <message>"

src/agents/pi-embedded-helpers/failover-matches.ts

  • Added /insufficient[_ ]balance/i to match underscore and space variants
  • Added /\binsufficient\s+\w+\s+balance\b/i to match "Insufficient MBT balance", "Insufficient token balance", etc. (one intervening word, avoids false positives like "insufficient to reconcile the final balance")

Tests

  • 2 new tests in formatassistanterrortext.test.ts: flat JSON billing payload through formatAssistantErrorText and formatRawAssistantErrorForUi
  • 3 new tests in isbillingerrormessage.test.ts: snake_case error code, intervening-word pattern, and full flat JSON payload classification

Fixes #74079

Changed files

  • src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts (modified, +19/-0)
  • src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts (modified, +15/-0)
  • src/agents/pi-embedded-helpers/failover-matches.ts (modified, +5/-1)
  • src/shared/assistant-error-format.ts (modified, +7/-0)

Code Example

{"error":"insufficient_balance","message":"Insufficient MBT balance. Top up or upgrade your subscription to continue.","upgradeUrl":"/settings/billing"}

---

const M = await import('@openclaw/.../errors-CJULmF31.js');
const formatAssistantErrorText = M.a;
const raw = '{"error":"insufficient_balance","message":"Insufficient MBT balance. Top up or upgrade your subscription to continue.","upgradeUrl":"/settings/billing"}';
const fakeMsg = { stopReason: 'error', errorMessage: raw, provider: 'google', model: 'gemini-3.1-pro-preview' };
console.log(formatAssistantErrorText(fakeMsg, { provider: 'google', model: 'gemini-3.1-pro-preview' }));
// → returns the raw JSON unchanged

---

{ "error": "<snake_case_code>", "message": "<human text>", "upgradeUrl"?: "..." }
RAW_BUFFERClick to expand / collapse

Summary

When an LLM provider/proxy returns a 402-style billing block as a JSON payload without using the OpenAI/Anthropic-style envelope, openclaw's formatAssistantErrorText falls through every classifier and emits the raw JSON unchanged into the assistant's user-facing reply.

In production this surfaces to end users as something like:

{"error":"insufficient_balance","message":"Insufficient MBT balance. Top up or upgrade your subscription to continue.","upgradeUrl":"/settings/billing"}

…instead of the friendly billing copy that the same code path produces for OpenAI/Anthropic-shaped payloads.

Repro

Reproducible against ~/.npm-global/lib/node_modules/openclaw/dist/errors-CJULmF31.js and assistant-error-format-tQ4SCN8S.js (openclaw 2026.4.26):

const M = await import('@openclaw/.../errors-CJULmF31.js');
const formatAssistantErrorText = M.a;
const raw = '{"error":"insufficient_balance","message":"Insufficient MBT balance. Top up or upgrade your subscription to continue.","upgradeUrl":"/settings/billing"}';
const fakeMsg = { stopReason: 'error', errorMessage: raw, provider: 'google', model: 'gemini-3.1-pro-preview' };
console.log(formatAssistantErrorText(fakeMsg, { provider: 'google', model: 'gemini-3.1-pro-preview' }));
// → returns the raw JSON unchanged

Why every classifier misses it

  1. isBillingErrorMessage(raw) returns false because:

    • The string "insufficient_balance" (snake-case, the value of the "error" field) is not in the billing pattern list. Lowercased, the substring "insufficient balance" (without the underscore) is in the list — but the actual rendered text in the JSON's "message" is "Insufficient MBT balance", which lowercased is "insufficient mbt balance". The "mbt" token between insufficient and balance breaks the substring match.
    • BILLING_ERROR_HEAD_RE requires the string to START with billing/credit balance/insufficient credits/payment required/http 402 — but the raw is JSON starting with {.
  2. isRawApiErrorPayload(raw) returns true (it parses fine), so the function falls into formatRawAssistantErrorForUi(raw).

  3. Inside formatRawAssistantErrorForUi, parseApiErrorInfo doesn't extract a usable message from this shape — it expects {"error":{"type":"…","message":"…"}} (OpenAI/Anthropic-style nested envelope) or a leading HTTP 4xx / LLM error prefix. Our shape has error as a string code at the top level, not a nested object. Result: it returns the trimmed raw payload (≤600 chars), unchanged.

End user sees the JSON.

What I'd suggest

Two small, independent fixes. Either alone would solve our case; both together close the gap permanently.

A. Recognize the snake-case top-level "error":"<code>" shape

In isRawApiErrorPayload / parseApiErrorInfo (assistant-error-format / sanitize-user-facing-text bundles), accept the shape:

{ "error": "<snake_case_code>", "message": "<human text>", "upgradeUrl"?: "..." }

When error is a string and message is a non-empty string, treat error as type and message as message. This is the Stripe-style and "PostgREST-style" error envelope — common enough to be worth handling alongside the OpenAI/Anthropic envelopes that already work.

B. Add billing-error keyword resilience

Augment the billing pattern list and/or BILLING_ERROR_HEAD_RE so that:

  • The snake-case codes insufficient_balance, insufficient_credits, payment_required, quota_exceeded are recognized when they appear as a value at the top level of an error JSON (not just substring-hits in free-form prose).
  • Strings of the form "insufficient <anything> balance" (where the middle token is a vendor/feature/currency word like mbt, usd, account, prepaid, etc.) are recognized. Today only the literal "insufficient balance" and "insufficient usd or diem balance" match.

Bonus: keep upgradeUrl if present

When the raw payload includes an upgradeUrl (or billing_url, manage_url, etc.), surface it in the friendly message as a clickable link. Today the URL is silently discarded.

Why we hit this

We run a transparent LLM proxy in front of multiple providers (OpenAI / Anthropic / Google). The proxy returns a uniform 402 with a JSON body that looks like the example above when a user is out of credit. Every billing-block path on every provider funnels through this format. Because the classifier doesn't recognize it, the JSON leaks to whatever surface the assistant happens to be talking to (Slack, Teams, etc.) — looks like a crash to users, not a billing message.

We have a sandbox-side workaround in flight on our end, but the right place for the friendly classification to live is in openclaw itself so every consumer benefits. Filing for upstream consideration.

Happy to send a PR for either A or B (or both) if you'd accept it. Let me know preferred shape.

Environment

  • openclaw 2026.4.26
  • Node v25.5.0
  • Provider/model in the repro: google / gemini-3.1-pro-preview (not specific to this provider — applies to any 402 with this payload shape)

extent analysis

TL;DR

The issue can be fixed by modifying the isRawApiErrorPayload and parseApiErrorInfo functions to recognize the snake-case top-level error shape and adding billing-error keyword resilience.

Guidance

  • Modify the isRawApiErrorPayload function to accept the shape { "error": "<snake_case_code>", "message": "<human text>", "upgradeUrl"?: "..." } and treat error as type and message as message.
  • Update the billing pattern list and/or BILLING_ERROR_HEAD_RE to recognize snake-case codes and strings of the form "insufficient <anything> balance".
  • Consider adding a feature to surface the upgradeUrl as a clickable link in the friendly message.
  • Test the changes with the provided repro code to ensure the fix works as expected.

Example

// Example of modified isRawApiErrorPayload function
function isRawApiErrorPayload(raw) {
  try {
    const payload = JSON.parse(raw);
    if (payload.error && typeof payload.error === 'string' && payload.message && typeof payload.message === 'string') {
      // Treat error as type and message as message
      return true;
    }
  } catch (e) {}
  return false;
}

Notes

The provided fixes are independent, and either one alone would solve the issue. However, implementing both fixes would provide a more robust solution. The changes should be tested thoroughly to ensure they do not introduce any regressions.

Recommendation

Apply the workaround by modifying the isRawApiErrorPayload and parseApiErrorInfo functions to recognize the snake-case top-level error shape and adding billing-error keyword resilience. This will provide a more robust solution and fix the issue for all consumers of the openclaw library.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix [Bug] Billing-error classifier misses snake_case top-level `{"error":"insufficient_balance",…}` payloads — raw JSON leaks to user-facing chat [2 pull requests, 1 comments, 2 participants]