@sahiljassal/opencode-anthropic-auth 2.2.0 → 2.4.1
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/dist/index.js +23 -9
- package/dist/transform.d.ts +17 -2
- package/dist/transform.js +114 -4
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { authorize, exchange } from "./auth.js";
|
|
2
2
|
import { CLIENT_ID, TOKEN_URL } from "./constants.js";
|
|
3
|
-
import { createStrippedStream, isInsecure, mergeHeaders, rewriteRequestBody, rewriteUrl, setOAuthHeaders, } from "./transform.js";
|
|
3
|
+
import { computeRetryAfterDelayMs, createStrippedStream, extractModelId, isInsecure, mergeHeaders, rewriteRequestBody, rewriteUrl, setOAuthHeaders, } from "./transform.js";
|
|
4
|
+
const MAX_429_RETRIES = 3;
|
|
4
5
|
export const AnthropicAuthPlugin = async ({ client }) => {
|
|
5
6
|
return {
|
|
6
7
|
auth: {
|
|
@@ -103,19 +104,32 @@ export const AnthropicAuthPlugin = async ({ client }) => {
|
|
|
103
104
|
auth.access = await refreshPromise;
|
|
104
105
|
}
|
|
105
106
|
const requestHeaders = mergeHeaders(input, init);
|
|
107
|
+
const rawBody = init?.body;
|
|
108
|
+
const modelId = typeof rawBody === 'string'
|
|
109
|
+
? extractModelId(rawBody)
|
|
110
|
+
: undefined;
|
|
106
111
|
// biome-ignore lint/style/noNonNullAssertion: access is guaranteed set above
|
|
107
|
-
setOAuthHeaders(requestHeaders, auth.access);
|
|
108
|
-
let body =
|
|
112
|
+
setOAuthHeaders(requestHeaders, auth.access, modelId);
|
|
113
|
+
let body = rawBody;
|
|
109
114
|
if (body && typeof body === 'string') {
|
|
110
115
|
body = rewriteRequestBody(body);
|
|
111
116
|
}
|
|
112
117
|
const rewritten = rewriteUrl(input);
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
118
|
+
let response;
|
|
119
|
+
for (let attempt = 0;; attempt++) {
|
|
120
|
+
response = await fetch(rewritten.input, {
|
|
121
|
+
...init,
|
|
122
|
+
body,
|
|
123
|
+
headers: requestHeaders,
|
|
124
|
+
...(isInsecure() && { tls: { rejectUnauthorized: false } }),
|
|
125
|
+
});
|
|
126
|
+
if (response.status !== 429 || attempt >= MAX_429_RETRIES) {
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
const delay = computeRetryAfterDelayMs(response.headers.get('retry-after'), attempt);
|
|
130
|
+
await response.body?.cancel();
|
|
131
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
132
|
+
}
|
|
119
133
|
return createStrippedStream(response);
|
|
120
134
|
},
|
|
121
135
|
};
|
package/dist/transform.d.ts
CHANGED
|
@@ -6,13 +6,21 @@ export type FetchInput = string | URL | Request;
|
|
|
6
6
|
export declare function mergeHeaders(input: FetchInput, init?: RequestInit): Headers;
|
|
7
7
|
/**
|
|
8
8
|
* Merge incoming beta headers with the required OAuth betas, deduplicating.
|
|
9
|
+
* Excludes betas unsupported by the target model (e.g. interleaved-thinking
|
|
10
|
+
* on Haiku models).
|
|
9
11
|
*/
|
|
10
|
-
export declare function mergeBetaHeaders(headers: Headers): string;
|
|
12
|
+
export declare function mergeBetaHeaders(headers: Headers, modelId?: string): string;
|
|
11
13
|
/**
|
|
12
14
|
* Set OAuth-required headers: authorization, beta, user-agent.
|
|
13
15
|
* Removes x-api-key since we're using OAuth.
|
|
14
16
|
*/
|
|
15
|
-
export declare function setOAuthHeaders(headers: Headers, accessToken: string): Headers;
|
|
17
|
+
export declare function setOAuthHeaders(headers: Headers, accessToken: string, modelId?: string): Headers;
|
|
18
|
+
/**
|
|
19
|
+
* Extract the `model` field from a JSON request body string, if present.
|
|
20
|
+
* Used to make header rewriting (e.g. beta exclusions) model-aware before
|
|
21
|
+
* the body itself is parsed and transformed.
|
|
22
|
+
*/
|
|
23
|
+
export declare function extractModelId(body: string): string | undefined;
|
|
16
24
|
/**
|
|
17
25
|
* Add TOOL_PREFIX to tool names in the request body.
|
|
18
26
|
* Prefixes both tool definitions and tool_use blocks in messages.
|
|
@@ -56,6 +64,13 @@ type SystemBlock = {
|
|
|
56
64
|
text: string;
|
|
57
65
|
[k: string]: unknown;
|
|
58
66
|
};
|
|
67
|
+
/**
|
|
68
|
+
* Compute the delay before retrying a 429 response. Prefers the
|
|
69
|
+
* `retry-after` header (seconds) when present and valid, otherwise falls
|
|
70
|
+
* back to exponential backoff. Always capped at RETRY_AFTER_CAP_MS to avoid
|
|
71
|
+
* honouring an excessively long server-provided wait.
|
|
72
|
+
*/
|
|
73
|
+
export declare function computeRetryAfterDelayMs(retryAfterHeader: string | null, attempt: number): number;
|
|
59
74
|
/**
|
|
60
75
|
* Sanitize system prompt and prepend Claude Code identity.
|
|
61
76
|
* Handles all Anthropic API system formats: undefined, string, or array of text blocks.
|
package/dist/transform.js
CHANGED
|
@@ -44,28 +44,54 @@ export function mergeHeaders(input, init) {
|
|
|
44
44
|
}
|
|
45
45
|
return headers;
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Anthropic rejects interleaved-thinking on Haiku models — this checks
|
|
49
|
+
* whether a model id refers to a Haiku model.
|
|
50
|
+
*/
|
|
51
|
+
function isHaikuModel(modelId) {
|
|
52
|
+
return /haiku/i.test(modelId ?? '');
|
|
53
|
+
}
|
|
47
54
|
/**
|
|
48
55
|
* Merge incoming beta headers with the required OAuth betas, deduplicating.
|
|
56
|
+
* Excludes betas unsupported by the target model (e.g. interleaved-thinking
|
|
57
|
+
* on Haiku models).
|
|
49
58
|
*/
|
|
50
|
-
export function mergeBetaHeaders(headers) {
|
|
59
|
+
export function mergeBetaHeaders(headers, modelId) {
|
|
51
60
|
const incomingBeta = headers.get('anthropic-beta') || '';
|
|
52
61
|
const incomingBetasList = incomingBeta
|
|
53
62
|
.split(',')
|
|
54
63
|
.map((b) => b.trim())
|
|
55
64
|
.filter(Boolean);
|
|
56
|
-
|
|
65
|
+
const requiredBetas = isHaikuModel(modelId)
|
|
66
|
+
? REQUIRED_BETAS.filter((beta) => beta !== 'interleaved-thinking-2025-05-14')
|
|
67
|
+
: REQUIRED_BETAS;
|
|
68
|
+
return [...new Set([...requiredBetas, ...incomingBetasList])].join(',');
|
|
57
69
|
}
|
|
58
70
|
/**
|
|
59
71
|
* Set OAuth-required headers: authorization, beta, user-agent.
|
|
60
72
|
* Removes x-api-key since we're using OAuth.
|
|
61
73
|
*/
|
|
62
|
-
export function setOAuthHeaders(headers, accessToken) {
|
|
74
|
+
export function setOAuthHeaders(headers, accessToken, modelId) {
|
|
63
75
|
headers.set('authorization', `Bearer ${accessToken}`);
|
|
64
|
-
headers.set('anthropic-beta', mergeBetaHeaders(headers));
|
|
76
|
+
headers.set('anthropic-beta', mergeBetaHeaders(headers, modelId));
|
|
65
77
|
headers.set('user-agent', USER_AGENT);
|
|
66
78
|
headers.delete('x-api-key');
|
|
67
79
|
return headers;
|
|
68
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Extract the `model` field from a JSON request body string, if present.
|
|
83
|
+
* Used to make header rewriting (e.g. beta exclusions) model-aware before
|
|
84
|
+
* the body itself is parsed and transformed.
|
|
85
|
+
*/
|
|
86
|
+
export function extractModelId(body) {
|
|
87
|
+
try {
|
|
88
|
+
const parsed = JSON.parse(body);
|
|
89
|
+
return typeof parsed?.model === 'string' ? parsed.model : undefined;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
69
95
|
/**
|
|
70
96
|
* Add TOOL_PREFIX to tool names in the request body.
|
|
71
97
|
* Prefixes both tool definitions and tool_use blocks in messages.
|
|
@@ -399,6 +425,88 @@ function setHybridSystemAnchor(parsed) {
|
|
|
399
425
|
.filter(isRecord);
|
|
400
426
|
setWireCacheControl(afterIdentity[afterIdentity.length - 1]);
|
|
401
427
|
}
|
|
428
|
+
const RETRY_AFTER_CAP_MS = 30000;
|
|
429
|
+
const RETRY_BASE_DELAY_MS = 500;
|
|
430
|
+
/**
|
|
431
|
+
* Compute the delay before retrying a 429 response. Prefers the
|
|
432
|
+
* `retry-after` header (seconds) when present and valid, otherwise falls
|
|
433
|
+
* back to exponential backoff. Always capped at RETRY_AFTER_CAP_MS to avoid
|
|
434
|
+
* honouring an excessively long server-provided wait.
|
|
435
|
+
*/
|
|
436
|
+
export function computeRetryAfterDelayMs(retryAfterHeader, attempt) {
|
|
437
|
+
const seconds = retryAfterHeader ? Number(retryAfterHeader) : Number.NaN;
|
|
438
|
+
const delay = Number.isFinite(seconds)
|
|
439
|
+
? seconds * 1000
|
|
440
|
+
: RETRY_BASE_DELAY_MS * 2 ** attempt;
|
|
441
|
+
return Math.min(delay, RETRY_AFTER_CAP_MS);
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Anthropic rejects the `effort` parameter for Haiku models. Strips
|
|
445
|
+
* `output_config.effort` and `thinking.effort`, removing the parent object
|
|
446
|
+
* entirely if `effort` was its only field.
|
|
447
|
+
*/
|
|
448
|
+
function stripUnsupportedEffortForHaiku(parsed) {
|
|
449
|
+
if (!isHaikuModel(typeof parsed.model === 'string' ? parsed.model : undefined))
|
|
450
|
+
return;
|
|
451
|
+
const outputConfig = parsed.output_config;
|
|
452
|
+
if (isRecord(outputConfig) && 'effort' in outputConfig) {
|
|
453
|
+
delete outputConfig.effort;
|
|
454
|
+
if (Object.keys(outputConfig).length === 0)
|
|
455
|
+
delete parsed.output_config;
|
|
456
|
+
}
|
|
457
|
+
const thinking = parsed.thinking;
|
|
458
|
+
if (isRecord(thinking) && 'effort' in thinking) {
|
|
459
|
+
delete thinking.effort;
|
|
460
|
+
if (Object.keys(thinking).length === 0)
|
|
461
|
+
delete parsed.thinking;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Remove tool_use blocks with no matching tool_result, and tool_result
|
|
466
|
+
* blocks that reference a non-existent tool_use. Anthropic rejects requests
|
|
467
|
+
* with unpaired tool_use/tool_result blocks — this can happen when OpenCode
|
|
468
|
+
* truncates or reconstructs conversation history after an interruption.
|
|
469
|
+
* Messages left with no content blocks are dropped entirely.
|
|
470
|
+
*/
|
|
471
|
+
function repairOrphanedToolPairs(parsed) {
|
|
472
|
+
if (!Array.isArray(parsed.messages))
|
|
473
|
+
return;
|
|
474
|
+
const toolUseIds = new Set();
|
|
475
|
+
const toolResultIds = new Set();
|
|
476
|
+
for (const msg of parsed.messages) {
|
|
477
|
+
if (!isRecord(msg) || !Array.isArray(msg.content))
|
|
478
|
+
continue;
|
|
479
|
+
for (const block of msg.content) {
|
|
480
|
+
if (!isRecord(block))
|
|
481
|
+
continue;
|
|
482
|
+
if (block.type === 'tool_use' && typeof block.id === 'string') {
|
|
483
|
+
toolUseIds.add(block.id);
|
|
484
|
+
}
|
|
485
|
+
else if (block.type === 'tool_result' &&
|
|
486
|
+
typeof block.tool_use_id === 'string') {
|
|
487
|
+
toolResultIds.add(block.tool_use_id);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
parsed.messages = parsed.messages.filter((msg) => {
|
|
492
|
+
if (!isRecord(msg) || !Array.isArray(msg.content))
|
|
493
|
+
return true;
|
|
494
|
+
const filteredContent = msg.content.filter((block) => {
|
|
495
|
+
if (!isRecord(block))
|
|
496
|
+
return true;
|
|
497
|
+
if (block.type === 'tool_use' && typeof block.id === 'string') {
|
|
498
|
+
return toolResultIds.has(block.id);
|
|
499
|
+
}
|
|
500
|
+
if (block.type === 'tool_result' &&
|
|
501
|
+
typeof block.tool_use_id === 'string') {
|
|
502
|
+
return toolUseIds.has(block.tool_use_id);
|
|
503
|
+
}
|
|
504
|
+
return true;
|
|
505
|
+
});
|
|
506
|
+
msg.content = filteredContent;
|
|
507
|
+
return filteredContent.length > 0;
|
|
508
|
+
});
|
|
509
|
+
}
|
|
402
510
|
/**
|
|
403
511
|
* Remove trailing assistant-role messages. OAuth endpoints reject requests
|
|
404
512
|
* that end with an assistant turn (assistant prefill is not supported).
|
|
@@ -516,6 +624,8 @@ export function rewriteRequestBody(body) {
|
|
|
516
624
|
try {
|
|
517
625
|
const parsed = JSON.parse(body);
|
|
518
626
|
parsed.system = prependClaudeCodeIdentity(parsed.system);
|
|
627
|
+
repairOrphanedToolPairs(parsed);
|
|
628
|
+
stripUnsupportedEffortForHaiku(parsed);
|
|
519
629
|
stripTrailingAssistantMessages(parsed);
|
|
520
630
|
applyHybridCache1h(parsed);
|
|
521
631
|
return prefixToolNames(parsed);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sahiljassal/opencode-anthropic-auth",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.1",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/shljsl75891/opencode-anthropic-auth.git"
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
"@opencode-ai/plugin": "*"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@biomejs/biome": "2.
|
|
34
|
+
"@biomejs/biome": "2.5.2",
|
|
35
35
|
"@changesets/changelog-github": "^0.7.0",
|
|
36
36
|
"@changesets/cli": "^2.31.0",
|
|
37
|
-
"@opencode-ai/plugin": "1.17.
|
|
37
|
+
"@opencode-ai/plugin": "1.17.13",
|
|
38
38
|
"@tsconfig/bun": "1.0.10",
|
|
39
39
|
"@types/bun": "1.3.14",
|
|
40
40
|
"dedent": "^1.7.2",
|