diff --git a/src/hooks/runtime-fallback/constants.ts b/src/hooks/runtime-fallback/constants.ts index 206762961..43df0f936 100644 --- a/src/hooks/runtime-fallback/constants.ts +++ b/src/hooks/runtime-fallback/constants.ts @@ -41,6 +41,12 @@ export const RETRYABLE_ERROR_PATTERNS = [ /(?:^|\s)429(?:\s|$)/, /(?:^|\s)503(?:\s|$)/, /(?:^|\s)529(?:\s|$)/, + /使用上限/, + /频率限制/, + /请求过于频繁/, + /暂时不可用/, + /服务不可用/, + /请稍后重试/, ] /** diff --git a/src/hooks/runtime-fallback/error-classifier.test.ts b/src/hooks/runtime-fallback/error-classifier.test.ts index c9eef6e06..a7f43210a 100644 --- a/src/hooks/runtime-fallback/error-classifier.test.ts +++ b/src/hooks/runtime-fallback/error-classifier.test.ts @@ -59,6 +59,44 @@ describe("runtime-fallback error classifier", () => { expect(retryable).toBe(true) }) + test("treats localized transient provider messages as retryable", () => { + //#given + const errors = [ + { message: "请求过于频繁,请稍后重试" }, + { message: "服务暂时不可用" }, + { message: "触发频率限制" }, + ] + + //#when + const retryable = errors.map((error) => isRetryableError(error, [429, 503, 529])) + + //#then + expect(retryable).toEqual([true, true, true]) + }) + + test("classifies localized quota exhaustion messages as quota_exceeded", () => { + //#given + const errors = [ + { message: "已达到 5 小时的使用上限" }, + { message: "已达到每日调用限制" }, + { message: "额度不足" }, + { message: "账户余额不足" }, + { message: "免费额度已耗尽" }, + ] + + //#when + const classifications = errors.map((error) => classifyErrorType(error)) + + //#then + expect(classifications).toEqual([ + "quota_exceeded", + "quota_exceeded", + "quota_exceeded", + "quota_exceeded", + "quota_exceeded", + ]) + }) + test("classifies ProviderModelNotFoundError as model_not_found", () => { //#given const error = { diff --git a/src/hooks/runtime-fallback/error-classifier.ts b/src/hooks/runtime-fallback/error-classifier.ts index 0901a4e05..33b17ccf3 100644 --- a/src/hooks/runtime-fallback/error-classifier.ts +++ b/src/hooks/runtime-fallback/error-classifier.ts @@ -143,6 +143,11 @@ export function classifyErrorType(error: unknown): string | undefined { /payment.?required/i.test(message) || /usage\s+limit/i.test(message) || /credit\s+balance.*too\s+low/i.test(message) || + /使用上限/.test(message) || + /达到.*限制/.test(message) || + /额度.*不足/.test(message) || + /余额.*不足/.test(message) || + /已耗尽/.test(message) || isLocalizedQuotaExhaustionMessage(message) ) { return "quota_exceeded" diff --git a/src/shared/model-error-classifier.test.ts b/src/shared/model-error-classifier.test.ts index 33fd3f4c1..0ab40e8eb 100644 --- a/src/shared/model-error-classifier.test.ts +++ b/src/shared/model-error-classifier.test.ts @@ -216,6 +216,21 @@ describe("model-error-classifier", () => { expect(result).toBe(true) }) + test("treats localized transient provider messages as retryable", () => { + //#given + const errors = [ + { message: "请求过于频繁,请稍后重试" }, + { message: "服务暂时不可用" }, + { message: "触发频率限制" }, + ] + + //#when + const results = errors.map((error) => shouldRetryError(error)) + + //#then + expect(results).toEqual([true, true, true]) + }) + test("treats subscription quota message as non-retryable", () => { //#given const error = { message: "Subscription quota exceeded. You can continue using free models." } @@ -227,6 +242,22 @@ describe("model-error-classifier", () => { expect(result).toBe(false) }) + test("treats localized quota exhaustion messages as non-retryable stop errors", () => { + //#given + const errors = [ + { message: "已达到 5 小时的使用上限" }, + { message: "额度不足" }, + { message: "账户余额不足" }, + { message: "免费额度已耗尽" }, + ] + + //#when + const results = errors.map((error) => shouldRetryError(error)) + + //#then + expect(results).toEqual([false, false, false, false]) + }) + test("treats HTTP 429 rate limit message as retryable", () => { //#given const error = { message: "429 Too Many Requests: rate limit reached" } diff --git a/src/shared/model-error-classifier.ts b/src/shared/model-error-classifier.ts index fb3fdf480..787f4825a 100644 --- a/src/shared/model-error-classifier.ts +++ b/src/shared/model-error-classifier.ts @@ -67,6 +67,7 @@ const RETRYABLE_MESSAGE_PATTERNS = [ "balance", "temporarily unavailable", "try again", + "请稍后重试", "503", "502", "504", @@ -74,6 +75,11 @@ const RETRYABLE_MESSAGE_PATTERNS = [ "529", "selected provider is forbidden", "provider is forbidden", + // Chinese retryable patterns (Zhipu, etc.) + "频率限制", // "rate limit" + "请求过于频繁", // "too many requests" + "暂时不可用", // "temporarily unavailable" + "服务不可用", // "service unavailable" ] /** @@ -106,6 +112,10 @@ const STOP_MESSAGE_PATTERNS = [ "in arrears", "fair use policy", "recharge and try", + "使用上限", + "额度不足", + "余额不足", + "已耗尽", ] const AUTO_RETRY_GATE_PATTERNS = [