From fd568c4f743d1cec6abfba5dbfcb2c5534f0c9c9 Mon Sep 17 00:00:00 2001 From: bwjoke Date: Fri, 13 Mar 2026 05:06:43 +0800 Subject: [PATCH] fix(failover): classify ZenMux quota-refresh 402 as rate_limit (#43917) Merged via squash. Prepared head SHA: 1d58a36a774d06b1493971e8f14f9abc806be6b0 Co-authored-by: bwjoke <1284814+bwjoke@users.noreply.github.com> Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com> Reviewed-by: @altaywtf --- CHANGELOG.md | 1 + src/agents/failover-error.test.ts | 7 +++++++ src/agents/pi-embedded-helpers/errors.ts | 11 +++++++++++ 3 files changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc2976fb87d..8805acc7ec8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -188,6 +188,7 @@ Docs: https://docs.openclaw.ai - Status/context windows: normalize provider-qualified override cache keys so `/status` resolves the active provider's configured context window even when `models.providers` keys use mixed case or surrounding whitespace. (#36389) Thanks @haoruilee. - ACP/main session aliases: canonicalize `main` before ACP session lookup so restarted ACP main sessions rehydrate instead of failing closed with `Session is not ACP-enabled: main`. (#43285, fixes #25692) - Agents/embedded runner: recover canonical allowlisted tool names from malformed `toolCallId` and malformed non-blank tool-name variants before dispatch, while failing closed on ambiguous matches. (#34485) thanks @yuweuii. +- Agents/failover: classify ZenMux quota-refresh `402` responses as `rate_limit` so model fallback retries continue instead of stopping on a temporary subscription window. (#43917) thanks @bwjoke. ## 2026.3.8 diff --git a/src/agents/failover-error.test.ts b/src/agents/failover-error.test.ts index b3988d2dc55..16f5b1d5fc8 100644 --- a/src/agents/failover-error.test.ts +++ b/src/agents/failover-error.test.ts @@ -204,6 +204,13 @@ describe("failover-error", () => { message: "Workspace spend limit reached. Contact your admin.", }), ).toBe("rate_limit"); + expect( + resolveFailoverReasonFromError({ + status: 402, + message: + "You have reached your subscription quota limit. Please wait for automatic quota refresh in the rolling time window, upgrade to a higher plan, or use a Pay-As-You-Go API Key for unlimited access. Learn more: https://zenmux.ai/docs/guide/subscription.html", + }), + ).toBe("rate_limit"); expect( resolveFailoverReasonFromError({ status: 402, diff --git a/src/agents/pi-embedded-helpers/errors.ts b/src/agents/pi-embedded-helpers/errors.ts index 28fcf328e87..ba42c77da13 100644 --- a/src/agents/pi-embedded-helpers/errors.ts +++ b/src/agents/pi-embedded-helpers/errors.ts @@ -288,6 +288,13 @@ function hasExplicit402BillingSignal(text: string): boolean { ); } +function hasQuotaRefreshWindowSignal(text: string): boolean { + return ( + text.includes("subscription quota limit") && + (text.includes("automatic quota refresh") || text.includes("rolling time window")) + ); +} + function hasRetryable402TransientSignal(text: string): boolean { const hasPeriodicHint = includesAnyHint(text, PERIODIC_402_HINTS); const hasSpendLimit = text.includes("spend limit") || text.includes("spending limit"); @@ -313,6 +320,10 @@ function classify402Message(message: string): PaymentRequiredFailoverReason { return "billing"; } + if (hasQuotaRefreshWindowSignal(normalized)) { + return "rate_limit"; + } + if (hasExplicit402BillingSignal(normalized)) { return "billing"; }