This commit is contained in:
brokemac79 2026-03-15 22:37:02 +00:00 committed by GitHub
commit b770849967
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 30 additions and 1 deletions

View File

@ -467,4 +467,18 @@ describe("failover-error", () => {
expect(described.message).toBe("123");
expect(described.reason).toBeUndefined();
});
it("coerces bare leading 402 assistant error string to FailoverError (#47576)", () => {
// ZenMux quota-refresh 402 arrives as a bare assistant error string — must coerce to FailoverError
// so the embedded runner can enter model fallback.
const zenmuxQuota402 =
"402 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.";
const err = coerceToFailoverError(new Error(zenmuxQuota402), {
provider: "zenmux",
model: "gpt-4o",
});
expect(err).not.toBeNull();
expect(err?.reason).toBe("rate_limit");
expect(err?.provider).toBe("zenmux");
});
});

View File

@ -860,4 +860,19 @@ describe("classifyFailoverReason", () => {
),
).toBe("timeout");
});
it("classifies bare leading 402 assistant error strings as rate_limit (#47576)", () => {
// ZenMux quota-refresh 402 surfaces as a bare assistant error string — must enter model fallback.
expect(
classifyFailoverReason(
"402 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.",
),
).toBe("rate_limit");
// Bare 402 with generic text should be treated as a 402 marker.
expect(classifyFailoverReason("402 Quota exceeded")).toBe("rate_limit");
// Bare 402 with typical billing text should classify as billing.
expect(
classifyFailoverReason("402 Please add more credits to continue using this service."),
).toBe("billing");
});
});

View File

@ -270,7 +270,7 @@ const RETRYABLE_402_SCOPED_RESULT_HINTS = [
"exhausted",
] as const;
const RAW_402_MARKER_RE =
/["']?(?:status|code)["']?\s*[:=]\s*402\b|\bhttp\s*402\b|\berror(?:\s+code)?\s*[:=]?\s*402\b|\b(?:got|returned|received)\s+(?:a\s+)?402\b|^\s*402\s+payment required\b|^\s*402\s+.*used up your points\b/i;
/["']?(?:status|code)["']?\s*[:=]\s*402\b|\bhttp\s*402\b|\berror(?:\s+code)?\s*[:=]?\s*402\b|\b(?:got|returned|received)\s+(?:a\s+)?402\b|^\s*402\s+\S/i;
const LEADING_402_WRAPPER_RE =
/^(?:error[:\s-]+)?(?:(?:http\s*)?402(?:\s+payment required)?|payment required)(?:[:\s-]+|$)/i;