@vacbo/opencode-anthropic-fix 0.1.7 → 0.1.9

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 (107) hide show
  1. package/README.md +88 -88
  2. package/dist/opencode-anthropic-auth-cli.mjs +804 -507
  3. package/dist/opencode-anthropic-auth-plugin.js +4751 -4109
  4. package/package.json +67 -59
  5. package/src/__tests__/billing-edge-cases.test.ts +59 -59
  6. package/src/__tests__/bun-proxy.parallel.test.ts +388 -382
  7. package/src/__tests__/cc-comparison.test.ts +87 -87
  8. package/src/__tests__/cc-credentials.test.ts +254 -250
  9. package/src/__tests__/cch-drift-checker.test.ts +51 -51
  10. package/src/__tests__/cch-native-style.test.ts +56 -56
  11. package/src/__tests__/debug-gating.test.ts +42 -42
  12. package/src/__tests__/decomposition-smoke.test.ts +68 -68
  13. package/src/__tests__/fingerprint-regression.test.ts +575 -566
  14. package/src/__tests__/helpers/conversation-history.smoke.test.ts +271 -271
  15. package/src/__tests__/helpers/conversation-history.ts +119 -119
  16. package/src/__tests__/helpers/deferred.smoke.test.ts +103 -103
  17. package/src/__tests__/helpers/deferred.ts +69 -69
  18. package/src/__tests__/helpers/in-memory-storage.smoke.test.ts +155 -155
  19. package/src/__tests__/helpers/in-memory-storage.ts +88 -88
  20. package/src/__tests__/helpers/mock-bun-proxy.smoke.test.ts +68 -68
  21. package/src/__tests__/helpers/mock-bun-proxy.ts +189 -189
  22. package/src/__tests__/helpers/plugin-fetch-harness.smoke.test.ts +273 -273
  23. package/src/__tests__/helpers/plugin-fetch-harness.ts +288 -288
  24. package/src/__tests__/helpers/sse.smoke.test.ts +236 -236
  25. package/src/__tests__/helpers/sse.ts +209 -209
  26. package/src/__tests__/index.parallel.test.ts +605 -595
  27. package/src/__tests__/sanitization-regex.test.ts +112 -112
  28. package/src/__tests__/state-bounds.test.ts +90 -90
  29. package/src/account-identity.test.ts +197 -192
  30. package/src/account-identity.ts +69 -67
  31. package/src/account-state.test.ts +86 -86
  32. package/src/account-state.ts +25 -25
  33. package/src/accounts/matching.test.ts +335 -0
  34. package/src/accounts/matching.ts +167 -0
  35. package/src/accounts/persistence.test.ts +345 -0
  36. package/src/accounts/persistence.ts +432 -0
  37. package/src/accounts/repair.test.ts +276 -0
  38. package/src/accounts/repair.ts +407 -0
  39. package/src/accounts.dedup.test.ts +621 -621
  40. package/src/accounts.test.ts +933 -929
  41. package/src/accounts.ts +633 -989
  42. package/src/backoff.test.ts +345 -345
  43. package/src/backoff.ts +219 -219
  44. package/src/betas.ts +124 -124
  45. package/src/bun-fetch.test.ts +345 -342
  46. package/src/bun-fetch.ts +424 -424
  47. package/src/bun-proxy.test.ts +25 -25
  48. package/src/bun-proxy.ts +209 -209
  49. package/src/cc-credentials.ts +111 -111
  50. package/src/circuit-breaker.test.ts +184 -184
  51. package/src/circuit-breaker.ts +169 -169
  52. package/src/cli/commands/auth.ts +963 -0
  53. package/src/cli/commands/config.ts +547 -0
  54. package/src/cli/formatting.test.ts +406 -0
  55. package/src/cli/formatting.ts +219 -0
  56. package/src/cli.ts +255 -2022
  57. package/src/commands/handlers/betas.ts +100 -0
  58. package/src/commands/handlers/config.ts +99 -0
  59. package/src/commands/handlers/files.ts +375 -0
  60. package/src/commands/oauth-flow.ts +181 -166
  61. package/src/commands/prompts.ts +61 -61
  62. package/src/commands/router.test.ts +421 -0
  63. package/src/commands/router.ts +143 -635
  64. package/src/config.test.ts +482 -482
  65. package/src/config.ts +412 -404
  66. package/src/constants.ts +48 -48
  67. package/src/drift/cch-constants.ts +95 -95
  68. package/src/env.ts +111 -105
  69. package/src/headers/billing.ts +33 -33
  70. package/src/headers/builder.ts +130 -130
  71. package/src/headers/cch.ts +75 -75
  72. package/src/headers/stainless.ts +25 -25
  73. package/src/headers/user-agent.ts +23 -23
  74. package/src/index.ts +436 -828
  75. package/src/models.ts +27 -27
  76. package/src/oauth.test.ts +102 -102
  77. package/src/oauth.ts +178 -178
  78. package/src/parent-pid-watcher.test.ts +148 -148
  79. package/src/parent-pid-watcher.ts +69 -69
  80. package/src/plugin-helpers.ts +82 -82
  81. package/src/refresh-helpers.ts +145 -139
  82. package/src/refresh-lock.test.ts +94 -94
  83. package/src/refresh-lock.ts +93 -93
  84. package/src/request/body.history.test.ts +579 -571
  85. package/src/request/body.ts +255 -255
  86. package/src/request/metadata.ts +65 -65
  87. package/src/request/retry.test.ts +156 -156
  88. package/src/request/retry.ts +67 -67
  89. package/src/request/url.ts +21 -21
  90. package/src/request-orchestration-helpers.ts +648 -0
  91. package/src/response/index.ts +5 -5
  92. package/src/response/mcp.ts +58 -58
  93. package/src/response/streaming.test.ts +313 -311
  94. package/src/response/streaming.ts +412 -410
  95. package/src/rotation.test.ts +304 -301
  96. package/src/rotation.ts +205 -205
  97. package/src/storage.test.ts +547 -547
  98. package/src/storage.ts +315 -291
  99. package/src/system-prompt/builder.ts +38 -38
  100. package/src/system-prompt/index.ts +5 -5
  101. package/src/system-prompt/normalize.ts +60 -60
  102. package/src/system-prompt/sanitize.ts +30 -30
  103. package/src/thinking.ts +21 -20
  104. package/src/token-refresh.test.ts +265 -265
  105. package/src/token-refresh.ts +219 -214
  106. package/src/types.ts +30 -30
  107. package/dist/bun-proxy.mjs +0 -291
