@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 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. 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.
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) uses top-level automatic caching while keeping stable explicit anchors near the beginning of the request.
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, 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';
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
- 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();
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
- const body = await response.text().catch(() => '');
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: json.refresh_token,
283
- access: json.access_token,
284
- expires: Date.now() + json.expires_in * 1000,
310
+ refresh: refreshed.refresh,
311
+ access: refreshed.access,
312
+ expires: refreshed.expires,
285
313
  },
286
314
  });
287
- return json.access_token;
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
- setOAuthHeaders(requestHeaders, accessToken);
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', {
@@ -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): Headers;
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, USER_AGENT, } from '@cortexkit/anthropic-auth-core';
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.set('authorization', `Bearer ${accessToken}`);
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
- for (const block of parsed.system.slice(2).filter(isRecord).slice(0, 2)) {
357
- setWireCacheControl(block, true);
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
- const firstMessage = parsed.messages.find((message) => isRecord(message));
366
- if (!isRecord(firstMessage))
367
- return;
368
- const content = normalizeContentToArray(firstMessage.content);
369
- if (!content?.length) {
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
- let text = decoder.decode(value, { stream: true });
441
- text = stripToolPrefix(text);
442
- controller.enqueue(encoder.encode(text));
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.0.0",
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.0.0"
43
+ "@cortexkit/anthropic-auth-core": "1.1.0"
44
44
  }
45
45
  }