Files
openclaw/src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts
Glucksberg d4c560853c 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
2026-02-05 13:58:43 -08:00

72 lines
3.5 KiB
TypeScript

import type { AssistantMessage } from "@mariozechner/pi-ai";
import { describe, expect, it } from "vitest";
import { BILLING_ERROR_USER_MESSAGE, formatAssistantErrorText } from "./pi-embedded-helpers.js";
describe("formatAssistantErrorText", () => {
const makeAssistantError = (errorMessage: string): AssistantMessage =>
({
stopReason: "error",
errorMessage,
}) as AssistantMessage;
it("returns a friendly message for context overflow", () => {
const msg = makeAssistantError("request_too_large");
expect(formatAssistantErrorText(msg)).toContain("Context overflow");
});
it("returns context overflow for Anthropic 'Request size exceeds model context window'", () => {
// This is the new Anthropic error format that wasn't being detected.
// Without the fix, this falls through to the invalidRequest regex and returns
// "LLM request rejected: Request size exceeds model context window"
// instead of the context overflow message, preventing auto-compaction.
const msg = makeAssistantError(
'{"type":"error","error":{"type":"invalid_request_error","message":"Request size exceeds model context window"}}',
);
expect(formatAssistantErrorText(msg)).toContain("Context overflow");
});
it("returns a friendly message for Anthropic role ordering", () => {
const msg = makeAssistantError('messages: roles must alternate between "user" and "assistant"');
expect(formatAssistantErrorText(msg)).toContain("Message ordering conflict");
});
it("returns a friendly message for Anthropic overload errors", () => {
const msg = makeAssistantError(
'{"type":"error","error":{"details":null,"type":"overloaded_error","message":"Overloaded"},"request_id":"req_123"}',
);
expect(formatAssistantErrorText(msg)).toBe(
"The AI service is temporarily overloaded. Please try again in a moment.",
);
});
it("returns a recovery hint when tool call input is missing", () => {
const msg = makeAssistantError("tool_use.input: Field required");
const result = formatAssistantErrorText(msg);
expect(result).toContain("Session history looks corrupted");
expect(result).toContain("/new");
});
it("handles JSON-wrapped role errors", () => {
const msg = makeAssistantError('{"error":{"message":"400 Incorrect role information"}}');
const result = formatAssistantErrorText(msg);
expect(result).toContain("Message ordering conflict");
expect(result).not.toContain("400");
});
it("suppresses raw error JSON payloads that are not otherwise classified", () => {
const msg = makeAssistantError(
'{"type":"error","error":{"message":"Something exploded","type":"server_error"}}',
);
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);
});
});