fix: zai error handling (#1047)
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
|
||||
import type { MessageCreate } from "@letta-ai/letta-client/resources/agents/agents";
|
||||
import type { ApprovalCreate } from "@letta-ai/letta-client/resources/agents/messages";
|
||||
import { isZaiNonRetryableError } from "../cli/helpers/zaiErrors";
|
||||
|
||||
// ── Error fragment constants ────────────────────────────────────────
|
||||
|
||||
@@ -81,6 +82,7 @@ export function isRetryableProviderErrorDetail(detail: unknown): boolean {
|
||||
/** Non-transient auth/validation style provider detail that should not be retried. */
|
||||
export function isNonRetryableProviderErrorDetail(detail: unknown): boolean {
|
||||
if (typeof detail !== "string") return false;
|
||||
if (isZaiNonRetryableError(detail)) return true;
|
||||
const normalized = detail.toLowerCase();
|
||||
if (NON_RETRYABLE_4XX_PATTERN.test(detail)) return true;
|
||||
return NON_RETRYABLE_PROVIDER_DETAIL_PATTERNS.some((pattern) =>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { APIError } from "@letta-ai/letta-client/core/error";
|
||||
import { getErrorContext } from "./errorContext";
|
||||
import { checkZaiError } from "./zaiErrors";
|
||||
|
||||
const LETTA_USAGE_URL = "https://app.letta.com/settings/organization/usage";
|
||||
const LETTA_AGENTS_URL =
|
||||
@@ -293,6 +294,20 @@ export function formatErrorDetails(
|
||||
const encryptedContentMsg = checkEncryptedContentError(e);
|
||||
if (encryptedContentMsg) return encryptedContentMsg;
|
||||
|
||||
// Check for Z.ai provider errors (wrapped in generic "OpenAI" messages)
|
||||
const errorText =
|
||||
e instanceof APIError
|
||||
? e.message
|
||||
: e instanceof Error
|
||||
? e.message
|
||||
: typeof e === "string"
|
||||
? e
|
||||
: undefined;
|
||||
if (errorText) {
|
||||
const zaiMsg = checkZaiError(errorText);
|
||||
if (zaiMsg) return zaiMsg;
|
||||
}
|
||||
|
||||
// Handle APIError from streaming (event: error)
|
||||
if (e instanceof APIError) {
|
||||
const reasons = getErrorReasons(e);
|
||||
@@ -446,6 +461,8 @@ export function getRetryStatusMessage(
|
||||
): string {
|
||||
if (!errorDetail) return DEFAULT_RETRY_MESSAGE;
|
||||
|
||||
if (checkZaiError(errorDetail)) return "Z.ai API error, retrying...";
|
||||
|
||||
if (errorDetail.includes("Anthropic API is overloaded"))
|
||||
return "Anthropic API is overloaded, retrying...";
|
||||
if (
|
||||
|
||||
97
src/cli/helpers/zaiErrors.ts
Normal file
97
src/cli/helpers/zaiErrors.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Z.ai-specific error detection, parsing, and formatting.
|
||||
*
|
||||
* Z.ai is an upstream LLM provider using the OpenAI-compatible API.
|
||||
* Errors arrive wrapped in generic "OpenAI" error messages from the server's
|
||||
* openai_client.py. This module extracts Z.ai's own error codes and presents
|
||||
* clear, actionable messages attributed to Z.ai.
|
||||
*
|
||||
* Z.ai error code ranges:
|
||||
* 1000-1004 Auth (authentication failed, token expired, invalid token)
|
||||
* 1100-1121 Account (inactive, locked, arrears, irregular activity)
|
||||
* 1200-1234 API call (invalid params, unsupported model, permissions, network)
|
||||
* 1300-1310 Rate/policy (content filtered, rate limit, quota, subscription expired)
|
||||
* 500 Internal server error
|
||||
*/
|
||||
|
||||
// Regex patterns to extract Z.ai's {code, message} from error detail strings.
|
||||
// Python dict repr: {'code': 1302, 'message': 'High concurrency...'}
|
||||
const PYTHON_REPR_PATTERN = /'code':\s*(\d{3,4}),\s*'message':\s*'([^']+)'/;
|
||||
// JSON format: {"code": 1302, "message": "High concurrency..."}
|
||||
const JSON_FORMAT_PATTERN = /"code":\s*(\d{3,4}),\s*"message":\s*"([^"]+)"/;
|
||||
|
||||
function isKnownZaiCode(code: number): boolean {
|
||||
return (
|
||||
code === 500 ||
|
||||
(code >= 1000 && code <= 1004) ||
|
||||
(code >= 1100 && code <= 1121) ||
|
||||
(code >= 1200 && code <= 1234) ||
|
||||
(code >= 1300 && code <= 1310)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a Z.ai error from an error detail string.
|
||||
* Returns the extracted code and message, or null if not a Z.ai error.
|
||||
*/
|
||||
export function parseZaiError(
|
||||
text: string,
|
||||
): { code: number; message: string } | null {
|
||||
for (const pattern of [PYTHON_REPR_PATTERN, JSON_FORMAT_PATTERN]) {
|
||||
const match = text.match(pattern);
|
||||
if (match?.[1] && match[2]) {
|
||||
const code = parseInt(match[1], 10);
|
||||
if (isKnownZaiCode(code)) {
|
||||
return { code, message: match[2] };
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a Z.ai error code and message into a user-friendly string.
|
||||
*/
|
||||
export function formatZaiError(code: number, message: string): string {
|
||||
if (code >= 1000 && code <= 1004) {
|
||||
return `Z.ai authentication error: ${message}. Check your Z.ai API key with /connect.`;
|
||||
}
|
||||
if (code >= 1100 && code <= 1121) {
|
||||
return `Z.ai account issue: ${message}. Check your Z.ai account status.`;
|
||||
}
|
||||
if (code >= 1200 && code <= 1234) {
|
||||
return `Z.ai API error: ${message}. Try again later or switch providers with /model.`;
|
||||
}
|
||||
if (code >= 1300 && code <= 1310) {
|
||||
return `Z.ai rate limit: ${message}. This is a Z.ai limitation. Try again later or switch providers with /model.`;
|
||||
}
|
||||
if (code === 500) {
|
||||
return `Z.ai internal error. Try again later or switch providers with /model.`;
|
||||
}
|
||||
return `Z.ai error (${code}): ${message}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an error string contains a Z.ai error. If so, return a formatted
|
||||
* user-friendly message; otherwise return undefined.
|
||||
*/
|
||||
export function checkZaiError(errorText: string): string | undefined {
|
||||
const parsed = parseZaiError(errorText);
|
||||
if (!parsed) return undefined;
|
||||
return formatZaiError(parsed.code, parsed.message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the error detail contains a Z.ai error code in ranges that
|
||||
* should not be retried (auth, account, rate/policy).
|
||||
*/
|
||||
export function isZaiNonRetryableError(detail: string): boolean {
|
||||
const parsed = parseZaiError(detail);
|
||||
if (!parsed) return false;
|
||||
const { code } = parsed;
|
||||
return (
|
||||
(code >= 1000 && code <= 1004) ||
|
||||
(code >= 1100 && code <= 1121) ||
|
||||
(code >= 1300 && code <= 1310)
|
||||
);
|
||||
}
|
||||
@@ -181,4 +181,22 @@ describe("formatErrorDetails", () => {
|
||||
expect(message).toContain("minimax-m2.1");
|
||||
expect(message).toContain("/model");
|
||||
});
|
||||
|
||||
test("formats Z.ai error from APIError with embedded error code", () => {
|
||||
const error = new APIError(
|
||||
429,
|
||||
{
|
||||
error:
|
||||
"Rate limited by OpenAI: Error code: 429 - {'error': {'code': 1302, 'message': 'High concurrency usage exceeds limits'}}",
|
||||
},
|
||||
undefined,
|
||||
new Headers(),
|
||||
);
|
||||
|
||||
const message = formatErrorDetails(error);
|
||||
|
||||
expect(message).toContain("Z.ai rate limit");
|
||||
expect(message).toContain("High concurrency usage exceeds limits");
|
||||
expect(message).not.toContain("OpenAI");
|
||||
});
|
||||
});
|
||||
|
||||
157
src/tests/cli/zaiErrors.test.ts
Normal file
157
src/tests/cli/zaiErrors.test.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
checkZaiError,
|
||||
formatZaiError,
|
||||
isZaiNonRetryableError,
|
||||
parseZaiError,
|
||||
} from "../../cli/helpers/zaiErrors";
|
||||
|
||||
describe("parseZaiError", () => {
|
||||
test("extracts error from Python repr format", () => {
|
||||
const text =
|
||||
"Rate limited by OpenAI: Error code: 429 - {'error': {'code': 1302, 'message': 'High concurrency usage exceeds limits'}}";
|
||||
const result = parseZaiError(text);
|
||||
expect(result).toEqual({
|
||||
code: 1302,
|
||||
message: "High concurrency usage exceeds limits",
|
||||
});
|
||||
});
|
||||
|
||||
test("extracts error from JSON format", () => {
|
||||
const text =
|
||||
'Rate limited by OpenAI: Error code: 429 - {"error": {"code": 1302, "message": "High concurrency usage exceeds limits"}}';
|
||||
const result = parseZaiError(text);
|
||||
expect(result).toEqual({
|
||||
code: 1302,
|
||||
message: "High concurrency usage exceeds limits",
|
||||
});
|
||||
});
|
||||
|
||||
test("extracts auth error code", () => {
|
||||
const text =
|
||||
"Error from OpenAI: {'error': {'code': 1001, 'message': 'Token expired'}}";
|
||||
const result = parseZaiError(text);
|
||||
expect(result).toEqual({ code: 1001, message: "Token expired" });
|
||||
});
|
||||
|
||||
test("extracts account error code", () => {
|
||||
const text =
|
||||
"Error: {'error': {'code': 1110, 'message': 'Account in arrears'}}";
|
||||
const result = parseZaiError(text);
|
||||
expect(result).toEqual({ code: 1110, message: "Account in arrears" });
|
||||
});
|
||||
|
||||
test("extracts internal server error code", () => {
|
||||
const text =
|
||||
"Error: {'error': {'code': 500, 'message': 'Internal server error'}}";
|
||||
const result = parseZaiError(text);
|
||||
expect(result).toEqual({ code: 500, message: "Internal server error" });
|
||||
});
|
||||
|
||||
test("returns null for non-Z.ai errors", () => {
|
||||
expect(parseZaiError("Connection timed out")).toBeNull();
|
||||
expect(parseZaiError("OpenAI API error: rate limit")).toBeNull();
|
||||
expect(parseZaiError("")).toBeNull();
|
||||
});
|
||||
|
||||
test("returns null for out-of-range codes", () => {
|
||||
const text = "Error: {'error': {'code': 9999, 'message': 'Unknown'}}";
|
||||
expect(parseZaiError(text)).toBeNull();
|
||||
});
|
||||
|
||||
test("returns null for codes between known ranges", () => {
|
||||
const text =
|
||||
"Error: {'error': {'code': 1050, 'message': 'Not a real code'}}";
|
||||
expect(parseZaiError(text)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatZaiError", () => {
|
||||
test("formats auth errors (1000-1004)", () => {
|
||||
const result = formatZaiError(1001, "Token expired");
|
||||
expect(result).toBe(
|
||||
"Z.ai authentication error: Token expired. Check your Z.ai API key with /connect.",
|
||||
);
|
||||
});
|
||||
|
||||
test("formats account errors (1100-1121)", () => {
|
||||
const result = formatZaiError(1110, "Account in arrears");
|
||||
expect(result).toBe(
|
||||
"Z.ai account issue: Account in arrears. Check your Z.ai account status.",
|
||||
);
|
||||
});
|
||||
|
||||
test("formats API errors (1200-1234)", () => {
|
||||
const result = formatZaiError(1210, "Unsupported model");
|
||||
expect(result).toBe(
|
||||
"Z.ai API error: Unsupported model. Try again later or switch providers with /model.",
|
||||
);
|
||||
});
|
||||
|
||||
test("formats rate/policy errors (1300-1310)", () => {
|
||||
const result = formatZaiError(
|
||||
1302,
|
||||
"High concurrency usage exceeds limits",
|
||||
);
|
||||
expect(result).toBe(
|
||||
"Z.ai rate limit: High concurrency usage exceeds limits. This is a Z.ai limitation. Try again later or switch providers with /model.",
|
||||
);
|
||||
});
|
||||
|
||||
test("formats internal server error (500)", () => {
|
||||
const result = formatZaiError(500, "Internal server error");
|
||||
expect(result).toBe(
|
||||
"Z.ai internal error. Try again later or switch providers with /model.",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkZaiError", () => {
|
||||
test("returns formatted message for realistic server error", () => {
|
||||
const errorText =
|
||||
"Rate limited by OpenAI: Error code: 429 - {'error': {'code': 1302, 'message': 'Rate limit reached for requests'}}";
|
||||
const result = checkZaiError(errorText);
|
||||
expect(result).toBe(
|
||||
"Z.ai rate limit: Rate limit reached for requests. This is a Z.ai limitation. Try again later or switch providers with /model.",
|
||||
);
|
||||
});
|
||||
|
||||
test("returns undefined for non-Z.ai errors", () => {
|
||||
expect(checkZaiError("Connection timed out")).toBeUndefined();
|
||||
expect(checkZaiError("OpenAI rate limit exceeded")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("isZaiNonRetryableError", () => {
|
||||
test("returns true for auth errors", () => {
|
||||
const detail =
|
||||
"Error: {'error': {'code': 1001, 'message': 'Token expired'}}";
|
||||
expect(isZaiNonRetryableError(detail)).toBe(true);
|
||||
});
|
||||
|
||||
test("returns true for account errors", () => {
|
||||
const detail =
|
||||
"Error: {'error': {'code': 1110, 'message': 'Account locked'}}";
|
||||
expect(isZaiNonRetryableError(detail)).toBe(true);
|
||||
});
|
||||
|
||||
test("returns true for rate/policy errors", () => {
|
||||
const detail = "Error: {'error': {'code': 1302, 'message': 'Rate limit'}}";
|
||||
expect(isZaiNonRetryableError(detail)).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false for API errors (retryable)", () => {
|
||||
const detail =
|
||||
"Error: {'error': {'code': 1210, 'message': 'Network error'}}";
|
||||
expect(isZaiNonRetryableError(detail)).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false for internal server errors (retryable)", () => {
|
||||
const detail = "Error: {'error': {'code': 500, 'message': 'Server error'}}";
|
||||
expect(isZaiNonRetryableError(detail)).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false for non-Z.ai errors", () => {
|
||||
expect(isZaiNonRetryableError("Connection timed out")).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user