@cortexkit/opencode-anthropic-auth 1.0.0 → 1.1.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/README.md +23 -5
- package/dist/index.js +131 -26
- 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,9 @@
|
|
|
1
|
-
import { authorize, buildClaudeQuotaSummary, buildFallbackQuotaSummaries, CACHE_1H_COMMAND_NAME, CLAUDE_DUMP_COMMAND_NAME, CLAUDE_QUOTAS_COMMAND_NAME,
|
|
1
|
+
import { authorize, buildClaudeQuotaSummary, buildFallbackQuotaSummaries, CACHE_1H_COMMAND_NAME, CLAUDE_DUMP_COMMAND_NAME, CLAUDE_FAST_COMMAND_NAME, CLAUDE_QUOTAS_COMMAND_NAME, ClaudeOAuthRefreshError, exchange, executeCache1hCommand, executeDumpCommand, executeFastModeCommand, FallbackAccountManager, fetchOAuthQuotaSnapshot, getCache1hMode, getCache1hPersistentMode, getQuotaCheckIntervalMs, getQuotaNextRefreshAt, getRelayConfig, isCache1hEnabled, isCache1hPersistentlyEnabled, isDumpPersistentlyEnabled, isFastModeEnabled, isFastModePersistentlyEnabled, isFastModeSupportedModel, loadAccounts, log, parseCache1hCommandAction, parseDumpCommandAction, parseFastModeCommandAction, quotaSnapshotPassesPolicy, refreshClaudeOAuthToken, resolveClaudeCodeIdentity, sendViaRelay, setCache1hPersistentEnabled, setCache1hPersistentMode, setCache1hState, setDumpEnabled, setDumpPersistentEnabled, setFastModeEnabled, setFastModePersistentEnabled, shouldFallbackStatus, } from '@cortexkit/anthropic-auth-core';
|
|
2
2
|
import { resolvePromptContext } from "./prompt-context.js";
|
|
3
|
-
import { createStrippedStream, isInsecure, mergeHeaders, rewriteRequestBody, rewriteUrl, setOAuthHeaders, } from "./transform.js";
|
|
3
|
+
import { addFastModeBetaHeader, createStrippedStream, isInsecure, mergeHeaders, rewriteRequestBody, rewriteUrl, setOAuthHeaders, } from "./transform.js";
|
|
4
4
|
const HANDLED_SENTINEL = '__OPENCODE_ANTHROPIC_AUTH_COMMAND_HANDLED__';
|
|
5
|
+
const MAIN_AUTH_REFRESH_TICK_MS = 60_000;
|
|
6
|
+
const DEFAULT_MAIN_REFRESH_BEFORE_EXPIRY_MINUTES = 30;
|
|
5
7
|
let nextPerfRequestId = 1;
|
|
6
8
|
let eventLoopLagMonitorStarted = false;
|
|
7
9
|
function nowMs() {
|
|
@@ -102,7 +104,17 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
102
104
|
mode: getCache1hPersistentMode(initialCache1hStorage),
|
|
103
105
|
});
|
|
104
106
|
setDumpEnabled(isDumpPersistentlyEnabled(initialCache1hStorage));
|
|
107
|
+
setFastModeEnabled(isFastModePersistentlyEnabled(initialCache1hStorage));
|
|
105
108
|
let latestGetAuth = null;
|
|
109
|
+
let mainBackgroundRefreshTimer = null;
|
|
110
|
+
function mainRefreshBeforeExpiryMs(storage) {
|
|
111
|
+
const minutes = storage?.refresh?.refreshBeforeExpiryMinutes ??
|
|
112
|
+
DEFAULT_MAIN_REFRESH_BEFORE_EXPIRY_MINUTES;
|
|
113
|
+
return Math.max(0, minutes) * 60_000;
|
|
114
|
+
}
|
|
115
|
+
function mainRefreshEnabled(storage) {
|
|
116
|
+
return storage?.refresh?.enabled !== false;
|
|
117
|
+
}
|
|
106
118
|
async function buildQuotaCommandSummary() {
|
|
107
119
|
const accounts = [];
|
|
108
120
|
if (latestGetAuth) {
|
|
@@ -180,6 +192,19 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
180
192
|
setDumpEnabled(enabled);
|
|
181
193
|
return executeDumpCommand({ argumentsText, enabled });
|
|
182
194
|
}
|
|
195
|
+
async function executePersistentFastModeCommand(argumentsText) {
|
|
196
|
+
const action = parseFastModeCommandAction(argumentsText);
|
|
197
|
+
if (action.type === 'enable' || action.type === 'disable') {
|
|
198
|
+
const enabled = action.type === 'enable';
|
|
199
|
+
await setFastModePersistentEnabled(enabled);
|
|
200
|
+
setFastModeEnabled(enabled);
|
|
201
|
+
return executeFastModeCommand({ argumentsText, enabled });
|
|
202
|
+
}
|
|
203
|
+
const storage = await loadAccounts();
|
|
204
|
+
const enabled = isFastModePersistentlyEnabled(storage);
|
|
205
|
+
setFastModeEnabled(enabled);
|
|
206
|
+
return executeFastModeCommand({ argumentsText, enabled });
|
|
207
|
+
}
|
|
183
208
|
return {
|
|
184
209
|
config: async (config) => {
|
|
185
210
|
config.command = {
|
|
@@ -196,6 +221,10 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
196
221
|
template: CLAUDE_DUMP_COMMAND_NAME,
|
|
197
222
|
description: 'Show or toggle Anthropic request dump capture for debugging.',
|
|
198
223
|
},
|
|
224
|
+
[CLAUDE_FAST_COMMAND_NAME]: {
|
|
225
|
+
template: CLAUDE_FAST_COMMAND_NAME,
|
|
226
|
+
description: 'Show or toggle Anthropic fast mode for supported Opus models.',
|
|
227
|
+
},
|
|
199
228
|
};
|
|
200
229
|
},
|
|
201
230
|
'command.execute.before': async (input) => {
|
|
@@ -211,6 +240,10 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
211
240
|
await sendIgnoredMessage(ctx, input.sessionID, await executePersistentDumpCommand(input.arguments));
|
|
212
241
|
throwHandledSentinel();
|
|
213
242
|
}
|
|
243
|
+
if (input.command === CLAUDE_FAST_COMMAND_NAME) {
|
|
244
|
+
await sendIgnoredMessage(ctx, input.sessionID, await executePersistentFastModeCommand(input.arguments));
|
|
245
|
+
throwHandledSentinel();
|
|
246
|
+
}
|
|
214
247
|
},
|
|
215
248
|
auth: {
|
|
216
249
|
provider: 'anthropic',
|
|
@@ -250,28 +283,23 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
250
283
|
// The outer `auth` snapshot may be stale if tokens
|
|
251
284
|
// were rotated since the fetch() call was made.
|
|
252
285
|
const freshAuth = await getAuth();
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if (!response.ok) {
|
|
267
|
-
if (response.status >= 500 && attempt < maxRetries) {
|
|
268
|
-
await response.body?.cancel();
|
|
286
|
+
if (!freshAuth.refresh) {
|
|
287
|
+
throw new Error('Token refresh failed: missing refresh token');
|
|
288
|
+
}
|
|
289
|
+
let refreshed;
|
|
290
|
+
try {
|
|
291
|
+
refreshed = await refreshClaudeOAuthToken({
|
|
292
|
+
refreshToken: freshAuth.refresh,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
if (error instanceof ClaudeOAuthRefreshError &&
|
|
297
|
+
error.status >= 500 &&
|
|
298
|
+
attempt < maxRetries) {
|
|
269
299
|
continue;
|
|
270
300
|
}
|
|
271
|
-
|
|
272
|
-
throw new Error(`Token refresh failed: ${response.status} — ${body}`);
|
|
301
|
+
throw error;
|
|
273
302
|
}
|
|
274
|
-
const json = (await response.json());
|
|
275
303
|
// biome-ignore lint/suspicious/noExplicitAny: SDK types don't expose auth.set
|
|
276
304
|
await client.auth.set({
|
|
277
305
|
path: {
|
|
@@ -279,12 +307,12 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
279
307
|
},
|
|
280
308
|
body: {
|
|
281
309
|
type: 'oauth',
|
|
282
|
-
refresh:
|
|
283
|
-
access:
|
|
284
|
-
expires:
|
|
310
|
+
refresh: refreshed.refresh,
|
|
311
|
+
access: refreshed.access,
|
|
312
|
+
expires: refreshed.expires,
|
|
285
313
|
},
|
|
286
314
|
});
|
|
287
|
-
return
|
|
315
|
+
return refreshed.access;
|
|
288
316
|
}
|
|
289
317
|
catch (error) {
|
|
290
318
|
const isNetworkError = error instanceof Error &&
|
|
@@ -309,6 +337,44 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
309
337
|
}
|
|
310
338
|
return refreshPromise;
|
|
311
339
|
}
|
|
340
|
+
function startMainBackgroundRefresh() {
|
|
341
|
+
if (mainBackgroundRefreshTimer) {
|
|
342
|
+
clearInterval(mainBackgroundRefreshTimer);
|
|
343
|
+
mainBackgroundRefreshTimer = null;
|
|
344
|
+
}
|
|
345
|
+
const run = async () => {
|
|
346
|
+
try {
|
|
347
|
+
const storage = await loadAccounts();
|
|
348
|
+
if (!mainRefreshEnabled(storage))
|
|
349
|
+
return;
|
|
350
|
+
const latestAuth = await getAuth();
|
|
351
|
+
if (latestAuth.type !== 'oauth')
|
|
352
|
+
return;
|
|
353
|
+
if (!latestAuth.expires)
|
|
354
|
+
return;
|
|
355
|
+
if (latestAuth.expires - Date.now() >
|
|
356
|
+
mainRefreshBeforeExpiryMs(storage)) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
await refreshMainAccessToken();
|
|
360
|
+
log('[refresh] opencode main oauth refreshed in background', {
|
|
361
|
+
expires: latestAuth.expires,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
log('[refresh] opencode main oauth refresh failed', {
|
|
366
|
+
message: error instanceof Error ? error.message : String(error),
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
mainBackgroundRefreshTimer = setInterval(() => {
|
|
371
|
+
void run();
|
|
372
|
+
}, MAIN_AUTH_REFRESH_TICK_MS);
|
|
373
|
+
if ('unref' in mainBackgroundRefreshTimer) {
|
|
374
|
+
mainBackgroundRefreshTimer.unref();
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
startMainBackgroundRefresh();
|
|
312
378
|
function isReplayableRequest(input, body) {
|
|
313
379
|
if (input instanceof Request && input.body)
|
|
314
380
|
return false;
|
|
@@ -395,17 +461,54 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
395
461
|
async function sendWithAccessToken(input, init, accessToken, trace, route = 'unknown') {
|
|
396
462
|
const start = nowMs();
|
|
397
463
|
const requestHeaders = mergeHeaders(input, init);
|
|
464
|
+
const relayAffinity = requestHeaders.get('x-session-affinity') ||
|
|
465
|
+
requestHeaders.get('x-opencode-session');
|
|
398
466
|
const subagentRequest = isSubagentRequest(requestHeaders);
|
|
399
467
|
requestHeaders.delete('x-parent-session-id');
|
|
400
|
-
|
|
468
|
+
requestHeaders.delete('x-session-affinity');
|
|
469
|
+
requestHeaders.delete('x-opencode-session');
|
|
401
470
|
let body = init?.body;
|
|
471
|
+
let modelForIdentity;
|
|
472
|
+
if (body && typeof body === 'string') {
|
|
473
|
+
try {
|
|
474
|
+
const parsedBody = JSON.parse(body);
|
|
475
|
+
if (typeof parsedBody.model === 'string') {
|
|
476
|
+
modelForIdentity = parsedBody.model;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
catch { }
|
|
480
|
+
}
|
|
481
|
+
const identity = await resolveClaudeCodeIdentity(accessToken, modelForIdentity);
|
|
402
482
|
const originalBytes = typeof body === 'string' ? body.length : undefined;
|
|
403
483
|
if (body && typeof body === 'string') {
|
|
404
484
|
const rewriteStart = nowMs();
|
|
485
|
+
const fastModeRequested = (() => {
|
|
486
|
+
if (!isFastModeEnabled())
|
|
487
|
+
return false;
|
|
488
|
+
try {
|
|
489
|
+
return isFastModeSupportedModel(JSON.parse(body).model);
|
|
490
|
+
}
|
|
491
|
+
catch {
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
})();
|
|
405
495
|
body = await rewriteRequestBody(body, {
|
|
406
496
|
cache1hEnabled: !subagentRequest && isCache1hEnabled(),
|
|
407
497
|
cache1hMode: getCache1hMode(),
|
|
498
|
+
fastModeEnabled: fastModeRequested,
|
|
499
|
+
identity,
|
|
408
500
|
});
|
|
501
|
+
try {
|
|
502
|
+
setOAuthHeaders(requestHeaders, accessToken, {
|
|
503
|
+
body: JSON.parse(body),
|
|
504
|
+
identity,
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
catch {
|
|
508
|
+
setOAuthHeaders(requestHeaders, accessToken, { identity });
|
|
509
|
+
}
|
|
510
|
+
if (fastModeRequested)
|
|
511
|
+
addFastModeBetaHeader(requestHeaders);
|
|
409
512
|
trace?.mark('rewrite_body', {
|
|
410
513
|
route,
|
|
411
514
|
ms: roundMs(nowMs() - rewriteStart),
|
|
@@ -413,6 +516,7 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
413
516
|
rewrittenBytes: body.length,
|
|
414
517
|
cacheEnabled: !subagentRequest && isCache1hEnabled(),
|
|
415
518
|
cacheMode: getCache1hMode(),
|
|
519
|
+
fastModeEnabled: fastModeRequested,
|
|
416
520
|
subagent: subagentRequest,
|
|
417
521
|
});
|
|
418
522
|
}
|
|
@@ -431,6 +535,7 @@ export const AnthropicAuthPlugin = async (ctx) => {
|
|
|
431
535
|
headers: requestHeaders,
|
|
432
536
|
body,
|
|
433
537
|
fallback: directFetch,
|
|
538
|
+
affinity: relayAffinity,
|
|
434
539
|
optimisticResponse: relayConfig?.transport === 'websocket',
|
|
435
540
|
});
|
|
436
541
|
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.0",
|
|
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.0"
|
|
44
44
|
}
|
|
45
45
|
}
|