@sahiljassal/opencode-anthropic-auth 2.1.1 → 2.2.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
@@ -33,7 +33,7 @@ This fork applies **hybrid 1-hour ephemeral prompt caching** on every request, p
33
33
  |---|---|
34
34
  | **System anchor** | Last system block after the identity block (skipped when bridge occupies the slot) |
35
35
  | **messages[0]** | Magic-context split: anchors block[0] + block[1] when stable prefix and volatile delta are merged; otherwise anchors the last cacheable block |
36
- | **messages[1] / bridge** | Last cacheable block of messages[1]; replaced by a bridge anchor when a tool-heavy session pushes the latest user boundary outside Anthropic's 20-block lookback window |
36
+ | **messages[1] / bridge** | Last cacheable block of messages[1]; replaced by a bridge anchor when the block count between two user anchors (counting ALL block types across every role) exceeds Anthropic's 20-block lookback window |
37
37
  | **Rolling latest** | Most recent user message beyond index 1, keeping cache hot across long sessions |
38
38
 
39
39
  Additional behaviours:
@@ -8,10 +8,12 @@ export declare const TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
8
8
  export declare const OAUTH_SCOPES: string[];
9
9
  export declare const TOOL_PREFIX = "mcp_";
10
10
  /**
11
- * Anthropic's sliding-window lookback for cache breakpoints.
12
- * If the distance (in content blocks) between the previous user-role
13
- * message anchor and the latest one exceeds this threshold, a bridge
14
- * anchor is needed so the earlier slots are still within the window.
11
+ * Anthropic's cache lookback window size. Each explicit breakpoint scans at
12
+ * most this many content blocks backward (counting the breakpoint block as
13
+ * position 1, across all roles and types text, thinking, tool_use,
14
+ * tool_result, …). When the estimated block count between two user-role anchors
15
+ * would exceed this threshold, a bridge anchor is inserted to keep the older
16
+ * breakpoint inside the window and the cached prefix reachable.
15
17
  */
16
18
  export declare const ANTHROPIC_CACHE_LOOKBACK_BLOCKS = 20;
17
19
  export declare const REQUIRED_BETAS: string[];
package/dist/constants.js CHANGED
@@ -15,15 +15,19 @@ export const OAUTH_SCOPES = [
15
15
  ];
16
16
  export const TOOL_PREFIX = 'mcp_';
17
17
  /**
18
- * Anthropic's sliding-window lookback for cache breakpoints.
19
- * If the distance (in content blocks) between the previous user-role
20
- * message anchor and the latest one exceeds this threshold, a bridge
21
- * anchor is needed so the earlier slots are still within the window.
18
+ * Anthropic's cache lookback window size. Each explicit breakpoint scans at
19
+ * most this many content blocks backward (counting the breakpoint block as
20
+ * position 1, across all roles and types text, thinking, tool_use,
21
+ * tool_result, …). When the estimated block count between two user-role anchors
22
+ * would exceed this threshold, a bridge anchor is inserted to keep the older
23
+ * breakpoint inside the window and the cached prefix reachable.
22
24
  */
23
25
  export const ANTHROPIC_CACHE_LOOKBACK_BLOCKS = 20;
24
26
  export const REQUIRED_BETAS = [
25
27
  'oauth-2025-04-20',
28
+ 'claude-code-20250219',
26
29
  'interleaved-thinking-2025-05-14',
30
+ 'fine-grained-tool-streaming-2025-05-14',
27
31
  ];
28
32
  export const OPENCODE_IDENTITY_PREFIX = 'You are OpenCode';
29
33
  export const CLAUDE_CODE_IDENTITY = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
@@ -9,7 +9,7 @@ export declare function mergeHeaders(input: FetchInput, init?: RequestInit): Hea
9
9
  */
10
10
  export declare function mergeBetaHeaders(headers: Headers): string;
11
11
  /**
12
- * Set OAuth-required headers on the request: authorization, beta, user-agent.
12
+ * Set OAuth-required headers: authorization, beta, user-agent.
13
13
  * Removes x-api-key since we're using OAuth.
14
14
  */
15
15
  export declare function setOAuthHeaders(headers: Headers, accessToken: string): Headers;
@@ -31,7 +31,6 @@ export declare function isInsecure(): boolean;
31
31
  * Rewrite the request URL to add ?beta=true for /v1/messages requests.
