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:
Logan Ye
2026-04-29 19:15:45 +08:00
committed by GitHub
parent 1f8ccf2d2a
commit ef7c528c8a
4 changed files with 46 additions and 1 deletions

View File

@@ -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.",
);
});
});

View File

@@ -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) + '"}}';

View File

@@ -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,

View File

@@ -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 {