@sahiljassal/opencode-anthropic-auth 2.0.0 → 2.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 +3 -3
- package/dist/constants.d.ts +7 -0
- package/dist/constants.js +7 -0
- package/dist/transform.js +192 -25
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -36,9 +36,9 @@ This reduces token usage and latency on repeated requests by reusing cached prom
|
|
|
36
36
|
|
|
37
37
|
## Configuration
|
|
38
38
|
|
|
39
|
-
| Variable
|
|
40
|
-
|
|
41
|
-
| `ANTHROPIC_BASE_URL` | Override API endpoint URL (e.g. for proxying). Must be a valid HTTP(S) URL.
|
|
39
|
+
| Variable | Description |
|
|
40
|
+
| -------------------- | ---------------------------------------------------------------------------------------- |
|
|
41
|
+
| `ANTHROPIC_BASE_URL` | Override API endpoint URL (e.g. for proxying). Must be a valid HTTP(S) URL. |
|
|
42
42
|
| `ANTHROPIC_INSECURE` | Set to `1` or `true` to skip TLS verification. Only effective with `ANTHROPIC_BASE_URL`. |
|
|
43
43
|
|
|
44
44
|
## License
|
package/dist/constants.d.ts
CHANGED
|
@@ -7,6 +7,13 @@ export declare const CODE_CALLBACK_URL = "https://platform.claude.com/oauth/code
|
|
|
7
7
|
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
|
+
/**
|
|
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.
|
|
15
|
+
*/
|
|
16
|
+
export declare const ANTHROPIC_CACHE_LOOKBACK_BLOCKS = 20;
|
|
10
17
|
export declare const REQUIRED_BETAS: string[];
|
|
11
18
|
export declare const OPENCODE_IDENTITY_PREFIX = "You are OpenCode";
|
|
12
19
|
export declare const CLAUDE_CODE_IDENTITY = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
package/dist/constants.js
CHANGED
|
@@ -14,6 +14,13 @@ export const OAUTH_SCOPES = [
|
|
|
14
14
|
'user:file_upload',
|
|
15
15
|
];
|
|
16
16
|
export const TOOL_PREFIX = 'mcp_';
|
|
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.
|
|
22
|
+
*/
|
|
23
|
+
export const ANTHROPIC_CACHE_LOOKBACK_BLOCKS = 20;
|
|
17
24
|
export const REQUIRED_BETAS = [
|
|
18
25
|
'oauth-2025-04-20',
|
|
19
26
|
'interleaved-thinking-2025-05-14',
|
package/dist/transform.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CLAUDE_CODE_IDENTITY, OPENCODE_IDENTITY_PREFIX, PARAGRAPH_REMOVAL_ANCHORS, REQUIRED_BETAS, TEXT_REPLACEMENTS, TOOL_PREFIX, USER_AGENT, } from "./constants.js";
|
|
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
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);
|
|
@@ -217,6 +217,9 @@ function isRecord(value) {
|
|
|
217
217
|
return value != null && typeof value === 'object' && !Array.isArray(value);
|
|
218
218
|
}
|
|
219
219
|
const CACHE_1H = { type: 'ephemeral', ttl: '1h' };
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// Cache control primitives
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
220
223
|
function removeCacheControl(value) {
|
|
221
224
|
if (!isRecord(value))
|
|
222
225
|
return;
|
|
@@ -245,36 +248,199 @@ function setWireCacheControl(value) {
|
|
|
245
248
|
value.cache_control = { ...CACHE_1H };
|
|
246
249
|
return true;
|
|
247
250
|
}
|
|
248
|
-
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
// Content-block helpers
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
/**
|
|
255
|
+
* Returns true for content block types that accept cache_control.
|
|
256
|
+
* Anthropic rejects cache_control on thinking / redacted_thinking blocks.
|
|
257
|
+
*/
|
|
258
|
+
function isCacheableContentBlock(block) {
|
|
259
|
+
if (!isRecord(block))
|
|
260
|
+
return false;
|
|
261
|
+
return block.type !== 'thinking' && block.type !== 'redacted_thinking';
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Normalises message content to an array of blocks, then filters to only
|
|
265
|
+
* cacheable types. Returns undefined when there is nothing to anchor.
|
|
266
|
+
*/
|
|
267
|
+
function getCacheableContentBlocks(message) {
|
|
249
268
|
if (!isRecord(message))
|
|
269
|
+
return undefined;
|
|
270
|
+
let blocks;
|
|
271
|
+
if (Array.isArray(message.content)) {
|
|
272
|
+
blocks = message.content;
|
|
273
|
+
}
|
|
274
|
+
else if (typeof message.content === 'string') {
|
|
275
|
+
// Normalise inline string to block array in place so downstream sees array
|
|
276
|
+
const normalised = [{ type: 'text', text: message.content }];
|
|
277
|
+
message.content = normalised;
|
|
278
|
+
blocks = normalised;
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
const cacheable = blocks.filter(isCacheableContentBlock);
|
|
284
|
+
return cacheable.length > 0 ? cacheable : undefined;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Total cacheable content-block count for a message (used for lookback math).
|
|
288
|
+
*/
|
|
289
|
+
function messageContentBlockCount(message) {
|
|
290
|
+
return getCacheableContentBlocks(message)?.length ?? 0;
|
|
291
|
+
}
|
|
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
|
+
function setMessageCacheAnchor(message) {
|
|
300
|
+
const blocks = getCacheableContentBlocks(message);
|
|
301
|
+
if (!blocks)
|
|
250
302
|
return false;
|
|
251
|
-
|
|
252
|
-
? message.content
|
|
253
|
-
: typeof message.content === 'string'
|
|
254
|
-
? [{ type: 'text', text: message.content }]
|
|
255
|
-
: null;
|
|
256
|
-
if (!content?.length)
|
|
257
|
-
return setWireCacheControl(message);
|
|
258
|
-
message.content = content;
|
|
259
|
-
const target = [...content]
|
|
260
|
-
.reverse()
|
|
261
|
-
.find((b) => isRecord(b) && b.type !== 'thinking');
|
|
262
|
-
return setWireCacheControl(target ?? message);
|
|
303
|
+
return setWireCacheControl(blocks[blocks.length - 1]);
|
|
263
304
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
305
|
+
/**
|
|
306
|
+
* Anchor the FIRST cacheable block of a message (for magic-context split).
|
|
307
|
+
*/
|
|
308
|
+
function setFirstMessageCacheAnchor(message) {
|
|
309
|
+
const blocks = getCacheableContentBlocks(message);
|
|
310
|
+
if (!blocks)
|
|
311
|
+
return false;
|
|
312
|
+
return setWireCacheControl(blocks[0]);
|
|
313
|
+
}
|
|
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
|
+
function setSecondMessageCacheAnchor(message) {
|
|
319
|
+
const blocks = getCacheableContentBlocks(message);
|
|
320
|
+
if (!blocks || blocks.length < 2)
|
|
321
|
+
return false;
|
|
322
|
+
return setWireCacheControl(blocks[1]);
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
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.
|
|
329
|
+
*/
|
|
330
|
+
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) {
|
|
347
|
+
return { latest: undefined, bridge: undefined };
|
|
348
|
+
}
|
|
349
|
+
// Non-null: rollingPositions is non-empty (early return guards above)
|
|
350
|
+
// 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
|
|
353
|
+
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;
|
|
361
|
+
}
|
|
273
362
|
}
|
|
363
|
+
return { latest, bridge };
|
|
364
|
+
}
|
|
365
|
+
// ---------------------------------------------------------------------------
|
|
366
|
+
// System-anchor setter
|
|
367
|
+
// ---------------------------------------------------------------------------
|
|
368
|
+
/**
|
|
369
|
+
* Place a cache anchor on the last system block that follows the
|
|
370
|
+
* CLAUDE_CODE_IDENTITY block. When there are no system blocks, or all
|
|
371
|
+
* system blocks precede the identity, nothing is anchored.
|
|
372
|
+
*/
|
|
373
|
+
function setHybridSystemAnchor(parsed) {
|
|
374
|
+
if (!Array.isArray(parsed.system))
|
|
375
|
+
return;
|
|
376
|
+
const identityIdx = parsed.system.findIndex((b) => isRecord(b) && b.text === CLAUDE_CODE_IDENTITY);
|
|
377
|
+
const afterIdentity = parsed.system
|
|
378
|
+
.slice(identityIdx >= 0 ? identityIdx + 1 : 0)
|
|
379
|
+
.filter(isRecord);
|
|
380
|
+
setWireCacheControl(afterIdentity[afterIdentity.length - 1]);
|
|
381
|
+
}
|
|
382
|
+
// ---------------------------------------------------------------------------
|
|
383
|
+
// Trailing-assistant strip (Tier 2)
|
|
384
|
+
// ---------------------------------------------------------------------------
|
|
385
|
+
/**
|
|
386
|
+
* Remove trailing assistant-role messages. OAuth endpoints reject requests
|
|
387
|
+
* that end with an assistant turn (assistant prefill is not supported).
|
|
388
|
+
*/
|
|
389
|
+
function stripTrailingAssistantMessages(parsed) {
|
|
274
390
|
if (!Array.isArray(parsed.messages))
|
|
275
391
|
return;
|
|
276
|
-
|
|
277
|
-
|
|
392
|
+
while (parsed.messages.length > 0 &&
|
|
393
|
+
isRecord(parsed.messages[parsed.messages.length - 1]) &&
|
|
394
|
+
parsed.messages[parsed.messages.length - 1].role === 'assistant') {
|
|
395
|
+
parsed.messages.pop();
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
// ---------------------------------------------------------------------------
|
|
399
|
+
// Main hybrid cache logic
|
|
400
|
+
// ---------------------------------------------------------------------------
|
|
401
|
+
/**
|
|
402
|
+
* Apply hybrid 1h prompt-caching breakpoints to parsed request body.
|
|
403
|
+
*
|
|
404
|
+
* Breakpoint slots (Anthropic supports max 4 per request):
|
|
405
|
+
* 1. Last system block after the identity block (skipped when bridge used)
|
|
406
|
+
* 2. messages[0] — first cacheable block (magic-context: slot 2a)
|
|
407
|
+
* 3. messages[0] — second cacheable block (magic-context split only)
|
|
408
|
+
* OR messages[1] last cacheable block (normal path)
|
|
409
|
+
* OR bridge user message (when bridge detected)
|
|
410
|
+
* 4. Latest user/tool-result message (index > 1) (when present)
|
|
411
|
+
*/
|
|
412
|
+
function applyHybridCache1h(parsed) {
|
|
413
|
+
removeAllCacheControls(parsed);
|
|
414
|
+
const messages = Array.isArray(parsed.messages) ? parsed.messages : [];
|
|
415
|
+
const { latest, bridge } = selectHybridMessageAnchors(messages);
|
|
416
|
+
// --- Slot 1: system anchor (skip when bridge will occupy a slot) ---
|
|
417
|
+
if (!bridge) {
|
|
418
|
+
setHybridSystemAnchor(parsed);
|
|
419
|
+
}
|
|
420
|
+
// --- Slots 2 & 3: messages[0] ---
|
|
421
|
+
const msg0 = messages[0];
|
|
422
|
+
const msg0Blocks = getCacheableContentBlocks(msg0);
|
|
423
|
+
if (msg0Blocks && msg0Blocks.length >= 2) {
|
|
424
|
+
// Magic-context split: stable prefix is in block[0] and block[1];
|
|
425
|
+
// anchoring last block would bust cache every turn.
|
|
426
|
+
setFirstMessageCacheAnchor(msg0);
|
|
427
|
+
setSecondMessageCacheAnchor(msg0);
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
setMessageCacheAnchor(msg0);
|
|
431
|
+
// --- Slot 3 (normal): messages[1] or bridge ---
|
|
432
|
+
if (bridge) {
|
|
433
|
+
setHybridSystemAnchor(parsed); // system anchor reclaimed for bridge support
|
|
434
|
+
setMessageCacheAnchor(messages[bridge.index]);
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
setMessageCacheAnchor(messages[1]);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
// --- Slot 4: rolling latest user anchor ---
|
|
441
|
+
if (latest) {
|
|
442
|
+
setMessageCacheAnchor(messages[latest.index]);
|
|
443
|
+
}
|
|
278
444
|
}
|
|
279
445
|
/**
|
|
280
446
|
* Sanitize system prompt and prepend Claude Code identity.
|
|
@@ -329,6 +495,7 @@ export function rewriteRequestBody(body) {
|
|
|
329
495
|
try {
|
|
330
496
|
const parsed = JSON.parse(body);
|
|
331
497
|
parsed.system = prependClaudeCodeIdentity(parsed.system);
|
|
498
|
+
stripTrailingAssistantMessages(parsed);
|
|
332
499
|
applyHybridCache1h(parsed);
|
|
333
500
|
return prefixToolNames(parsed);
|
|
334
501
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sahiljassal/opencode-anthropic-auth",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/shljsl75891/opencode-anthropic-auth.git"
|
|
@@ -31,14 +31,14 @@
|
|
|
31
31
|
"@opencode-ai/plugin": "*"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@biomejs/biome": "2.4.
|
|
34
|
+
"@biomejs/biome": "2.4.16",
|
|
35
35
|
"@changesets/changelog-github": "^0.7.0",
|
|
36
36
|
"@changesets/cli": "^2.31.0",
|
|
37
|
-
"@opencode-ai/plugin": "1.
|
|
37
|
+
"@opencode-ai/plugin": "1.17.3",
|
|
38
38
|
"@tsconfig/bun": "1.0.10",
|
|
39
39
|
"@types/bun": "1.3.14",
|
|
40
40
|
"dedent": "^1.7.2",
|
|
41
|
-
"lefthook": "2.1.
|
|
41
|
+
"lefthook": "2.1.9",
|
|
42
42
|
"typescript": "6.0.3"
|
|
43
43
|
}
|
|
44
44
|
}
|