@ex-machina/opencode-anthropic-auth 1.8.1 → 1.8.3
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 -2
- package/dist/constants.d.ts +12 -2
- package/dist/constants.js +12 -2
- package/dist/transform.d.ts +2 -0
- package/dist/transform.js +271 -11
- package/package.json +8 -5
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
> Use your best judgment and don't try to abuse the subscriptions. Plugins like oh-my-openagent are _known_ to trigger bans. Please be careful when using Ralph loops or insanely heavy usage patterns.
|
|
7
7
|
|
|
8
8
|
> [!IMPORTANT]
|
|
9
|
-
> If you are seeing issues, please try to `rm -rf ~/.cache/opencode` and check your `opencode.json` config to make sure you're on the latest version.
|
|
9
|
+
> If you are seeing issues, please try to `rm -rf ~/.cache/opencode/packages/@ex-machina` and check your `opencode.json` config to make sure you're on the latest version.
|
|
10
10
|
>
|
|
11
11
|
> Try this FIRST before making an Issue. Thanks!
|
|
12
12
|
|
|
@@ -31,7 +31,7 @@ Add the plugin to your OpenCode configuration:
|
|
|
31
31
|
|
|
32
32
|
```json
|
|
33
33
|
{
|
|
34
|
-
"plugin": ["@ex-machina/opencode-anthropic-auth@1.8.
|
|
34
|
+
"plugin": ["@ex-machina/opencode-anthropic-auth@1.8.2"]
|
|
35
35
|
}
|
|
36
36
|
```
|
|
37
37
|
|
|
@@ -40,6 +40,7 @@ Add the plugin to your OpenCode configuration:
|
|
|
40
40
|
The plugin provides three authentication options:
|
|
41
41
|
|
|
42
42
|
- **Claude Pro/Max** - OAuth flow via `claude.ai` for Pro/Max subscribers. Uses your existing subscription at no additional API cost.
|
|
43
|
+
- run the `/connect` command, select `Anthropic (API key)` -> `Claude Pro/Max` and do OAuth
|
|
43
44
|
- **Create an API Key** - OAuth flow via `console.anthropic.com` that creates an API key on your behalf.
|
|
44
45
|
- **Manually enter API Key** - Standard API key entry for users who already have one.
|
|
45
46
|
|
package/dist/constants.d.ts
CHANGED
|
@@ -12,9 +12,19 @@ export declare const OPENCODE_IDENTITY_PREFIX = "You are OpenCode";
|
|
|
12
12
|
export declare const CLAUDE_CODE_IDENTITY = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
|
13
13
|
export declare const CCH_SALT = "59cf53e54c78";
|
|
14
14
|
export declare const CCH_POSITIONS: number[];
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* The Claude Code version we report to Anthropic, via both the `user-agent`
|
|
17
|
+
* header and the `cc_version` field of the billing header.
|
|
18
|
+
*
|
|
19
|
+
* Anthropic gates new models on this value server-side: requesting a model
|
|
20
|
+
* newer than the reported client returns a 400 `claude_code_version_too_old`
|
|
21
|
+
* (e.g. "Claude Code 2.1.87 does not support this model; version 2.1.251 or
|
|
22
|
+
* newer is required"). Keep this at or above the latest published
|
|
23
|
+
* `@anthropic-ai/claude-code` release, otherwise new models are unreachable.
|
|
24
|
+
*/
|
|
25
|
+
export declare const CLAUDE_CODE_VERSION = "2.1.258";
|
|
16
26
|
export declare const CLAUDE_CODE_ENTRYPOINT = "sdk-cli";
|
|
17
|
-
export declare const USER_AGENT = "claude-cli/2.1.
|
|
27
|
+
export declare const USER_AGENT = "claude-cli/2.1.258 (external, cli)";
|
|
18
28
|
/**
|
|
19
29
|
* Anchors that identify paragraphs to remove from the system prompt.
|
|
20
30
|
* Any paragraph (text between blank lines) containing one of these
|
package/dist/constants.js
CHANGED
|
@@ -22,9 +22,19 @@ export const OPENCODE_IDENTITY_PREFIX = 'You are OpenCode';
|
|
|
22
22
|
export const CLAUDE_CODE_IDENTITY = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
|
23
23
|
export const CCH_SALT = '59cf53e54c78';
|
|
24
24
|
export const CCH_POSITIONS = [4, 7, 20];
|
|
25
|
-
|
|
25
|
+
/**
|
|
26
|
+
* The Claude Code version we report to Anthropic, via both the `user-agent`
|
|
27
|
+
* header and the `cc_version` field of the billing header.
|
|
28
|
+
*
|
|
29
|
+
* Anthropic gates new models on this value server-side: requesting a model
|
|
30
|
+
* newer than the reported client returns a 400 `claude_code_version_too_old`
|
|
31
|
+
* (e.g. "Claude Code 2.1.87 does not support this model; version 2.1.251 or
|
|
32
|
+
* newer is required"). Keep this at or above the latest published
|
|
33
|
+
* `@anthropic-ai/claude-code` release, otherwise new models are unreachable.
|
|
34
|
+
*/
|
|
35
|
+
export const CLAUDE_CODE_VERSION = '2.1.258';
|
|
26
36
|
export const CLAUDE_CODE_ENTRYPOINT = 'sdk-cli';
|
|
27
|
-
export const USER_AGENT =
|
|
37
|
+
export const USER_AGENT = `claude-cli/${CLAUDE_CODE_VERSION} (external, cli)`;
|
|
28
38
|
/**
|
|
29
39
|
* Anchors that identify paragraphs to remove from the system prompt.
|
|
30
40
|
* Any paragraph (text between blank lines) containing one of these
|
package/dist/transform.d.ts
CHANGED
package/dist/transform.js
CHANGED
|
@@ -1,5 +1,208 @@
|
|
|
1
1
|
import { buildBillingHeaderValue } from "./cch.js";
|
|
2
2
|
import { CLAUDE_CODE_ENTRYPOINT, CLAUDE_CODE_IDENTITY, OPENCODE_IDENTITY_PREFIX, PARAGRAPH_REMOVAL_ANCHORS, REQUIRED_BETAS, TEXT_REPLACEMENTS, TOOL_PREFIX, USER_AGENT, } from "./constants.js";
|
|
3
|
+
// Bound an incomplete SSE line so malformed streams cannot grow memory forever.
|
|
4
|
+
export const MAX_SSE_LINE_BYTES = 5 * 1024 * 1024;
|
|
5
|
+
function headersAfterBodyTransform(source) {
|
|
6
|
+
const headers = new Headers(source);
|
|
7
|
+
for (const name of [
|
|
8
|
+
'content-digest',
|
|
9
|
+
'content-encoding',
|
|
10
|
+
'content-length',
|
|
11
|
+
'content-md5',
|
|
12
|
+
'content-range',
|
|
13
|
+
'digest',
|
|
14
|
+
'etag',
|
|
15
|
+
]) {
|
|
16
|
+
headers.delete(name);
|
|
17
|
+
}
|
|
18
|
+
return headers;
|
|
19
|
+
}
|
|
20
|
+
const JSON_NAME_KEY_SUFFIX = new TextEncoder().encode('name"');
|
|
21
|
+
const JSON_TOOL_PREFIX = new TextEncoder().encode(TOOL_PREFIX);
|
|
22
|
+
const UTF8_ENCODER = new TextEncoder();
|
|
23
|
+
const UTF8_FATAL_DECODER = new TextDecoder('utf-8', { fatal: true });
|
|
24
|
+
export const MAX_JSON_TOOL_NAME_BYTES = 1024;
|
|
25
|
+
function isJsonWhitespace(byte) {
|
|
26
|
+
return byte === 0x20 || byte === 0x09 || byte === 0x0a || byte === 0x0d;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Rewrite JSON `name` string values without buffering the whole document.
|
|
30
|
+
* Only a bounded tool-name candidate is retained across chunks; all document
|
|
31
|
+
* content outside that string value is emitted immediately.
|
|
32
|
+
*/
|
|
33
|
+
function createJsonToolNameStream(body) {
|
|
34
|
+
let state = 'outside';
|
|
35
|
+
let held = [];
|
|
36
|
+
let candidateIndex = 0;
|
|
37
|
+
let escaped = false;
|
|
38
|
+
const enterStringAfter = (byte) => {
|
|
39
|
+
if (byte === 0x22) {
|
|
40
|
+
state = 'outside';
|
|
41
|
+
escaped = false;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
state = 'string';
|
|
45
|
+
escaped = byte === 0x5c;
|
|
46
|
+
};
|
|
47
|
+
return body.pipeThrough(new TransformStream({
|
|
48
|
+
transform(chunk, controller) {
|
|
49
|
+
const output = new Uint8Array(chunk.byteLength + 32);
|
|
50
|
+
let outputLength = 0;
|
|
51
|
+
const write = (byte) => {
|
|
52
|
+
output[outputLength++] = byte;
|
|
53
|
+
};
|
|
54
|
+
const enqueueOutput = () => {
|
|
55
|
+
if (outputLength === 0)
|
|
56
|
+
return;
|
|
57
|
+
controller.enqueue(output.slice(0, outputLength));
|
|
58
|
+
outputLength = 0;
|
|
59
|
+
};
|
|
60
|
+
const writeHeld = () => {
|
|
61
|
+
for (const byte of held)
|
|
62
|
+
write(byte);
|
|
63
|
+
held = [];
|
|
64
|
+
};
|
|
65
|
+
const processOutside = (byte) => {
|
|
66
|
+
if (byte === 0x22) {
|
|
67
|
+
held = [byte];
|
|
68
|
+
candidateIndex = 0;
|
|
69
|
+
state = 'key-candidate';
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
write(byte);
|
|
73
|
+
};
|
|
74
|
+
for (const byte of chunk) {
|
|
75
|
+
if (state === 'outside') {
|
|
76
|
+
processOutside(byte);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (state === 'key-candidate') {
|
|
80
|
+
if (byte === JSON_NAME_KEY_SUFFIX[candidateIndex]) {
|
|
81
|
+
held.push(byte);
|
|
82
|
+
candidateIndex++;
|
|
83
|
+
if (candidateIndex === JSON_NAME_KEY_SUFFIX.byteLength) {
|
|
84
|
+
writeHeld();
|
|
85
|
+
state = 'after-name-key';
|
|
86
|
+
}
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
writeHeld();
|
|
90
|
+
write(byte);
|
|
91
|
+
enterStringAfter(byte);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (state === 'string') {
|
|
95
|
+
write(byte);
|
|
96
|
+
if (escaped) {
|
|
97
|
+
escaped = false;
|
|
98
|
+
}
|
|
99
|
+
else if (byte === 0x5c) {
|
|
100
|
+
escaped = true;
|
|
101
|
+
}
|
|
102
|
+
else if (byte === 0x22) {
|
|
103
|
+
state = 'outside';
|
|
104
|
+
}
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (state === 'after-name-key') {
|
|
108
|
+
if (isJsonWhitespace(byte)) {
|
|
109
|
+
write(byte);
|
|
110
|
+
}
|
|
111
|
+
else if (byte === 0x3a) {
|
|
112
|
+
write(byte);
|
|
113
|
+
state = 'after-colon';
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
processOutside(byte);
|
|
117
|
+
}
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (state === 'after-colon') {
|
|
121
|
+
if (isJsonWhitespace(byte)) {
|
|
122
|
+
write(byte);
|
|
123
|
+
}
|
|
124
|
+
else if (byte === 0x22) {
|
|
125
|
+
write(byte);
|
|
126
|
+
held = [];
|
|
127
|
+
candidateIndex = 0;
|
|
128
|
+
state = 'prefix-candidate';
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
processOutside(byte);
|
|
132
|
+
}
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (state === 'prefix-candidate') {
|
|
136
|
+
if (byte === JSON_TOOL_PREFIX[candidateIndex]) {
|
|
137
|
+
held.push(byte);
|
|
138
|
+
candidateIndex++;
|
|
139
|
+
if (candidateIndex === JSON_TOOL_PREFIX.byteLength) {
|
|
140
|
+
held = [];
|
|
141
|
+
candidateIndex = 0;
|
|
142
|
+
escaped = false;
|
|
143
|
+
state = 'tool-name-candidate';
|
|
144
|
+
}
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
writeHeld();
|
|
148
|
+
write(byte);
|
|
149
|
+
enterStringAfter(byte);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (escaped) {
|
|
153
|
+
held.push(byte);
|
|
154
|
+
escaped = false;
|
|
155
|
+
if (held.length > MAX_JSON_TOOL_NAME_BYTES) {
|
|
156
|
+
throw new Error(`JSON tool name exceeds ${MAX_JSON_TOOL_NAME_BYTES} byte limit`);
|
|
157
|
+
}
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (byte === 0x5c) {
|
|
161
|
+
held.push(byte);
|
|
162
|
+
escaped = true;
|
|
163
|
+
if (held.length > MAX_JSON_TOOL_NAME_BYTES) {
|
|
164
|
+
throw new Error(`JSON tool name exceeds ${MAX_JSON_TOOL_NAME_BYTES} byte limit`);
|
|
165
|
+
}
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (byte === 0x22) {
|
|
169
|
+
let replacement;
|
|
170
|
+
if (held.length === 0) {
|
|
171
|
+
replacement = JSON_TOOL_PREFIX;
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
try {
|
|
175
|
+
replacement = UTF8_ENCODER.encode(unprefixName(UTF8_FATAL_DECODER.decode(Uint8Array.from(held))));
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
replacement = Uint8Array.from([...JSON_TOOL_PREFIX, ...held]);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
enqueueOutput();
|
|
182
|
+
controller.enqueue(replacement);
|
|
183
|
+
write(byte);
|
|
184
|
+
held = [];
|
|
185
|
+
candidateIndex = 0;
|
|
186
|
+
state = 'outside';
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
held.push(byte);
|
|
190
|
+
if (held.length > MAX_JSON_TOOL_NAME_BYTES) {
|
|
191
|
+
throw new Error(`JSON tool name exceeds ${MAX_JSON_TOOL_NAME_BYTES} byte limit`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
enqueueOutput();
|
|
195
|
+
},
|
|
196
|
+
flush(controller) {
|
|
197
|
+
const trailing = Uint8Array.from(state === 'tool-name-candidate'
|
|
198
|
+
? [...JSON_TOOL_PREFIX, ...held]
|
|
199
|
+
: held);
|
|
200
|
+
if (trailing.byteLength === 0)
|
|
201
|
+
return;
|
|
202
|
+
controller.enqueue(trailing);
|
|
203
|
+
},
|
|
204
|
+
}));
|
|
205
|
+
}
|
|
3
206
|
/**
|
|
4
207
|
* Prefix a tool name with TOOL_PREFIX and uppercase the first character.
|
|
5
208
|
* Claude Code uses PascalCase tool names (e.g. mcp_Bash, mcp_Read);
|
|
@@ -289,26 +492,83 @@ export function rewriteRequestBody(body) {
|
|
|
289
492
|
* Create a streaming response that strips the tool prefix from tool names.
|
|
290
493
|
*/
|
|
291
494
|
export function createStrippedStream(response) {
|
|
495
|
+
const mediaType = response.headers
|
|
496
|
+
.get('content-type')
|
|
497
|
+
?.split(';', 1)[0]
|
|
498
|
+
?.trim()
|
|
499
|
+
.toLowerCase();
|
|
292
500
|
if (!response.body)
|
|
293
501
|
return response;
|
|
294
|
-
|
|
502
|
+
if (mediaType === 'application/json' || mediaType?.endsWith('+json')) {
|
|
503
|
+
const stream = createJsonToolNameStream(response.body);
|
|
504
|
+
const headers = headersAfterBodyTransform(response.headers);
|
|
505
|
+
return new Response(stream, {
|
|
506
|
+
status: response.status,
|
|
507
|
+
statusText: response.statusText,
|
|
508
|
+
headers,
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
if (mediaType !== 'text/event-stream')
|
|
512
|
+
return response;
|
|
295
513
|
const decoder = new TextDecoder();
|
|
296
514
|
const encoder = new TextEncoder();
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
515
|
+
let pending = new Uint8Array(0);
|
|
516
|
+
let pendingLength = 0;
|
|
517
|
+
const appendPending = (bytes) => {
|
|
518
|
+
const requiredLength = pendingLength + bytes.byteLength;
|
|
519
|
+
if (requiredLength > MAX_SSE_LINE_BYTES) {
|
|
520
|
+
throw new Error(`SSE line exceeds ${MAX_SSE_LINE_BYTES} byte limit`);
|
|
521
|
+
}
|
|
522
|
+
if (requiredLength > pending.byteLength) {
|
|
523
|
+
let capacity = Math.max(1024, pending.byteLength);
|
|
524
|
+
while (capacity < requiredLength) {
|
|
525
|
+
capacity = Math.min(MAX_SSE_LINE_BYTES, capacity * 2);
|
|
526
|
+
}
|
|
527
|
+
const expanded = new Uint8Array(capacity);
|
|
528
|
+
expanded.set(pending.subarray(0, pendingLength));
|
|
529
|
+
pending = expanded;
|
|
530
|
+
}
|
|
531
|
+
pending.set(bytes, pendingLength);
|
|
532
|
+
pendingLength = requiredLength;
|
|
533
|
+
};
|
|
534
|
+
const stream = response.body.pipeThrough(new TransformStream({
|
|
535
|
+
transform(chunk, controller) {
|
|
536
|
+
let lastLineBreak = -1;
|
|
537
|
+
let lineLength = pendingLength;
|
|
538
|
+
for (let index = 0; index < chunk.byteLength; index++) {
|
|
539
|
+
if (chunk[index] === 0x0a || chunk[index] === 0x0d) {
|
|
540
|
+
lastLineBreak = index;
|
|
541
|
+
lineLength = 0;
|
|
542
|
+
}
|
|
543
|
+
else {
|
|
544
|
+
lineLength++;
|
|
545
|
+
if (lineLength > MAX_SSE_LINE_BYTES) {
|
|
546
|
+
throw new Error(`SSE line exceeds ${MAX_SSE_LINE_BYTES} byte limit`);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
if (lastLineBreak < 0) {
|
|
551
|
+
appendPending(chunk);
|
|
302
552
|
return;
|
|
303
553
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
554
|
+
const completeLines = decoder.decode(pending.subarray(0, pendingLength), {
|
|
555
|
+
stream: true,
|
|
556
|
+
}) + decoder.decode(chunk.subarray(0, lastLineBreak + 1));
|
|
557
|
+
pendingLength = 0;
|
|
558
|
+
appendPending(chunk.subarray(lastLineBreak + 1));
|
|
559
|
+
controller.enqueue(encoder.encode(stripToolPrefix(completeLines)));
|
|
307
560
|
},
|
|
308
|
-
|
|
561
|
+
flush(controller) {
|
|
562
|
+
const trailing = decoder.decode(pending.subarray(0, pendingLength));
|
|
563
|
+
if (trailing) {
|
|
564
|
+
controller.enqueue(encoder.encode(stripToolPrefix(trailing)));
|
|
565
|
+
}
|
|
566
|
+
},
|
|
567
|
+
}));
|
|
568
|
+
const headers = headersAfterBodyTransform(response.headers);
|
|
309
569
|
return new Response(stream, {
|
|
310
570
|
status: response.status,
|
|
311
571
|
statusText: response.statusText,
|
|
312
|
-
headers
|
|
572
|
+
headers,
|
|
313
573
|
});
|
|
314
574
|
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ex-machina/opencode-anthropic-auth",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.3",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/ex-machina-co/opencode-anthropic-auth"
|
|
7
7
|
},
|
|
8
8
|
"main": "./dist/index.js",
|
|
9
9
|
"types": "./dist/index.d.ts",
|
|
10
|
+
"packageManager": "bun@1.3.14",
|
|
10
11
|
"engines": {
|
|
11
12
|
"bun": "1.3.14"
|
|
12
13
|
},
|
|
@@ -19,11 +20,12 @@
|
|
|
19
20
|
"dev": "bun scripts/dev.ts",
|
|
20
21
|
"dev:clean": "bun scripts/dev-clean.ts",
|
|
21
22
|
"extract": "bun scripts/extract-system-prompt.ts",
|
|
23
|
+
"check": "bun turbo check:all",
|
|
22
24
|
"test": "bun test",
|
|
23
25
|
"types": "tsc",
|
|
24
26
|
"format": "biome check --write --unsafe",
|
|
25
27
|
"format:check": "biome format .",
|
|
26
|
-
"lint": "biome lint .",
|
|
28
|
+
"lint": "biome lint --error-on-warnings .",
|
|
27
29
|
"change": "changeset",
|
|
28
30
|
"release": "bun run build && bun change publish"
|
|
29
31
|
},
|
|
@@ -31,14 +33,15 @@
|
|
|
31
33
|
"@opencode-ai/plugin": "*"
|
|
32
34
|
},
|
|
33
35
|
"devDependencies": {
|
|
34
|
-
"@biomejs/biome": "2.
|
|
36
|
+
"@biomejs/biome": "2.5.2",
|
|
35
37
|
"@changesets/changelog-github": "^0.7.0",
|
|
36
38
|
"@changesets/cli": "^2.31.0",
|
|
37
|
-
"@opencode-ai/plugin": "1.
|
|
39
|
+
"@opencode-ai/plugin": "1.17.13",
|
|
38
40
|
"@tsconfig/bun": "1.0.10",
|
|
39
41
|
"@types/bun": "1.3.14",
|
|
40
42
|
"dedent": "^1.7.2",
|
|
41
|
-
"lefthook": "2.1.
|
|
43
|
+
"lefthook": "2.1.9",
|
|
44
|
+
"turbo": "2.9.18",
|
|
42
45
|
"typescript": "6.0.3"
|
|
43
46
|
}
|
|
44
47
|
}
|