fix(errors): show clear billing error instead of cryptic API response (#8391)
* fix(errors): return clear billing error message instead of cryptic raw error (#8136) When an LLM API provider returns a credit/billing-related error (HTTP 402, insufficient credits, low balance, etc.), OpenClaw now shows a clear, actionable message instead of passing through the raw/cryptic error text: ⚠️ 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. Changes: - formatAssistantErrorText: detect billing errors via isBillingErrorMessage() and return a user-friendly message (placed before the generic HTTP/JSON error fallthrough) - sanitizeUserFacingText: same billing detection for the sanitization path - pi-embedded-runner/run.ts: add billingFailure detection in the profile exhaustion fallback, so the FailoverError message is billing-specific - Added 3 new tests for credit balance, HTTP 402, and insufficient credits * fix: extract billing error message to shared constant
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ export {
|
||||
stripThoughtSignatures,
|
||||
} from "./pi-embedded-helpers/bootstrap.js";
|
||||
export {
|
||||
BILLING_ERROR_USER_MESSAGE,
|
||||
classifyFailoverReason,
|
||||
formatRawAssistantErrorForUi,
|
||||
formatAssistantErrorText,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user