diff --git a/src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts b/src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts index 55ca28388..a6ad08f9f 100644 --- a/src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts +++ b/src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts @@ -1,6 +1,6 @@ import type { AssistantMessage } from "@mariozechner/pi-ai"; import { describe, expect, it } from "vitest"; -import { formatAssistantErrorText } from "./pi-embedded-helpers.js"; +import { BILLING_ERROR_USER_MESSAGE, formatAssistantErrorText } from "./pi-embedded-helpers.js"; describe("formatAssistantErrorText", () => { const makeAssistantError = (errorMessage: string): AssistantMessage => @@ -53,4 +53,19 @@ describe("formatAssistantErrorText", () => { ); expect(formatAssistantErrorText(msg)).toBe("LLM error server_error: Something exploded"); }); + it("returns a friendly billing message for credit balance errors", () => { + const msg = makeAssistantError("Your credit balance is too low to access the Anthropic API."); + const result = formatAssistantErrorText(msg); + expect(result).toBe(BILLING_ERROR_USER_MESSAGE); + }); + it("returns a friendly billing message for HTTP 402 errors", () => { + const msg = makeAssistantError("HTTP 402 Payment Required"); + const result = formatAssistantErrorText(msg); + expect(result).toBe(BILLING_ERROR_USER_MESSAGE); + }); + it("returns a friendly billing message for insufficient credits", () => { + const msg = makeAssistantError("insufficient credits"); + const result = formatAssistantErrorText(msg); + expect(result).toBe(BILLING_ERROR_USER_MESSAGE); + }); }); diff --git a/src/agents/pi-embedded-helpers.ts b/src/agents/pi-embedded-helpers.ts index 88443756f..f8fb4f0ec 100644 --- a/src/agents/pi-embedded-helpers.ts +++ b/src/agents/pi-embedded-helpers.ts @@ -6,6 +6,7 @@ export { stripThoughtSignatures, } from "./pi-embedded-helpers/bootstrap.js"; export { + BILLING_ERROR_USER_MESSAGE, classifyFailoverReason, formatRawAssistantErrorForUi, formatAssistantErrorText, diff --git a/src/agents/pi-embedded-helpers/errors.ts b/src/agents/pi-embedded-helpers/errors.ts index c230f0fd7..92a47fd75 100644 --- a/src/agents/pi-embedded-helpers/errors.ts +++ b/src/agents/pi-embedded-helpers/errors.ts @@ -3,6 +3,9 @@ import type { OpenClawConfig } from "../../config/config.js"; import type { FailoverReason } from "./types.js"; import { formatSandboxToolPolicyBlockedMessage } from "../sandbox.js"; +export const BILLING_ERROR_USER_MESSAGE = + "⚠️ API provider returned a billing error — your API key has run out of credits or has an insufficient balance. Check your provider's billing dashboard and top up or switch to a different API key."; + export function isContextOverflowError(errorMessage?: string): boolean { if (!errorMessage) { return false; @@ -368,6 +371,10 @@ export function formatAssistantErrorText( return "The AI service is temporarily overloaded. Please try again in a moment."; } + if (isBillingErrorMessage(raw)) { + return BILLING_ERROR_USER_MESSAGE; + } + if (isLikelyHttpErrorText(raw) || isRawApiErrorPayload(raw)) { return formatRawAssistantErrorForUi(raw); } @@ -403,6 +410,10 @@ export function sanitizeUserFacingText(text: string): string { ); } + if (isBillingErrorMessage(trimmed)) { + return BILLING_ERROR_USER_MESSAGE; + } + if (isRawApiErrorPayload(trimmed) || isLikelyHttpErrorText(trimmed)) { return formatRawAssistantErrorForUi(trimmed); } diff --git a/src/agents/pi-embedded-runner/run.ts b/src/agents/pi-embedded-runner/run.ts index 45b179f0a..d7fb2693d 100644 --- a/src/agents/pi-embedded-runner/run.ts +++ b/src/agents/pi-embedded-runner/run.ts @@ -29,9 +29,11 @@ import { import { normalizeProviderId } from "../model-selection.js"; import { ensureOpenClawModelsJson } from "../models-config.js"; import { + BILLING_ERROR_USER_MESSAGE, classifyFailoverReason, formatAssistantErrorText, isAuthAssistantError, + isBillingAssistantError, isCompactionFailureError, isContextOverflowError, isFailoverAssistantError, @@ -549,6 +551,7 @@ export async function runEmbeddedPiAgent( const authFailure = isAuthAssistantError(lastAssistant); const rateLimitFailure = isRateLimitAssistantError(lastAssistant); + const billingFailure = isBillingAssistantError(lastAssistant); const failoverFailure = isFailoverAssistantError(lastAssistant); const assistantFailoverReason = classifyFailoverReason(lastAssistant?.errorMessage ?? ""); const cloudCodeAssistFormatError = attempt.cloudCodeAssistFormatError; @@ -620,9 +623,11 @@ export async function runEmbeddedPiAgent( ? "LLM request timed out." : rateLimitFailure ? "LLM request rate limited." - : authFailure - ? "LLM request unauthorized." - : "LLM request failed."); + : billingFailure + ? BILLING_ERROR_USER_MESSAGE + : authFailure + ? "LLM request unauthorized." + : "LLM request failed."); const status = resolveFailoverStatus(assistantFailoverReason ?? "unknown") ?? (isTimeoutErrorMessage(message) ? 408 : undefined);