diff --git a/src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts b/src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts index a3dcc97fb4d..fd26408718e 100644 --- a/src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts +++ b/src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts @@ -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.", + ); + }); }); diff --git a/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts b/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts index a6352504f11..3d42c1e1e3b 100644 --- a/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts +++ b/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts @@ -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) + '"}}'; diff --git a/src/agents/pi-embedded-helpers/failover-matches.ts b/src/agents/pi-embedded-helpers/failover-matches.ts index bb3ed5ad1ed..220a2fde527 100644 --- a/src/agents/pi-embedded-helpers/failover-matches.ts +++ b/src/agents/pi-embedded-helpers/failover-matches.ts @@ -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, diff --git a/src/shared/assistant-error-format.ts b/src/shared/assistant-error-format.ts index 198556e08c1..42fcc0971d6 100644 --- a/src/shared/assistant-error-format.ts +++ b/src/shared/assistant-error-format.ts @@ -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 {