File size: 11,198 Bytes
e249c6d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 | // Covers provider-specific failover matcher regressions.
import { describe, expect, it } from "vitest";
import {
classifyFailoverReason,
isAuthErrorMessage,
isBillingErrorMessage,
isOverloadedErrorMessage,
isProviderCompletedErrorFinishReasonMessage,
isRateLimitErrorMessage,
isServerErrorMessage,
isTimeoutErrorMessage,
} from "./classify.js";
import { renderRateLimitOrOverloadedCopy } from "./user-copy.js";
describe("Z.ai vendor error codes (#48988)", () => {
describe("error 1311 — model not included in subscription plan", () => {
it("classifies Z.ai 1311 JSON body as billing", () => {
// Z.ai 1311 is a plan entitlement failure, not rate limiting.
const raw =
'{"code":1311,"message":"The model you requested is not available in your current plan"}';
expect(isBillingErrorMessage(raw)).toBe(true);
});
it("classifies prose-only subscription plan access denials as billing", () => {
const raw =
"FailoverError: Your current subscription plan does not yet include access to GLM-5V-Turbo";
expect(isBillingErrorMessage(raw)).toBe(true);
});
it("classifies Z.ai 1311 with spaces as billing", () => {
const raw = '{"code": 1311, "message": "model not on plan"}';
expect(isBillingErrorMessage(raw)).toBe(true);
});
it("does not misclassify 1311 as rate_limit", () => {
const raw =
'{"code":1311,"message":"The model you requested is not available in your current plan"}';
expect(isRateLimitErrorMessage(raw)).toBe(false);
});
it("does not misclassify 1311 as auth", () => {
const raw =
'{"code":1311,"message":"The model you requested is not available in your current plan"}';
expect(isAuthErrorMessage(raw)).toBe(false);
});
it("classifies long Z.ai 1311 payloads as billing", () => {
const raw = JSON.stringify({
code: 1311,
message: "The model you requested is not available in your current plan",
details: "x".repeat(700),
});
expect(raw.length).toBeGreaterThan(512);
expect(isBillingErrorMessage(raw)).toBe(true);
});
});
describe("error 1113 — wrong endpoint or invalid credentials", () => {
it("classifies Z.ai 1113 JSON body as auth", () => {
const raw = '{"code":1113,"message":"invalid api endpoint or credentials"}';
expect(isAuthErrorMessage(raw)).toBe(true);
});
it("classifies Z.ai 1113 with spaces as auth", () => {
const raw = '{"code": 1113, "message": "invalid api endpoint or credentials"}';
expect(isAuthErrorMessage(raw)).toBe(true);
});
it("does not misclassify 1113 as rate_limit", () => {
const raw = '{"code":1113,"message":"invalid api endpoint or credentials"}';
expect(isRateLimitErrorMessage(raw)).toBe(false);
});
it("does not misclassify 1113 as billing", () => {
const raw = '{"code":1113,"message":"invalid api endpoint or credentials"}';
expect(isBillingErrorMessage(raw)).toBe(false);
});
});
describe("existing patterns are unaffected", () => {
it("rate limit still classified correctly", () => {
expect(isRateLimitErrorMessage("rate limit exceeded")).toBe(true);
});
it("OpenAI model-capacity text is classified as overloaded", () => {
expect(
isOverloadedErrorMessage("Selected model is at capacity. Please try a different model."),
).toBe(true);
});
it("OpenRouter high-load text is classified as overloaded", () => {
expect(
isOverloadedErrorMessage(
"The service is currently experiencing high load and cannot process your request.",
),
).toBe(true);
});
it("billing still classified correctly", () => {
expect(isBillingErrorMessage("insufficient credits")).toBe(true);
});
it("auth still classified correctly", () => {
expect(isAuthErrorMessage("invalid api key provided")).toBe(true);
});
});
});
describe("Google invalid API key errors (#114784)", () => {
it("classifies Google Generative AI's invalid-key response as auth", () => {
const raw =
"Google Generative AI API error (400): API key not valid. Please pass a valid API key. [code=INVALID_ARGUMENT]";
expect(isAuthErrorMessage(raw)).toBe(true);
expect(classifyFailoverReason(raw)).toBe("auth");
});
it.each([
"invalid_api_key_error",
"API key is invalid",
'{"code":"API_KEY_INVALID"}',
'{"code":"API_KEY_INVALID_ERROR"}',
])("classifies the %s variant as auth", (raw) => {
expect(isAuthErrorMessage(raw)).toBe(true);
expect(classifyFailoverReason(raw)).toBe("auth");
});
it("does not treat unrelated Google invalid arguments as auth", () => {
const raw =
"Google Generative AI API error (400): Request contains an invalid argument. [code=INVALID_ARGUMENT]";
expect(isAuthErrorMessage(raw)).toBe(false);
expect(classifyFailoverReason(raw)).toBeNull();
expect(isAuthErrorMessage("API key invalidation policy updated")).toBe(false);
expect(isAuthErrorMessage("INVALID API KEYSTORE configuration")).toBe(false);
});
});
describe("Chinese provider overload messages", () => {
const ZHIPU_OVERLOAD = "[1305][该模型当前访问量过大,请您稍后再试]";
it("classifies the Zhipu GLM overload body as overloaded", () => {
expect(isOverloadedErrorMessage(ZHIPU_OVERLOAD)).toBe(true);
});
it("does not misclassify the GLM overload body as rate limit or auth", () => {
expect(isRateLimitErrorMessage(ZHIPU_OVERLOAD)).toBe(false);
expect(isAuthErrorMessage(ZHIPU_OVERLOAD)).toBe(false);
});
});
describe("Volcengine Coding Plan subscription errors", () => {
it("classifies InvalidSubscription JSON body as billing", () => {
const raw =
'{"error":{"code":"InvalidSubscription","message":"Your account does not have a valid CodingPlan subscription, or your subscription has expired."}}';
expect(isBillingErrorMessage(raw)).toBe(true);
});
it("classifies long InvalidSubscription payloads as billing", () => {
const raw = JSON.stringify({
error: {
code: "InvalidSubscription",
message:
"Your account does not have a valid coding plan subscription, or your subscription has expired.",
details: "x".repeat(700),
},
});
expect(raw.length).toBeGreaterThan(512);
expect(isBillingErrorMessage(raw)).toBe(true);
});
it("classifies InvalidSubscription as billing before auth or rate limit", () => {
const raw =
'{"error":{"code":"InvalidSubscription","message":"Your account does not have a valid CodingPlan subscription, or your subscription has expired."}}';
expect(isRateLimitErrorMessage(raw)).toBe(false);
expect(classifyFailoverReason(raw)).toBe("billing");
});
});
describe("agent harness provider mismatch (#91710)", () => {
it("classifies harness provider rejection as format error", () => {
expect(
classifyFailoverReason(
'Requested agent harness "codex" does not support openai/gpt-5.3-codex (provider is not one of: codex).',
),
).toBe("format");
});
it("classifies harness provider rejection with multiple providers as format error", () => {
expect(
classifyFailoverReason(
'Requested agent harness "codex" does not support openrouter/gpt-5.4 (provider is not one of: codex, openai).',
),
).toBe("format");
});
});
describe("server error status classification", () => {
it("classifies a bare internal server error status as server error", () => {
// Bare status lines from providers should classify, while prefixed prose is
// too ambiguous and tested below as a non-match.
expect(isServerErrorMessage("status: internal server error")).toBe(true);
});
it("classifies provider HTTP 5xx wrapper errors as server errors", () => {
expect(isServerErrorMessage("provider failed (HTTP 500): upstream apiKey is empty")).toBe(true);
});
it("does not classify prefixed plain internal server error status prose", () => {
expect(isServerErrorMessage("Proxy notice: Status: Internal Server Error")).toBe(false);
});
});
describe("provider-completed finish_reason error (#109218)", () => {
it("matches bare finish/stop error reasons as provider-completed failures", () => {
expect(isProviderCompletedErrorFinishReasonMessage("Provider finish_reason: error")).toBe(true);
expect(isTimeoutErrorMessage("Provider finish_reason: error")).toBe(false);
expect(classifyFailoverReason("Provider finish_reason: error")).toBe("server_error");
});
it("keeps abort/network/malformed finish reasons in the timeout lane", () => {
for (const sample of [
"Provider finish_reason: abort",
"Provider finish_reason: network_error",
"Provider finish_reason: malformed_response",
]) {
expect(isProviderCompletedErrorFinishReasonMessage(sample)).toBe(false);
expect(isTimeoutErrorMessage(sample)).toBe(true);
expect(classifyFailoverReason(sample)).toBe("timeout");
}
});
});
describe("generic assistant error text classification (#93931)", () => {
it("classifies the generic 'LLM request failed.' as a timeout (transient)", () => {
// The generic error text wraps provider availability failures (model not
// loaded, endpoint unreachable) that should engage retry/fallback.
expect(classifyFailoverReason("LLM request failed.")).toBe("timeout");
});
it("classifies lowercase 'llm request failed.' as a timeout", () => {
expect(classifyFailoverReason("llm request failed.")).toBe("timeout");
});
it("does NOT match 'LLM request failed:' variants as timeout via this pattern", () => {
// Variants with specific reasons should be classified by their own patterns,
// not by the generic LLM request failed match. The schema rejection variant
// is a format error, not a transient timeout.
expect(
isTimeoutErrorMessage(
"LLM request failed: provider rejected the request schema or tool payload.",
),
).toBe(false);
});
it("does NOT match 'LLM request failed: connection refused' as timeout via this exact-match pattern", () => {
// The connection-refused variant is a sanitized user-facing string, not
// the raw error that cron/failover classifiers see. The exact-match regex
// /^llm request failed\.$/i should NOT match it because of the colon suffix.
expect(
isTimeoutErrorMessage("LLM request failed: connection refused by the provider endpoint."),
).toBe(false);
});
});
describe("HTTP 429 overload wording (#98101)", () => {
it("keeps Z.AI code 1305 in rate-limit backoff while preserving overload copy", () => {
const message =
"429 status code (exceeded limit)\n" +
'{"code":1305,"message":"The service may be temporarily overloaded, please try again later."}';
expect(classifyFailoverReason(message)).toBe("rate_limit");
expect(classifyFailoverReason(`HTTP 429: ${message}`)).toBe("rate_limit");
expect(renderRateLimitOrOverloadedCopy({ reason: "rate_limit", raw: message })).toBe(
"⚠️ API rate limit reached. Please try again later.",
);
});
});
|