opencode-anthropic-fix 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +24 -0
  2. package/index.mjs +155 -149
  3. package/lib/config.mjs +47 -25
  4. package/lib/mimicry/adapter-input.mjs +671 -0
  5. package/lib/mimicry/context-hint-threshold.mjs +127 -0
  6. package/lib/mimicry/headers.mjs +71 -64
  7. package/lib/mimicry/request-body.mjs +23 -22
  8. package/lib/mimicry/system-prompt.mjs +54 -23
  9. package/lib/mimicry/wire-compat.mjs +305 -0
  10. package/package.json +3 -3
  11. package/lib/account-state.test.mjs +0 -109
  12. package/lib/accounts.test.mjs +0 -912
  13. package/lib/adaptive-thinking-models.test.mjs +0 -30
  14. package/lib/backoff.test.mjs +0 -411
  15. package/lib/cc-credentials.test.mjs +0 -391
  16. package/lib/config.test.mjs +0 -764
  17. package/lib/context-hint-persist.mjs +0 -68
  18. package/lib/diagnose.test.mjs +0 -122
  19. package/lib/env.test.mjs +0 -25
  20. package/lib/haiku-call.test.mjs +0 -72
  21. package/lib/message-transform.test.mjs +0 -173
  22. package/lib/mimicry/cache.test.mjs +0 -112
  23. package/lib/mimicry/headers.test.mjs +0 -50
  24. package/lib/mimicry/models.test.mjs +0 -132
  25. package/lib/mimicry/request-body.test.mjs +0 -34
  26. package/lib/mimicry/request-helpers.test.mjs +0 -155
  27. package/lib/mimicry/response-stream.test.mjs +0 -127
  28. package/lib/mimicry/system-prompt.test.mjs +0 -73
  29. package/lib/oauth.test.mjs +0 -343
  30. package/lib/redact.test.mjs +0 -154
  31. package/lib/refresh-lock.test.mjs +0 -131
  32. package/lib/request-headers.test.mjs +0 -164
  33. package/lib/retry/overload-loop.test.mjs +0 -107
  34. package/lib/rotation.test.mjs +0 -392
  35. package/lib/session-metrics.test.mjs +0 -67
  36. package/lib/storage.test.mjs +0 -696
  37. package/lib/token-economy/microcompact.test.mjs +0 -28
  38. package/lib/token-economy/transforms.test.mjs +0 -192
  39. package/lib/tuning.test.mjs +0 -89