32
32
  * When ANTHROPIC_BASE_URL is set, overrides the origin (protocol + host)
33
33
  * for all API requests flowing through the fetch wrapper.
34
- * Returns the modified input and URL (if applicable).
35
34
  */
36
35
  export declare function rewriteUrl(input: FetchInput): {
37
36
  input: FetchInput;
@@ -62,14 +61,10 @@ type SystemBlock = {
62
61
  * Handles all Anthropic API system formats: undefined, string, or array of text blocks.
63
62
  */
64
63
  export declare function prependClaudeCodeIdentity(system: unknown): SystemBlock[];
65
- /**
66
- * Rewrite the full request body: sanitize system prompt, prefix tool names,
67
- * and apply hybrid 1h prompt caching.
68
- */
69
64
  export declare function rewriteRequestBody(body: string): string;
70
65
  /**
71
- * Error thrown when Anthropic emits a retryable server-side error *inside*
72
- * an HTTP 200 stream. OpenCode recognises ECONNRESET + anthropic-sse syscall
66
+ * Error thrown when Anthropic emits a retryable server-side error inside
67
+ * an HTTP 200 stream. OpenCode recognises ECONNRESET + anthropic-sse syscall
73
68
  * and applies its normal auto-retry flow instead of surfacing an unknown error.
74
69
  */
75
70
  export type RetryableAnthropicStreamError = Error & {
package/dist/transform.js CHANGED
@@ -1,17 +1,8 @@
1
1
  import { ANTHROPIC_CACHE_LOOKBACK_BLOCKS, CLAUDE_CODE_IDENTITY, OPENCODE_IDENTITY_PREFIX, PARAGRAPH_REMOVAL_ANCHORS, REQUIRED_BETAS, TEXT_REPLACEMENTS, TOOL_PREFIX, USER_AGENT, } from "./constants.js";
2
- /**
3
- * Prefix a tool name with TOOL_PREFIX and uppercase the first character.
4
- * Claude Code uses PascalCase tool names (e.g. mcp_Bash, mcp_Read);
5
- * lowercase names (mcp_bash, mcp_read) are flagged as non-Claude-Code clients.
6
- */
7
2
  function prefixName(name) {
8
3
  return `${TOOL_PREFIX}${name.charAt(0).toUpperCase()}${name.slice(1)}`;
9
4
  }
10
- /**
11
- * Reverse prefixName: strip TOOL_PREFIX and restore the original leading case.
12
- */
13
5
  function unprefixName(name) {
14
- // StructuredOutput is still used as StructuredOutput
15
6
  if (name === 'StructuredOutput') {
16
7
  return name;
17
8
  }
@@ -65,7 +56,7 @@ export function mergeBetaHeaders(headers) {
65
56
  return [...new Set([...REQUIRED_BETAS, ...incomingBetasList])].join(',');
66
57
  }
67
58
  /**
68
- * Set OAuth-required headers on the request: authorization, beta, user-agent.
59
+ * Set OAuth-required headers: authorization, beta, user-agent.
69
60
  * Removes x-api-key since we're using OAuth.
70
61
  */
71
62
  export function setOAuthHeaders(headers, accessToken) {
@@ -142,7 +133,6 @@ function resolveBaseUrl() {
142
133
  * Rewrite the request URL to add ?beta=true for /v1/messages requests.
143
134
  * When ANTHROPIC_BASE_URL is set, overrides the origin (protocol + host)
144
135
  * for all API requests flowing through the fetch wrapper.
145
- * Returns the modified input and URL (if applicable).
146
136
  */
147
137
  export function rewriteUrl(input) {
148
138
  let requestUrl = null;
@@ -192,14 +182,11 @@ export function rewriteUrl(input) {
192
182
  * somewhere in the paragraph, the removal works.
193
183
  */
194
184
  export function sanitizeSystemText(text) {
195
- // Split into paragraphs (separated by one or more blank lines)
196
185
  const paragraphs = text.split(/\n\n+/);
197
186
  const filtered = paragraphs.filter((paragraph) => {
198
187
  if (paragraph.includes(OPENCODE_IDENTITY_PREFIX)) {
199
- // If the paragraph contains the identity, drop it entirely
200
188
  return false;
201
189
  }
202
- // Remove paragraphs containing any removal anchor
203
190
  for (const anchor of PARAGRAPH_REMOVAL_ANCHORS) {
204
191
  if (paragraph.includes(anchor))
205
192
  return false;
@@ -207,7 +194,6 @@ export function sanitizeSystemText(text) {
207
194
  return true;
208
195
  });
209
196
  let result = filtered.join('\n\n');
210
- // Apply inline text replacements
211
197
  for (const rule of TEXT_REPLACEMENTS) {
212
198
  result = result.replace(rule.match, rule.replacement);
213
199
  }
@@ -217,9 +203,6 @@ function isRecord(value) {
217
203
  return value != null && typeof value === 'object' && !Array.isArray(value);
218
204
  }
219
205
  const CACHE_1H = { type: 'ephemeral', ttl: '1h' };
220
- // ---------------------------------------------------------------------------
221
- // Cache control primitives
222
- // ---------------------------------------------------------------------------
223
206
  function removeCacheControl(value) {
224
207
  if (!isRecord(value))
225
208
  return;
@@ -248,21 +231,23 @@ function setWireCacheControl(value) {
248
231
  value.cache_control = { ...CACHE_1H };
249
232
  return true;
250
233
  }
251
- // ---------------------------------------------------------------------------
252
- // Content-block helpers
253
- // ---------------------------------------------------------------------------
254
234
  /**
255
235
  * Returns true for content block types that accept cache_control.
256
- * Anthropic rejects cache_control on thinking / redacted_thinking blocks.
236
+ * Anthropic rejects cache_control on thinking / redacted_thinking blocks
237
+ * and silently skips empty text blocks without caching them.
257
238
  */
258
239
  function isCacheableContentBlock(block) {
259
240
  if (!isRecord(block))
260
241
  return false;
261
- return block.type !== 'thinking' && block.type !== 'redacted_thinking';
242
+ if (block.type === 'thinking' || block.type === 'redacted_thinking')
243
+ return false;
244
+ if (block.type === 'text' && !String(block.text ?? '').trim())
245
+ return false;
246
+ return true;
262
247
  }
263
248
  /**
264
249
  * Normalises message content to an array of blocks, then filters to only
265
- * cacheable types. Returns undefined when there is nothing to anchor.
250
+ * cacheable types. Returns undefined when there is nothing to anchor.
266
251
  */
267
252
  function getCacheableContentBlocks(message) {
268
253
  if (!isRecord(message))
@@ -272,7 +257,6 @@ function getCacheableContentBlocks(message) {
272
257
  blocks = message.content;
273
258
  }
274
259
  else if (typeof message.content === 'string') {
275
- // Normalise inline string to block array in place so downstream sees array
276
260
  const normalised = [{ type: 'text', text: message.content }];
277
261
  message.content = normalised;
278
262
  blocks = normalised;
@@ -283,38 +267,21 @@ function getCacheableContentBlocks(message) {
283
267
  const cacheable = blocks.filter(isCacheableContentBlock);
284
268
  return cacheable.length > 0 ? cacheable : undefined;
285
269
  }
286
- /**
287
- * Total cacheable content-block count for a message (used for lookback math).
288
- */
289
270
  function messageContentBlockCount(message) {
290
271
  return getCacheableContentBlocks(message)?.length ?? 0;
291
272
  }
292
- // ---------------------------------------------------------------------------
293
- // Message-anchor setters
294
- // ---------------------------------------------------------------------------
295
- /**
296
- * Anchor the last cacheable block of a message.
297
- * Returns false (and sets nothing) when there are no cacheable blocks.
298
- */
299
273
  function setMessageCacheAnchor(message) {
300
274
  const blocks = getCacheableContentBlocks(message);
301
275
  if (!blocks)
302
276
  return false;
303
277
  return setWireCacheControl(blocks[blocks.length - 1]);
304
278
  }
305
- /**
306
- * Anchor the FIRST cacheable block of a message (for magic-context split).
307
- */
308
279
  function setFirstMessageCacheAnchor(message) {
309
280
  const blocks = getCacheableContentBlocks(message);
310
281
  if (!blocks)
311
282
  return false;
312
283
  return setWireCacheControl(blocks[0]);
313
284
  }
314
- /**
315
- * Anchor the SECOND cacheable block of a message (for magic-context split).
316
- * Returns false when fewer than two cacheable blocks exist.
317
- */
318
285
  function setSecondMessageCacheAnchor(message) {
319
286
  const blocks = getCacheableContentBlocks(message);
320
287
  if (!blocks || blocks.length < 2)
@@ -322,49 +289,60 @@ function setSecondMessageCacheAnchor(message) {
322
289
  return setWireCacheControl(blocks[1]);
323
290
  }
324
291
  /**
325
- * Walk all messages and collect user-role (or tool_result) anchor positions.
326
- * Returns the `latest` position (index > 1) and a `bridge` position placed
327
- * whenever the cumulative block distance from bridge→latest exceeds
328
- * ANTHROPIC_CACHE_LOOKBACK_BLOCKS, ensuring both are within the sliding window.
292
+ * Total positional block count for a message regardless of role or type.
293
+ * Anthropic's cache lookback window counts every content block (text,
294
+ * thinking, tool_use, tool_result, …), so distance math must use raw counts,
295
+ * not the cacheable-only filter used to validate anchor placement.
296
+ */
297
+ function rawBlockCount(message) {
298
+ if (!isRecord(message))
299
+ return 0;
300
+ const { content } = message;
301
+ if (Array.isArray(content))
302
+ return content.length;
303
+ if (typeof content === 'string')
304
+ return 1;
305
+ return 0;
306
+ }
307
+ /**
308
+ * Collect user-role anchor indices that hold at least one cacheable block,
309
+ * then pick the `latest` (index > 1) and a `bridge`. The bridge is the
310
+ * furthest-back valid user anchor where the block count between it and
311
+ * `latest` — counting ALL content blocks across user and assistant turns,
312
+ * excluding the anchor's own blocks — does not exceed
313
+ * ANTHROPIC_CACHE_LOOKBACK_BLOCKS. This keeps the older breakpoint inside
314
+ * Anthropic's sliding lookback window so the cached prefix stays reachable.
329
315
  */
330
316
  function selectHybridMessageAnchors(messages) {
331
- // Collect positions of user-role messages that have cacheable content
332
- const userPositions = messages
333
- .map((msg, index) => {
334
- if (!isRecord(msg))
335
- return null;
336
- if (msg.role !== 'user')
337
- return null;
338
- const blockCount = messageContentBlockCount(msg);
339
- if (blockCount === 0)
340
- return null;
341
- return { index, blockCount };
342
- })
343
- .filter((p) => p !== null);
344
- // We only care about positions beyond index 1 (0 and 1 are always anchored)
345
- const rollingPositions = userPositions.filter((p) => p.index > 1);
346
- if (rollingPositions.length === 0) {
317
+ // Collect indices of user messages beyond index 1 that have ≥1 cacheable block.
318
+ // Indices 0 and 1 are handled as fixed slots in applyHybridCache1h, not rolling.
319
+ const rollingIndices = [];
320
+ messages.forEach((msg, index) => {
321
+ if (index > 1 &&
322
+ isRecord(msg) &&
323
+ msg.role === 'user' &&
324
+ messageContentBlockCount(msg) > 0) {
325
+ rollingIndices.push(index);
326
+ }
327
+ });
328
+ if (rollingIndices.length === 0) {
347
329
  return { latest: undefined, bridge: undefined };
348
330
  }
349
- // Non-null: rollingPositions is non-empty (early return guards above)
350
331
  // biome-ignore lint/style/noNonNullAssertion: guarded by length check above
351
- const latest = rollingPositions[rollingPositions.length - 1];
352
- // Find a bridge: walk back from latest until cumulative blocks exceed window
332
+ const latestIndex = rollingIndices[rollingIndices.length - 1];
333
+ const rollingSet = new Set(rollingIndices);
353
334
  let bridge;
354
- let cumulativeBlocks = latest.blockCount;
355
- for (let i = rollingPositions.length - 2; i >= 0; i--) {
356
- // biome-ignore lint/style/noNonNullAssertion: index is within bounds
357
- cumulativeBlocks += rollingPositions[i].blockCount;
358
- if (cumulativeBlocks > ANTHROPIC_CACHE_LOOKBACK_BLOCKS) {
359
- bridge = rollingPositions[i];
360
- break;
335
+ let cumulativeBlocks = 0;
336
+ for (let i = latestIndex - 1; i >= 0; i--) {
337
+ if (rollingSet.has(i)) {
338
+ if (cumulativeBlocks > ANTHROPIC_CACHE_LOOKBACK_BLOCKS)
339
+ break;
340
+ bridge = i;
361
341
  }
342
+ cumulativeBlocks += rawBlockCount(messages[i]);
362
343
  }
363
- return { latest, bridge };
344
+ return { latest: latestIndex, bridge };
364
345
  }
365
- // ---------------------------------------------------------------------------
366
- // System-anchor setter
367
- // ---------------------------------------------------------------------------
368
346
  function systemBlockText(block) {
369
347
  return isRecord(block) && typeof block.text === 'string' ? block.text : '';
370
348
  }
@@ -374,7 +352,7 @@ function systemBlockText(block) {
374
352
  * system cache anchor.
375
353
  *
376
354
  * OpenCode normally emits these as one merged block, but some hooks can cause
377
- * them to arrive split across multiple blocks. Without coalescing, byte-
355
+ * them to arrive split across multiple blocks. Without coalescing, byte-
378
356
  * identical system text flips between merged/split layouts and moves the
379
357
  * cache_control breakpoint — busting the cache every turn.
380
358
  *
@@ -388,15 +366,12 @@ function coalesceHybridSystemTail(parsed) {
388
366
  return;
389
367
  const system = parsed.system;
390
368
  let prefixCount = 0;
391
- // Skip optional billing-header block (not present in this fork but guard is safe)
392
369
  if (systemBlockText(system[prefixCount]).startsWith('x-anthropic-billing-header:')) {
393
370
  prefixCount++;
394
371
  }
395
- // Skip identity block
396
372
  if (systemBlockText(system[prefixCount]) === CLAUDE_CODE_IDENTITY) {
397
373
  prefixCount++;
398
374
  }
399
- // tailStart points to the first plugin-added block (one after primary prompt)
400
375
  const tailStart = prefixCount + 1;
401
376
  if (tailStart >= system.length - 1)
402
377
  return;
@@ -412,8 +387,8 @@ function coalesceHybridSystemTail(parsed) {
412
387
  }
413
388
  /**
414
389
  * Place a cache anchor on the last system block that follows the
415
- * CLAUDE_CODE_IDENTITY block. When there are no system blocks, or all
416
- * system blocks precede the identity, nothing is anchored.
390
+ * CLAUDE_CODE_IDENTITY block. When there are no system blocks after the
391
+ * identity, nothing is anchored.
417
392
  */
418
393
  function setHybridSystemAnchor(parsed) {
419
394
  if (!Array.isArray(parsed.system))
@@ -424,11 +399,8 @@ function setHybridSystemAnchor(parsed) {
424
399
  .filter(isRecord);
425
400
  setWireCacheControl(afterIdentity[afterIdentity.length - 1]);
426
401
  }
427
- // ---------------------------------------------------------------------------
428
- // Trailing-assistant strip (Tier 2)
429
- // ---------------------------------------------------------------------------
430
402
  /**
431
- * Remove trailing assistant-role messages. OAuth endpoints reject requests
403
+ * Remove trailing assistant-role messages. OAuth endpoints reject requests
432
404
  * that end with an assistant turn (assistant prefill is not supported).
433
405
  */
434
406
  function stripTrailingAssistantMessages(parsed) {
@@ -440,52 +412,60 @@ function stripTrailingAssistantMessages(parsed) {
440
412
  parsed.messages.pop();
441
413
  }
442
414
  }
443
- // ---------------------------------------------------------------------------
444
- // Main hybrid cache logic
445
- // ---------------------------------------------------------------------------
415
+ /**
416
+ * Returns true when messages[0] carries a merged stable-prefix layout
417
+ * (≥2 cacheable blocks). In that case anchoring the last block would bust
418
+ * the cache every turn because the tail is volatile; instead we anchor
419
+ * block[0] and block[1] (the two stable-prefix blocks).
420
+ */
421
+ function isMagicContextLayout(blocks) {
422
+ return blocks.length >= 2;
423
+ }
446
424
  /**
447
425
  * Apply hybrid 1h prompt-caching breakpoints to parsed request body.
448
426
  *
449
- * Breakpoint slots (Anthropic supports max 4 per request):
450
- * 1. Last system block after the identity block (skipped when bridge used)
451
- * 2. messages[0]first cacheable block (magic-context: slot 2a)
452
- * 3. messages[0] second cacheable block (magic-context split only)
453
- * OR messages[1] last cacheable block (normal path)
454
- * OR bridge user message (when bridge detected)
455
- * 4. Latest user/tool-result message (index > 1) (when present)
427
+ * Anthropic supports max 4 cache breakpoints per request. Slot allocation:
428
+ *
429
+ * Slot 1system anchor: last block after identity.
430
+ * Skipped when a bridge is used (bridge takes that slot).
431
+ *
432
+ * Slots 2+3 messages[0]:
433
+ * Normal layout (1 cacheable block): slot 2 = last block of msg[0];
434
+ * slot 3 = bridge message OR messages[1] (no bridge).
435
+ * • Magic-context layout (≥2 cacheable blocks): slot 2 = block[0],
436
+ * slot 3 = block[1]. messages[1] is skipped (msg[0] uses both slots).
437
+ *
438
+ * Slot 4 — rolling latest: last user/tool-result message beyond index 1.
439
+ *
440
+ * Bridge and magic-context are independent: when both apply simultaneously,
441
+ * the slot budget is: system(skipped) + msg0-block0 + bridge + latest = 4.
442
+ * The bridge anchor is ALWAYS placed when detected, regardless of msg0 layout.
456
443
  */
457
444
  function applyHybridCache1h(parsed) {
458
445
  removeAllCacheControls(parsed);
459
446
  coalesceHybridSystemTail(parsed);
460
447
  const messages = Array.isArray(parsed.messages) ? parsed.messages : [];
461
448
  const { latest, bridge } = selectHybridMessageAnchors(messages);
462
- // --- Slot 1: system anchor (skip when bridge will occupy a slot) ---
463
- if (!bridge) {
449
+ if (bridge === undefined) {
464
450
  setHybridSystemAnchor(parsed);
465
451
  }
466
- // --- Slots 2 & 3: messages[0] ---
467
452
  const msg0 = messages[0];
468
453
  const msg0Blocks = getCacheableContentBlocks(msg0);
469
- if (msg0Blocks && msg0Blocks.length >= 2) {
470
- // Magic-context split: stable prefix is in block[0] and block[1];
471
- // anchoring last block would bust cache every turn.
454
+ if (msg0Blocks && isMagicContextLayout(msg0Blocks)) {
472
455
  setFirstMessageCacheAnchor(msg0);
473
456
  setSecondMessageCacheAnchor(msg0);
474
457
  }
475
458
  else {
476
459
  setMessageCacheAnchor(msg0);
477
- // --- Slot 3 (normal): messages[1] or bridge ---
478
- if (bridge) {
479
- setHybridSystemAnchor(parsed); // system anchor reclaimed for bridge support
480
- setMessageCacheAnchor(messages[bridge.index]);
481
- }
482
- else {
460
+ if (bridge === undefined) {
483
461
  setMessageCacheAnchor(messages[1]);
484
462
  }
485
463
  }
486
- // --- Slot 4: rolling latest user anchor ---
487
- if (latest) {
488
- setMessageCacheAnchor(messages[latest.index]);
464
+ if (bridge !== undefined) {
465
+ setMessageCacheAnchor(messages[bridge]);
466
+ }
467
+ if (latest !== undefined) {
468
+ setMessageCacheAnchor(messages[latest]);
489
469
  }
490
470
  }
491
471
  /**
@@ -527,16 +507,11 @@ export function prependClaudeCodeIdentity(system) {
527
507
  }
528
508
  return { type: 'text', text: String(item) };
529
509
  });
530
- // Idempotency: don't double-prepend if first block already has the identity
531
510
  if (sanitized[0]?.text === CLAUDE_CODE_IDENTITY) {
532
511
  return sanitized;
533
512
  }
534
513
  return [identityBlock, ...sanitized];
535
514
  }
536
- /**
537
- * Rewrite the full request body: sanitize system prompt, prefix tool names,
538
- * and apply hybrid 1h prompt caching.
539
- */
540
515
  export function rewriteRequestBody(body) {
541
516
  try {
542
517
  const parsed = JSON.parse(body);
@@ -549,7 +524,6 @@ export function rewriteRequestBody(body) {
549
524
  return body;
550
525
  }
551
526
  }
552
- /** Find the first SSE event boundary (\n\n or \r\n\r\n) in a text buffer. */
553
527
  function findSseBoundary(value) {
554
528
  const lf = value.indexOf('\n\n');
555
529
  const crlf = value.indexOf('\r\n\r\n');
@@ -615,9 +589,7 @@ function retryableAnthropicStreamErrorFromRawEvent(rawEvent) {
615
589
  if (eventName !== 'error' && stringField(data, 'type') !== 'error')
616
590
  return null;
617
591
  const errorObj = asDiagnosticRecord(data?.error);
618
- const errorType = stringField(errorObj, 'type') ??
619
- stringField(errorObj, 'code') ??
620
- undefined;
592
+ const errorType = stringField(errorObj, 'type') ?? stringField(errorObj, 'code') ?? undefined;
621
593
  const message = stringField(errorObj, 'message') ??
622
594
  stringField(data, 'message') ??
623
595
  errorType ??
@@ -645,20 +617,16 @@ function updateSseErrorState(state, text) {
645
617
  }
646
618
  return null;
647
619
  }
648
- // ---------------------------------------------------------------------------
649
- // Buffered tool-prefix rewriting (v1.10.0)
650
- // ---------------------------------------------------------------------------
651
620
  /**
652
621
  * Rewrite the tool prefix from the safe portion of a text buffer.
653
622
  * Holds back any suffix that could be the start of a partial `"name"` marker
654
- * spanning a chunk boundary. Pass flush=true on stream end to emit everything.
623
+ * spanning a chunk boundary. Pass flush=true on stream end to emit everything.
655
624
  */
656
625
  function splitToolPrefixRewriteBuffer(buffer, flush = false) {
657
626
  if (flush)
658
627
  return { ready: stripToolPrefix(buffer), pending: '' };
659
628
  let keepFrom = buffer.length;
660
629
  const marker = '"name"';
661
- // Hold back any suffix that starts a partial marker
662
630
  const partialStart = Math.max(0, buffer.length - marker.length + 1);
663
631
  for (let i = partialStart; i < buffer.length; i++) {
664
632
  if (marker.startsWith(buffer.slice(i))) {
@@ -666,7 +634,6 @@ function splitToolPrefixRewriteBuffer(buffer, flush = false) {
666
634
  break;
667
635
  }
668
636
  }
669
- // Also hold back if the last occurrence of the marker is incomplete
670
637
  const lastMarker = buffer.lastIndexOf(marker);
671
638
  if (lastMarker !== -1) {
672
639
  const tail = buffer.slice(lastMarker);
@@ -682,9 +649,6 @@ function splitToolPrefixRewriteBuffer(buffer, flush = false) {
682
649
  }
683
650
  return { ready: stripToolPrefix(buffer), pending: '' };
684
651
  }
685
- // ---------------------------------------------------------------------------
686
- // Stream response wrapper
687
- // ---------------------------------------------------------------------------
688
652
  /**
689
653
  * Create a streaming response that strips the tool prefix from tool names.
690
654
  * Detects retryable Anthropic server errors inside HTTP 200 streams and
@@ -711,8 +675,17 @@ export function createStrippedStream(response) {
711
675
  const { done, value } = await reader.read();
712
676
  if (done) {
713
677
  const finalDecoded = decoder.decode();
714
- const retryableError = updateSseErrorState(sseErrors, finalDecoded);
678
+ let retryableError = updateSseErrorState(sseErrors, finalDecoded);
679
+ if (!retryableError && sseErrors.pending) {
680
+ retryableError = retryableAnthropicStreamErrorFromRawEvent(sseErrors.pending);
681
+ }
715
682
  if (retryableError) {
683
+ try {
684
+ await reader.cancel();
685
+ }
686
+ catch {
687
+ /* ignore cancel failure */
688
+ }
716
689
  releaseReader();
717
690
  throw retryableError;
718
691
  }
@@ -726,6 +699,12 @@ export function createStrippedStream(response) {
726
699
  const decoded = decoder.decode(value, { stream: true });
727
700
  const retryableError = updateSseErrorState(sseErrors, decoded);
728
701
  if (retryableError) {
702
+ try {
703
+ await reader.cancel();
704
+ }
705
+ catch {
706
+ /* ignore cancel failure */
707
+ }
729
708
  releaseReader();
730
709
  throw retryableError;
731
710
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sahiljassal/opencode-anthropic-auth",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/shljsl75891/opencode-anthropic-auth.git"