@cortexkit/opencode-anthropic-auth 1.0.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.
- package/LICENSE +21 -0
- package/README.md +500 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +260 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +665 -0
- package/dist/prompt-context.d.ts +18 -0
- package/dist/prompt-context.js +95 -0
- package/dist/transform.d.ts +77 -0
- package/dist/transform.js +450 -0
- package/package.json +45 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
import { authorize, buildClaudeQuotaSummary, buildFallbackQuotaSummaries, CACHE_1H_COMMAND_NAME, CLAUDE_DUMP_COMMAND_NAME, CLAUDE_QUOTAS_COMMAND_NAME, CLIENT_ID, exchange, executeCache1hCommand, executeDumpCommand, FallbackAccountManager, fetchOAuthQuotaSnapshot, getCache1hMode, getCache1hPersistentMode, getQuotaCheckIntervalMs, getQuotaNextRefreshAt, getRelayConfig, isCache1hEnabled, isCache1hPersistentlyEnabled, isDumpPersistentlyEnabled, loadAccounts, log, parseCache1hCommandAction, parseDumpCommandAction, quotaSnapshotPassesPolicy, sendViaRelay, setCache1hPersistentEnabled, setCache1hPersistentMode, setCache1hState, setDumpEnabled, setDumpPersistentEnabled, shouldFallbackStatus, TOKEN_URL, } from '@cortexkit/anthropic-auth-core';
|
|
2
|
+
import { resolvePromptContext } from "./prompt-context.js";
|
|
3
|
+
import { createStrippedStream, isInsecure, mergeHeaders, rewriteRequestBody, rewriteUrl, setOAuthHeaders, } from "./transform.js";
|
|
4
|
+
const HANDLED_SENTINEL = '__OPENCODE_ANTHROPIC_AUTH_COMMAND_HANDLED__';
|
|
5
|
+
let nextPerfRequestId = 1;
|
|
6
|
+
let eventLoopLagMonitorStarted = false;
|
|
7
|
+
function nowMs() {
|
|
8
|
+
return performance.now();
|
|
9
|
+
}
|
|
10
|
+
function roundMs(value) {
|
|
11
|
+
return Math.round(value * 10) / 10;
|
|
12
|
+
}
|
|
13
|
+
function startEventLoopLagMonitor() {
|
|
14
|
+
if (eventLoopLagMonitorStarted || process.env.NODE_ENV === 'test')
|
|
15
|
+
return;
|
|
16
|
+
eventLoopLagMonitorStarted = true;
|
|
17
|
+
const intervalMs = 100;
|
|
18
|
+
const thresholdMs = 250;
|
|
19
|
+
let expected = nowMs() + intervalMs;
|
|
20
|
+
setInterval(() => {
|
|
21
|
+
const current = nowMs();
|
|
22
|
+
const lag = current - expected;
|
|
23
|
+
expected = current + intervalMs;
|
|
24
|
+
if (lag < thresholdMs)
|
|
25
|
+
return;
|
|
26
|
+
log('[perf] opencode event_loop_lag', {
|
|
27
|
+
lagMs: roundMs(lag),
|
|
28
|
+
thresholdMs,
|
|
29
|
+
});
|
|
30
|
+
}, intervalMs).unref?.();
|
|
31
|
+
}
|
|
32
|
+
function createPerfTrace(data) {
|
|
33
|
+
const start = nowMs();
|
|
34
|
+
const trace = {
|
|
35
|
+
requestId: String(nextPerfRequestId++),
|
|
36
|
+
start,
|
|
37
|
+
last: start,
|
|
38
|
+
mark(stage, stageData) {
|
|
39
|
+
const current = nowMs();
|
|
40
|
+
log('[perf] opencode request stage', {
|
|
41
|
+
requestId: trace.requestId,
|
|
42
|
+
stage,
|
|
43
|
+
deltaMs: roundMs(current - trace.last),
|
|
44
|
+
totalMs: roundMs(current - trace.start),
|
|
45
|
+
...stageData,
|
|
46
|
+
});
|
|
47
|
+
trace.last = current;
|
|
48
|
+
},
|
|
49
|
+
done(stage, stageData) {
|
|
50
|
+
const current = nowMs();
|
|
51
|
+
log('[perf] opencode request done', {
|
|
52
|
+
requestId: trace.requestId,
|
|
53
|
+
stage,
|
|
54
|
+
deltaMs: roundMs(current - trace.last),
|
|
55
|
+
totalMs: roundMs(current - trace.start),
|
|
56
|
+
...stageData,
|
|
57
|
+
});
|
|
58
|
+
trace.last = current;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
log('[perf] opencode request start', { requestId: trace.requestId, ...data });
|
|
62
|
+
return trace;
|
|
63
|
+
}
|
|
64
|
+
async function sendIgnoredMessage(ctx, sessionId, text) {
|
|
65
|
+
const session = ctx.client.session;
|
|
66
|
+
const promptContext = await resolvePromptContext(ctx.client, sessionId);
|
|
67
|
+
const request = {
|
|
68
|
+
path: { id: sessionId },
|
|
69
|
+
body: {
|
|
70
|
+
noReply: true,
|
|
71
|
+
parts: [{ type: 'text', text, ignored: true }],
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
if (promptContext?.agent)
|
|
75
|
+
request.body.agent = promptContext.agent;
|
|
76
|
+
if (promptContext?.model)
|
|
77
|
+
request.body.model = promptContext.model;
|
|
78
|
+
if (promptContext?.variant)
|
|
79
|
+
request.body.variant = promptContext.variant;
|
|
80
|
+
if (typeof session?.promptAsync === 'function') {
|
|
81
|
+
await session.promptAsync(request);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (typeof session?.prompt === 'function') {
|
|
85
|
+
await Promise.resolve(session.prompt(request));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
throw new Error('OpenCode session prompt API is unavailable for ignored replies.');
|
|
89
|
+
}
|
|
90
|
+
function throwHandledSentinel() {
|
|
91
|
+
throw new Error(HANDLED_SENTINEL);
|
|
92
|
+
}
|
|
93
|
+
export const AnthropicAuthPlugin = async (ctx) => {
|
|
94
|
+
startEventLoopLagMonitor();
|
|
95
|
+
const { client } = ctx;
|
|
96
|
+
const fallbackManager = new FallbackAccountManager();
|
|
97
|
+
fallbackManager.startBackgroundRefresh();
|
|
98
|
+
const initialCache1hStorage = await loadAccounts();
|
|
99
|
+
const relayConfig = getRelayConfig(initialCache1hStorage);
|
|
100
|
+
setCache1hState({
|
|
101
|
+
enabled: isCache1hPersistentlyEnabled(initialCache1hStorage),
|
|
102
|
+
mode: getCache1hPersistentMode(initialCache1hStorage),
|
|
103
|
+
});
|
|
104
|
+
setDumpEnabled(isDumpPersistentlyEnabled(initialCache1hStorage));
|
|
105
|
+
let latestGetAuth = null;
|
|
106
|
+
async function buildQuotaCommandSummary() {
|
|
107
|
+
const accounts = [];
|
|
108
|
+
if (latestGetAuth) {
|
|
109
|
+
try {
|
|
110
|
+
const auth = await latestGetAuth();
|
|
111
|
+
if (auth.type === 'oauth' && auth.access) {
|
|
112
|
+
accounts.push({
|
|
113
|
+
name: 'OpenCode anthropic',
|
|
114
|
+
role: 'main',
|
|
115
|
+
quota: await fetchOAuthQuotaSnapshot({ accessToken: auth.access }),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
else if (auth.type === 'oauth') {
|
|
119
|
+
accounts.push({
|
|
120
|
+
name: 'OpenCode anthropic',
|
|
121
|
+
role: 'main',
|
|
122
|
+
error: 'missing access token; send a request first or reconnect auth',
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
accounts.push({
|
|
128
|
+
name: 'OpenCode anthropic',
|
|
129
|
+
role: 'main',
|
|
130
|
+
error: error instanceof Error ? error.message : String(error),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const { storage, errors } = await fallbackManager.refreshQuotaForAllAccounts();
|
|
135
|
+
accounts.push(...buildFallbackQuotaSummaries(storage, new Map(errors.map((error) => [error.accountId, error.message]))));
|
|
136
|
+
if (!latestGetAuth) {
|
|
137
|
+
accounts.unshift({
|
|
138
|
+
name: 'OpenCode anthropic',
|
|
139
|
+
role: 'main',
|
|
140
|
+
error: 'auth loader has not run yet; send a request first',
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return buildClaudeQuotaSummary({ accounts, refreshedAt: Date.now() });
|
|
144
|
+
}
|
|
145
|
+
async function executePersistentCache1hCommand(argumentsText) {
|
|
146
|
+
const action = parseCache1hCommandAction(argumentsText);
|
|
147
|
+
if (action.type === 'enable' || action.type === 'disable') {
|
|
148
|
+
const enabled = action.type === 'enable';
|
|
149
|
+
const storage = await setCache1hPersistentEnabled(enabled);
|
|
150
|
+
const mode = getCache1hPersistentMode(storage);
|
|
151
|
+
setCache1hState({ enabled, mode });
|
|
152
|
+
return executeCache1hCommand({ argumentsText, enabled, mode });
|
|
153
|
+
}
|
|
154
|
+
if (action.type === 'mode') {
|
|
155
|
+
const storage = await setCache1hPersistentMode(action.mode);
|
|
156
|
+
const enabled = isCache1hPersistentlyEnabled(storage);
|
|
157
|
+
setCache1hState({ enabled, mode: action.mode });
|
|
158
|
+
return executeCache1hCommand({
|
|
159
|
+
argumentsText,
|
|
160
|
+
enabled,
|
|
161
|
+
mode: action.mode,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
const storage = await loadAccounts();
|
|
165
|
+
const enabled = isCache1hPersistentlyEnabled(storage);
|
|
166
|
+
const mode = getCache1hPersistentMode(storage);
|
|
167
|
+
setCache1hState({ enabled, mode });
|
|
168
|
+
return executeCache1hCommand({ argumentsText, enabled, mode });
|
|
169
|
+
}
|
|
170
|
+
async function executePersistentDumpCommand(argumentsText) {
|
|
171
|
+
const action = parseDumpCommandAction(argumentsText);
|
|
172
|
+
if (action.type === 'enable' || action.type === 'disable') {
|
|
173
|
+
const enabled = action.type === 'enable';
|
|
174
|
+
await setDumpPersistentEnabled(enabled);
|
|
175
|
+
setDumpEnabled(enabled);
|
|
176
|
+
return executeDumpCommand({ argumentsText, enabled });
|
|
177
|
+
}
|
|
178
|
+
const storage = await loadAccounts();
|
|
179
|
+
const enabled = isDumpPersistentlyEnabled(storage);
|
|
180
|
+
setDumpEnabled(enabled);
|
|
181
|
+
return executeDumpCommand({ argumentsText, enabled });
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
config: async (config) => {
|
|
185
|
+
config.command = {
|
|
186
|
+
...(config.command ?? {}),
|
|
187
|
+
[CACHE_1H_COMMAND_NAME]: {
|
|
188
|
+
template: CACHE_1H_COMMAND_NAME,
|
|
189
|
+
description: 'Show or toggle 1-hour Anthropic ephemeral prompt cache TTL.',
|
|
190
|
+
},
|
|
191
|
+
[CLAUDE_QUOTAS_COMMAND_NAME]: {
|
|
192
|
+
template: CLAUDE_QUOTAS_COMMAND_NAME,
|
|
193
|
+
description: 'Show current Claude OAuth quota usage for all accounts.',
|
|
194
|
+
},
|
|
195
|
+
[CLAUDE_DUMP_COMMAND_NAME]: {
|
|
196
|
+
template: CLAUDE_DUMP_COMMAND_NAME,
|
|
197
|
+
description: 'Show or toggle Anthropic request dump capture for debugging.',
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
},
|
|
201
|
+
'command.execute.before': async (input) => {
|
|
202
|
+
if (input.command === CACHE_1H_COMMAND_NAME) {
|
|
203
|
+
await sendIgnoredMessage(ctx, input.sessionID, await executePersistentCache1hCommand(input.arguments));
|
|
204
|
+
throwHandledSentinel();
|
|
205
|
+
}
|
|
206
|
+
if (input.command === CLAUDE_QUOTAS_COMMAND_NAME) {
|
|
207
|
+
await sendIgnoredMessage(ctx, input.sessionID, await buildQuotaCommandSummary());
|
|
208
|
+
throwHandledSentinel();
|
|
209
|
+
}
|
|
210
|
+
if (input.command === CLAUDE_DUMP_COMMAND_NAME) {
|
|
211
|
+
await sendIgnoredMessage(ctx, input.sessionID, await executePersistentDumpCommand(input.arguments));
|
|
212
|
+
throwHandledSentinel();
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
auth: {
|
|
216
|
+
provider: 'anthropic',
|
|
217
|
+
async loader(getAuth, provider) {
|
|
218
|
+
latestGetAuth = getAuth;
|
|
219
|
+
const auth = await getAuth();
|
|
220
|
+
if (auth.type === 'oauth') {
|
|
221
|
+
// zero out cost for max plan
|
|
222
|
+
for (const model of Object.values(provider.models)) {
|
|
223
|
+
model.cost = {
|
|
224
|
+
input: 0,
|
|
225
|
+
output: 0,
|
|
226
|
+
cache: {
|
|
227
|
+
read: 0,
|
|
228
|
+
write: 0,
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
// Shared inflight refresh promise — prevents concurrent token refreshes
|
|
233
|
+
// from racing against each other (and causing 401 cascades with token rotation)
|
|
234
|
+
let refreshPromise = null;
|
|
235
|
+
let mainQuotaCache = null;
|
|
236
|
+
let mainQuotaRefreshPromise = null;
|
|
237
|
+
let mainQuotaRetryAfter = 0;
|
|
238
|
+
async function refreshMainAccessToken() {
|
|
239
|
+
if (!refreshPromise) {
|
|
240
|
+
refreshPromise = (async () => {
|
|
241
|
+
const maxRetries = 2;
|
|
242
|
+
const baseDelayMs = 500;
|
|
243
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
244
|
+
try {
|
|
245
|
+
if (attempt > 0) {
|
|
246
|
+
const delay = baseDelayMs * 2 ** (attempt - 1);
|
|
247
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
248
|
+
}
|
|
249
|
+
// Re-read auth to get the latest refresh token.
|
|
250
|
+
// The outer `auth` snapshot may be stale if tokens
|
|
251
|
+
// were rotated since the fetch() call was made.
|
|
252
|
+
const freshAuth = await getAuth();
|
|
253
|
+
const response = await fetch(TOKEN_URL, {
|
|
254
|
+
method: 'POST',
|
|
255
|
+
headers: {
|
|
256
|
+
'Content-Type': 'application/json',
|
|
257
|
+
Accept: 'application/json, text/plain, */*',
|
|
258
|
+
'User-Agent': 'axios/1.13.6',
|
|
259
|
+
},
|
|
260
|
+
body: JSON.stringify({
|
|
261
|
+
grant_type: 'refresh_token',
|
|
262
|
+
refresh_token: freshAuth.refresh,
|
|
263
|
+
client_id: CLIENT_ID,
|
|
264
|
+
}),
|
|
265
|
+
});
|
|
266
|
+
if (!response.ok) {
|
|
267
|
+
if (response.status >= 500 && attempt < maxRetries) {
|
|
268
|
+
await response.body?.cancel();
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const body = await response.text().catch(() => '');
|
|
272
|
+
throw new Error(`Token refresh failed: ${response.status} — ${body}`);
|
|
273
|
+
}
|
|
274
|
+
const json = (await response.json());
|
|
275
|
+
// biome-ignore lint/suspicious/noExplicitAny: SDK types don't expose auth.set
|
|
276
|
+
await client.auth.set({
|
|
277
|
+
path: {
|
|
278
|
+
id: 'anthropic',
|
|
279
|
+
},
|
|
280
|
+
body: {
|
|
281
|
+
type: 'oauth',
|
|
282
|
+
refresh: json.refresh_token,
|
|
283
|
+
access: json.access_token,
|
|
284
|
+
expires: Date.now() + json.expires_in * 1000,
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
return json.access_token;
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
const isNetworkError = error instanceof Error &&
|
|
291
|
+
(error.message.includes('fetch failed') ||
|
|
292
|
+
('code' in error &&
|
|
293
|
+
(error.code === 'ECONNRESET' ||
|
|
294
|
+
error.code === 'ECONNREFUSED' ||
|
|
295
|
+
error.code === 'ETIMEDOUT' ||
|
|
296
|
+
error.code === 'UND_ERR_CONNECT_TIMEOUT')));
|
|
297
|
+
if (attempt < maxRetries && isNetworkError) {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
throw error;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
// Unreachable — each iteration either returns or throws.
|
|
304
|
+
// Kept as a TypeScript exhaustiveness guard.
|
|
305
|
+
throw new Error('Token refresh exhausted all retries');
|
|
306
|
+
})().finally(() => {
|
|
307
|
+
refreshPromise = null;
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
return refreshPromise;
|
|
311
|
+
}
|
|
312
|
+
function isReplayableRequest(input, body) {
|
|
313
|
+
if (input instanceof Request && input.body)
|
|
314
|
+
return false;
|
|
315
|
+
return body == null || typeof body === 'string';
|
|
316
|
+
}
|
|
317
|
+
function isSubagentRequest(headers) {
|
|
318
|
+
return headers.has('x-parent-session-id');
|
|
319
|
+
}
|
|
320
|
+
function isStreamingRateLimitText(text) {
|
|
321
|
+
return (text.includes('rate_limit_error') ||
|
|
322
|
+
/exceed your account'?s rate limit/i.test(text));
|
|
323
|
+
}
|
|
324
|
+
function mainQuotaRoutingEnabled(storage) {
|
|
325
|
+
return storage?.quota?.enabled === true;
|
|
326
|
+
}
|
|
327
|
+
async function inspectStreamingRateLimit(response, trace) {
|
|
328
|
+
if (!response.body || response.status !== 200) {
|
|
329
|
+
trace?.mark('inspect_stream_skip', { status: response.status });
|
|
330
|
+
return { response, rateLimited: false };
|
|
331
|
+
}
|
|
332
|
+
if (response.headers.get('x-cortexkit-relay-optimistic') === 'true') {
|
|
333
|
+
trace?.mark('inspect_stream_skip', {
|
|
334
|
+
status: response.status,
|
|
335
|
+
reason: 'optimistic_relay',
|
|
336
|
+
});
|
|
337
|
+
return { response, rateLimited: false };
|
|
338
|
+
}
|
|
339
|
+
const start = nowMs();
|
|
340
|
+
const reader = response.body.getReader();
|
|
341
|
+
const chunks = [];
|
|
342
|
+
const decoder = new TextDecoder();
|
|
343
|
+
let text = '';
|
|
344
|
+
let bytes = 0;
|
|
345
|
+
while (!text.includes('\n\n') && text.length < 65_536) {
|
|
346
|
+
const { done, value } = await reader.read();
|
|
347
|
+
if (done)
|
|
348
|
+
break;
|
|
349
|
+
chunks.push(value);
|
|
350
|
+
bytes += value.byteLength;
|
|
351
|
+
text += decoder.decode(value, { stream: true });
|
|
352
|
+
if (isStreamingRateLimitText(text))
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
if (isStreamingRateLimitText(text)) {
|
|
356
|
+
await reader.cancel().catch(() => { });
|
|
357
|
+
trace?.mark('inspect_stream_first_event', {
|
|
358
|
+
ms: roundMs(nowMs() - start),
|
|
359
|
+
bytes,
|
|
360
|
+
rateLimited: true,
|
|
361
|
+
});
|
|
362
|
+
return { response, rateLimited: true };
|
|
363
|
+
}
|
|
364
|
+
const stream = new ReadableStream({
|
|
365
|
+
start(controller) {
|
|
366
|
+
for (const chunk of chunks)
|
|
367
|
+
controller.enqueue(chunk);
|
|
368
|
+
},
|
|
369
|
+
async pull(controller) {
|
|
370
|
+
const { done, value } = await reader.read();
|
|
371
|
+
if (done) {
|
|
372
|
+
controller.close();
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
controller.enqueue(value);
|
|
376
|
+
},
|
|
377
|
+
cancel(reason) {
|
|
378
|
+
return reader.cancel(reason);
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
trace?.mark('inspect_stream_first_event', {
|
|
382
|
+
ms: roundMs(nowMs() - start),
|
|
383
|
+
bytes,
|
|
384
|
+
rateLimited: false,
|
|
385
|
+
});
|
|
386
|
+
return {
|
|
387
|
+
response: new Response(stream, {
|
|
388
|
+
status: response.status,
|
|
389
|
+
statusText: response.statusText,
|
|
390
|
+
headers: response.headers,
|
|
391
|
+
}),
|
|
392
|
+
rateLimited: false,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
async function sendWithAccessToken(input, init, accessToken, trace, route = 'unknown') {
|
|
396
|
+
const start = nowMs();
|
|
397
|
+
const requestHeaders = mergeHeaders(input, init);
|
|
398
|
+
const subagentRequest = isSubagentRequest(requestHeaders);
|
|
399
|
+
requestHeaders.delete('x-parent-session-id');
|
|
400
|
+
setOAuthHeaders(requestHeaders, accessToken);
|
|
401
|
+
let body = init?.body;
|
|
402
|
+
const originalBytes = typeof body === 'string' ? body.length : undefined;
|
|
403
|
+
if (body && typeof body === 'string') {
|
|
404
|
+
const rewriteStart = nowMs();
|
|
405
|
+
body = await rewriteRequestBody(body, {
|
|
406
|
+
cache1hEnabled: !subagentRequest && isCache1hEnabled(),
|
|
407
|
+
cache1hMode: getCache1hMode(),
|
|
408
|
+
});
|
|
409
|
+
trace?.mark('rewrite_body', {
|
|
410
|
+
route,
|
|
411
|
+
ms: roundMs(nowMs() - rewriteStart),
|
|
412
|
+
originalBytes,
|
|
413
|
+
rewrittenBytes: body.length,
|
|
414
|
+
cacheEnabled: !subagentRequest && isCache1hEnabled(),
|
|
415
|
+
cacheMode: getCache1hMode(),
|
|
416
|
+
subagent: subagentRequest,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
const rewritten = rewriteUrl(input);
|
|
420
|
+
const directFetch = () => fetch(rewritten.input, {
|
|
421
|
+
...init,
|
|
422
|
+
body,
|
|
423
|
+
headers: requestHeaders,
|
|
424
|
+
...(isInsecure() && { tls: { rejectUnauthorized: false } }),
|
|
425
|
+
});
|
|
426
|
+
const sendStart = nowMs();
|
|
427
|
+
const response = await sendViaRelay({
|
|
428
|
+
config: relayConfig,
|
|
429
|
+
input: rewritten.input,
|
|
430
|
+
init,
|
|
431
|
+
headers: requestHeaders,
|
|
432
|
+
body,
|
|
433
|
+
fallback: directFetch,
|
|
434
|
+
optimisticResponse: relayConfig?.transport === 'websocket',
|
|
435
|
+
});
|
|
436
|
+
trace?.mark('send_headers_received', {
|
|
437
|
+
route,
|
|
438
|
+
ms: roundMs(nowMs() - sendStart),
|
|
439
|
+
status: response.status,
|
|
440
|
+
relayConfigured: relayConfig != null,
|
|
441
|
+
totalSendWithAccessMs: roundMs(nowMs() - start),
|
|
442
|
+
});
|
|
443
|
+
return response;
|
|
444
|
+
}
|
|
445
|
+
async function refreshMainQuotaCache(accessToken, storage) {
|
|
446
|
+
const now = Date.now();
|
|
447
|
+
const quota = await fetchOAuthQuotaSnapshot({ accessToken });
|
|
448
|
+
mainQuotaCache = {
|
|
449
|
+
accessToken,
|
|
450
|
+
refreshAfter: getQuotaNextRefreshAt(quota, storage, now),
|
|
451
|
+
quota,
|
|
452
|
+
};
|
|
453
|
+
return quota;
|
|
454
|
+
}
|
|
455
|
+
function refreshMainQuotaCacheInBackground(accessToken, storage) {
|
|
456
|
+
const now = Date.now();
|
|
457
|
+
if (mainQuotaRefreshPromise || now < mainQuotaRetryAfter)
|
|
458
|
+
return;
|
|
459
|
+
mainQuotaRefreshPromise = refreshMainQuotaCache(accessToken, storage)
|
|
460
|
+
.catch((error) => {
|
|
461
|
+
mainQuotaRetryAfter = now + getQuotaCheckIntervalMs(storage);
|
|
462
|
+
throw error;
|
|
463
|
+
})
|
|
464
|
+
.finally(() => {
|
|
465
|
+
mainQuotaRefreshPromise = null;
|
|
466
|
+
});
|
|
467
|
+
void mainQuotaRefreshPromise.catch(() => { });
|
|
468
|
+
}
|
|
469
|
+
async function getMainQuotaForRouting(accessToken, storage) {
|
|
470
|
+
const now = Date.now();
|
|
471
|
+
if (mainQuotaCache?.accessToken !== accessToken) {
|
|
472
|
+
return await refreshMainQuotaCache(accessToken, storage);
|
|
473
|
+
}
|
|
474
|
+
if (now >= mainQuotaCache.refreshAfter) {
|
|
475
|
+
refreshMainQuotaCacheInBackground(accessToken, storage);
|
|
476
|
+
}
|
|
477
|
+
return mainQuotaCache.quota;
|
|
478
|
+
}
|
|
479
|
+
async function tryUsableFallbackAccounts(input, init, accounts, storage, currentResponse, trace) {
|
|
480
|
+
if (!accounts.length)
|
|
481
|
+
return currentResponse ?? null;
|
|
482
|
+
await currentResponse?.body?.cancel().catch(() => { });
|
|
483
|
+
let lastResponse = currentResponse ?? null;
|
|
484
|
+
for (const [index, account] of accounts.entries()) {
|
|
485
|
+
const access = account.access;
|
|
486
|
+
if (!access)
|
|
487
|
+
continue;
|
|
488
|
+
let response = await sendWithAccessToken(input, init, access, trace, `fallback_${index}`);
|
|
489
|
+
lastResponse = response;
|
|
490
|
+
let fallbackAgain = shouldFallbackStatus(response.status, storage);
|
|
491
|
+
if (!fallbackAgain) {
|
|
492
|
+
const inspected = await inspectStreamingRateLimit(response, trace);
|
|
493
|
+
response = inspected.response;
|
|
494
|
+
lastResponse = response;
|
|
495
|
+
fallbackAgain = inspected.rateLimited;
|
|
496
|
+
}
|
|
497
|
+
if (!fallbackAgain) {
|
|
498
|
+
await fallbackManager.markUsed(account);
|
|
499
|
+
return response;
|
|
500
|
+
}
|
|
501
|
+
if (index < accounts.length - 1) {
|
|
502
|
+
await response.body?.cancel().catch(() => { });
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return lastResponse;
|
|
506
|
+
}
|
|
507
|
+
async function tryFallbackAccounts(input, init, mainResponse, preselectedAccounts, trace) {
|
|
508
|
+
if (!isReplayableRequest(input, init?.body))
|
|
509
|
+
return mainResponse;
|
|
510
|
+
const loadStart = nowMs();
|
|
511
|
+
const storage = await loadAccounts();
|
|
512
|
+
trace?.mark('fallback_load_storage', {
|
|
513
|
+
ms: roundMs(nowMs() - loadStart),
|
|
514
|
+
});
|
|
515
|
+
let currentResponse = mainResponse;
|
|
516
|
+
let shouldFallback = shouldFallbackStatus(currentResponse.status, storage);
|
|
517
|
+
if (!shouldFallback) {
|
|
518
|
+
const inspected = await inspectStreamingRateLimit(currentResponse, trace);
|
|
519
|
+
currentResponse = inspected.response;
|
|
520
|
+
shouldFallback = inspected.rateLimited;
|
|
521
|
+
}
|
|
522
|
+
if (!shouldFallback) {
|
|
523
|
+
return currentResponse;
|
|
524
|
+
}
|
|
525
|
+
let accounts = preselectedAccounts;
|
|
526
|
+
if (!accounts) {
|
|
527
|
+
const accountsStart = nowMs();
|
|
528
|
+
accounts = await fallbackManager.getUsableFallbackAccounts();
|
|
529
|
+
trace?.mark('fallback_get_accounts', {
|
|
530
|
+
ms: roundMs(nowMs() - accountsStart),
|
|
531
|
+
accounts: accounts.length,
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
return ((await tryUsableFallbackAccounts(input, init, accounts, storage, currentResponse, trace)) ?? currentResponse);
|
|
535
|
+
}
|
|
536
|
+
return {
|
|
537
|
+
apiKey: '',
|
|
538
|
+
async fetch(input, init) {
|
|
539
|
+
const initialBody = init?.body;
|
|
540
|
+
const trace = createPerfTrace({
|
|
541
|
+
bodyBytes: typeof initialBody === 'string'
|
|
542
|
+
? initialBody.length
|
|
543
|
+
: undefined,
|
|
544
|
+
relayConfigured: relayConfig != null,
|
|
545
|
+
});
|
|
546
|
+
const authStart = nowMs();
|
|
547
|
+
const auth = await getAuth();
|
|
548
|
+
trace.mark('get_auth', {
|
|
549
|
+
ms: roundMs(nowMs() - authStart),
|
|
550
|
+
authType: auth.type,
|
|
551
|
+
hasAccess: Boolean(auth.access),
|
|
552
|
+
});
|
|
553
|
+
if (auth.type !== 'oauth') {
|
|
554
|
+
const response = await fetch(input, init);
|
|
555
|
+
trace.done('non_oauth_passthrough', { status: response.status });
|
|
556
|
+
return response;
|
|
557
|
+
}
|
|
558
|
+
if (!auth.access || !auth.expires || auth.expires < Date.now()) {
|
|
559
|
+
const refreshStart = nowMs();
|
|
560
|
+
auth.access = await refreshMainAccessToken();
|
|
561
|
+
trace.mark('refresh_main_access', {
|
|
562
|
+
ms: roundMs(nowMs() - refreshStart),
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
if (!auth.access) {
|
|
566
|
+
trace.done('missing_access_error');
|
|
567
|
+
throw new Error('OAuth access token is missing after refresh');
|
|
568
|
+
}
|
|
569
|
+
const loadStart = nowMs();
|
|
570
|
+
const storage = await loadAccounts();
|
|
571
|
+
trace.mark('load_storage', { ms: roundMs(nowMs() - loadStart) });
|
|
572
|
+
let preselectedFallbackAccounts;
|
|
573
|
+
if (isReplayableRequest(input, init?.body) &&
|
|
574
|
+
mainQuotaRoutingEnabled(storage)) {
|
|
575
|
+
try {
|
|
576
|
+
const quotaStart = nowMs();
|
|
577
|
+
const mainQuota = await getMainQuotaForRouting(auth.access, storage);
|
|
578
|
+
trace.mark('main_quota_for_routing', {
|
|
579
|
+
ms: roundMs(nowMs() - quotaStart),
|
|
580
|
+
passes: quotaSnapshotPassesPolicy(mainQuota, storage),
|
|
581
|
+
});
|
|
582
|
+
if (!quotaSnapshotPassesPolicy(mainQuota, storage)) {
|
|
583
|
+
const fallbackStart = nowMs();
|
|
584
|
+
preselectedFallbackAccounts =
|
|
585
|
+
await fallbackManager.getUsableFallbackAccounts();
|
|
586
|
+
trace.mark('preselect_fallback_accounts', {
|
|
587
|
+
ms: roundMs(nowMs() - fallbackStart),
|
|
588
|
+
accounts: preselectedFallbackAccounts.length,
|
|
589
|
+
});
|
|
590
|
+
const fallbackResponse = await tryUsableFallbackAccounts(input, init, preselectedFallbackAccounts, storage, undefined, trace);
|
|
591
|
+
if (fallbackResponse) {
|
|
592
|
+
trace.done('return_preselected_fallback', {
|
|
593
|
+
status: fallbackResponse.status,
|
|
594
|
+
});
|
|
595
|
+
return createStrippedStream(fallbackResponse);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
catch (error) {
|
|
600
|
+
trace.mark('main_quota_for_routing_error', {
|
|
601
|
+
error: error instanceof Error ? error.message : String(error),
|
|
602
|
+
});
|
|
603
|
+
// Main quota checks should optimize routing, not break requests.
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
const mainResponse = await sendWithAccessToken(input, init, auth.access, trace, 'main');
|
|
607
|
+
const response = await tryFallbackAccounts(input, init, mainResponse, preselectedFallbackAccounts, trace);
|
|
608
|
+
trace.done('return_response', { status: response.status });
|
|
609
|
+
return createStrippedStream(response);
|
|
610
|
+
},
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
return {};
|
|
614
|
+
},
|
|
615
|
+
methods: [
|
|
616
|
+
{
|
|
617
|
+
label: 'Claude Pro/Max',
|
|
618
|
+
type: 'oauth',
|
|
619
|
+
authorize: async () => {
|
|
620
|
+
const result = await authorize('max');
|
|
621
|
+
return {
|
|
622
|
+
url: result.url,
|
|
623
|
+
instructions: 'Paste the authorization code here:',
|
|
624
|
+
method: 'code',
|
|
625
|
+
callback: async (code) => {
|
|
626
|
+
return exchange(code, result.verifier, result.redirectUri, result.state);
|
|
627
|
+
},
|
|
628
|
+
};
|
|
629
|
+
},
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
label: 'Create an API Key',
|
|
633
|
+
type: 'oauth',
|
|
634
|
+
authorize: async () => {
|
|
635
|
+
const result = await authorize('console');
|
|
636
|
+
return {
|
|
637
|
+
url: result.url,
|
|
638
|
+
instructions: 'Paste the authorization code here:',
|
|
639
|
+
method: 'code',
|
|
640
|
+
callback: async (code) => {
|
|
641
|
+
const credentials = await exchange(code, result.verifier, result.redirectUri, result.state);
|
|
642
|
+
if (credentials.type === 'failed')
|
|
643
|
+
return credentials;
|
|
644
|
+
const apiKey = await fetch(`https://api.anthropic.com/api/oauth/claude_cli/create_api_key`, {
|
|
645
|
+
method: 'POST',
|
|
646
|
+
headers: {
|
|
647
|
+
'Content-Type': 'application/json',
|
|
648
|
+
authorization: `Bearer ${credentials.access}`,
|
|
649
|
+
},
|
|
650
|
+
}).then((r) => r.json());
|
|
651
|
+
return { type: 'success', key: apiKey.raw_key };
|
|
652
|
+
},
|
|
653
|
+
};
|
|
654
|
+
},
|
|
655
|
+
},
|
|
656
|
+
{
|
|
657
|
+
provider: 'anthropic',
|
|
658
|
+
label: 'Manually enter API Key',
|
|
659
|
+
type: 'api',
|
|
660
|
+
},
|
|
661
|
+
],
|
|
662
|
+
},
|
|
663
|
+
// biome-ignore lint/suspicious/noExplicitAny: Plugin type doesn't include undocumented auth/hooks
|
|
664
|
+
};
|
|
665
|
+
};
|