@@ -1,30 +0,0 @@
1
- import { describe, expect, test } from "vitest";
2
-
3
- import { isAdaptiveThinkingModel, isFable5Model, isMythos5Model } from "../index.mjs";
4
-
5
- describe("Claude 5 adaptive thinking model predicates", () => {
6
- test("isFable5Model matches version-anchored fable 5 model names", () => {
7
- expect(isFable5Model("claude-fable-5")).toBe(true);
8
- expect(isFable5Model("anthropic/claude-fable-5")).toBe(true);
9
- });
10
-
11
- test("isFable5Model rejects non-fable-5 model names", () => {
12
- expect(isFable5Model("claude-opus-4-8")).toBe(false);
13
- expect(isFable5Model("claude-haiku-4-5")).toBe(false);
14
- expect(isFable5Model("")).toBe(false);
15
- });
16
-
17
- test("isMythos5Model matches version-anchored mythos 5 model names", () => {
18
- expect(isMythos5Model("claude-mythos-5")).toBe(true);
19
- });
20
-
21
- test("isMythos5Model rejects non-mythos-5 model names", () => {
22
- expect(isMythos5Model("claude-opus-4-8")).toBe(false);
23
- expect(isMythos5Model("claude-haiku-4-5")).toBe(false);
24
- expect(isMythos5Model("")).toBe(false);
25
- });
26
-
27
- test("isAdaptiveThinkingModel includes fable 5 models", () => {
28
- expect(isAdaptiveThinkingModel("claude-fable-5")).toBe(true);
29
- });
30
- });
@@ -1,411 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import {
3
- parseRateLimitReason,
4
- calculateBackoffMs,
5
- parseRetryAfterHeader,
6
- parseUnifiedResetMsHeader,
7
- isAccountSpecificError,
8
- } from "./backoff.mjs";
9
-
10
- // ---------------------------------------------------------------------------
11
- // isAccountSpecificError
12
- // ---------------------------------------------------------------------------
13
-
14
- describe("isAccountSpecificError", () => {
15
- it("returns true for 429 (always account-specific)", () => {
16
- expect(isAccountSpecificError(429, null)).toBe(true);
17
- });
18
-
19
- it("returns true for 429 even without body", () => {
20
- expect(isAccountSpecificError(429)).toBe(true);
21
- });
22
-
23
- it("returns true for 401 (always account-specific)", () => {
24
- expect(isAccountSpecificError(401, null)).toBe(true);
25
- });
26
-
27
- it("returns true for 400 with rate limit language in JSON error message", () => {
28
- expect(
29
- isAccountSpecificError(
30
- 400,
31
- JSON.stringify({
32
- error: { type: "invalid_request_error", message: "This request would exceed your account's rate limit" },
33
- }),
34
- ),
35
- ).toBe(true);
36
- });
37
-
38
- it("returns true for 400 with quota language in JSON error message", () => {
39
- expect(
40
- isAccountSpecificError(
41
- 400,
42
- JSON.stringify({ error: { type: "invalid_request_error", message: "Your quota has been exhausted" } }),
43
- ),
44
- ).toBe(true);
45
- });
46
-
47
- it("returns true for 400 with credit balance language in JSON error message", () => {
48
- expect(
49
- isAccountSpecificError(
50
- 400,
51
- JSON.stringify({
52
- error: { type: "invalid_request_error", message: "Your credit balance is too low to complete this request" },
53
- }),
54
- ),
55
- ).toBe(true);
56
- });
57
-
58
- it("returns true for 400 with billing language in JSON error message", () => {
59
- expect(
60
- isAccountSpecificError(
61
- 400,
62
- JSON.stringify({ error: { type: "billing_error", message: "Billing issue on your account" } }),
63
- ),
64
- ).toBe(true);
65
- });
66
-
67
- it("returns true for 403 with permission language in JSON error message", () => {
68
- expect(
69
- isAccountSpecificError(
70
- 403,
71
- JSON.stringify({
72
- error: { type: "permission_error", message: "You do not have permission to access this model" },
73
- }),
74
- ),
75
- ).toBe(true);
76
- });
77
-
78
- it("returns false for plain text body without JSON structure (no false positives)", () => {
79
- // Plain text bodies that happen to contain keywords should NOT match
80
- // — only structured error.type and error.message fields are checked
81
- expect(isAccountSpecificError(400, "This request would exceed your account's rate limit")).toBe(false);
82
- expect(isAccountSpecificError(400, "Billing issue on your account")).toBe(false);
83
- expect(isAccountSpecificError(403, "You do not have permission to access this model")).toBe(false);
84
- });
85
-
86
- it("returns true for 403 permission_error type in JSON body", () => {
87
- expect(
88
- isAccountSpecificError(403, JSON.stringify({ error: { type: "permission_error", message: "Forbidden" } })),
89
- ).toBe(true);
90
- });
91
-
92
- it("returns true for 403 authentication_error type in JSON body", () => {
93
- expect(
94
- isAccountSpecificError(403, JSON.stringify({ error: { type: "authentication_error", message: "Unauthorized" } })),
95
- ).toBe(true);
96
- });
97
-
98
- it("returns true for object body with account-specific error type", () => {
99
- expect(
100
- isAccountSpecificError(400, {
101
- error: { type: "rate_limit_error", message: "too many requests" },
102
- }),
103
- ).toBe(true);
104
- });
105
-
106
- it("returns false for 400 without account-specific language", () => {
107
- expect(isAccountSpecificError(400, "Invalid request body")).toBe(false);
108
- });
109
-
110
- it("returns false for 400 with no body", () => {
111
- expect(isAccountSpecificError(400, null)).toBe(false);
112
- });
113
-
114
- it("returns false for 403 with no body", () => {
115
- expect(isAccountSpecificError(403, null)).toBe(false);
116
- });
117
-
118
- it("returns false for 500 (service-wide)", () => {
119
- expect(isAccountSpecificError(500, null)).toBe(false);
120
- });
121
-
122
- it("returns false for 503 (service-wide)", () => {
123
- expect(isAccountSpecificError(503, null)).toBe(false);
124
- });
125
-
126
- it("returns false for 529 (service-wide)", () => {
127
- expect(isAccountSpecificError(529, null)).toBe(false);
128
- });
129
-
130
- it("returns false for 200", () => {
131
- expect(isAccountSpecificError(200, null)).toBe(false);
132
- });
133
-
134
- it("is case-insensitive for JSON error message matching", () => {
135
- expect(
136
- isAccountSpecificError(
137
- 400,
138
- JSON.stringify({ error: { type: "invalid_request_error", message: "RATE LIMIT exceeded" } }),
139
- ),
140
- ).toBe(true);
141
- expect(
142
- isAccountSpecificError(
143
- 400,
144
- JSON.stringify({ error: { type: "invalid_request_error", message: "QUOTA Exhausted" } }),
145
- ),
146
- ).toBe(true);
147
- });
148
- });
149
-
150
- // ---------------------------------------------------------------------------
151
- // parseRateLimitReason
152
- // ---------------------------------------------------------------------------
153
-
154
- describe("parseRateLimitReason", () => {
155
- it("returns QUOTA_EXHAUSTED for 429 with quota in error type", () => {
156
- const body = JSON.stringify({
157
- error: { type: "quota_exceeded", message: "You have exceeded your quota" },
158
- });
159
- expect(parseRateLimitReason(429, body)).toBe("QUOTA_EXHAUSTED");
160
- });
161
-
162
- it("returns QUOTA_EXHAUSTED for 429 with exhausted in message", () => {
163
- const body = JSON.stringify({
164
- error: { type: "rate_error", message: "Token quota exhausted" },
165
- });
166
- expect(parseRateLimitReason(429, body)).toBe("QUOTA_EXHAUSTED");
167
- });
168
-
169
- it("returns QUOTA_EXHAUSTED for credit balance message", () => {
170
- const body = JSON.stringify({
171
- error: { message: "Your credit balance is too low" },
172
- });
173
- expect(parseRateLimitReason(429, body)).toBe("QUOTA_EXHAUSTED");
174
- });
175
-
176
- it("returns QUOTA_EXHAUSTED for billing message", () => {
177
- const body = JSON.stringify({
178
- error: { message: "Billing issue on account" },
179
- });
180
- expect(parseRateLimitReason(429, body)).toBe("QUOTA_EXHAUSTED");
181
- });
182
-
183
- it("returns AUTH_FAILED for 401 status", () => {
184
- expect(parseRateLimitReason(401, null)).toBe("AUTH_FAILED");
185
- });
186
-
187
- it("returns QUOTA_EXHAUSTED for permission_error type", () => {
188
- const body = JSON.stringify({
189
- error: { type: "permission_error", message: "Forbidden" },
190
- });
191
- expect(parseRateLimitReason(403, body)).toBe("QUOTA_EXHAUSTED");
192
- });
193
-
194
- it("returns AUTH_FAILED for authentication_error type", () => {
195
- const body = JSON.stringify({
196
- error: { type: "authentication_error", message: "Unauthorized" },
197
- });
198
- expect(parseRateLimitReason(403, body)).toBe("AUTH_FAILED");
199
- });
200
-
201
- it("returns AUTH_FAILED for invalid_api_key message", () => {
202
- const body = JSON.stringify({
203
- error: { type: "invalid_request_error", message: "Invalid API key" },
204
- });
205
- expect(parseRateLimitReason(400, body)).toBe("AUTH_FAILED");
206
- });
207
-
208
- it("returns RATE_LIMIT_EXCEEDED for 429 with rate limit message", () => {
209
- const body = JSON.stringify({
210
- error: { type: "rate_limit_error", message: "Rate limit exceeded" },
211
- });
212
- expect(parseRateLimitReason(429, body)).toBe("RATE_LIMIT_EXCEEDED");
213
- });
214
-
215
- it("returns RATE_LIMIT_EXCEEDED for 429 with too many requests", () => {
216
- const body = JSON.stringify({
217
- error: { message: "Too many requests" },
218
- });
219
- expect(parseRateLimitReason(429, body)).toBe("RATE_LIMIT_EXCEEDED");
220
- });
221
-
222
- it("returns RATE_LIMIT_EXCEEDED for 429 with per minute message", () => {
223
- const body = JSON.stringify({
224
- error: { message: "Exceeded 60 requests per minute" },
225
- });
226
- expect(parseRateLimitReason(429, body)).toBe("RATE_LIMIT_EXCEEDED");
227
- });
228
-
229
- it("defaults to RATE_LIMIT_EXCEEDED for 429 with no body", () => {
230
- expect(parseRateLimitReason(429, null)).toBe("RATE_LIMIT_EXCEEDED");
231
- });
232
-
233
- it("defaults to RATE_LIMIT_EXCEEDED for 429 with unparseable body", () => {
234
- expect(parseRateLimitReason(429, "not json")).toBe("RATE_LIMIT_EXCEEDED");
235
- });
236
-
237
- it("defaults to RATE_LIMIT_EXCEEDED for unrecognized status", () => {
238
- expect(parseRateLimitReason(418, null)).toBe("RATE_LIMIT_EXCEEDED");
239
- });
240
-
241
- it("handles body as object (not string)", () => {
242
- const body = { error: { type: "quota_exceeded", message: "quota" } };
243
- expect(parseRateLimitReason(429, body)).toBe("QUOTA_EXHAUSTED");
244
- });
245
- });
246
-
247
- // ---------------------------------------------------------------------------
248
- // calculateBackoffMs
249
- // ---------------------------------------------------------------------------
250
-
251
- describe("calculateBackoffMs", () => {
252
- it("uses Retry-After header when provided", () => {
253
- expect(calculateBackoffMs("RATE_LIMIT_EXCEEDED", 0, 5000)).toBe(5000);
254
- });
255
-
256
- it("enforces minimum backoff for Retry-After", () => {
257
- expect(calculateBackoffMs("RATE_LIMIT_EXCEEDED", 0, 500)).toBe(2000);
258
- });
259
-
260
- it("ignores null Retry-After", () => {
261
- const result = calculateBackoffMs("RATE_LIMIT_EXCEEDED", 0, null);
262
- expect(result).toBe(30_000);
263
- });
264
-
265
- it("ignores zero Retry-After", () => {
266
- const result = calculateBackoffMs("RATE_LIMIT_EXCEEDED", 0, 0);
267
- expect(result).toBe(30_000);
268
- });
269
-
270
- it("escalates QUOTA_EXHAUSTED backoffs", () => {
271
- expect(calculateBackoffMs("QUOTA_EXHAUSTED", 0)).toBe(60_000);
272
- expect(calculateBackoffMs("QUOTA_EXHAUSTED", 1)).toBe(300_000);
273
- expect(calculateBackoffMs("QUOTA_EXHAUSTED", 2)).toBe(1_800_000);
274
- expect(calculateBackoffMs("QUOTA_EXHAUSTED", 3)).toBe(7_200_000);
275
- });
276
-
277
- it("caps QUOTA_EXHAUSTED at max tier", () => {
278
- expect(calculateBackoffMs("QUOTA_EXHAUSTED", 100)).toBe(7_200_000);
279
- });
280
-
281
- it("returns fixed backoff for RATE_LIMIT_EXCEEDED", () => {
282
- expect(calculateBackoffMs("RATE_LIMIT_EXCEEDED", 0)).toBe(30_000);
283
- expect(calculateBackoffMs("RATE_LIMIT_EXCEEDED", 5)).toBe(30_000);
284
- });
285
-
286
- it("returns short fixed backoff for AUTH_FAILED", () => {
287
- expect(calculateBackoffMs("AUTH_FAILED", 0)).toBe(5_000);
288
- expect(calculateBackoffMs("AUTH_FAILED", 5)).toBe(5_000);
289
- });
290
-
291
- it("uses default (RATE_LIMIT_EXCEEDED) for unknown reason", () => {
292
- expect(calculateBackoffMs("SOMETHING_ELSE", 0)).toBe(30_000);
293
- });
294
- });
295
-
296
- // ---------------------------------------------------------------------------
297
- // parseRetryAfterHeader
298
- // ---------------------------------------------------------------------------
299
-
300
- describe("parseRetryAfterHeader", () => {
301
- it("returns null when no header present", () => {
302
- const response = new Response(null, { headers: {} });
303
- expect(parseRetryAfterHeader(response)).toBeNull();
304
- });
305
-
306
- it("parses integer seconds", () => {
307
- const response = new Response(null, {
308
- headers: { "retry-after": "30" },
309
- });
310
- expect(parseRetryAfterHeader(response)).toBe(30_000);
311
- });
312
-
313
- it("parses HTTP-date format", () => {
314
- const futureDate = new Date(Date.now() + 60_000);
315
- const response = new Response(null, {
316
- headers: { "retry-after": futureDate.toUTCString() },
317
- });
318
- const result = parseRetryAfterHeader(response);
319
- expect(result).toBeGreaterThan(50_000);
320
- expect(result).toBeLessThanOrEqual(61_000);
321
- });
322
-
323
- it("returns null for past HTTP-date", () => {
324
- const pastDate = new Date(Date.now() - 60_000);
325
- const response = new Response(null, {
326
- headers: { "retry-after": pastDate.toUTCString() },
327
- });
328
- expect(parseRetryAfterHeader(response)).toBeNull();
329
- });
330
-
331
- it("returns null for invalid header value", () => {
332
- const response = new Response(null, {
333
- headers: { "retry-after": "not-a-number-or-date" },
334
- });
335
- expect(parseRetryAfterHeader(response)).toBeNull();
336
- });
337
-
338
- it("returns null for zero seconds", () => {
339
- const response = new Response(null, {
340
- headers: { "retry-after": "0" },
341
- });
342
- expect(parseRetryAfterHeader(response)).toBeNull();
343
- });
344
-
345
- it("returns null for negative seconds", () => {
346
- const response = new Response(null, {
347
- headers: { "retry-after": "-5" },
348
- });
349
- expect(parseRetryAfterHeader(response)).toBeNull();
350
- });
351
- });
352
-
353
- // ---------------------------------------------------------------------------
354
- // parseUnifiedResetMsHeader
355
- // ---------------------------------------------------------------------------
356
-
357
- describe("parseUnifiedResetMsHeader", () => {
358
- it("returns null when no unified-reset header present", () => {
359
- const response = new Response(null, { headers: {} });
360
- expect(parseUnifiedResetMsHeader(response)).toBeNull();
361
- });
362
-
363
- it("returns ~ms-until-reset for a future epoch (seconds)", () => {
364
- const futureEpochSeconds = Math.floor(Date.now() / 1000) + 120;
365
- const response = new Response(null, {
366
- headers: { "anthropic-ratelimit-unified-reset": String(futureEpochSeconds) },
367
- });
368
- const result = parseUnifiedResetMsHeader(response);
369
- expect(result).toBeGreaterThan(110_000);
370
- expect(result).toBeLessThanOrEqual(120_000);
371
- });
372
-
373
- it("returns null for a non-numeric value (NaN)", () => {
374
- const response = new Response(null, {
375
- headers: { "anthropic-ratelimit-unified-reset": "not-a-number" },
376
- });
377
- expect(parseUnifiedResetMsHeader(response)).toBeNull();
378
- });
379
-
380
- it("returns null for a past epoch (<= now)", () => {
381
- const pastEpochSeconds = Math.floor(Date.now() / 1000) - 60;
382
- const response = new Response(null, {
383
- headers: { "anthropic-ratelimit-unified-reset": String(pastEpochSeconds) },
384
- });
385
- expect(parseUnifiedResetMsHeader(response)).toBeNull();
386
- });
387
-
388
- it("falls back to overage-reset when unified-reset is absent", () => {
389
- const futureEpochSeconds = Math.floor(Date.now() / 1000) + 90;
390
- const response = new Response(null, {
391
- headers: { "anthropic-ratelimit-unified-overage-reset": String(futureEpochSeconds) },
392
- });
393
- const result = parseUnifiedResetMsHeader(response);
394
- expect(result).toBeGreaterThan(80_000);
395
- expect(result).toBeLessThanOrEqual(90_000);
396
- });
397
-
398
- it("prefers unified-reset over overage-reset when both present", () => {
399
- const resetEpoch = Math.floor(Date.now() / 1000) + 120;
400
- const overageEpoch = Math.floor(Date.now() / 1000) + 300;
401
- const response = new Response(null, {
402
- headers: {
403
- "anthropic-ratelimit-unified-reset": String(resetEpoch),
404
- "anthropic-ratelimit-unified-overage-reset": String(overageEpoch),
405
- },
406
- });
407
- const result = parseUnifiedResetMsHeader(response);
408
- expect(result).toBeGreaterThan(110_000);
409
- expect(result).toBeLessThanOrEqual(120_000);
410
- });
411
- });