mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:00:43 +00:00
fix(agents): recognize flat JSON billing payloads and snake_case error codes (#74188)
* fix(agents): recognize flat JSON billing payloads and snake_case error codes
Two independent fixes for billing error detection:
1. isErrorPayloadObject/parseApiErrorInfo now recognize flat JSON like
{"error":"string_code","message":"..."} where error is a string code
at the top level, not just nested {"error":{"type":"...","message":"..."}}
envelopes.
2. isBillingErrorMessage now matches "insufficient_balance" (underscore)
and "Insufficient MBT balance" (one word between insufficient/balance)
via two new patterns in the billing pattern list.
Together these prevent raw JSON from leaking to user-facing chat when
providers return 402-style flat payloads.
Fixes #74079
* fix(agents): remove redundant billing pattern and fix misleading regex comment
This commit is contained in:
@@ -134,6 +134,16 @@ describe("formatAssistantErrorText", () => {
|
||||
expect(result).toContain("API provider");
|
||||
expect(result).toBe(BILLING_ERROR_USER_MESSAGE);
|
||||
});
|
||||
it("returns a friendly billing message for flat JSON insufficient_balance payloads (#74079)", () => {
|
||||
const msg = makeAssistantError(
|
||||
'{"error":"insufficient_balance","message":"Insufficient MBT balance. Top up or upgrade your subscription to continue.","upgradeUrl":"/settings/billing"}',
|
||||
);
|
||||
const result = formatAssistantErrorText(msg, {
|
||||
provider: "google",
|
||||
model: "gemini-3.1-pro-preview",
|
||||
});
|
||||
expect(result).toBe(formatBillingErrorMessage("google", "gemini-3.1-pro-preview"));
|
||||
});
|
||||
it("returns a friendly message for rate limit errors", () => {
|
||||
const msg = makeAssistantError("429 rate limit reached");
|
||||
expect(formatAssistantErrorText(msg)).toContain("rate limit reached");
|
||||
@@ -404,4 +414,13 @@ describe("raw API error payload helpers", () => {
|
||||
expect(getApiErrorPayloadFingerprint(raw)).toContain("server_error");
|
||||
expect(getApiErrorPayloadFingerprint(raw)).toContain("req_123");
|
||||
});
|
||||
|
||||
it("recognizes flat JSON payloads with string error code and message (#74079)", () => {
|
||||
const raw =
|
||||
'{"error":"insufficient_balance","message":"Insufficient MBT balance. Top up or upgrade your subscription to continue.","upgradeUrl":"/settings/billing"}';
|
||||
expect(isRawApiErrorPayload(raw)).toBe(true);
|
||||
expect(formatRawAssistantErrorForUi(raw)).toBe(
|
||||
"LLM error insufficient_balance: Insufficient MBT balance. Top up or upgrade your subscription to continue.",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -205,6 +205,21 @@ describe("isBillingErrorMessage", () => {
|
||||
expect(isBillingErrorMessage(sample)).toBe(false);
|
||||
expect(classifyFailoverReason(sample)).toBeNull();
|
||||
});
|
||||
it("matches insufficient_balance snake_case error codes (#74079)", () => {
|
||||
expect(isBillingErrorMessage("insufficient_balance")).toBe(true);
|
||||
expect(classifyFailoverReason("insufficient_balance")).toBe("billing");
|
||||
});
|
||||
it("matches 'Insufficient MBT balance' with intervening words (#74079)", () => {
|
||||
const msg = "Insufficient MBT balance. Top up or upgrade your subscription to continue.";
|
||||
expect(isBillingErrorMessage(msg)).toBe(true);
|
||||
expect(classifyFailoverReason(msg)).toBe("billing");
|
||||
});
|
||||
it("classifies flat JSON billing payloads with string error code (#74079)", () => {
|
||||
const raw =
|
||||
'{"error":"insufficient_balance","message":"Insufficient MBT balance. Top up or upgrade your subscription to continue.","upgradeUrl":"/settings/billing"}';
|
||||
expect(isBillingErrorMessage(raw)).toBe(true);
|
||||
expect(classifyFailoverReason(raw)).toBe("billing");
|
||||
});
|
||||
it("still matches explicit 402 markers in long payloads", () => {
|
||||
const longStructuredError =
|
||||
'{"error":{"code":402,"message":"payment required","details":"' + "x".repeat(700) + '"}}';
|
||||
|
||||
@@ -182,7 +182,11 @@ const ERROR_PATTERNS = {
|
||||
/insufficient[_ ]quota/i,
|
||||
"credit balance",
|
||||
"plans & billing",
|
||||
"insufficient balance",
|
||||
/insufficient[_ ]balance/i,
|
||||
// Fuzzy: "Insufficient MBT balance", "Insufficient token balance", etc.
|
||||
// Exactly one intervening word — avoids false positives like
|
||||
// "insufficient to reconcile the final balance"
|
||||
/\binsufficient\s+\w+\s+balance\b/i,
|
||||
"insufficient usd or diem balance",
|
||||
/requires?\s+more\s+credits/i,
|
||||
/out of extra usage/i,
|
||||
|
||||
@@ -47,6 +47,10 @@ function isErrorPayloadObject(payload: unknown): payload is ErrorPayload {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Flat error payloads: {"error":"insufficient_balance","message":"..."}
|
||||
if (typeof err === "string" && typeof record.message === "string") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -165,6 +169,9 @@ export function parseApiErrorInfo(raw?: string): ApiErrorInfo | null {
|
||||
if (typeof err.message === "string") {
|
||||
errMessage = err.message;
|
||||
}
|
||||
} else if (typeof payload.error === "string") {
|
||||
// Flat error payloads: {"error":"insufficient_balance","message":"..."}
|
||||
errType = payload.error;
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user