@ex-machina/opencode-anthropic-auth 1.8.2 → 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 +1 -1
- package/dist/transform.d.ts +2 -0
- package/dist/transform.js +271 -11
- package/package.json +5 -2
package/README.md
CHANGED
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
|
},
|
|
@@ -39,6 +41,7 @@
|
|
|
39
41
|
"@types/bun": "1.3.14",
|
|
40
42
|
"dedent": "^1.7.2",
|
|
41
43
|
"lefthook": "2.1.9",
|
|
44
|
+
"turbo": "2.9.18",
|
|
42
45
|
"typescript": "6.0.3"
|
|
43
46
|
}
|
|
44
47
|
}
|