@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 +1 -1
- package/dist/constants.d.ts +6 -4
- package/dist/constants.js +8 -4
- package/dist/transform.d.ts +3 -8
- package/dist/transform.js +109 -130
- package/package.json +1 -1
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
|
|
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:
|
package/dist/constants.d.ts
CHANGED
|
@@ -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
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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.";
|
package/dist/transform.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
72
|
-
* an HTTP 200 stream.
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
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
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
|
352
|
-
|
|
332
|
+
const latestIndex = rollingIndices[rollingIndices.length - 1];
|
|
333
|
+
const rollingSet = new Set(rollingIndices);
|
|
353
334
|
let bridge;
|
|
354
|
-
let cumulativeBlocks =
|
|
355
|
-
for (let i =
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
bridge =
|
|
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.
|
|
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.
|
|
416
|
-
*
|
|
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.
|
|
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
|
-
|
|
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
|
-
*
|
|
450
|
-
*
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
*
|
|
454
|
-
*
|
|
455
|
-
*
|
|
427
|
+
* Anthropic supports max 4 cache breakpoints per request. Slot allocation:
|
|
428
|
+
*
|
|
429
|
+
* Slot 1 — system 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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
}
|