@@ -1,12 +1,12 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import {
3
- calculateBackoffMs,
4
- isAccountSpecificError,
5
- isRetriableNetworkError,
6
- parseRateLimitReason,
7
- parseRetryAfterHeader,
8
- parseRetryAfterMsHeader,
9
- parseShouldRetryHeader,
3
+ calculateBackoffMs,
4
+ isAccountSpecificError,
5
+ isRetriableNetworkError,
6
+ parseRateLimitReason,
7
+ parseRetryAfterHeader,
8
+ parseRetryAfterMsHeader,
9
+ parseShouldRetryHeader,
10
10
  } from "./backoff.js";
11
11
 
12
12
  // ---------------------------------------------------------------------------
@@ -14,119 +14,119 @@ import {
14
14
  // ---------------------------------------------------------------------------
15
15
 
16
16
  describe("isAccountSpecificError", () => {
17
- it("returns true for 429 (always account-specific)", () => {
18
- expect(isAccountSpecificError(429, null)).toBe(true);
19
- });
20
-
21
- it("returns true for 429 even without body", () => {
22
- expect(isAccountSpecificError(429)).toBe(true);
23
- });
24
-
25
- it("returns true for 401 (always account-specific)", () => {
26
- expect(isAccountSpecificError(401, null)).toBe(true);
27
- });
28
-
29
- it("returns true for 400 with rate limit language in body", () => {
30
- expect(isAccountSpecificError(400, "This request would exceed your account's rate limit")).toBe(true);
31
- });
32
-
33
- it("returns true for 400 with quota language in body", () => {
34
- expect(isAccountSpecificError(400, "Your quota has been exhausted")).toBe(true);
35
- });
36
-
37
- it("returns true for 400 with credit balance language in body", () => {
38
- expect(isAccountSpecificError(400, "Your credit balance is too low to complete this request")).toBe(true);
39
- });
40
-
41
- it("returns true for 400 with billing language in body", () => {
42
- expect(isAccountSpecificError(400, "Billing issue on your account")).toBe(true);
43
- });
44
-
45
- it("returns true for 403 with permission language in body", () => {
46
- expect(isAccountSpecificError(403, "You do not have permission to access this model")).toBe(true);
47
- });
48
-
49
- it("returns true for 403 permission_error type in JSON body", () => {
50
- expect(
51
- isAccountSpecificError(
52
- 403,
53
- JSON.stringify({
54
- error: { type: "permission_error", message: "Forbidden" },
55
- }),
56
- ),
57
- ).toBe(true);
58
- });
59
-
60
- it("returns true for 403 authentication_error type in JSON body", () => {
61
- expect(
62
- isAccountSpecificError(
63
- 403,
64
- JSON.stringify({
65
- error: { type: "authentication_error", message: "Unauthorized" },
66
- }),
67
- ),
68
- ).toBe(true);
69
- });
70
-
71
- it("returns true for object body with account-specific error type", () => {
72
- expect(
73
- isAccountSpecificError(400, {
74
- error: { type: "rate_limit_error", message: "too many requests" },
75
- }),
76
- ).toBe(true);
77
- });
78
-
79
- it("returns false for 400 without account-specific language", () => {
80
- expect(isAccountSpecificError(400, "Invalid request body")).toBe(false);
81
- });
82
-
83
- it("returns false for 400 with no body", () => {
84
- expect(isAccountSpecificError(400, null)).toBe(false);
85
- });
86
-
87
- it("returns false for 403 with no body", () => {
88
- expect(isAccountSpecificError(403, null)).toBe(false);
89
- });
90
-
91
- it("returns false for 500 (service-wide)", () => {
92
- expect(isAccountSpecificError(500, null)).toBe(false);
93
- });
94
-
95
- it("returns false for 503 (service-wide)", () => {
96
- expect(isAccountSpecificError(503, null)).toBe(false);
97
- });
98
-
99
- it("returns false for 529 (service-wide)", () => {
100
- expect(isAccountSpecificError(529, null)).toBe(false);
101
- });
102
-
103
- it("returns false for 200", () => {
104
- expect(isAccountSpecificError(200, null)).toBe(false);
105
- });
106
-
107
- it("is case-insensitive for body matching", () => {
108
- expect(isAccountSpecificError(400, "RATE LIMIT exceeded")).toBe(true);
109
- expect(isAccountSpecificError(400, "QUOTA Exhausted")).toBe(true);
110
- });
111
-
112
- it("returns true for 403 with 'membership benefits' message (JSON body)", () => {
113
- const body = JSON.stringify({
114
- error: { message: "We're unable to verify your membership benefits" },
115
- });
116
- expect(isAccountSpecificError(403, body)).toBe(true);
117
- });
118
-
119
- it("returns true for 403 with 'membership benefits' message (string body)", () => {
120
- expect(isAccountSpecificError(403, "We're unable to verify your membership benefits")).toBe(true);
121
- });
122
-
123
- it("returns true for 403 with 'unable to verify' message", () => {
124
- expect(isAccountSpecificError(403, "unable to verify your account")).toBe(true);
125
- });
126
-
127
- it("returns false for 403 with unrelated error message (negative test)", () => {
128
- expect(isAccountSpecificError(403, "Some other error occurred")).toBe(false);
129
- });
17
+ it("returns true for 429 (always account-specific)", () => {
18
+ expect(isAccountSpecificError(429, null)).toBe(true);
19
+ });
20
+
21
+ it("returns true for 429 even without body", () => {
22
+ expect(isAccountSpecificError(429)).toBe(true);
23
+ });
24
+
25
+ it("returns true for 401 (always account-specific)", () => {
26
+ expect(isAccountSpecificError(401, null)).toBe(true);
27
+ });
28
+
29
+ it("returns true for 400 with rate limit language in body", () => {
30
+ expect(isAccountSpecificError(400, "This request would exceed your account's rate limit")).toBe(true);
31
+ });
32
+
33
+ it("returns true for 400 with quota language in body", () => {
34
+ expect(isAccountSpecificError(400, "Your quota has been exhausted")).toBe(true);
35
+ });
36
+
37
+ it("returns true for 400 with credit balance language in body", () => {
38
+ expect(isAccountSpecificError(400, "Your credit balance is too low to complete this request")).toBe(true);
39
+ });
40
+
41
+ it("returns true for 400 with billing language in body", () => {
42
+ expect(isAccountSpecificError(400, "Billing issue on your account")).toBe(true);
43
+ });
44
+
45
+ it("returns true for 403 with permission language in body", () => {
46
+ expect(isAccountSpecificError(403, "You do not have permission to access this model")).toBe(true);
47
+ });
48
+
49
+ it("returns true for 403 permission_error type in JSON body", () => {
50
+ expect(
51
+ isAccountSpecificError(
52
+ 403,
53
+ JSON.stringify({
54
+ error: { type: "permission_error", message: "Forbidden" },
55
+ }),
56
+ ),
57
+ ).toBe(true);
58
+ });
59
+
60
+ it("returns true for 403 authentication_error type in JSON body", () => {
61
+ expect(
62
+ isAccountSpecificError(
63
+ 403,
64
+ JSON.stringify({
65
+ error: { type: "authentication_error", message: "Unauthorized" },
66
+ }),
67
+ ),
68
+ ).toBe(true);
69
+ });
70
+
71
+ it("returns true for object body with account-specific error type", () => {
72
+ expect(
73
+ isAccountSpecificError(400, {
74
+ error: { type: "rate_limit_error", message: "too many requests" },
75
+ }),
76
+ ).toBe(true);
77
+ });
78
+
79
+ it("returns false for 400 without account-specific language", () => {
80
+ expect(isAccountSpecificError(400, "Invalid request body")).toBe(false);
81
+ });
82
+
83
+ it("returns false for 400 with no body", () => {
84
+ expect(isAccountSpecificError(400, null)).toBe(false);
85
+ });
86
+
87
+ it("returns false for 403 with no body", () => {
88
+ expect(isAccountSpecificError(403, null)).toBe(false);
89
+ });
90
+
91
+ it("returns false for 500 (service-wide)", () => {
92
+ expect(isAccountSpecificError(500, null)).toBe(false);
93
+ });
94
+
95
+ it("returns false for 503 (service-wide)", () => {
96
+ expect(isAccountSpecificError(503, null)).toBe(false);
97
+ });
98
+
99
+ it("returns false for 529 (service-wide)", () => {
100
+ expect(isAccountSpecificError(529, null)).toBe(false);
101
+ });
102
+
103
+ it("returns false for 200", () => {
104
+ expect(isAccountSpecificError(200, null)).toBe(false);
105
+ });
106
+
107
+ it("is case-insensitive for body matching", () => {
108
+ expect(isAccountSpecificError(400, "RATE LIMIT exceeded")).toBe(true);
109
+ expect(isAccountSpecificError(400, "QUOTA Exhausted")).toBe(true);
110
+ });
111
+
112
+ it("returns true for 403 with 'membership benefits' message (JSON body)", () => {
113
+ const body = JSON.stringify({
114
+ error: { message: "We're unable to verify your membership benefits" },
115
+ });
116
+ expect(isAccountSpecificError(403, body)).toBe(true);
117
+ });
118
+
119
+ it("returns true for 403 with 'membership benefits' message (string body)", () => {
120
+ expect(isAccountSpecificError(403, "We're unable to verify your membership benefits")).toBe(true);
121
+ });
122
+
123
+ it("returns true for 403 with 'unable to verify' message", () => {
124
+ expect(isAccountSpecificError(403, "unable to verify your account")).toBe(true);
125
+ });
126
+
127
+ it("returns false for 403 with unrelated error message (negative test)", () => {
128
+ expect(isAccountSpecificError(403, "Some other error occurred")).toBe(false);
129
+ });
130
130
  });
131
131
 
132
132
  // ---------------------------------------------------------------------------
@@ -134,106 +134,106 @@ describe("isAccountSpecificError", () => {
134
134
  // ---------------------------------------------------------------------------
135
135
 
136
136
  describe("parseRateLimitReason", () => {
137
- it("returns QUOTA_EXHAUSTED for 429 with quota in error type", () => {
138
- const body = JSON.stringify({
139
- error: {
140
- type: "quota_exceeded",
141
- message: "You have exceeded your quota",
142
- },
137
+ it("returns QUOTA_EXHAUSTED for 429 with quota in error type", () => {
138
+ const body = JSON.stringify({
139
+ error: {
140
+ type: "quota_exceeded",
141
+ message: "You have exceeded your quota",
142
+ },
143
+ });
144
+ expect(parseRateLimitReason(429, body)).toBe("QUOTA_EXHAUSTED");
143
145
  });
144
- expect(parseRateLimitReason(429, body)).toBe("QUOTA_EXHAUSTED");
145
- });
146
146
 
147
- it("returns QUOTA_EXHAUSTED for 429 with exhausted in message", () => {
148
- const body = JSON.stringify({
149
- error: { type: "rate_error", message: "Token quota exhausted" },
147
+ it("returns QUOTA_EXHAUSTED for 429 with exhausted in message", () => {
148
+ const body = JSON.stringify({
149
+ error: { type: "rate_error", message: "Token quota exhausted" },
150
+ });
151
+ expect(parseRateLimitReason(429, body)).toBe("QUOTA_EXHAUSTED");
150
152
  });
151
- expect(parseRateLimitReason(429, body)).toBe("QUOTA_EXHAUSTED");
152
- });
153
153
 
154
- it("returns QUOTA_EXHAUSTED for credit balance message", () => {
155
- const body = JSON.stringify({
156
- error: { message: "Your credit balance is too low" },
154
+ it("returns QUOTA_EXHAUSTED for credit balance message", () => {
155
+ const body = JSON.stringify({
156
+ error: { message: "Your credit balance is too low" },
157
+ });
158
+ expect(parseRateLimitReason(429, body)).toBe("QUOTA_EXHAUSTED");
157
159
  });
158
- expect(parseRateLimitReason(429, body)).toBe("QUOTA_EXHAUSTED");
159
- });
160
160
 
161
- it("returns QUOTA_EXHAUSTED for billing message", () => {
162
- const body = JSON.stringify({
163
- error: { message: "Billing issue on account" },
161
+ it("returns QUOTA_EXHAUSTED for billing message", () => {
162
+ const body = JSON.stringify({
163
+ error: { message: "Billing issue on account" },
164
+ });
165
+ expect(parseRateLimitReason(429, body)).toBe("QUOTA_EXHAUSTED");
164
166
  });
165
- expect(parseRateLimitReason(429, body)).toBe("QUOTA_EXHAUSTED");
166
- });
167
167
 
168
- it("returns AUTH_FAILED for 401 status", () => {
169
- expect(parseRateLimitReason(401, null)).toBe("AUTH_FAILED");
170
- });
168
+ it("returns AUTH_FAILED for 401 status", () => {
169
+ expect(parseRateLimitReason(401, null)).toBe("AUTH_FAILED");
170
+ });
171
171
 
172
- it("returns QUOTA_EXHAUSTED for permission_error type", () => {
173
- const body = JSON.stringify({
174
- error: { type: "permission_error", message: "Forbidden" },
172
+ it("returns QUOTA_EXHAUSTED for permission_error type", () => {
173
+ const body = JSON.stringify({
174
+ error: { type: "permission_error", message: "Forbidden" },
175
+ });
176
+ expect(parseRateLimitReason(403, body)).toBe("QUOTA_EXHAUSTED");
175
177
  });
176
- expect(parseRateLimitReason(403, body)).toBe("QUOTA_EXHAUSTED");
177
- });
178
178
 
179
- it("returns AUTH_FAILED for authentication_error type", () => {
180
- const body = JSON.stringify({
181
- error: { type: "authentication_error", message: "Unauthorized" },
179
+ it("returns AUTH_FAILED for authentication_error type", () => {
180
+ const body = JSON.stringify({
181
+ error: { type: "authentication_error", message: "Unauthorized" },
182
+ });
183
+ expect(parseRateLimitReason(403, body)).toBe("AUTH_FAILED");
182
184
  });
183
- expect(parseRateLimitReason(403, body)).toBe("AUTH_FAILED");
184
- });
185
185
 
186
- it("returns AUTH_FAILED for invalid_api_key message", () => {
187
- const body = JSON.stringify({
188
- error: { type: "invalid_request_error", message: "Invalid API key" },
186
+ it("returns AUTH_FAILED for invalid_api_key message", () => {
187
+ const body = JSON.stringify({
188
+ error: { type: "invalid_request_error", message: "Invalid API key" },
189
+ });
190
+ expect(parseRateLimitReason(400, body)).toBe("AUTH_FAILED");
189
191
  });
190
- expect(parseRateLimitReason(400, body)).toBe("AUTH_FAILED");
191
- });
192
192
 
193
- it("returns RATE_LIMIT_EXCEEDED for 429 with rate limit message", () => {
194
- const body = JSON.stringify({
195
- error: { type: "rate_limit_error", message: "Rate limit exceeded" },
193
+ it("returns RATE_LIMIT_EXCEEDED for 429 with rate limit message", () => {
194
+ const body = JSON.stringify({
195
+ error: { type: "rate_limit_error", message: "Rate limit exceeded" },
196
+ });
197
+ expect(parseRateLimitReason(429, body)).toBe("RATE_LIMIT_EXCEEDED");
196
198
  });
197
- expect(parseRateLimitReason(429, body)).toBe("RATE_LIMIT_EXCEEDED");
198
- });
199
199
 
200
- it("returns RATE_LIMIT_EXCEEDED for 429 with too many requests", () => {
201
- const body = JSON.stringify({
202
- error: { message: "Too many requests" },
200
+ it("returns RATE_LIMIT_EXCEEDED for 429 with too many requests", () => {
201
+ const body = JSON.stringify({
202
+ error: { message: "Too many requests" },
203
+ });
204
+ expect(parseRateLimitReason(429, body)).toBe("RATE_LIMIT_EXCEEDED");
203
205
  });
204
- expect(parseRateLimitReason(429, body)).toBe("RATE_LIMIT_EXCEEDED");
205
- });
206
206
 
207
- it("returns RATE_LIMIT_EXCEEDED for 429 with per minute message", () => {
208
- const body = JSON.stringify({
209
- error: { message: "Exceeded 60 requests per minute" },
207
+ it("returns RATE_LIMIT_EXCEEDED for 429 with per minute message", () => {
208
+ const body = JSON.stringify({
209
+ error: { message: "Exceeded 60 requests per minute" },
210
+ });
211
+ expect(parseRateLimitReason(429, body)).toBe("RATE_LIMIT_EXCEEDED");
210
212
  });
211
- expect(parseRateLimitReason(429, body)).toBe("RATE_LIMIT_EXCEEDED");
212
- });
213
213
 
214
- it("defaults to RATE_LIMIT_EXCEEDED for 429 with no body", () => {
215
- expect(parseRateLimitReason(429, null)).toBe("RATE_LIMIT_EXCEEDED");
216
- });
214
+ it("defaults to RATE_LIMIT_EXCEEDED for 429 with no body", () => {
215
+ expect(parseRateLimitReason(429, null)).toBe("RATE_LIMIT_EXCEEDED");
216
+ });
217
217
 
218
- it("defaults to RATE_LIMIT_EXCEEDED for 429 with unparseable body", () => {
219
- expect(parseRateLimitReason(429, "not json")).toBe("RATE_LIMIT_EXCEEDED");
220
- });
218
+ it("defaults to RATE_LIMIT_EXCEEDED for 429 with unparseable body", () => {
219
+ expect(parseRateLimitReason(429, "not json")).toBe("RATE_LIMIT_EXCEEDED");
220
+ });
221
221
 
222
- it("defaults to RATE_LIMIT_EXCEEDED for unrecognized status", () => {
223
- expect(parseRateLimitReason(418, null)).toBe("RATE_LIMIT_EXCEEDED");
224
- });
222
+ it("defaults to RATE_LIMIT_EXCEEDED for unrecognized status", () => {
223
+ expect(parseRateLimitReason(418, null)).toBe("RATE_LIMIT_EXCEEDED");
224
+ });
225
225
 
226
- it("handles body as object (not string)", () => {
227
- const body = { error: { type: "quota_exceeded", message: "quota" } };
228
- expect(parseRateLimitReason(429, body)).toBe("QUOTA_EXHAUSTED");
229
- });
226
+ it("handles body as object (not string)", () => {
227
+ const body = { error: { type: "quota_exceeded", message: "quota" } };
228
+ expect(parseRateLimitReason(429, body)).toBe("QUOTA_EXHAUSTED");
229
+ });
230
230
 
231
- it("returns AUTH_FAILED for membership benefits message", () => {
232
- const body = JSON.stringify({
233
- error: { message: "We're unable to verify your membership benefits" },
231
+ it("returns AUTH_FAILED for membership benefits message", () => {
232
+ const body = JSON.stringify({
233
+ error: { message: "We're unable to verify your membership benefits" },
234
+ });
235
+ expect(parseRateLimitReason(403, body)).toBe("AUTH_FAILED");
234
236
  });
235
- expect(parseRateLimitReason(403, body)).toBe("AUTH_FAILED");
236
- });
237
237
  });
238
238
 
239
239
  // ---------------------------------------------------------------------------
@@ -241,23 +241,23 @@ describe("parseRateLimitReason", () => {
241
241
  // ---------------------------------------------------------------------------
242
242
 
243
243
  describe("isRetriableNetworkError", () => {
244
- it("returns true for retryable connection reset codes", () => {
245
- const error = Object.assign(new Error("socket died"), {
246
- code: "ECONNRESET",
247
- });
244
+ it("returns true for retryable connection reset codes", () => {
245
+ const error = Object.assign(new Error("socket died"), {
246
+ code: "ECONNRESET",
247
+ });
248
248
 
249
- expect(isRetriableNetworkError(error)).toBe(true);
250
- });
249
+ expect(isRetriableNetworkError(error)).toBe(true);
250
+ });
251
251
 
252
- it("returns true for Bun proxy upstream reset messages", () => {
253
- expect(isRetriableNetworkError(new Error("Bun proxy upstream error: Connection reset by server"))).toBe(true);
254
- });
252
+ it("returns true for Bun proxy upstream reset messages", () => {
253
+ expect(isRetriableNetworkError(new Error("Bun proxy upstream error: Connection reset by server"))).toBe(true);
254
+ });
255
255
 
256
- it("returns false for user abort errors", () => {
257
- const error = new DOMException("The operation was aborted", "AbortError");
256
+ it("returns false for user abort errors", () => {
257
+ const error = new DOMException("The operation was aborted", "AbortError");
258
258
 
259
- expect(isRetriableNetworkError(error)).toBe(false);
260
- });
259
+ expect(isRetriableNetworkError(error)).toBe(false);
260
+ });
261
261
  });
262
262
 
263
263
  // ---------------------------------------------------------------------------
@@ -265,48 +265,48 @@ describe("isRetriableNetworkError", () => {
265
265
  // ---------------------------------------------------------------------------
266
266
 
267
267
  describe("calculateBackoffMs", () => {
268
- it("uses Retry-After header when provided", () => {
269
- expect(calculateBackoffMs("RATE_LIMIT_EXCEEDED", 0, 5000)).toBe(5000);
270
- });
271
-
272
- it("enforces minimum backoff for Retry-After", () => {
273
- expect(calculateBackoffMs("RATE_LIMIT_EXCEEDED", 0, 500)).toBe(2000);
274
- });
275
-
276
- it("ignores null Retry-After", () => {
277
- const result = calculateBackoffMs("RATE_LIMIT_EXCEEDED", 0, null);
278
- expect(result).toBe(30_000);
279
- });
280
-
281
- it("ignores zero Retry-After", () => {
282
- const result = calculateBackoffMs("RATE_LIMIT_EXCEEDED", 0, 0);
283
- expect(result).toBe(30_000);
284
- });
285
-
286
- it("escalates QUOTA_EXHAUSTED backoffs", () => {
287
- expect(calculateBackoffMs("QUOTA_EXHAUSTED", 0)).toBe(60_000);
288
- expect(calculateBackoffMs("QUOTA_EXHAUSTED", 1)).toBe(300_000);
289
- expect(calculateBackoffMs("QUOTA_EXHAUSTED", 2)).toBe(1_800_000);
290
- expect(calculateBackoffMs("QUOTA_EXHAUSTED", 3)).toBe(7_200_000);
291
- });
292
-
293
- it("caps QUOTA_EXHAUSTED at max tier", () => {
294
- expect(calculateBackoffMs("QUOTA_EXHAUSTED", 100)).toBe(7_200_000);
295
- });
296
-
297
- it("returns fixed backoff for RATE_LIMIT_EXCEEDED", () => {
298
- expect(calculateBackoffMs("RATE_LIMIT_EXCEEDED", 0)).toBe(30_000);
299
- expect(calculateBackoffMs("RATE_LIMIT_EXCEEDED", 5)).toBe(30_000);
300
- });
301
-
302
- it("returns short fixed backoff for AUTH_FAILED", () => {
303
- expect(calculateBackoffMs("AUTH_FAILED", 0)).toBe(5_000);
304
- expect(calculateBackoffMs("AUTH_FAILED", 5)).toBe(5_000);
305
- });
306
-
307
- it("uses default (RATE_LIMIT_EXCEEDED) for unknown reason", () => {
308
- expect(calculateBackoffMs("SOMETHING_ELSE", 0)).toBe(30_000);
309
- });
268
+ it("uses Retry-After header when provided", () => {
269
+ expect(calculateBackoffMs("RATE_LIMIT_EXCEEDED", 0, 5000)).toBe(5000);
270
+ });
271
+
272
+ it("enforces minimum backoff for Retry-After", () => {
273
+ expect(calculateBackoffMs("RATE_LIMIT_EXCEEDED", 0, 500)).toBe(2000);
274
+ });
275
+
276
+ it("ignores null Retry-After", () => {
277
+ const result = calculateBackoffMs("RATE_LIMIT_EXCEEDED", 0, null);
278
+ expect(result).toBe(30_000);
279
+ });
280
+
281
+ it("ignores zero Retry-After", () => {
282
+ const result = calculateBackoffMs("RATE_LIMIT_EXCEEDED", 0, 0);
283
+ expect(result).toBe(30_000);
284
+ });
285
+
286
+ it("escalates QUOTA_EXHAUSTED backoffs", () => {
287
+ expect(calculateBackoffMs("QUOTA_EXHAUSTED", 0)).toBe(60_000);
288
+ expect(calculateBackoffMs("QUOTA_EXHAUSTED", 1)).toBe(300_000);
289
+ expect(calculateBackoffMs("QUOTA_EXHAUSTED", 2)).toBe(1_800_000);
290
+ expect(calculateBackoffMs("QUOTA_EXHAUSTED", 3)).toBe(7_200_000);
291
+ });
292
+
293
+ it("caps QUOTA_EXHAUSTED at max tier", () => {
294
+ expect(calculateBackoffMs("QUOTA_EXHAUSTED", 100)).toBe(7_200_000);
295
+ });
296
+
297
+ it("returns fixed backoff for RATE_LIMIT_EXCEEDED", () => {
298
+ expect(calculateBackoffMs("RATE_LIMIT_EXCEEDED", 0)).toBe(30_000);
299
+ expect(calculateBackoffMs("RATE_LIMIT_EXCEEDED", 5)).toBe(30_000);
300
+ });
301
+
302
+ it("returns short fixed backoff for AUTH_FAILED", () => {
303
+ expect(calculateBackoffMs("AUTH_FAILED", 0)).toBe(5_000);
304
+ expect(calculateBackoffMs("AUTH_FAILED", 5)).toBe(5_000);
305
+ });
306
+
307
+ it("uses default (RATE_LIMIT_EXCEEDED) for unknown reason", () => {
308
+ expect(calculateBackoffMs("SOMETHING_ELSE", 0)).toBe(30_000);
309
+ });
310
310
  });
311
311
 
312
312
  // ---------------------------------------------------------------------------
@@ -314,56 +314,56 @@ describe("calculateBackoffMs", () => {
314
314
  // ---------------------------------------------------------------------------
315
315
 
316
316
  describe("parseRetryAfterHeader", () => {
317
- it("returns null when no header present", () => {
318
- const response = new Response(null, { headers: {} });
319
- expect(parseRetryAfterHeader(response)).toBeNull();
320
- });
317
+ it("returns null when no header present", () => {
318
+ const response = new Response(null, { headers: {} });
319
+ expect(parseRetryAfterHeader(response)).toBeNull();
320
+ });
321
321
 
322
- it("parses integer seconds", () => {
323
- const response = new Response(null, {
324
- headers: { "retry-after": "30" },
322
+ it("parses integer seconds", () => {
323
+ const response = new Response(null, {
324
+ headers: { "retry-after": "30" },
325
+ });
326
+ expect(parseRetryAfterHeader(response)).toBe(30_000);
325
327
  });
326
- expect(parseRetryAfterHeader(response)).toBe(30_000);
327
- });
328
328
 
329
- it("parses HTTP-date format", () => {
330
- const futureDate = new Date(Date.now() + 60_000);
331
- const response = new Response(null, {
332
- headers: { "retry-after": futureDate.toUTCString() },
329
+ it("parses HTTP-date format", () => {
330
+ const futureDate = new Date(Date.now() + 60_000);
331
+ const response = new Response(null, {
332
+ headers: { "retry-after": futureDate.toUTCString() },
333
+ });
334
+ const result = parseRetryAfterHeader(response);
335
+ expect(result).toBeGreaterThan(50_000);
336
+ expect(result).toBeLessThanOrEqual(61_000);
333
337
  });
334
- const result = parseRetryAfterHeader(response);
335
- expect(result).toBeGreaterThan(50_000);
336
- expect(result).toBeLessThanOrEqual(61_000);
337
- });
338
338
 
339
- it("returns null for past HTTP-date", () => {
340
- const pastDate = new Date(Date.now() - 60_000);
341
- const response = new Response(null, {
342
- headers: { "retry-after": pastDate.toUTCString() },
339
+ it("returns null for past HTTP-date", () => {
340
+ const pastDate = new Date(Date.now() - 60_000);
341
+ const response = new Response(null, {
342
+ headers: { "retry-after": pastDate.toUTCString() },
343
+ });
344
+ expect(parseRetryAfterHeader(response)).toBeNull();
343
345
  });
344
- expect(parseRetryAfterHeader(response)).toBeNull();
345
- });
346
346
 
347
- it("returns null for invalid header value", () => {
348
- const response = new Response(null, {
349
- headers: { "retry-after": "not-a-number-or-date" },
347
+ it("returns null for invalid header value", () => {
348
+ const response = new Response(null, {
349
+ headers: { "retry-after": "not-a-number-or-date" },
350
+ });
351
+ expect(parseRetryAfterHeader(response)).toBeNull();
350
352
  });
351
- expect(parseRetryAfterHeader(response)).toBeNull();
352
- });
353
353
 
354
- it("returns null for zero seconds", () => {
355
- const response = new Response(null, {
356
- headers: { "retry-after": "0" },
354
+ it("returns null for zero seconds", () => {
355
+ const response = new Response(null, {
356
+ headers: { "retry-after": "0" },
357
+ });
358
+ expect(parseRetryAfterHeader(response)).toBeNull();
357
359
  });
358
- expect(parseRetryAfterHeader(response)).toBeNull();
359
- });
360
360
 
361
- it("returns null for negative seconds", () => {
362
- const response = new Response(null, {
363
- headers: { "retry-after": "-5" },
361
+ it("returns null for negative seconds", () => {
362
+ const response = new Response(null, {
363
+ headers: { "retry-after": "-5" },
364
+ });
365
+ expect(parseRetryAfterHeader(response)).toBeNull();
364
366
  });
365
- expect(parseRetryAfterHeader(response)).toBeNull();
366
- });
367
367
  });
368
368
 
369
369
  // ---------------------------------------------------------------------------
@@ -371,45 +371,45 @@ describe("parseRetryAfterHeader", () => {
371
371
  // ---------------------------------------------------------------------------
372
372
 
373
373
  describe("parseRetryAfterMsHeader", () => {
374
- it("returns null when header not present", () => {
375
- const response = new Response(null, { headers: {} });
376
- expect(parseRetryAfterMsHeader(response)).toBeNull();
377
- });
374
+ it("returns null when header not present", () => {
375
+ const response = new Response(null, { headers: {} });
376
+ expect(parseRetryAfterMsHeader(response)).toBeNull();
377
+ });
378
378
 
379
- it("parses integer milliseconds", () => {
380
- const response = new Response(null, {
381
- headers: { "retry-after-ms": "1500" },
379
+ it("parses integer milliseconds", () => {
380
+ const response = new Response(null, {
381
+ headers: { "retry-after-ms": "1500" },
382
+ });
383
+ expect(parseRetryAfterMsHeader(response)).toBe(1500);
382
384
  });
383
- expect(parseRetryAfterMsHeader(response)).toBe(1500);
384
- });
385
385
 
386
- it("parses fractional milliseconds and rounds", () => {
387
- const response = new Response(null, {
388
- headers: { "retry-after-ms": "1500.7" },
386
+ it("parses fractional milliseconds and rounds", () => {
387
+ const response = new Response(null, {
388
+ headers: { "retry-after-ms": "1500.7" },
389
+ });
390
+ expect(parseRetryAfterMsHeader(response)).toBe(1501);
389
391
  });
390
- expect(parseRetryAfterMsHeader(response)).toBe(1501);
391
- });
392
392
 
393
- it("returns null for malformed value", () => {
394
- const response = new Response(null, {
395
- headers: { "retry-after-ms": "not-a-number" },
393
+ it("returns null for malformed value", () => {
394
+ const response = new Response(null, {
395
+ headers: { "retry-after-ms": "not-a-number" },
396
+ });
397
+ expect(parseRetryAfterMsHeader(response)).toBeNull();
396
398
  });
397
- expect(parseRetryAfterMsHeader(response)).toBeNull();
398
- });
399
399
 
400
- it("returns null for zero", () => {
401
- const response = new Response(null, {
402
- headers: { "retry-after-ms": "0" },
400
+ it("returns null for zero", () => {
401
+ const response = new Response(null, {
402
+ headers: { "retry-after-ms": "0" },
403
+ });
404
+ expect(parseRetryAfterMsHeader(response)).toBeNull();
403
405
  });
404
- expect(parseRetryAfterMsHeader(response)).toBeNull();
405
- });
406
406
 
407
- it("returns null for negative value", () => {
408
- const response = new Response(null, {
409
- headers: { "retry-after-ms": "-500" },
407
+ it("returns null for negative value", () => {
408
+ const response = new Response(null, {
409
+ headers: { "retry-after-ms": "-500" },
410
+ });
411
+ expect(parseRetryAfterMsHeader(response)).toBeNull();
410
412
  });
411
- expect(parseRetryAfterMsHeader(response)).toBeNull();
412
- });
413
413
  });
414
414
 
415
415
  // ---------------------------------------------------------------------------
@@ -417,43 +417,43 @@ describe("parseRetryAfterMsHeader", () => {
417
417
  // ---------------------------------------------------------------------------
418
418
 
419
419
  describe("parseShouldRetryHeader", () => {
420
- it("returns null when header not present", () => {
421
- const response = new Response(null, { headers: {} });
422
- expect(parseShouldRetryHeader(response)).toBeNull();
423
- });
420
+ it("returns null when header not present", () => {
421
+ const response = new Response(null, { headers: {} });
422
+ expect(parseShouldRetryHeader(response)).toBeNull();
423
+ });
424
424
 
425
- it("returns true for 'true'", () => {
426
- const response = new Response(null, {
427
- headers: { "x-should-retry": "true" },
425
+ it("returns true for 'true'", () => {
426
+ const response = new Response(null, {
427
+ headers: { "x-should-retry": "true" },
428
+ });
429
+ expect(parseShouldRetryHeader(response)).toBe(true);
428
430
  });
429
- expect(parseShouldRetryHeader(response)).toBe(true);
430
- });
431
431
 
432
- it("returns false for 'false'", () => {
433
- const response = new Response(null, {
434
- headers: { "x-should-retry": "false" },
432
+ it("returns false for 'false'", () => {
433
+ const response = new Response(null, {
434
+ headers: { "x-should-retry": "false" },
435
+ });
436
+ expect(parseShouldRetryHeader(response)).toBe(false);
435
437
  });
436
- expect(parseShouldRetryHeader(response)).toBe(false);
437
- });
438
438
 
439
- it("returns null for unrecognized value", () => {
440
- const response = new Response(null, {
441
- headers: { "x-should-retry": "maybe" },
439
+ it("returns null for unrecognized value", () => {
440
+ const response = new Response(null, {
441
+ headers: { "x-should-retry": "maybe" },
442
+ });
443
+ expect(parseShouldRetryHeader(response)).toBeNull();
442
444
  });
443
- expect(parseShouldRetryHeader(response)).toBeNull();
444
- });
445
445
 
446
- it("returns null for uppercase 'TRUE'", () => {
447
- const response = new Response(null, {
448
- headers: { "x-should-retry": "TRUE" },
446
+ it("returns null for uppercase 'TRUE'", () => {
447
+ const response = new Response(null, {
448
+ headers: { "x-should-retry": "TRUE" },
449
+ });
450
+ expect(parseShouldRetryHeader(response)).toBeNull();
449
451
  });
450
- expect(parseShouldRetryHeader(response)).toBeNull();
451
- });
452
452
 
453
- it("returns null for '1'", () => {
454
- const response = new Response(null, {
455
- headers: { "x-should-retry": "1" },
453
+ it("returns null for '1'", () => {
454
+ const response = new Response(null, {
455
+ headers: { "x-should-retry": "1" },
456
+ });
457
+ expect(parseShouldRetryHeader(response)).toBeNull();
456
458
  });
457
- expect(parseShouldRetryHeader(response)).toBeNull();
458
- });
459
459
  });