From d9b35895ff1d01d98499e61dda72fea5b911bd31 Mon Sep 17 00:00:00 2001 From: Ari Webb Date: Fri, 20 Feb 2026 12:01:43 -0800 Subject: [PATCH] fix: zai error handling (#1047) --- src/agent/turn-recovery-policy.ts | 2 + src/cli/helpers/errorFormatter.ts | 17 +++ src/cli/helpers/zaiErrors.ts | 97 +++++++++++++++++ src/tests/cli/errorFormatter.test.ts | 18 +++ src/tests/cli/zaiErrors.test.ts | 157 +++++++++++++++++++++++++++ 5 files changed, 291 insertions(+) create mode 100644 src/cli/helpers/zaiErrors.ts create mode 100644 src/tests/cli/zaiErrors.test.ts diff --git a/src/agent/turn-recovery-policy.ts b/src/agent/turn-recovery-policy.ts index 1044fa8..fe428ad 100644 --- a/src/agent/turn-recovery-policy.ts +++ b/src/agent/turn-recovery-policy.ts @@ -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) => diff --git a/src/cli/helpers/errorFormatter.ts b/src/cli/helpers/errorFormatter.ts index de3c294..df6b913 100644 --- a/src/cli/helpers/errorFormatter.ts +++ b/src/cli/helpers/errorFormatter.ts @@ -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 ( diff --git a/src/cli/helpers/zaiErrors.ts b/src/cli/helpers/zaiErrors.ts new file mode 100644 index 0000000..153ccfa --- /dev/null +++ b/src/cli/helpers/zaiErrors.ts @@ -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) + ); +} diff --git a/src/tests/cli/errorFormatter.test.ts b/src/tests/cli/errorFormatter.test.ts index f73addf..1a5c5ba 100644 --- a/src/tests/cli/errorFormatter.test.ts +++ b/src/tests/cli/errorFormatter.test.ts @@ -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"); + }); }); diff --git a/src/tests/cli/zaiErrors.test.ts b/src/tests/cli/zaiErrors.test.ts new file mode 100644 index 0000000..8941586 --- /dev/null +++ b/src/tests/cli/zaiErrors.test.ts @@ -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); + }); +});