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.",
    );
  });
});