@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.
- package/README.md +88 -88
- package/dist/opencode-anthropic-auth-cli.mjs +804 -507
- package/dist/opencode-anthropic-auth-plugin.js +4751 -4109
- package/package.json +67 -59
- package/src/__tests__/billing-edge-cases.test.ts +59 -59
- package/src/__tests__/bun-proxy.parallel.test.ts +388 -382
- package/src/__tests__/cc-comparison.test.ts +87 -87
- package/src/__tests__/cc-credentials.test.ts +254 -250
- package/src/__tests__/cch-drift-checker.test.ts +51 -51
- package/src/__tests__/cch-native-style.test.ts +56 -56
- package/src/__tests__/debug-gating.test.ts +42 -42
- package/src/__tests__/decomposition-smoke.test.ts +68 -68
- package/src/__tests__/fingerprint-regression.test.ts +575 -566
- package/src/__tests__/helpers/conversation-history.smoke.test.ts +271 -271
- package/src/__tests__/helpers/conversation-history.ts +119 -119
- package/src/__tests__/helpers/deferred.smoke.test.ts +103 -103
- package/src/__tests__/helpers/deferred.ts +69 -69
- package/src/__tests__/helpers/in-memory-storage.smoke.test.ts +155 -155
- package/src/__tests__/helpers/in-memory-storage.ts +88 -88
- package/src/__tests__/helpers/mock-bun-proxy.smoke.test.ts +68 -68
- package/src/__tests__/helpers/mock-bun-proxy.ts +189 -189
- package/src/__tests__/helpers/plugin-fetch-harness.smoke.test.ts +273 -273
- package/src/__tests__/helpers/plugin-fetch-harness.ts +288 -288
- package/src/__tests__/helpers/sse.smoke.test.ts +236 -236
- package/src/__tests__/helpers/sse.ts +209 -209
- package/src/__tests__/index.parallel.test.ts +605 -595
- package/src/__tests__/sanitization-regex.test.ts +112 -112
- package/src/__tests__/state-bounds.test.ts +90 -90
- package/src/account-identity.test.ts +197 -192
- package/src/account-identity.ts +69 -67
- package/src/account-state.test.ts +86 -86
- package/src/account-state.ts +25 -25
- package/src/accounts/matching.test.ts +335 -0
- package/src/accounts/matching.ts +167 -0
- package/src/accounts/persistence.test.ts +345 -0
- package/src/accounts/persistence.ts +432 -0
- package/src/accounts/repair.test.ts +276 -0
- package/src/accounts/repair.ts +407 -0
- package/src/accounts.dedup.test.ts +621 -621
- package/src/accounts.test.ts +933 -929
- package/src/accounts.ts +633 -989
- package/src/backoff.test.ts +345 -345
- package/src/backoff.ts +219 -219
- package/src/betas.ts +124 -124
- package/src/bun-fetch.test.ts +345 -342
- package/src/bun-fetch.ts +424 -424
- package/src/bun-proxy.test.ts +25 -25
- package/src/bun-proxy.ts +209 -209
- package/src/cc-credentials.ts +111 -111
- package/src/circuit-breaker.test.ts +184 -184
- package/src/circuit-breaker.ts +169 -169
- package/src/cli/commands/auth.ts +963 -0
- package/src/cli/commands/config.ts +547 -0
- package/src/cli/formatting.test.ts +406 -0
- package/src/cli/formatting.ts +219 -0
- package/src/cli.ts +255 -2022
- package/src/commands/handlers/betas.ts +100 -0
- package/src/commands/handlers/config.ts +99 -0
- package/src/commands/handlers/files.ts +375 -0
- package/src/commands/oauth-flow.ts +181 -166
- package/src/commands/prompts.ts +61 -61
- package/src/commands/router.test.ts +421 -0
- package/src/commands/router.ts +143 -635
- package/src/config.test.ts +482 -482
- package/src/config.ts +412 -404
- package/src/constants.ts +48 -48
- package/src/drift/cch-constants.ts +95 -95
- package/src/env.ts +111 -105
- package/src/headers/billing.ts +33 -33
- package/src/headers/builder.ts +130 -130
- package/src/headers/cch.ts +75 -75
- package/src/headers/stainless.ts +25 -25
- package/src/headers/user-agent.ts +23 -23
- package/src/index.ts +436 -828
- package/src/models.ts +27 -27
- package/src/oauth.test.ts +102 -102
- package/src/oauth.ts +178 -178
- package/src/parent-pid-watcher.test.ts +148 -148
- package/src/parent-pid-watcher.ts +69 -69
- package/src/plugin-helpers.ts +82 -82
- package/src/refresh-helpers.ts +145 -139
- package/src/refresh-lock.test.ts +94 -94
- package/src/refresh-lock.ts +93 -93
- package/src/request/body.history.test.ts +579 -571
- package/src/request/body.ts +255 -255
- package/src/request/metadata.ts +65 -65
- package/src/request/retry.test.ts +156 -156
- package/src/request/retry.ts +67 -67
- package/src/request/url.ts +21 -21
- package/src/request-orchestration-helpers.ts +648 -0
- package/src/response/index.ts +5 -5
- package/src/response/mcp.ts +58 -58
- package/src/response/streaming.test.ts +313 -311
- package/src/response/streaming.ts +412 -410
- package/src/rotation.test.ts +304 -301
- package/src/rotation.ts +205 -205
- package/src/storage.test.ts +547 -547
- package/src/storage.ts +315 -291
- package/src/system-prompt/builder.ts +38 -38
- package/src/system-prompt/index.ts +5 -5
- package/src/system-prompt/normalize.ts +60 -60
- package/src/system-prompt/sanitize.ts +30 -30
- package/src/thinking.ts +21 -20
- package/src/token-refresh.test.ts +265 -265
- package/src/token-refresh.ts +219 -214
- package/src/types.ts +30 -30
- package/dist/bun-proxy.mjs +0 -291
package/src/backoff.ts
CHANGED
|
@@ -7,22 +7,22 @@ const MIN_BACKOFF_MS = 2_000;
|
|
|
7
7
|
const RETRIABLE_NETWORK_ERROR_CODES = new Set(["ECONNRESET", "ECONNREFUSED", "EPIPE", "ETIMEDOUT", "UND_ERR_SOCKET"]);
|
|
8
8
|
const NON_RETRIABLE_ERROR_NAMES = new Set(["AbortError", "TimeoutError", "APIUserAbortError"]);
|
|
9
9
|
const RETRIABLE_NETWORK_ERROR_MESSAGES = [
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
10
|
+
"bun proxy upstream error",
|
|
11
|
+
"connection reset by peer",
|
|
12
|
+
"connection reset by server",
|
|
13
|
+
"econnreset",
|
|
14
|
+
"econnrefused",
|
|
15
|
+
"epipe",
|
|
16
|
+
"etimedout",
|
|
17
|
+
"fetch failed",
|
|
18
|
+
"network connection lost",
|
|
19
|
+
"socket hang up",
|
|
20
|
+
"und_err_socket",
|
|
21
21
|
];
|
|
22
22
|
|
|
23
23
|
interface ErrorWithCode extends Error {
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
code?: string;
|
|
25
|
+
cause?: unknown;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/**
|
|
@@ -30,23 +30,23 @@ interface ErrorWithCode extends Error {
|
|
|
30
30
|
* Supports both seconds (integer) and HTTP-date formats.
|
|
31
31
|
*/
|
|
32
32
|
export function parseRetryAfterHeader(response: Response): number | null {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
33
|
+
const header = response.headers.get("retry-after");
|
|
34
|
+
if (!header) return null;
|
|
35
|
+
|
|
36
|
+
// Try as integer (seconds)
|
|
37
|
+
const seconds = parseInt(header, 10);
|
|
38
|
+
if (!isNaN(seconds) && seconds > 0) {
|
|
39
|
+
return seconds * 1000;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Try as HTTP-date
|
|
43
|
+
const date = new Date(header);
|
|
44
|
+
if (!isNaN(date.getTime())) {
|
|
45
|
+
const ms = date.getTime() - Date.now();
|
|
46
|
+
return ms > 0 ? ms : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return null;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
/**
|
|
@@ -54,11 +54,11 @@ export function parseRetryAfterHeader(response: Response): number | null {
|
|
|
54
54
|
* Returns the value in milliseconds, rounded to nearest integer.
|
|
55
55
|
*/
|
|
56
56
|
export function parseRetryAfterMsHeader(response: Response): number | null {
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
const header = response.headers.get("retry-after-ms");
|
|
58
|
+
if (!header) return null;
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
const ms = parseFloat(header);
|
|
61
|
+
return !isNaN(ms) && ms > 0 ? Math.round(ms) : null;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
/**
|
|
@@ -66,153 +66,153 @@ export function parseRetryAfterMsHeader(response: Response): number | null {
|
|
|
66
66
|
* Returns true for "true", false for "false", null for absent or unrecognized.
|
|
67
67
|
*/
|
|
68
68
|
export function parseShouldRetryHeader(response: Response): boolean | null {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
const header = response.headers.get("x-should-retry");
|
|
70
|
+
if (header === "true") return true;
|
|
71
|
+
if (header === "false") return false;
|
|
72
|
+
return null; // not present or unrecognized
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
interface ErrorSignals {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
errorType: string;
|
|
77
|
+
message: string;
|
|
78
|
+
text: string;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
function extractErrorSignals(body: string | object | null | undefined): ErrorSignals {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
let errorType = "";
|
|
83
|
+
let message = "";
|
|
84
|
+
let text = "";
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (typeof body === "string") {
|
|
91
|
-
text = body.toLowerCase();
|
|
92
|
-
try {
|
|
93
|
-
const parsed = JSON.parse(body);
|
|
94
|
-
errorType = String(parsed?.error?.type || "").toLowerCase();
|
|
95
|
-
message = String(parsed?.error?.message || "").toLowerCase();
|
|
96
|
-
} catch {
|
|
97
|
-
// Not JSON — use raw text only.
|
|
86
|
+
if (body == null) {
|
|
87
|
+
return { errorType, message, text };
|
|
98
88
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
text = "";
|
|
89
|
+
|
|
90
|
+
if (typeof body === "string") {
|
|
91
|
+
text = body.toLowerCase();
|
|
92
|
+
try {
|
|
93
|
+
const parsed = JSON.parse(body);
|
|
94
|
+
errorType = String(parsed?.error?.type || "").toLowerCase();
|
|
95
|
+
message = String(parsed?.error?.message || "").toLowerCase();
|
|
96
|
+
} catch {
|
|
97
|
+
// Not JSON — use raw text only.
|
|
98
|
+
}
|
|
99
|
+
return { errorType, message, text };
|
|
111
100
|
}
|
|
112
|
-
}
|
|
113
101
|
|
|
114
|
-
|
|
102
|
+
if (typeof body === "object") {
|
|
103
|
+
const b = body as Record<string, unknown>;
|
|
104
|
+
const err = b.error as Record<string, unknown> | undefined;
|
|
105
|
+
errorType = String(err?.type || "").toLowerCase();
|
|
106
|
+
message = String(err?.message || "").toLowerCase();
|
|
107
|
+
try {
|
|
108
|
+
text = JSON.stringify(body).toLowerCase();
|
|
109
|
+
} catch {
|
|
110
|
+
text = "";
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { errorType, message, text };
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
function bodyHasAccountError(body: string | object | null | undefined): boolean {
|
|
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
|
-
|
|
118
|
+
const { errorType, message, text } = extractErrorSignals(body);
|
|
119
|
+
|
|
120
|
+
const typeSignals = [
|
|
121
|
+
"rate_limit",
|
|
122
|
+
"quota",
|
|
123
|
+
"billing",
|
|
124
|
+
"permission",
|
|
125
|
+
"authentication",
|
|
126
|
+
"invalid_api_key",
|
|
127
|
+
"insufficient_permissions",
|
|
128
|
+
"invalid_grant",
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
const messageSignals = [
|
|
132
|
+
"rate limit",
|
|
133
|
+
"would exceed",
|
|
134
|
+
"quota",
|
|
135
|
+
"exhausted",
|
|
136
|
+
"credit balance",
|
|
137
|
+
"billing",
|
|
138
|
+
"permission",
|
|
139
|
+
"forbidden",
|
|
140
|
+
"unauthorized",
|
|
141
|
+
"authentication",
|
|
142
|
+
"not authorized",
|
|
143
|
+
// Anthropic returns "We're unable to verify your membership benefits" on 403 when access token is stale
|
|
144
|
+
"membership",
|
|
145
|
+
"unable to verify",
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
typeSignals.some((signal) => errorType.includes(signal)) ||
|
|
150
|
+
messageSignals.some((signal) => message.includes(signal)) ||
|
|
151
|
+
messageSignals.some((signal) => text.includes(signal))
|
|
152
|
+
);
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
function collectErrorChain(error: unknown): ErrorWithCode[] {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
156
|
+
const queue: unknown[] = [error];
|
|
157
|
+
const visited = new Set<unknown>();
|
|
158
|
+
const chain: ErrorWithCode[] = [];
|
|
159
|
+
|
|
160
|
+
while (queue.length > 0) {
|
|
161
|
+
const candidate = queue.shift();
|
|
162
|
+
if (candidate == null || visited.has(candidate)) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
visited.add(candidate);
|
|
167
|
+
|
|
168
|
+
if (candidate instanceof Error) {
|
|
169
|
+
const typedCandidate = candidate as ErrorWithCode;
|
|
170
|
+
chain.push(typedCandidate);
|
|
171
|
+
if (typedCandidate.cause !== undefined) {
|
|
172
|
+
queue.push(typedCandidate.cause);
|
|
173
|
+
}
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (typeof candidate === "object" && "cause" in candidate) {
|
|
178
|
+
queue.push((candidate as { cause?: unknown }).cause);
|
|
179
|
+
}
|
|
179
180
|
}
|
|
180
|
-
}
|
|
181
181
|
|
|
182
|
-
|
|
182
|
+
return chain;
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
/**
|
|
186
186
|
* Check whether an error represents a transient transport/network failure.
|
|
187
187
|
*/
|
|
188
188
|
export function isRetriableNetworkError(error: unknown): boolean {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const chain = collectErrorChain(error);
|
|
195
|
-
if (chain.length === 0) {
|
|
196
|
-
return false;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
for (const candidate of chain) {
|
|
200
|
-
if (NON_RETRIABLE_ERROR_NAMES.has(candidate.name)) {
|
|
201
|
-
return false;
|
|
189
|
+
if (typeof error === "string") {
|
|
190
|
+
const text = error.toLowerCase();
|
|
191
|
+
return RETRIABLE_NETWORK_ERROR_MESSAGES.some((signal) => text.includes(signal));
|
|
202
192
|
}
|
|
203
193
|
|
|
204
|
-
const
|
|
205
|
-
if (
|
|
206
|
-
|
|
194
|
+
const chain = collectErrorChain(error);
|
|
195
|
+
if (chain.length === 0) {
|
|
196
|
+
return false;
|
|
207
197
|
}
|
|
208
198
|
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
199
|
+
for (const candidate of chain) {
|
|
200
|
+
if (NON_RETRIABLE_ERROR_NAMES.has(candidate.name)) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const code = candidate.code?.toUpperCase();
|
|
205
|
+
if (code && RETRIABLE_NETWORK_ERROR_CODES.has(code)) {
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const message = candidate.message.toLowerCase();
|
|
210
|
+
if (RETRIABLE_NETWORK_ERROR_MESSAGES.some((signal) => message.includes(signal))) {
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
212
213
|
}
|
|
213
|
-
}
|
|
214
214
|
|
|
215
|
-
|
|
215
|
+
return false;
|
|
216
216
|
}
|
|
217
217
|
|
|
218
218
|
/**
|
|
@@ -220,89 +220,89 @@ export function isRetriableNetworkError(error: unknown): boolean {
|
|
|
220
220
|
* that would benefit from switching to a different account.
|
|
221
221
|
*/
|
|
222
222
|
export function isAccountSpecificError(status: number, body?: string | object | null): boolean {
|
|
223
|
-
|
|
224
|
-
|
|
223
|
+
// 429 is always account-specific (per-account rate limits)
|
|
224
|
+
if (status === 429) return true;
|
|
225
225
|
|
|
226
|
-
|
|
227
|
-
|
|
226
|
+
// 401 is always account-specific (per-account auth)
|
|
227
|
+
if (status === 401) return true;
|
|
228
228
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
229
|
+
// 400/403 are account-specific only if the body contains relevant language
|
|
230
|
+
if ((status === 400 || status === 403) && body) {
|
|
231
|
+
return bodyHasAccountError(body);
|
|
232
|
+
}
|
|
233
233
|
|
|
234
|
-
|
|
235
|
-
|
|
234
|
+
// Everything else (529, 503, 500, etc.) is service-wide
|
|
235
|
+
return false;
|
|
236
236
|
}
|
|
237
237
|
|
|
238
238
|
/**
|
|
239
239
|
* Parse the rate limit reason from an HTTP status and response body.
|
|
240
240
|
*/
|
|
241
241
|
export function parseRateLimitReason(status: number, body?: string | object | null): RateLimitReason {
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
242
|
+
const { errorType, message, text } = extractErrorSignals(body);
|
|
243
|
+
|
|
244
|
+
const authSignals = [
|
|
245
|
+
"authentication",
|
|
246
|
+
"invalid_api_key",
|
|
247
|
+
"invalid api key",
|
|
248
|
+
"invalid_grant",
|
|
249
|
+
"unauthorized",
|
|
250
|
+
"invalid access token",
|
|
251
|
+
"expired token",
|
|
252
|
+
"membership",
|
|
253
|
+
];
|
|
254
|
+
|
|
255
|
+
const isAuthFailure =
|
|
256
|
+
status === 401 ||
|
|
257
|
+
authSignals.some((signal) => errorType.includes(signal)) ||
|
|
258
|
+
authSignals.some((signal) => message.includes(signal)) ||
|
|
259
|
+
authSignals.some((signal) => text.includes(signal));
|
|
260
|
+
|
|
261
|
+
if (isAuthFailure) {
|
|
262
|
+
return "AUTH_FAILED";
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (
|
|
266
|
+
errorType.includes("quota") ||
|
|
267
|
+
errorType.includes("billing") ||
|
|
268
|
+
errorType.includes("permission") ||
|
|
269
|
+
errorType.includes("insufficient_permissions") ||
|
|
270
|
+
message.includes("quota") ||
|
|
271
|
+
message.includes("exhausted") ||
|
|
272
|
+
message.includes("credit balance") ||
|
|
273
|
+
message.includes("billing") ||
|
|
274
|
+
message.includes("permission") ||
|
|
275
|
+
message.includes("forbidden") ||
|
|
276
|
+
text.includes("permission")
|
|
277
|
+
) {
|
|
278
|
+
return "QUOTA_EXHAUSTED";
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return "RATE_LIMIT_EXCEEDED";
|
|
282
282
|
}
|
|
283
283
|
|
|
284
284
|
/**
|
|
285
285
|
* Calculate backoff duration in milliseconds.
|
|
286
286
|
*/
|
|
287
287
|
export function calculateBackoffMs(
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
288
|
+
reason: RateLimitReason,
|
|
289
|
+
consecutiveFailures: number,
|
|
290
|
+
retryAfterMs?: number | null,
|
|
291
291
|
): number {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
292
|
+
// Retry-After header takes precedence
|
|
293
|
+
if (retryAfterMs && retryAfterMs > 0) {
|
|
294
|
+
return Math.max(retryAfterMs, MIN_BACKOFF_MS);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
switch (reason) {
|
|
298
|
+
case "AUTH_FAILED":
|
|
299
|
+
return AUTH_FAILED_BACKOFF;
|
|
300
|
+
case "QUOTA_EXHAUSTED": {
|
|
301
|
+
const index = Math.min(consecutiveFailures, QUOTA_EXHAUSTED_BACKOFFS.length - 1);
|
|
302
|
+
return QUOTA_EXHAUSTED_BACKOFFS[index]!;
|
|
303
|
+
}
|
|
304
|
+
case "RATE_LIMIT_EXCEEDED":
|
|
305
|
+
default:
|
|
306
|
+
return RATE_LIMIT_EXCEEDED_BACKOFF;
|
|
303
307
|
}
|
|
304
|
-
case "RATE_LIMIT_EXCEEDED":
|
|
305
|
-
default:
|
|
306
|
-
return RATE_LIMIT_EXCEEDED_BACKOFF;
|
|
307
|
-
}
|
|
308
308
|
}
|