@cortexkit/opencode-anthropic-auth 1.0.0 → 1.1.1
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 +23 -5
- package/dist/index.js +220 -29
- package/dist/transform.d.ts +8 -2
- package/dist/transform.js +75 -27
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -19,14 +19,15 @@ This repo is a Bun workspace monorepo with two user-facing integrations and one
|
|
|
19
19
|
| Primary Claude Pro/Max OAuth | OpenCode `/connect anthropic` | Pi `/login anthropic` |
|
|
20
20
|
| Provider integration point | OpenCode plugin fetch/request transform | Pi `registerProvider("anthropic")` provider override |
|
|
21
21
|
| Sidecar config | `~/.config/opencode/anthropic-auth.json` | `~/.pi/agent/anthropic-auth.json` |
|
|
22
|
-
| Commands | `/claude-cache`, `/claude-quota`, `/claude-dump` | `/claude-cache`, `/claude-quota`, `/claude-dump` |
|
|
23
|
-
| Fallback accounts, quota routing, relay, dumps | Supported | Supported through the same shared core and Pi sidecar |
|
|
22
|
+
| Commands | `/claude-cache`, `/claude-fast`, `/claude-quota`, `/claude-dump` | `/claude-cache`, `/claude-fast`, `/claude-quota`, `/claude-dump` |
|
|
23
|
+
| Fallback accounts, quota routing, relay, dumps, fast mode | Supported | Supported through the same shared core and Pi sidecar |
|
|
24
24
|
|
|
25
25
|
## What CortexKit adds over the original plugin
|
|
26
26
|
|
|
27
27
|
- **Fallback Claude accounts**: keep each agent's normal Anthropic login as the primary account, then route to ordered fallback OAuth accounts on auth/quota/rate-limit failures.
|
|
28
28
|
- **Quota-aware routing**: skip main or fallback accounts when their 5-hour or 7-day Claude quota falls below your configured minimum.
|
|
29
29
|
- **Persistent Claude cache controls**: manage Anthropic 1-hour prompt caching from `/claude-cache` with explicit, automatic, or hybrid modes.
|
|
30
|
+
- **Fast mode toggle**: use `/claude-fast on|off` to request Anthropic fast mode for supported Opus models.
|
|
30
31
|
- **Live quota visibility**: use `/claude-quota` to see main and fallback quota state, reset times, and refresh errors.
|
|
31
32
|
- **User-owned Cloudflare relay**: optionally provision your own Worker relay to reduce repeated client upload bytes for large OpenCode or Pi requests.
|
|
32
33
|
- **Claude-compatible request hardening**: final-body billing signing, safer token refresh persistence, replay-safe fallback retries, and subagent cache isolation.
|
|
@@ -40,7 +41,7 @@ This repo is a Bun workspace monorepo with two user-facing integrations and one
|
|
|
40
41
|
- Support fallback Claude accounts stored in a local per-agent sidecar file.
|
|
41
42
|
- Keep fallback OAuth tokens fresh in the background.
|
|
42
43
|
- Apply quota thresholds before routing to main or fallback accounts.
|
|
43
|
-
- Add `/claude-cache`, `/claude-quota`, and `/claude-dump` commands.
|
|
44
|
+
- Add `/claude-cache`, `/claude-fast`, `/claude-quota`, and `/claude-dump` commands.
|
|
44
45
|
- Optionally relay large requests through a Cloudflare Worker owned by the user.
|
|
45
46
|
|
|
46
47
|
## Install
|
|
@@ -174,6 +175,9 @@ Example:
|
|
|
174
175
|
"dump": {
|
|
175
176
|
"enabled": false
|
|
176
177
|
},
|
|
178
|
+
"claudeFast": {
|
|
179
|
+
"enabled": false
|
|
180
|
+
},
|
|
177
181
|
"relay": {
|
|
178
182
|
"enabled": false,
|
|
179
183
|
"url": "https://opencode-anthropic-relay.example.workers.dev",
|
|
@@ -185,7 +189,7 @@ Example:
|
|
|
185
189
|
}
|
|
186
190
|
```
|
|
187
191
|
|
|
188
|
-
The `claudeCache` block controls the `/claude-cache` command's persisted mode
|
|
192
|
+
The `claudeCache` block controls the `/claude-cache` command's persisted mode, and `claudeFast` controls `/claude-fast`. The `main` field identifies OpenCode's primary auth entry; Pi keeps primary OAuth credentials in Pi's own credential store, but uses the same sidecar shape for CortexKit settings and fallback accounts.
|
|
189
193
|
|
|
190
194
|
## Fallback accounts
|
|
191
195
|
|
|
@@ -267,10 +271,24 @@ Modes:
|
|
|
267
271
|
|
|
268
272
|
- `explicit` keeps OpenCode's explicit cache breakpoints and adds `ttl: "1h"` to them.
|
|
269
273
|
- `automatic` removes block-level cache controls and sends a top-level `cache_control` object.
|
|
270
|
-
- `hybrid` (recommended)
|
|
274
|
+
- `hybrid` (recommended) removes top-level automatic caching and uses explicit anchors on the last stable system block, the first two messages, and `messages[n-2]` when that anchor is distinct.
|
|
271
275
|
|
|
272
276
|
In OpenCode, subagent requests do not receive 1-hour TTL caching. The plugin detects child sessions through OpenCode's `x-parent-session-id` header, strips that internal header before forwarding to Anthropic, and leaves default ephemeral caching in place for those requests.
|
|
273
277
|
|
|
278
|
+
## Claude fast mode
|
|
279
|
+
|
|
280
|
+
Both OpenCode and Pi packages can persistently request Anthropic fast mode for supported Opus models:
|
|
281
|
+
|
|
282
|
+
```text
|
|
283
|
+
/claude-fast
|
|
284
|
+
/claude-fast on
|
|
285
|
+
/claude-fast off
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
When enabled, supported requests add `speed: "fast"` to the Anthropic JSON body and include the `fast-mode-2026-02-01` beta header. Unsupported models are left at standard speed. Anthropic currently documents fast mode for `claude-opus-4-6` and `claude-opus-4-7`.
|
|
289
|
+
|
|
290
|
+
Fast and standard speeds do not share prompt-cache prefixes, so switching this setting can cause cache misses.
|
|
291
|
+
|
|
274
292
|
### Estimate cache savings from OpenCode history
|
|
275
293
|
|
|
276
294
|
The repo includes an OpenCode SQLite analyzer that compares estimated Claude cost under three scenarios: no prompt cache, Anthropic's default 5-minute cache, and this plugin's 1-hour cache mode.
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { authorize, buildClaudeQuotaSummary, buildFallbackQuotaSummaries, buildRefreshOperationError, CACHE_1H_COMMAND_NAME, CLAUDE_DUMP_COMMAND_NAME, CLAUDE_FAST_COMMAND_NAME, CLAUDE_QUOTAS_COMMAND_NAME, ClaudeOAuthRefreshError, exchange, executeCache1hCommand, executeDumpCommand, executeFastModeCommand, FallbackAccountManager, fetchOAuthQuotaSnapshot, formatRefreshBackoffMessage, getCache1hMode, getCache1hPersistentMode, getQuotaCheckIntervalMs, getQuotaNextRefreshAt, getRelayConfig, hashRefreshToken, isCache1hEnabled, isCache1hPersistentlyEnabled, isDumpPersistentlyEnabled, isFastModeEnabled, isFastModePersistentlyEnabled, isFastModeSupportedModel, loadAccounts, log, parseCache1hCommandAction, parseDumpCommandAction, parseFastModeCommandAction, quotaSnapshotPassesPolicy, refreshBackoffActive, refreshClaudeOAuthToken, resolveClaudeCodeIdentity, saveAccounts, sendViaRelay, setCache1hPersistentEnabled, setCache1hPersistentMode, setCache1hState, setDumpEnabled, setDumpPersistentEnabled, setFastModeEnabled, setFastModePersistentEnabled, shouldFallbackStatus, } from '@cortexkit/anthropic-auth-core';
|
|
2
3
|
import { resolvePromptContext } from "./prompt-context.js";
|
|
3
|
-
import { createStrippedStream, isInsecure, mergeHeaders, rewriteRequestBody, rewriteUrl, setOAuthHeaders, } from "./transform.js";
|
|
4
|
+
import { addFastModeBetaHeader, createStrippedStream, isInsecure, mergeHeaders, rewriteRequestBody, rewriteUrl, setOAuthHeaders, } from "./transform.js";
|
|
4
5
|
const HANDLED_SENTINEL = '__OPENCODE_ANTHROPIC_AUTH_COMMAND_HANDLED__';
|
|
6
|
+
const MAIN_AUTH_REFRESH_TICK_MS = 60_000;
|
|
7
|
+
const DEFAULT_MAIN_REFRESH_BEFORE_EXPIRY_MINUTES = 30;
|
|
5
8
|
let nextPerfRequestId = 1;
|
|
6
9
|
let eventLoopLagMonitorStarted = false;
|
|
7
10
|
function nowMs() {
|
|
@@ -102,7 +105,17 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
102
105
|
mode: getCache1hPersistentMode(initialCache1hStorage),
|
|
103
106
|
});
|
|
104
107
|
setDumpEnabled(isDumpPersistentlyEnabled(initialCache1hStorage));
|
|
108
|
+
setFastModeEnabled(isFastModePersistentlyEnabled(initialCache1hStorage));
|
|
105
109
|
let latestGetAuth = null;
|
|
110
|
+
let mainBackgroundRefreshTimer = null;
|
|
111
|
+
function mainRefreshBeforeExpiryMs(storage) {
|
|
112
|
+
const minutes = storage?.refresh?.refreshBeforeExpiryMinutes ??
|
|
113
|
+
DEFAULT_MAIN_REFRESH_BEFORE_EXPIRY_MINUTES;
|
|
114
|
+
return Math.max(0, minutes) * 60_000;
|
|
115
|
+
}
|
|
116
|
+
function mainRefreshEnabled(storage) {
|
|
117
|
+
return storage?.refresh?.enabled !== false;
|
|
118
|
+
}
|
|
106
119
|
async function buildQuotaCommandSummary() {
|
|
107
120
|
const accounts = [];
|
|
108
121
|
if (latestGetAuth) {
|
|
@@ -180,6 +193,19 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
180
193
|
setDumpEnabled(enabled);
|
|
181
194
|
return executeDumpCommand({ argumentsText, enabled });
|
|
182
195
|
}
|
|
196
|
+
async function executePersistentFastModeCommand(argumentsText) {
|
|
197
|
+
const action = parseFastModeCommandAction(argumentsText);
|
|
198
|
+
if (action.type === 'enable' || action.type === 'disable') {
|
|
199
|
+
const enabled = action.type === 'enable';
|
|
200
|
+
await setFastModePersistentEnabled(enabled);
|
|
201
|
+
setFastModeEnabled(enabled);
|
|
202
|
+
return executeFastModeCommand({ argumentsText, enabled });
|
|
203
|
+
}
|
|
204
|
+
const storage = await loadAccounts();
|
|
205
|
+
const enabled = isFastModePersistentlyEnabled(storage);
|
|
206
|
+
setFastModeEnabled(enabled);
|
|
207
|
+
return executeFastModeCommand({ argumentsText, enabled });
|
|
208
|
+
}
|
|
183
209
|
return {
|
|
184
210
|
config: async (config) => {
|
|
185
211
|
config.command = {
|
|
@@ -196,6 +222,10 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
196
222
|
template: CLAUDE_DUMP_COMMAND_NAME,
|
|
197
223
|
description: 'Show or toggle Anthropic request dump capture for debugging.',
|
|
198
224
|
},
|
|
225
|
+
[CLAUDE_FAST_COMMAND_NAME]: {
|
|
226
|
+
template: CLAUDE_FAST_COMMAND_NAME,
|
|
227
|
+
description: 'Show or toggle Anthropic fast mode for supported Opus models.',
|
|
228
|
+
},
|
|
199
229
|
};
|
|
200
230
|
},
|
|
201
231
|
'command.execute.before': async (input) => {
|
|
@@ -211,6 +241,10 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
211
241
|
await sendIgnoredMessage(ctx, input.sessionID, await executePersistentDumpCommand(input.arguments));
|
|
212
242
|
throwHandledSentinel();
|
|
213
243
|
}
|
|
244
|
+
if (input.command === CLAUDE_FAST_COMMAND_NAME) {
|
|
245
|
+
await sendIgnoredMessage(ctx, input.sessionID, await executePersistentFastModeCommand(input.arguments));
|
|
246
|
+
throwHandledSentinel();
|
|
247
|
+
}
|
|
214
248
|
},
|
|
215
249
|
auth: {
|
|
216
250
|
provider: 'anthropic',
|
|
@@ -240,7 +274,20 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
240
274
|
refreshPromise = (async () => {
|
|
241
275
|
const maxRetries = 2;
|
|
242
276
|
const baseDelayMs = 500;
|
|
277
|
+
let leaseId = null;
|
|
278
|
+
let leaseTokenHash = null;
|
|
279
|
+
async function updateMainRefreshState(update) {
|
|
280
|
+
const storage = (await loadAccounts()) ?? {
|
|
281
|
+
version: 1,
|
|
282
|
+
main: { type: 'opencode', provider: 'anthropic' },
|
|
283
|
+
accounts: [],
|
|
284
|
+
};
|
|
285
|
+
storage.refresh = storage.refresh ?? {};
|
|
286
|
+
update(storage);
|
|
287
|
+
await saveAccounts(storage);
|
|
288
|
+
}
|
|
243
289
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
290
|
+
let freshAuth = null;
|
|
244
291
|
try {
|
|
245
292
|
if (attempt > 0) {
|
|
246
293
|
const delay = baseDelayMs * 2 ** (attempt - 1);
|
|
@@ -249,29 +296,43 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
249
296
|
// Re-read auth to get the latest refresh token.
|
|
250
297
|
// The outer `auth` snapshot may be stale if tokens
|
|
251
298
|
// were rotated since the fetch() call was made.
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
299
|
+
freshAuth = await getAuth();
|
|
300
|
+
if (!freshAuth.refresh) {
|
|
301
|
+
throw new Error('Token refresh failed: missing refresh token');
|
|
302
|
+
}
|
|
303
|
+
const storage = await loadAccounts();
|
|
304
|
+
const refreshTokenHash = hashRefreshToken(freshAuth.refresh);
|
|
305
|
+
const mainError = storage?.refresh?.mainLastRefreshError;
|
|
306
|
+
if (mainError &&
|
|
307
|
+
refreshBackoffActive(mainError, freshAuth.refresh, Date.now())) {
|
|
308
|
+
throw new Error(formatRefreshBackoffMessage(mainError, Date.now()));
|
|
309
|
+
}
|
|
310
|
+
if (storage?.refresh?.mainRefreshLeaseUntil &&
|
|
311
|
+
storage.refresh.mainRefreshLeaseUntil > Date.now() &&
|
|
312
|
+
storage.refresh.mainRefreshLeaseTokenHash ===
|
|
313
|
+
refreshTokenHash) {
|
|
314
|
+
throw new Error('Claude OAuth refresh is already in progress');
|
|
315
|
+
}
|
|
316
|
+
leaseId = randomUUID();
|
|
317
|
+
leaseTokenHash = refreshTokenHash;
|
|
318
|
+
await updateMainRefreshState((nextStorage) => {
|
|
319
|
+
nextStorage.refresh = nextStorage.refresh ?? {};
|
|
320
|
+
nextStorage.refresh.mainRefreshLeaseId =
|
|
321
|
+
leaseId ?? undefined;
|
|
322
|
+
nextStorage.refresh.mainRefreshLeaseUntil =
|
|
323
|
+
Date.now() + 2 * 60_000;
|
|
324
|
+
nextStorage.refresh.mainRefreshLeaseTokenHash =
|
|
325
|
+
refreshTokenHash;
|
|
265
326
|
});
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const body = await response.text().catch(() => '');
|
|
272
|
-
throw new Error(`Token refresh failed: ${response.status} — ${body}`);
|
|
327
|
+
const latestLease = await loadAccounts();
|
|
328
|
+
if (latestLease?.refresh?.mainRefreshLeaseId !== leaseId ||
|
|
329
|
+
latestLease.refresh.mainRefreshLeaseTokenHash !==
|
|
330
|
+
refreshTokenHash) {
|
|
331
|
+
throw new Error('Claude OAuth refresh is already in progress');
|
|
273
332
|
}
|
|
274
|
-
const
|
|
333
|
+
const refreshed = await refreshClaudeOAuthToken({
|
|
334
|
+
refreshToken: freshAuth.refresh,
|
|
335
|
+
});
|
|
275
336
|
// biome-ignore lint/suspicious/noExplicitAny: SDK types don't expose auth.set
|
|
276
337
|
await client.auth.set({
|
|
277
338
|
path: {
|
|
@@ -279,12 +340,22 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
279
340
|
},
|
|
280
341
|
body: {
|
|
281
342
|
type: 'oauth',
|
|
282
|
-
refresh:
|
|
283
|
-
access:
|
|
284
|
-
expires:
|
|
343
|
+
refresh: refreshed.refresh,
|
|
344
|
+
access: refreshed.access,
|
|
345
|
+
expires: refreshed.expires,
|
|
285
346
|
},
|
|
286
347
|
});
|
|
287
|
-
|
|
348
|
+
await updateMainRefreshState((storage) => {
|
|
349
|
+
if (!storage?.refresh)
|
|
350
|
+
return;
|
|
351
|
+
storage.refresh.mainLastRefreshError = undefined;
|
|
352
|
+
if (storage.refresh.mainRefreshLeaseId === leaseId) {
|
|
353
|
+
storage.refresh.mainRefreshLeaseId = undefined;
|
|
354
|
+
storage.refresh.mainRefreshLeaseUntil = undefined;
|
|
355
|
+
storage.refresh.mainRefreshLeaseTokenHash = undefined;
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
return refreshed.access;
|
|
288
359
|
}
|
|
289
360
|
catch (error) {
|
|
290
361
|
const isNetworkError = error instanceof Error &&
|
|
@@ -294,11 +365,43 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
294
365
|
error.code === 'ECONNREFUSED' ||
|
|
295
366
|
error.code === 'ETIMEDOUT' ||
|
|
296
367
|
error.code === 'UND_ERR_CONNECT_TIMEOUT')));
|
|
297
|
-
if (attempt < maxRetries &&
|
|
368
|
+
if (attempt < maxRetries &&
|
|
369
|
+
(isNetworkError ||
|
|
370
|
+
(error instanceof ClaudeOAuthRefreshError &&
|
|
371
|
+
error.status >= 500))) {
|
|
298
372
|
continue;
|
|
299
373
|
}
|
|
374
|
+
const failedRefreshToken = freshAuth?.refresh;
|
|
375
|
+
if (failedRefreshToken &&
|
|
376
|
+
error instanceof ClaudeOAuthRefreshError) {
|
|
377
|
+
await updateMainRefreshState((storage) => {
|
|
378
|
+
storage.refresh = storage.refresh ?? {};
|
|
379
|
+
storage.refresh.mainLastRefreshError =
|
|
380
|
+
buildRefreshOperationError({
|
|
381
|
+
error,
|
|
382
|
+
now: Date.now(),
|
|
383
|
+
refreshToken: failedRefreshToken,
|
|
384
|
+
previous: storage.refresh.mainLastRefreshError,
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
}
|
|
300
388
|
throw error;
|
|
301
389
|
}
|
|
390
|
+
finally {
|
|
391
|
+
if (leaseId) {
|
|
392
|
+
await updateMainRefreshState((storage) => {
|
|
393
|
+
if (!storage?.refresh)
|
|
394
|
+
return;
|
|
395
|
+
if (storage.refresh.mainRefreshLeaseId === leaseId &&
|
|
396
|
+
storage.refresh.mainRefreshLeaseTokenHash ===
|
|
397
|
+
leaseTokenHash) {
|
|
398
|
+
storage.refresh.mainRefreshLeaseId = undefined;
|
|
399
|
+
storage.refresh.mainRefreshLeaseUntil = undefined;
|
|
400
|
+
storage.refresh.mainRefreshLeaseTokenHash = undefined;
|
|
401
|
+
}
|
|
402
|
+
}).catch(() => { });
|
|
403
|
+
}
|
|
404
|
+
}
|
|
302
405
|
}
|
|
303
406
|
// Unreachable — each iteration either returns or throws.
|
|
304
407
|
// Kept as a TypeScript exhaustiveness guard.
|
|
@@ -309,6 +412,55 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
309
412
|
}
|
|
310
413
|
return refreshPromise;
|
|
311
414
|
}
|
|
415
|
+
function startMainBackgroundRefresh() {
|
|
416
|
+
if (mainBackgroundRefreshTimer) {
|
|
417
|
+
clearInterval(mainBackgroundRefreshTimer);
|
|
418
|
+
mainBackgroundRefreshTimer = null;
|
|
419
|
+
}
|
|
420
|
+
const run = async () => {
|
|
421
|
+
try {
|
|
422
|
+
const storage = await loadAccounts();
|
|
423
|
+
if (!mainRefreshEnabled(storage))
|
|
424
|
+
return;
|
|
425
|
+
const latestAuth = await getAuth();
|
|
426
|
+
if (latestAuth.type !== 'oauth')
|
|
427
|
+
return;
|
|
428
|
+
if (!latestAuth.expires)
|
|
429
|
+
return;
|
|
430
|
+
if (latestAuth.expires - Date.now() >
|
|
431
|
+
mainRefreshBeforeExpiryMs(storage)) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (latestAuth.refresh &&
|
|
435
|
+
refreshBackoffActive(storage?.refresh?.mainLastRefreshError, latestAuth.refresh, Date.now())) {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (latestAuth.refresh &&
|
|
439
|
+
storage?.refresh?.mainRefreshLeaseUntil &&
|
|
440
|
+
storage.refresh.mainRefreshLeaseUntil > Date.now() &&
|
|
441
|
+
storage.refresh.mainRefreshLeaseTokenHash ===
|
|
442
|
+
hashRefreshToken(latestAuth.refresh)) {
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
await refreshMainAccessToken();
|
|
446
|
+
log('[refresh] opencode main oauth refreshed in background', {
|
|
447
|
+
expires: latestAuth.expires,
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
catch (error) {
|
|
451
|
+
log('[refresh] opencode main oauth refresh failed', {
|
|
452
|
+
message: error instanceof Error ? error.message : String(error),
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
mainBackgroundRefreshTimer = setInterval(() => {
|
|
457
|
+
void run();
|
|
458
|
+
}, MAIN_AUTH_REFRESH_TICK_MS);
|
|
459
|
+
if ('unref' in mainBackgroundRefreshTimer) {
|
|
460
|
+
mainBackgroundRefreshTimer.unref();
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
startMainBackgroundRefresh();
|
|
312
464
|
function isReplayableRequest(input, body) {
|
|
313
465
|
if (input instanceof Request && input.body)
|
|
314
466
|
return false;
|
|
@@ -395,17 +547,54 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
395
547
|
async function sendWithAccessToken(input, init, accessToken, trace, route = 'unknown') {
|
|
396
548
|
const start = nowMs();
|
|
397
549
|
const requestHeaders = mergeHeaders(input, init);
|
|
550
|
+
const relayAffinity = requestHeaders.get('x-session-affinity') ||
|
|
551
|
+
requestHeaders.get('x-opencode-session');
|
|
398
552
|
const subagentRequest = isSubagentRequest(requestHeaders);
|
|
399
553
|
requestHeaders.delete('x-parent-session-id');
|
|
400
|
-
|
|
554
|
+
requestHeaders.delete('x-session-affinity');
|
|
555
|
+
requestHeaders.delete('x-opencode-session');
|
|
401
556
|
let body = init?.body;
|
|
557
|
+
let modelForIdentity;
|
|
558
|
+
if (body && typeof body === 'string') {
|
|
559
|
+
try {
|
|
560
|
+
const parsedBody = JSON.parse(body);
|
|
561
|
+
if (typeof parsedBody.model === 'string') {
|
|
562
|
+
modelForIdentity = parsedBody.model;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
catch { }
|
|
566
|
+
}
|
|
567
|
+
const identity = await resolveClaudeCodeIdentity(accessToken, modelForIdentity);
|
|
402
568
|
const originalBytes = typeof body === 'string' ? body.length : undefined;
|
|
403
569
|
if (body && typeof body === 'string') {
|
|
404
570
|
const rewriteStart = nowMs();
|
|
571
|
+
const fastModeRequested = (() => {
|
|
572
|
+
if (!isFastModeEnabled())
|
|
573
|
+
return false;
|
|
574
|
+
try {
|
|
575
|
+
return isFastModeSupportedModel(JSON.parse(body).model);
|
|
576
|
+
}
|
|
577
|
+
catch {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
})();
|
|
405
581
|
body = await rewriteRequestBody(body, {
|
|
406
582
|
cache1hEnabled: !subagentRequest && isCache1hEnabled(),
|
|
407
583
|
cache1hMode: getCache1hMode(),
|
|
584
|
+
fastModeEnabled: fastModeRequested,
|
|
585
|
+
identity,
|
|
408
586
|
});
|
|
587
|
+
try {
|
|
588
|
+
setOAuthHeaders(requestHeaders, accessToken, {
|
|
589
|
+
body: JSON.parse(body),
|
|
590
|
+
identity,
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
catch {
|
|
594
|
+
setOAuthHeaders(requestHeaders, accessToken, { identity });
|
|
595
|
+
}
|
|
596
|
+
if (fastModeRequested)
|
|
597
|
+
addFastModeBetaHeader(requestHeaders);
|
|
409
598
|
trace?.mark('rewrite_body', {
|
|
410
599
|
route,
|
|
411
600
|
ms: roundMs(nowMs() - rewriteStart),
|
|
@@ -413,6 +602,7 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
413
602
|
rewrittenBytes: body.length,
|
|
414
603
|
cacheEnabled: !subagentRequest && isCache1hEnabled(),
|
|
415
604
|
cacheMode: getCache1hMode(),
|
|
605
|
+
fastModeEnabled: fastModeRequested,
|
|
416
606
|
subagent: subagentRequest,
|
|
417
607
|
});
|
|
418
608
|
}
|
|
@@ -431,6 +621,7 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
431
621
|
headers: requestHeaders,
|
|
432
622
|
body,
|
|
433
623
|
fallback: directFetch,
|
|
624
|
+
affinity: relayAffinity,
|
|
434
625
|
optimisticResponse: relayConfig?.transport === 'websocket',
|
|
435
626
|
});
|
|
436
627
|
trace?.mark('send_headers_received', {
|
package/dist/transform.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Cache1hMode } from '@cortexkit/anthropic-auth-core';
|
|
1
|
+
import { type Cache1hMode, type ClaudeCodeIdentity } from '@cortexkit/anthropic-auth-core';
|
|
2
2
|
export type FetchInput = string | URL | Request;
|
|
3
3
|
/**
|
|
4
4
|
* Merge headers from a Request object and/or a RequestInit headers value
|
|
@@ -9,11 +9,15 @@ export declare function mergeHeaders(input: FetchInput, init?: RequestInit): Hea
|
|
|
9
9
|
* Merge incoming beta headers with the required OAuth betas, deduplicating.
|
|
10
10
|
*/
|
|
11
11
|
export declare function mergeBetaHeaders(headers: Headers): string;
|
|
12
|
+
export declare function addFastModeBetaHeader(headers: Headers): Headers;
|
|
12
13
|
/**
|
|
13
14
|
* Set OAuth-required headers on the request: authorization, beta, user-agent.
|
|
14
15
|
* Removes x-api-key since we're using OAuth.
|
|
15
16
|
*/
|
|
16
|
-
export declare function setOAuthHeaders(headers: Headers, accessToken: string
|
|
17
|
+
export declare function setOAuthHeaders(headers: Headers, accessToken: string, options?: {
|
|
18
|
+
body?: Record<string, unknown> | null;
|
|
19
|
+
identity?: ClaudeCodeIdentity;
|
|
20
|
+
}): Headers;
|
|
17
21
|
/**
|
|
18
22
|
* Add TOOL_PREFIX to tool names in the request body.
|
|
19
23
|
* Prefixes both tool definitions and tool_use blocks in messages.
|
|
@@ -69,6 +73,8 @@ export declare function prependClaudeCodeIdentity(system: unknown): SystemBlock[
|
|
|
69
73
|
export declare function rewriteRequestBody(body: string, options?: {
|
|
70
74
|
cache1hEnabled?: boolean;
|
|
71
75
|
cache1hMode?: Cache1hMode;
|
|
76
|
+
fastModeEnabled?: boolean;
|
|
77
|
+
identity?: ClaudeCodeIdentity;
|
|
72
78
|
}): Promise<string>;
|
|
73
79
|
/**
|
|
74
80
|
* Create a streaming response that strips the tool prefix from tool names.
|
package/dist/transform.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildBillingHeaderValue, CLAUDE_CODE_ENTRYPOINT, CLAUDE_CODE_IDENTITY, OPENCODE_IDENTITY_PREFIX, PARAGRAPH_REMOVAL_ANCHORS, REQUIRED_BETAS, signRequestBody, TEXT_REPLACEMENTS, TOOL_PREFIX,
|
|
1
|
+
import { applyClaudeCodeHeaders, applyClaudeCodeMetadata, buildBillingHeaderValue, CLAUDE_CODE_ENTRYPOINT, CLAUDE_CODE_IDENTITY, FAST_MODE_BETA, isFastModeSupportedModel, mergeAnthropicBetas, OPENCODE_IDENTITY_PREFIX, orderClaudeCodeBody, PARAGRAPH_REMOVAL_ANCHORS, REQUIRED_BETAS, signRequestBody, TEXT_REPLACEMENTS, TOOL_PREFIX, } from '@cortexkit/anthropic-auth-core';
|
|
2
2
|
/**
|
|
3
3
|
* Prefix a tool name with TOOL_PREFIX and uppercase the first character.
|
|
4
4
|
* Claude Code uses PascalCase tool names (e.g. mcp_Bash, mcp_Read);
|
|
@@ -64,16 +64,16 @@ export function mergeBetaHeaders(headers) {
|
|
|
64
64
|
.filter(Boolean);
|
|
65
65
|
return [...new Set([...REQUIRED_BETAS, ...incomingBetasList])].join(',');
|
|
66
66
|
}
|
|
67
|
+
export function addFastModeBetaHeader(headers) {
|
|
68
|
+
headers.set('anthropic-beta', mergeAnthropicBetas(headers.get('anthropic-beta'), [FAST_MODE_BETA]));
|
|
69
|
+
return headers;
|
|
70
|
+
}
|
|
67
71
|
/**
|
|
68
72
|
* Set OAuth-required headers on the request: authorization, beta, user-agent.
|
|
69
73
|
* Removes x-api-key since we're using OAuth.
|
|
70
74
|
*/
|
|
71
|
-
export function setOAuthHeaders(headers, accessToken) {
|
|
72
|
-
headers
|
|
73
|
-
headers.set('anthropic-beta', mergeBetaHeaders(headers));
|
|
74
|
-
headers.set('user-agent', USER_AGENT);
|
|
75
|
-
headers.delete('x-api-key');
|
|
76
|
-
return headers;
|
|
75
|
+
export function setOAuthHeaders(headers, accessToken, options = {}) {
|
|
76
|
+
return applyClaudeCodeHeaders(headers, accessToken, options);
|
|
77
77
|
}
|
|
78
78
|
/**
|
|
79
79
|
* Add TOOL_PREFIX to tool names in the request body.
|
|
@@ -99,7 +99,7 @@ export function prefixToolNames(parsed) {
|
|
|
99
99
|
return msg;
|
|
100
100
|
});
|
|
101
101
|
}
|
|
102
|
-
return JSON.stringify(parsed);
|
|
102
|
+
return JSON.stringify(orderClaudeCodeBody(parsed));
|
|
103
103
|
}
|
|
104
104
|
/**
|
|
105
105
|
* Strip TOOL_PREFIX from tool names in streaming response text.
|
|
@@ -107,6 +107,33 @@ export function prefixToolNames(parsed) {
|
|
|
107
107
|
export function stripToolPrefix(text) {
|
|
108
108
|
return text.replace(/"name"\s*:\s*"mcp_([^"]+)"/g, (_match, name) => `"name": "${unprefixName(name)}"`);
|
|
109
109
|
}
|
|
110
|
+
function splitToolPrefixRewriteBuffer(buffer, flush = false) {
|
|
111
|
+
if (flush)
|
|
112
|
+
return { ready: stripToolPrefix(buffer), pending: '' };
|
|
113
|
+
let keepFrom = buffer.length;
|
|
114
|
+
const marker = '"name"';
|
|
115
|
+
const partialMarkerStart = Math.max(0, buffer.length - marker.length + 1);
|
|
116
|
+
for (let index = partialMarkerStart; index < buffer.length; index++) {
|
|
117
|
+
if (marker.startsWith(buffer.slice(index))) {
|
|
118
|
+
keepFrom = Math.min(keepFrom, index);
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const lastName = buffer.lastIndexOf(marker);
|
|
123
|
+
if (lastName !== -1) {
|
|
124
|
+
const tail = buffer.slice(lastName);
|
|
125
|
+
if (/^"name"\s*(?::\s*(?:"[^"]*)?)?$/.test(tail)) {
|
|
126
|
+
keepFrom = Math.min(keepFrom, lastName);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (keepFrom < buffer.length) {
|
|
130
|
+
return {
|
|
131
|
+
ready: stripToolPrefix(buffer.slice(0, keepFrom)),
|
|
132
|
+
pending: buffer.slice(keepFrom),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return { ready: stripToolPrefix(buffer), pending: '' };
|
|
136
|
+
}
|
|
110
137
|
/**
|
|
111
138
|
* Check if TLS verification should be skipped for custom API endpoints.
|
|
112
139
|
* Only effective when ANTHROPIC_BASE_URL is also set.
|
|
@@ -349,32 +376,39 @@ function applyAutomaticCache1h(parsed) {
|
|
|
349
376
|
removeAllCacheControls(parsed);
|
|
350
377
|
parsed.cache_control = { ...CACHE_1H_CONTROL };
|
|
351
378
|
}
|
|
379
|
+
function setMessageCacheAnchor(message) {
|
|
380
|
+
if (!isRecord(message))
|
|
381
|
+
return false;
|
|
382
|
+
const content = normalizeContentToArray(message.content);
|
|
383
|
+
if (!content?.length)
|
|
384
|
+
return setWireCacheControl(message, true);
|
|
385
|
+
message.content = content;
|
|
386
|
+
const lastCacheableBlock = [...content]
|
|
387
|
+
.reverse()
|
|
388
|
+
.find((block) => isRecord(block) && block.type !== 'thinking');
|
|
389
|
+
return setWireCacheControl(lastCacheableBlock ?? message, true);
|
|
390
|
+
}
|
|
352
391
|
function applyHybridCache1h(parsed) {
|
|
353
392
|
removeAllCacheControls(parsed);
|
|
354
|
-
parsed.cache_control = { ...CACHE_1H_CONTROL };
|
|
355
393
|
if (Array.isArray(parsed.system)) {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
394
|
+
const identityIndex = parsed.system.findIndex((block) => isRecord(block) && block.text === CLAUDE_CODE_IDENTITY);
|
|
395
|
+
const cacheableSystemBlocks = parsed.system
|
|
396
|
+
.slice(identityIndex >= 0 ? identityIndex + 1 : 0)
|
|
397
|
+
.filter(isRecord);
|
|
398
|
+
const lastSystemBlock = cacheableSystemBlocks[cacheableSystemBlocks.length - 1];
|
|
399
|
+
setWireCacheControl(lastSystemBlock, true);
|
|
359
400
|
}
|
|
360
401
|
else {
|
|
361
402
|
setWireCacheControl(parsed.system, true);
|
|
362
403
|
}
|
|
363
404
|
if (!Array.isArray(parsed.messages))
|
|
364
405
|
return;
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
setWireCacheControl(firstMessage, true);
|
|
371
|
-
return;
|
|
406
|
+
setMessageCacheAnchor(parsed.messages[0]);
|
|
407
|
+
setMessageCacheAnchor(parsed.messages[1]);
|
|
408
|
+
const movingMessageIndex = parsed.messages.length - 2;
|
|
409
|
+
if (movingMessageIndex > 1) {
|
|
410
|
+
setMessageCacheAnchor(parsed.messages[movingMessageIndex]);
|
|
372
411
|
}
|
|
373
|
-
firstMessage.content = content;
|
|
374
|
-
const lastCacheableBlock = [...content]
|
|
375
|
-
.reverse()
|
|
376
|
-
.find((block) => isRecord(block) && block.type !== 'thinking');
|
|
377
|
-
setWireCacheControl(lastCacheableBlock ?? firstMessage, true);
|
|
378
412
|
}
|
|
379
413
|
function applyCache1hStrategy(parsed, options) {
|
|
380
414
|
if (!options.enabled) {
|
|
@@ -415,6 +449,14 @@ export async function rewriteRequestBody(body, options = {}) {
|
|
|
415
449
|
enabled: options.cache1hEnabled ?? false,
|
|
416
450
|
mode: options.cache1hMode ?? 'explicit',
|
|
417
451
|
});
|
|
452
|
+
if (options.fastModeEnabled && isFastModeSupportedModel(parsed.model)) {
|
|
453
|
+
parsed.speed = 'fast';
|
|
454
|
+
}
|
|
455
|
+
else if (parsed.speed === 'fast') {
|
|
456
|
+
delete parsed.speed;
|
|
457
|
+
}
|
|
458
|
+
if (options.identity)
|
|
459
|
+
applyClaudeCodeMetadata(parsed, options.identity);
|
|
418
460
|
return await signRequestBody(prefixToolNames(parsed));
|
|
419
461
|
}
|
|
420
462
|
catch {
|
|
@@ -430,16 +472,22 @@ export function createStrippedStream(response) {
|
|
|
430
472
|
const reader = response.body.getReader();
|
|
431
473
|
const decoder = new TextDecoder();
|
|
432
474
|
const encoder = new TextEncoder();
|
|
475
|
+
let pending = '';
|
|
433
476
|
const stream = new ReadableStream({
|
|
434
477
|
async pull(controller) {
|
|
435
478
|
const { done, value } = await reader.read();
|
|
436
479
|
if (done) {
|
|
480
|
+
const flushed = splitToolPrefixRewriteBuffer(`${pending}${decoder.decode()}`, true);
|
|
481
|
+
if (flushed.ready)
|
|
482
|
+
controller.enqueue(encoder.encode(flushed.ready));
|
|
437
483
|
controller.close();
|
|
438
484
|
return;
|
|
439
485
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
486
|
+
const text = pending + decoder.decode(value, { stream: true });
|
|
487
|
+
const rewritten = splitToolPrefixRewriteBuffer(text);
|
|
488
|
+
pending = rewritten.pending;
|
|
489
|
+
if (rewritten.ready)
|
|
490
|
+
controller.enqueue(encoder.encode(rewritten.ready));
|
|
443
491
|
},
|
|
444
492
|
});
|
|
445
493
|
return new Response(stream, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cortexkit/opencode-anthropic-auth",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -40,6 +40,6 @@
|
|
|
40
40
|
"@opencode-ai/plugin": "*"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@cortexkit/anthropic-auth-core": "1.
|
|
43
|
+
"@cortexkit/anthropic-auth-core": "1.1.1"
|
|
44
44
|
}
|
|
45
45
|
}
|