fix: add CJK error patterns to failover classification (#56242)

* fix: add CJK error patterns to failover classification

Chinese LLM providers (ZhipuAI/GLM, Bailian, Kimi/Moonshot, DeepSeek,
etc.) return error messages in Chinese. The existing failover
classification only matches English patterns, causing these errors to
fall through as unclassified — surfacing raw provider errors to users
instead of triggering model fallback.

Real production example: ZhipuAI error code 1234 returns
'网络错误,错误id:xxx,请联系客服。' (network error). This was not
matched by the existing 'network error' English pattern, so no failover
was triggered despite having a configured fallback model.

Changes:
- Add Chinese patterns to all error categories in failover-matches.ts:
  timeout, serverError, rateLimit, billing, auth, overloaded
- Add Chinese network error detection in formatTransportErrorCopy()
  for user-friendly error messages
- Add comprehensive test coverage for all CJK error categories

Follows the existing precedent set by Chinese context overflow patterns
in isContextOverflowError().

* fix: narrow billing pattern and fix placeholder issue URL

- Change '账户余额' to '账户余额不足' to avoid false positives on
  messages that merely mention account balance (per greptile review)
- Replace XXXXX placeholder with actual issue #56242

* fix: wire CJK auth failover patterns

* fix: classify CJK provider failover errors

* fix: place failover changelog entry in unreleased

---------

Co-authored-by: Altay <altay@uinaf.dev>
This commit is contained in:
Zhang Xiaofeng
2026-04-28 15:44:17 +08:00
committed by GitHub
parent 47b6d3a334
commit a0900926c3
5 changed files with 129 additions and 0 deletions

View File

@@ -37,6 +37,7 @@ Docs: https://docs.openclaw.ai
- Zalo Personal: persist refreshed `zca-js` session cookies after QR login, session restore, and successful API calls so gateway restarts restore the freshest local session. (#73277) Thanks @darkamenosa.
- Logging/security: redact sensitive tokens (sk-\* keys, Bearer/Authorization values, etc.) at the subsystem console sink so `createSubsystemLogger().info/warn/error` output that bypasses the patched console-capture handler still applies the same redaction the file transport already does. Fixes #73284; refs #67953 and #64046. Thanks @edwin-rivera-dev.
- Plugins/runtime deps: reuse enclosing versioned cache roots when bundled plugins resolve from nested staged paths, so plugin-runtime-deps no longer mints `openclaw-unknown-*` directories or loops on `ENOTEMPTY`. Fixes #72956. (#73205) Thanks @SymbolStar.
- Agents/failover: classify CJK provider transport, quota, billing, auth, and overload error text so Chinese-language provider failures trigger fallback and user-facing transport copy instead of surfacing as unclassified raw errors. (#56242) Thanks @tomcatzh.
## 2026.4.27

View File

@@ -602,6 +602,33 @@ describe("failover-error", () => {
).toBe("rate_limit");
});
it("treats Chinese provider network/server errors as timeout for failover", () => {
// ZhipuAI/GLM error code 1234: "网络错误" — real production error
expect(
resolveFailoverReasonFromError({
message:
"LLM error 1234: 网络错误错误id202603281427587491f4467f1c4712请联系客服。 (request_id: 202603281427587491f4467f1c4712)",
}),
).toBe("timeout");
// JSON payload variant
expect(
resolveFailoverReasonFromError({
message:
'{"error":{"code":"1234","message":"网络错误错误idabc123请联系客服。"},"request_id":"abc123"}',
}),
).toBe("timeout");
// Generic Chinese server errors
expect(resolveFailoverReasonFromError({ message: "系统错误,请稍后重试" })).toBe("timeout");
expect(resolveFailoverReasonFromError({ message: "服务器内部错误" })).toBe("timeout");
});
it("treats Chinese provider auth errors as auth for failover", () => {
// ZhipuAI/GLM 403: "您无权访问glm-5.1" — real production error
expect(resolveFailoverReasonFromError({ message: "403 您无权访问glm-5.1。" })).toBe("auth");
expect(resolveFailoverReasonFromError({ message: "认证失败" })).toBe("auth");
expect(resolveFailoverReasonFromError({ message: "鉴权失败请检查API Key" })).toBe("auth");
});
it("treats overloaded provider payloads as overloaded", () => {
expect(
resolveFailoverReasonFromError({

View File

@@ -1266,6 +1266,62 @@ describe("classifyFailoverReason", () => {
),
).toBe("auth_permanent");
});
it("classifies Chinese provider error messages correctly", () => {
// ZhipuAI/GLM error code 1234: "网络错误" (network error) — real production error
// from https://github.com/openclaw/openclaw/issues/56242
expect(
classifyFailoverReason(
"LLM error 1234: 网络错误错误id202603281427587491f4467f1c4712请联系客服。 (request_id: 202603281427587491f4467f1c4712)",
),
).toBe("timeout");
expect(
classifyFailoverReason(
'{"error":{"code":"1234","message":"网络错误错误idabc123请联系客服。"},"request_id":"abc123"}',
),
).toBe("timeout");
// Network/connection errors
expect(classifyFailoverReason("网络异常,请稍后重试")).toBe("timeout");
expect(classifyFailoverReason("连接超时")).toBe("timeout");
expect(classifyFailoverReason("请求超时,请重试")).toBe("timeout");
expect(classifyFailoverReason("服务暂时不可用")).toBe("timeout");
expect(classifyFailoverReason("连接错误")).toBe("timeout");
expect(classifyFailoverReason("服务繁忙,请稍后再试")).toBe("timeout");
// Server errors
expect(classifyFailoverReason("内部错误")).toBe("timeout");
expect(classifyFailoverReason("服务器错误")).toBe("timeout");
expect(classifyFailoverReason("服务器内部错误")).toBe("timeout");
expect(classifyFailoverReason("系统错误,请稍后重试")).toBe("timeout");
expect(classifyFailoverReason("系统繁忙")).toBe("timeout");
expect(classifyFailoverReason("系统异常")).toBe("timeout");
// Rate limit errors
expect(classifyFailoverReason("请求过于频繁,请稍后重试")).toBe("rate_limit");
expect(classifyFailoverReason("调用频率超限")).toBe("rate_limit");
expect(classifyFailoverReason("频率限制")).toBe("rate_limit");
expect(classifyFailoverReason("配额不足")).toBe("rate_limit");
expect(classifyFailoverReason("配额已用尽")).toBe("rate_limit");
expect(classifyFailoverReason("额度不足,请充值")).toBe("rate_limit");
expect(classifyFailoverReason("额度已用尽")).toBe("rate_limit");
// Billing errors
expect(classifyFailoverReason("余额不足,请充值")).toBe("billing");
expect(classifyFailoverReason("账户余额不足")).toBe("billing");
expect(classifyFailoverReason("账户已欠费")).toBe("billing");
// Auth errors
expect(classifyFailoverReason("无权访问该模型")).toBe("auth");
expect(classifyFailoverReason("403 您无权访问glm-5.1。")).toBe("auth");
expect(classifyFailoverReason("认证失败")).toBe("auth");
expect(classifyFailoverReason("鉴权失败请检查API Key")).toBe("auth");
expect(classifyFailoverReason("密钥无效")).toBe("auth");
// Overloaded errors
expect(classifyFailoverReason("服务过载,请稍后重试")).toBe("overloaded");
expect(classifyFailoverReason("当前负载过高")).toBe("overloaded");
});
});
describe("classifyProviderRuntimeFailureKind", () => {

View File

@@ -40,6 +40,14 @@ const COMMON_AUTH_ERROR_PATTERNS = [
/\bfailed to (?:extract|parse|validate|decode)\b.*\btoken\b/,
] as const satisfies readonly ErrorPattern[];
const CJK_AUTH_ERROR_PATTERNS = [
"无权访问",
"认证失败",
"鉴权失败",
"密钥无效",
"apikey 无效",
] as const satisfies readonly ErrorPattern[];
const ZAI_BILLING_CODE_1311_RE = /"code"\s*:\s*1311\b/;
const ZAI_AUTH_CODE_1113_RE = /"code"\s*:\s*1113\b/;
const STATUS_INTERNAL_SERVER_ERROR_RE = /\bstatus:\s*internal server error\b/i;
@@ -69,6 +77,14 @@ const ERROR_PATTERNS = {
/\btpm\b/i,
"tokens per minute",
"tokens per day",
// Chinese provider rate-limit messages
"请求过于频繁",
"调用频率",
"频率限制",
"配额不足",
"配额已用尽",
"额度不足",
"额度已用尽",
],
overloaded: [
/overloaded_error|"type"\s*:\s*"overloaded_error"/i,
@@ -79,6 +95,9 @@ const ERROR_PATTERNS = {
// provider-overload (#32828).
/service[_ ]unavailable.*(?:overload|capacity|high[_ ]demand)|(?:overload|capacity|high[_ ]demand).*service[_ ]unavailable/i,
"high demand",
// Chinese provider overloaded messages
"服务过载",
"当前负载过高",
],
serverError: [
"an error occurred while processing",
@@ -92,6 +111,13 @@ const ERROR_PATTERNS = {
"upstream error",
"upstream connect error",
"connection reset",
// Chinese provider server error messages
"内部错误",
"服务器错误",
"服务器内部错误",
"系统错误",
"系统繁忙",
"系统异常",
],
timeout: [
"timeout",
@@ -106,6 +132,14 @@ const ERROR_PATTERNS = {
"network request failed",
"fetch failed",
"socket hang up",
// Chinese provider error messages (ZhipuAI/GLM, Bailian, Kimi/Moonshot, DeepSeek, etc.)
"网络错误",
"网络异常",
"服务暂时不可用",
"服务繁忙",
"请求超时",
"连接超时",
"连接错误",
/\beconn(?:refused|reset|aborted)\b/i,
/\benetunreach\b/i,
/\behostunreach\b/i,
@@ -153,6 +187,11 @@ const ERROR_PATTERNS = {
/out of extra usage/i,
/draw from your extra usage/i,
/extra usage is required(?: for long context requests)?/i,
// Chinese provider billing messages
"余额不足",
"账户余额不足",
"欠费",
"账户已欠费",
// Z.ai: error 1311 = model not included in current subscription plan (#48988)
ZAI_BILLING_CODE_1311_RE,
],
@@ -161,6 +200,7 @@ const ERROR_PATTERNS = {
...AMBIGUOUS_AUTH_ERROR_PATTERNS,
...COMMON_AUTH_ERROR_PATTERNS,
...ZAI_AUTH_ERROR_PATTERNS,
...CJK_AUTH_ERROR_PATTERNS,
],
format: [
"string should match pattern",
@@ -245,6 +285,7 @@ export function isAuthErrorMessage(raw: string): boolean {
AMBIGUOUS_AUTH_ERROR_PATTERNS,
COMMON_AUTH_ERROR_PATTERNS,
ZAI_AUTH_ERROR_PATTERNS,
CJK_AUTH_ERROR_PATTERNS,
]);
}

View File

@@ -159,6 +159,10 @@ export function formatTransportErrorCopy(raw: string): string | undefined {
return "LLM request failed: network connection error.";
}
if (raw.includes("网络错误") || raw.includes("网络异常") || raw.includes("连接错误")) {
return "LLM request failed: provider reported a network error.";
}
return undefined;
}