@ex-machina/opencode-anthropic-auth 2.0.0-next.1 → 2.0.0-next.2
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/auth.d.ts +3 -3
- package/dist/auth.js +161 -82
- package/dist/bounded.d.ts +8 -0
- package/dist/bounded.js +59 -0
- package/dist/config.js +6 -5
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +521 -54
- package/dist/json-response-stream.d.ts +10 -0
- package/dist/json-response-stream.js +638 -0
- package/dist/rate-limit.d.ts +18 -0
- package/dist/rate-limit.js +354 -0
- package/dist/transform.d.ts +41 -19
- package/dist/transform.js +592 -329
- package/package.json +3 -2
package/dist/transform.js
CHANGED
|
@@ -1,8 +1,167 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
1
3
|
import { buildBillingHeaderValue } from "./cch.js";
|
|
2
4
|
import { CLAUDE_CODE_ENTRYPOINT, CLAUDE_CODE_IDENTITY, CLAUDE_CODE_VERSION, formatUserAgent, OPENCODE_IDENTITY_PREFIX, PARAGRAPH_REMOVAL_ANCHORS, REQUIRED_BETAS, TEXT_REPLACEMENTS, TOOL_PREFIX, } from "./constants.js";
|
|
5
|
+
import { assertWellFormedUtf16, createBoundedJsonToolNameStream, MAX_JSON_NODES, MAX_JSON_OBJECT_KEYS, MAX_JSON_TOOL_NAME_BYTES, } from "./json-response-stream.js";
|
|
3
6
|
// Bound an incomplete SSE line so malformed streams cannot grow memory forever.
|
|
4
7
|
export const MAX_SSE_LINE_BYTES = 5 * 1024 * 1024;
|
|
5
|
-
|
|
8
|
+
export const MAX_SSE_EVENT_BYTES = 8 * 1024 * 1024;
|
|
9
|
+
export { MAX_JSON_TOOL_NAME_BYTES };
|
|
10
|
+
const SHORT_TOOL_ALIAS_PREFIX = `${TOOL_PREFIX}T`;
|
|
11
|
+
const LONG_TOOL_ALIAS_PREFIX = `${TOOL_PREFIX}H`;
|
|
12
|
+
const MAX_TOOL_ALIAS_BYTES = 64;
|
|
13
|
+
const MAX_INLINE_TOOL_NAME_BYTES = 44;
|
|
14
|
+
const DEFAULT_ALIAS_ENTRIES = 4096;
|
|
15
|
+
const DEFAULT_ALIAS_BYTES = 256 * 1024;
|
|
16
|
+
const utf8Encoder = new TextEncoder();
|
|
17
|
+
const utf8Decoder = new TextDecoder('utf-8', { fatal: true });
|
|
18
|
+
function base64url(bytes) {
|
|
19
|
+
return Buffer.from(bytes).toString('base64url');
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Reversible tool-name aliases bounded to Anthropic's 64-byte limit.
|
|
23
|
+
*
|
|
24
|
+
* Names up to 44 UTF-8 bytes are encoded inline. Longer names use a stable
|
|
25
|
+
* SHA-256 alias and are retained in a bounded table because a lossless,
|
|
26
|
+
* stateless 64-byte source -> 60-byte payload mapping is impossible.
|
|
27
|
+
*/
|
|
28
|
+
export class ToolNameAliasTable {
|
|
29
|
+
maxEntries;
|
|
30
|
+
maxBytes;
|
|
31
|
+
longByName = new Map();
|
|
32
|
+
longByAlias = new Map();
|
|
33
|
+
retainedBytes = 0;
|
|
34
|
+
disposed = false;
|
|
35
|
+
constructor(options = {}) {
|
|
36
|
+
this.maxEntries = options.maxEntries ?? DEFAULT_ALIAS_ENTRIES;
|
|
37
|
+
this.maxBytes = options.maxBytes ?? DEFAULT_ALIAS_BYTES;
|
|
38
|
+
if (!Number.isSafeInteger(this.maxEntries) ||
|
|
39
|
+
this.maxEntries < 0 ||
|
|
40
|
+
!Number.isSafeInteger(this.maxBytes) ||
|
|
41
|
+
this.maxBytes < 0) {
|
|
42
|
+
throw new TypeError('Tool-name alias limits must be safe non-negative integers');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
assertActive() {
|
|
46
|
+
if (this.disposed)
|
|
47
|
+
throw new Error('Tool-name alias table is disposed');
|
|
48
|
+
}
|
|
49
|
+
encode(name) {
|
|
50
|
+
this.assertActive();
|
|
51
|
+
assertWellFormedUtf16(name);
|
|
52
|
+
const bytes = utf8Encoder.encode(name);
|
|
53
|
+
if (bytes.byteLength === 0) {
|
|
54
|
+
throw new Error('Tool names must not be empty');
|
|
55
|
+
}
|
|
56
|
+
if (bytes.byteLength <= MAX_INLINE_TOOL_NAME_BYTES) {
|
|
57
|
+
const alias = `${SHORT_TOOL_ALIAS_PREFIX}${base64url(bytes)}`;
|
|
58
|
+
if (utf8Encoder.encode(alias).byteLength > MAX_TOOL_ALIAS_BYTES) {
|
|
59
|
+
throw new Error('Encoded tool name exceeds Anthropic alias limit');
|
|
60
|
+
}
|
|
61
|
+
return alias;
|
|
62
|
+
}
|
|
63
|
+
const existing = this.longByName.get(name);
|
|
64
|
+
if (existing)
|
|
65
|
+
return existing;
|
|
66
|
+
const alias = `${LONG_TOOL_ALIAS_PREFIX}${createHash('sha256')
|
|
67
|
+
.update(bytes)
|
|
68
|
+
.digest('base64url')}`;
|
|
69
|
+
const collision = this.longByAlias.get(alias);
|
|
70
|
+
if (collision !== undefined && collision !== name) {
|
|
71
|
+
throw new Error('Tool-name alias collision');
|
|
72
|
+
}
|
|
73
|
+
if (this.longByName.size >= this.maxEntries) {
|
|
74
|
+
throw new Error('Tool-name alias entry limit exceeded');
|
|
75
|
+
}
|
|
76
|
+
if (bytes.byteLength > this.maxBytes - this.retainedBytes) {
|
|
77
|
+
throw new Error('Tool-name alias byte limit exceeded');
|
|
78
|
+
}
|
|
79
|
+
this.longByName.set(name, alias);
|
|
80
|
+
this.longByAlias.set(alias, name);
|
|
81
|
+
this.retainedBytes += bytes.byteLength;
|
|
82
|
+
return alias;
|
|
83
|
+
}
|
|
84
|
+
decode(alias) {
|
|
85
|
+
this.assertActive();
|
|
86
|
+
if (alias.startsWith(SHORT_TOOL_ALIAS_PREFIX)) {
|
|
87
|
+
const encoded = alias.slice(SHORT_TOOL_ALIAS_PREFIX.length);
|
|
88
|
+
if (!encoded || !/^[A-Za-z0-9_-]+$/.test(encoded))
|
|
89
|
+
return undefined;
|
|
90
|
+
try {
|
|
91
|
+
const bytes = Buffer.from(encoded, 'base64url');
|
|
92
|
+
if (bytes.byteLength > MAX_INLINE_TOOL_NAME_BYTES)
|
|
93
|
+
return undefined;
|
|
94
|
+
if (base64url(bytes) !== encoded)
|
|
95
|
+
return undefined;
|
|
96
|
+
const name = utf8Decoder.decode(bytes);
|
|
97
|
+
const canonical = `${SHORT_TOOL_ALIAS_PREFIX}${base64url(utf8Encoder.encode(name))}`;
|
|
98
|
+
return canonical === alias ? name : undefined;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (alias.startsWith(LONG_TOOL_ALIAS_PREFIX)) {
|
|
105
|
+
return this.longByAlias.get(alias);
|
|
106
|
+
}
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
get hasStatefulAliases() {
|
|
110
|
+
this.assertActive();
|
|
111
|
+
return this.longByName.size > 0;
|
|
112
|
+
}
|
|
113
|
+
dispose() {
|
|
114
|
+
if (this.disposed)
|
|
115
|
+
return;
|
|
116
|
+
this.disposed = true;
|
|
117
|
+
this.longByName.clear();
|
|
118
|
+
this.longByAlias.clear();
|
|
119
|
+
this.retainedBytes = 0;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function statelessToolNameAliases() {
|
|
123
|
+
return new ToolNameAliasTable({ maxEntries: 0, maxBytes: 0 });
|
|
124
|
+
}
|
|
125
|
+
function finalizeStream(stream, finalize) {
|
|
126
|
+
const reader = stream.getReader();
|
|
127
|
+
let released = false;
|
|
128
|
+
const release = () => {
|
|
129
|
+
if (released)
|
|
130
|
+
return;
|
|
131
|
+
released = true;
|
|
132
|
+
reader.releaseLock();
|
|
133
|
+
};
|
|
134
|
+
return new ReadableStream({
|
|
135
|
+
async pull(controller) {
|
|
136
|
+
try {
|
|
137
|
+
const result = await reader.read();
|
|
138
|
+
if (result.done) {
|
|
139
|
+
release();
|
|
140
|
+
finalize();
|
|
141
|
+
controller.close();
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
controller.enqueue(result.value);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
release();
|
|
149
|
+
finalize();
|
|
150
|
+
controller.error(error);
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
async cancel(reason) {
|
|
154
|
+
try {
|
|
155
|
+
await reader.cancel(reason);
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
release();
|
|
159
|
+
finalize();
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
export function headersAfterBodyTransform(source) {
|
|
6
165
|
const headers = new Headers(source);
|
|
7
166
|
for (const name of [
|
|
8
167
|
'content-digest',
|
|
@@ -17,210 +176,6 @@ function headersAfterBodyTransform(source) {
|
|
|
17
176
|
}
|
|
18
177
|
return headers;
|
|
19
178
|
}
|
|
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
|
-
}
|
|
206
|
-
/**
|
|
207
|
-
* Prefix a tool name with TOOL_PREFIX and uppercase the first character.
|
|
208
|
-
* Claude Code uses PascalCase tool names (e.g. mcp_Bash, mcp_Read);
|
|
209
|
-
* lowercase names (mcp_bash, mcp_read) are flagged as non-Claude-Code clients.
|
|
210
|
-
*/
|
|
211
|
-
function prefixName(name) {
|
|
212
|
-
return `${TOOL_PREFIX}${name.charAt(0).toUpperCase()}${name.slice(1)}`;
|
|
213
|
-
}
|
|
214
|
-
/**
|
|
215
|
-
* Reverse prefixName: strip TOOL_PREFIX and restore the original leading case.
|
|
216
|
-
*/
|
|
217
|
-
function unprefixName(name) {
|
|
218
|
-
// StructuredOutput is still used as StructuredOutput
|
|
219
|
-
if (name === 'StructuredOutput') {
|
|
220
|
-
return name;
|
|
221
|
-
}
|
|
222
|
-
return `${name.charAt(0).toLowerCase()}${name.slice(1)}`;
|
|
223
|
-
}
|
|
224
179
|
/**
|
|
225
180
|
* Merge headers from a Request object and/or a RequestInit headers value
|
|
226
181
|
* into a single Headers instance.
|
|
@@ -271,9 +226,6 @@ export function mergeBetaHeaders(headers) {
|
|
|
271
226
|
/**
|
|
272
227
|
* Set OAuth-required headers on the request: authorization, beta, user-agent.
|
|
273
228
|
* Removes x-api-key since we're using OAuth.
|
|
274
|
-
*
|
|
275
|
-
* `version` must be the same value passed to rewriteRequestBody for the same
|
|
276
|
-
* request, otherwise the two reported versions disagree.
|
|
277
229
|
*/
|
|
278
230
|
export function setOAuthHeaders(headers, accessToken, version = CLAUDE_CODE_VERSION) {
|
|
279
231
|
headers.set('authorization', `Bearer ${accessToken}`);
|
|
@@ -286,24 +238,28 @@ export function setOAuthHeaders(headers, accessToken, version = CLAUDE_CODE_VERS
|
|
|
286
238
|
* Add TOOL_PREFIX to tool names in the request body.
|
|
287
239
|
* Prefixes both tool definitions and tool_use blocks in messages.
|
|
288
240
|
*/
|
|
289
|
-
export function prefixToolNames(parsed) {
|
|
241
|
+
export function prefixToolNames(parsed, alreadyPrefixed = false, aliases = statelessToolNameAliases()) {
|
|
242
|
+
const prefix = (name) => alreadyPrefixed && name.startsWith(TOOL_PREFIX)
|
|
243
|
+
? name
|
|
244
|
+
: aliases.encode(name);
|
|
290
245
|
if (parsed.tools && Array.isArray(parsed.tools)) {
|
|
291
|
-
parsed.tools = parsed.tools.map((tool) => (
|
|
292
|
-
...tool,
|
|
293
|
-
|
|
294
|
-
}));
|
|
246
|
+
parsed.tools = parsed.tools.map((tool) => isRecord(tool) && typeof tool.name === 'string' && tool.name.length > 0
|
|
247
|
+
? { ...tool, name: prefix(tool.name) }
|
|
248
|
+
: tool);
|
|
295
249
|
}
|
|
296
250
|
if (parsed.messages && Array.isArray(parsed.messages)) {
|
|
297
|
-
parsed.messages = parsed.messages.map((
|
|
298
|
-
if (
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
251
|
+
parsed.messages = parsed.messages.map((message) => {
|
|
252
|
+
if (!isRecord(message) || !Array.isArray(message.content))
|
|
253
|
+
return message;
|
|
254
|
+
return {
|
|
255
|
+
...message,
|
|
256
|
+
content: message.content.map((block) => isRecord(block) &&
|
|
257
|
+
block.type === 'tool_use' &&
|
|
258
|
+
typeof block.name === 'string' &&
|
|
259
|
+
block.name.length > 0
|
|
260
|
+
? { ...block, name: prefix(block.name) }
|
|
261
|
+
: block),
|
|
262
|
+
};
|
|
307
263
|
});
|
|
308
264
|
}
|
|
309
265
|
return JSON.stringify(parsed);
|
|
@@ -311,23 +267,324 @@ export function prefixToolNames(parsed) {
|
|
|
311
267
|
/**
|
|
312
268
|
* Strip TOOL_PREFIX from tool names in streaming response text.
|
|
313
269
|
*/
|
|
314
|
-
export function stripToolPrefix(text) {
|
|
315
|
-
return text.replace(/"name"\s*:\s*"mcp_
|
|
270
|
+
export function stripToolPrefix(text, aliases = statelessToolNameAliases()) {
|
|
271
|
+
return text.replace(/"name"\s*:\s*"(mcp_[A-Za-z0-9_-]+)"/g, (match, alias) => {
|
|
272
|
+
const name = aliases.decode(alias);
|
|
273
|
+
return name === undefined ? match : `"name": ${JSON.stringify(name)}`;
|
|
274
|
+
});
|
|
316
275
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
276
|
+
const MAX_RESPONSE_JSON_DEPTH = 256;
|
|
277
|
+
function isToolUseWithAlias(value) {
|
|
278
|
+
return (isRecord(value) &&
|
|
279
|
+
value.type === 'tool_use' &&
|
|
280
|
+
typeof value.name === 'string' &&
|
|
281
|
+
value.name.startsWith(TOOL_PREFIX));
|
|
282
|
+
}
|
|
283
|
+
function validateResponseToolName(name) {
|
|
284
|
+
assertWellFormedUtf16(name);
|
|
285
|
+
if (utf8Encoder.encode(name).byteLength > MAX_JSON_TOOL_NAME_BYTES) {
|
|
286
|
+
throw new Error(`JSON tool name exceeds ${MAX_JSON_TOOL_NAME_BYTES} byte limit`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function responseToolNameReplacements(value, aliases) {
|
|
290
|
+
if (!isRecord(value))
|
|
291
|
+
return [];
|
|
292
|
+
if (value.type === 'content_block_start' &&
|
|
293
|
+
isToolUseWithAlias(value.content_block)) {
|
|
294
|
+
validateResponseToolName(value.content_block.name);
|
|
295
|
+
const name = aliases.decode(value.content_block.name);
|
|
296
|
+
return name === undefined
|
|
297
|
+
? []
|
|
298
|
+
: [{ path: ['content_block', 'name'], value: name }];
|
|
299
|
+
}
|
|
300
|
+
if (value.type !== 'message' || !Array.isArray(value.content))
|
|
301
|
+
return [];
|
|
302
|
+
const replacements = [];
|
|
303
|
+
for (let index = 0; index < value.content.length; index++) {
|
|
304
|
+
const block = value.content[index];
|
|
305
|
+
if (!isToolUseWithAlias(block))
|
|
306
|
+
continue;
|
|
307
|
+
validateResponseToolName(block.name);
|
|
308
|
+
const name = aliases.decode(block.name);
|
|
309
|
+
if (name !== undefined) {
|
|
310
|
+
replacements.push({ path: ['content', index, 'name'], value: name });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return replacements;
|
|
314
|
+
}
|
|
315
|
+
function pathKey(path) {
|
|
316
|
+
return JSON.stringify(path);
|
|
317
|
+
}
|
|
318
|
+
function rewriteJsonStringTokens(text, replacements) {
|
|
319
|
+
const wanted = new Map(replacements.map((replacement) => [
|
|
320
|
+
pathKey(replacement.path),
|
|
321
|
+
JSON.stringify(replacement.value),
|
|
322
|
+
]));
|
|
323
|
+
const edits = new Map();
|
|
324
|
+
let visited = 0;
|
|
325
|
+
let objectKeys = 0;
|
|
326
|
+
function failLimits() {
|
|
327
|
+
throw new Error('Anthropic response JSON exceeds traversal limits');
|
|
328
|
+
}
|
|
329
|
+
function skipWhitespace(offset) {
|
|
330
|
+
while (/\s/.test(text[offset] ?? ''))
|
|
331
|
+
offset++;
|
|
332
|
+
return offset;
|
|
333
|
+
}
|
|
334
|
+
function scanString(offset) {
|
|
335
|
+
let cursor = offset + 1;
|
|
336
|
+
while (cursor < text.length) {
|
|
337
|
+
const char = text[cursor];
|
|
338
|
+
if (char === '"')
|
|
339
|
+
return cursor + 1;
|
|
340
|
+
if (char === '\\')
|
|
341
|
+
cursor++;
|
|
342
|
+
cursor++;
|
|
343
|
+
}
|
|
344
|
+
return text.length;
|
|
345
|
+
}
|
|
346
|
+
function walkValue(offset, path, depth) {
|
|
347
|
+
visited++;
|
|
348
|
+
if (visited > MAX_JSON_NODES || depth > MAX_RESPONSE_JSON_DEPTH) {
|
|
349
|
+
failLimits();
|
|
350
|
+
}
|
|
351
|
+
let cursor = skipWhitespace(offset);
|
|
352
|
+
const char = text[cursor];
|
|
353
|
+
if (char === '"') {
|
|
354
|
+
const end = scanString(cursor);
|
|
355
|
+
const key = pathKey(path);
|
|
356
|
+
const replacement = wanted.get(key);
|
|
357
|
+
if (replacement !== undefined) {
|
|
358
|
+
edits.set(key, { start: cursor, end, value: replacement });
|
|
359
|
+
}
|
|
360
|
+
return end;
|
|
361
|
+
}
|
|
362
|
+
if (char === '{') {
|
|
363
|
+
cursor = skipWhitespace(cursor + 1);
|
|
364
|
+
const seenKeys = new Set();
|
|
365
|
+
while (text[cursor] !== '}' && cursor < text.length) {
|
|
366
|
+
const keyStart = cursor;
|
|
367
|
+
const keyEnd = scanString(keyStart);
|
|
368
|
+
const key = JSON.parse(text.slice(keyStart, keyEnd));
|
|
369
|
+
objectKeys += 1;
|
|
370
|
+
if (objectKeys > MAX_JSON_OBJECT_KEYS) {
|
|
371
|
+
throw new Error('Anthropic response JSON exceeds object-key limit');
|
|
372
|
+
}
|
|
373
|
+
if (seenKeys.has(key)) {
|
|
374
|
+
throw new Error('Malformed Anthropic response JSON: duplicate object key');
|
|
375
|
+
}
|
|
376
|
+
seenKeys.add(key);
|
|
377
|
+
cursor = skipWhitespace(keyEnd);
|
|
378
|
+
cursor = skipWhitespace(cursor + 1);
|
|
379
|
+
cursor = walkValue(cursor, [...path, key], depth + 1);
|
|
380
|
+
cursor = skipWhitespace(cursor);
|
|
381
|
+
if (text[cursor] === ',')
|
|
382
|
+
cursor = skipWhitespace(cursor + 1);
|
|
383
|
+
}
|
|
384
|
+
return cursor + 1;
|
|
385
|
+
}
|
|
386
|
+
if (char === '[') {
|
|
387
|
+
cursor = skipWhitespace(cursor + 1);
|
|
388
|
+
let index = 0;
|
|
389
|
+
while (text[cursor] !== ']' && cursor < text.length) {
|
|
390
|
+
cursor = walkValue(cursor, [...path, index], depth + 1);
|
|
391
|
+
index++;
|
|
392
|
+
cursor = skipWhitespace(cursor);
|
|
393
|
+
if (text[cursor] === ',')
|
|
394
|
+
cursor = skipWhitespace(cursor + 1);
|
|
395
|
+
}
|
|
396
|
+
return cursor + 1;
|
|
397
|
+
}
|
|
398
|
+
while (cursor < text.length && !/[\s,\]}]/.test(text[cursor] ?? '')) {
|
|
399
|
+
cursor++;
|
|
400
|
+
}
|
|
401
|
+
return cursor;
|
|
402
|
+
}
|
|
403
|
+
walkValue(0, [], 0);
|
|
404
|
+
let output = text;
|
|
405
|
+
const orderedEdits = [...edits.values()].sort((left, right) => right.start - left.start);
|
|
406
|
+
for (const edit of orderedEdits) {
|
|
407
|
+
output = output.slice(0, edit.start) + edit.value + output.slice(edit.end);
|
|
408
|
+
}
|
|
409
|
+
return output;
|
|
410
|
+
}
|
|
411
|
+
function rewriteResponseJson(text, aliases) {
|
|
412
|
+
let value;
|
|
413
|
+
try {
|
|
414
|
+
value = JSON.parse(text);
|
|
415
|
+
}
|
|
416
|
+
catch {
|
|
417
|
+
return undefined;
|
|
418
|
+
}
|
|
419
|
+
return rewriteJsonStringTokens(text, responseToolNameReplacements(value, aliases));
|
|
420
|
+
}
|
|
421
|
+
function splitSseLines(event) {
|
|
422
|
+
const lines = [];
|
|
423
|
+
let offset = 0;
|
|
424
|
+
while (offset < event.length) {
|
|
425
|
+
let cursor = offset;
|
|
426
|
+
while (cursor < event.length &&
|
|
427
|
+
event[cursor] !== '\r' &&
|
|
428
|
+
event[cursor] !== '\n') {
|
|
429
|
+
cursor += 1;
|
|
430
|
+
}
|
|
431
|
+
let ending = '';
|
|
432
|
+
if (event[cursor] === '\r' && event[cursor + 1] === '\n')
|
|
433
|
+
ending = '\r\n';
|
|
434
|
+
else if (event[cursor] === '\r')
|
|
435
|
+
ending = '\r';
|
|
436
|
+
else if (event[cursor] === '\n')
|
|
437
|
+
ending = '\n';
|
|
438
|
+
lines.push({ content: event.slice(offset, cursor), ending });
|
|
439
|
+
offset = cursor + ending.length;
|
|
440
|
+
}
|
|
441
|
+
return lines;
|
|
442
|
+
}
|
|
443
|
+
function sseDataValue(line) {
|
|
444
|
+
if (line === 'data')
|
|
445
|
+
return '';
|
|
446
|
+
if (!line.startsWith('data:'))
|
|
447
|
+
return undefined;
|
|
448
|
+
const value = line.slice('data:'.length);
|
|
449
|
+
return value.startsWith(' ') ? value.slice(1) : value;
|
|
450
|
+
}
|
|
451
|
+
function transformSseEvent(bytes, aliases) {
|
|
452
|
+
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
453
|
+
const encoder = new TextEncoder();
|
|
454
|
+
const event = decoder.decode(bytes);
|
|
455
|
+
const lines = splitSseLines(event);
|
|
456
|
+
const dataIndexes = [];
|
|
457
|
+
const dataValues = [];
|
|
458
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
459
|
+
const value = sseDataValue(lines[index]?.content ?? '');
|
|
460
|
+
if (value === undefined)
|
|
461
|
+
continue;
|
|
462
|
+
dataIndexes.push(index);
|
|
463
|
+
dataValues.push(value);
|
|
464
|
+
}
|
|
465
|
+
if (dataIndexes.length === 0)
|
|
466
|
+
return bytes;
|
|
467
|
+
const payload = dataValues.join('\n');
|
|
468
|
+
if (payload.trim() === '[DONE]')
|
|
469
|
+
return bytes;
|
|
470
|
+
const firstPayloadCharacter = payload.trimStart()[0];
|
|
471
|
+
const jsonLooking = firstPayloadCharacter === '{' || firstPayloadCharacter === '[';
|
|
472
|
+
if (!payload.includes(TOOL_PREFIX) && !jsonLooking)
|
|
473
|
+
return bytes;
|
|
474
|
+
const rewritten = rewriteResponseJson(payload, aliases);
|
|
475
|
+
if (rewritten === undefined) {
|
|
476
|
+
if (jsonLooking) {
|
|
477
|
+
throw new Error('Malformed or truncated Anthropic response JSON');
|
|
478
|
+
}
|
|
479
|
+
return bytes;
|
|
480
|
+
}
|
|
481
|
+
if (rewritten === payload)
|
|
482
|
+
return bytes;
|
|
483
|
+
const canonical = /[\r\n]/.test(rewritten) || dataIndexes.length > 1
|
|
484
|
+
? JSON.stringify(JSON.parse(rewritten))
|
|
485
|
+
: rewritten;
|
|
486
|
+
const firstIndex = dataIndexes[0];
|
|
487
|
+
const dataIndexSet = new Set(dataIndexes);
|
|
488
|
+
const output = lines
|
|
489
|
+
.map((line, index) => {
|
|
490
|
+
if (index === firstIndex)
|
|
491
|
+
return `data: ${canonical}${line.ending}`;
|
|
492
|
+
if (dataIndexSet.has(index))
|
|
493
|
+
return '';
|
|
494
|
+
return `${line.content}${line.ending}`;
|
|
495
|
+
})
|
|
496
|
+
.join('');
|
|
497
|
+
return encoder.encode(output);
|
|
498
|
+
}
|
|
499
|
+
function createBoundedSseToolNameStream(body, aliases) {
|
|
500
|
+
let event = new Uint8Array(1024);
|
|
501
|
+
let eventLength = 0;
|
|
502
|
+
let lineLength = 0;
|
|
503
|
+
let pendingCr = false;
|
|
504
|
+
const appendEvent = (bytes) => {
|
|
505
|
+
const required = eventLength + bytes.byteLength;
|
|
506
|
+
if (required > MAX_SSE_EVENT_BYTES) {
|
|
507
|
+
throw new Error(`SSE event exceeds ${MAX_SSE_EVENT_BYTES} byte limit`);
|
|
508
|
+
}
|
|
509
|
+
if (required > event.byteLength) {
|
|
510
|
+
let capacity = event.byteLength;
|
|
511
|
+
while (capacity < required) {
|
|
512
|
+
capacity = Math.min(MAX_SSE_EVENT_BYTES, capacity * 2);
|
|
513
|
+
}
|
|
514
|
+
const expanded = new Uint8Array(capacity);
|
|
515
|
+
expanded.set(event.subarray(0, eventLength));
|
|
516
|
+
event = expanded;
|
|
517
|
+
}
|
|
518
|
+
event.set(bytes, eventLength);
|
|
519
|
+
eventLength = required;
|
|
520
|
+
};
|
|
521
|
+
const appendContent = (bytes) => {
|
|
522
|
+
if (lineLength + bytes.byteLength > MAX_SSE_LINE_BYTES) {
|
|
523
|
+
throw new Error(`SSE line exceeds ${MAX_SSE_LINE_BYTES} byte limit`);
|
|
524
|
+
}
|
|
525
|
+
lineLength += bytes.byteLength;
|
|
526
|
+
appendEvent(bytes);
|
|
527
|
+
};
|
|
528
|
+
const dispatch = (controller) => {
|
|
529
|
+
if (eventLength === 0)
|
|
530
|
+
return;
|
|
531
|
+
controller.enqueue(transformSseEvent(event.slice(0, eventLength), aliases));
|
|
532
|
+
eventLength = 0;
|
|
533
|
+
};
|
|
534
|
+
const finishLine = (controller) => {
|
|
535
|
+
const blank = lineLength === 0;
|
|
536
|
+
lineLength = 0;
|
|
537
|
+
if (blank)
|
|
538
|
+
dispatch(controller);
|
|
539
|
+
};
|
|
540
|
+
return body.pipeThrough(new TransformStream({
|
|
541
|
+
transform(chunk, controller) {
|
|
542
|
+
let segmentStart = 0;
|
|
543
|
+
if (pendingCr) {
|
|
544
|
+
pendingCr = false;
|
|
545
|
+
if (chunk[0] === 0x0a) {
|
|
546
|
+
appendEvent(chunk.subarray(0, 1));
|
|
547
|
+
segmentStart = 1;
|
|
548
|
+
}
|
|
549
|
+
finishLine(controller);
|
|
550
|
+
}
|
|
551
|
+
for (let index = segmentStart; index < chunk.byteLength; index += 1) {
|
|
552
|
+
const byte = chunk[index];
|
|
553
|
+
if (byte !== 0x0d && byte !== 0x0a)
|
|
554
|
+
continue;
|
|
555
|
+
appendContent(chunk.subarray(segmentStart, index));
|
|
556
|
+
appendEvent(chunk.subarray(index, index + 1));
|
|
557
|
+
if (byte === 0x0d && index + 1 === chunk.byteLength) {
|
|
558
|
+
pendingCr = true;
|
|
559
|
+
segmentStart = index + 1;
|
|
560
|
+
break;
|
|
561
|
+
}
|
|
562
|
+
if (byte === 0x0d && chunk[index + 1] === 0x0a) {
|
|
563
|
+
appendEvent(chunk.subarray(index + 1, index + 2));
|
|
564
|
+
index += 1;
|
|
565
|
+
}
|
|
566
|
+
finishLine(controller);
|
|
567
|
+
segmentStart = index + 1;
|
|
568
|
+
}
|
|
569
|
+
appendContent(chunk.subarray(segmentStart));
|
|
570
|
+
},
|
|
571
|
+
flush(controller) {
|
|
572
|
+
if (pendingCr) {
|
|
573
|
+
pendingCr = false;
|
|
574
|
+
finishLine(controller);
|
|
575
|
+
}
|
|
576
|
+
dispatch(controller);
|
|
577
|
+
},
|
|
578
|
+
}));
|
|
579
|
+
}
|
|
580
|
+
/** Check whether TLS verification was explicitly requested off for a custom endpoint. */
|
|
321
581
|
export function isInsecure() {
|
|
322
582
|
if (!process.env.ANTHROPIC_BASE_URL?.trim())
|
|
323
583
|
return false;
|
|
324
584
|
const raw = process.env.ANTHROPIC_INSECURE?.trim();
|
|
325
585
|
return raw === '1' || raw === 'true';
|
|
326
586
|
}
|
|
327
|
-
/**
|
|
328
|
-
* Parse ANTHROPIC_BASE_URL from the environment.
|
|
329
|
-
* Returns a valid HTTP(S) URL or null if unset/invalid.
|
|
330
|
-
*/
|
|
587
|
+
/** Resolve the configured custom endpoint without accepting embedded credentials. */
|
|
331
588
|
function resolveBaseUrl() {
|
|
332
589
|
const raw = process.env.ANTHROPIC_BASE_URL?.trim();
|
|
333
590
|
if (!raw)
|
|
@@ -339,6 +596,11 @@ function resolveBaseUrl() {
|
|
|
339
596
|
baseUrl.password) {
|
|
340
597
|
return null;
|
|
341
598
|
}
|
|
599
|
+
const loopback = baseUrl.hostname === 'localhost' ||
|
|
600
|
+
baseUrl.hostname === '127.0.0.1' ||
|
|
601
|
+
baseUrl.hostname === '[::1]';
|
|
602
|
+
if (baseUrl.protocol === 'http:' && !loopback)
|
|
603
|
+
return null;
|
|
342
604
|
return baseUrl;
|
|
343
605
|
}
|
|
344
606
|
catch {
|
|
@@ -346,10 +608,26 @@ function resolveBaseUrl() {
|
|
|
346
608
|
}
|
|
347
609
|
}
|
|
348
610
|
/**
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
|
|
352
|
-
|
|
611
|
+
* Allow OAuth bearer credentials only for Anthropic's official API or the
|
|
612
|
+
* exact origin of an explicitly configured, validated custom endpoint.
|
|
613
|
+
*/
|
|
614
|
+
export function isTrustedAnthropicUrl(input) {
|
|
615
|
+
try {
|
|
616
|
+
const url = new URL(input.toString());
|
|
617
|
+
const configuredOrigin = resolveBaseUrl()?.origin;
|
|
618
|
+
return (url.username === '' &&
|
|
619
|
+
url.password === '' &&
|
|
620
|
+
(url.origin === 'https://api.anthropic.com' ||
|
|
621
|
+
(configuredOrigin !== undefined && url.origin === configuredOrigin)));
|
|
622
|
+
}
|
|
623
|
+
catch {
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Rewrite the request URL to add ?beta=true for /v1/messages requests. When
|
|
629
|
+
* ANTHROPIC_BASE_URL is set, override the origin for API requests while
|
|
630
|
+
* retaining the original path and query.
|
|
353
631
|
*/
|
|
354
632
|
export function rewriteUrl(input) {
|
|
355
633
|
let requestUrl = null;
|
|
@@ -436,77 +714,114 @@ export function prependClaudeCodeIdentity(system) {
|
|
|
436
714
|
return [identityBlock];
|
|
437
715
|
if (typeof system === 'string') {
|
|
438
716
|
const sanitized = sanitizeSystemText(system);
|
|
439
|
-
if (sanitized === CLAUDE_CODE_IDENTITY)
|
|
717
|
+
if (!sanitized || sanitized === CLAUDE_CODE_IDENTITY) {
|
|
440
718
|
return [identityBlock];
|
|
719
|
+
}
|
|
441
720
|
return [identityBlock, { type: 'text', text: sanitized }];
|
|
442
721
|
}
|
|
443
722
|
if (isRecord(system)) {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
723
|
+
if (system.type !== 'text' || typeof system.text !== 'string') {
|
|
724
|
+
return [identityBlock];
|
|
725
|
+
}
|
|
726
|
+
const text = sanitizeSystemText(system.text);
|
|
727
|
+
if (!text || text === CLAUDE_CODE_IDENTITY)
|
|
728
|
+
return [identityBlock];
|
|
729
|
+
return [identityBlock, { ...system, type: 'text', text }];
|
|
447
730
|
}
|
|
448
731
|
if (!Array.isArray(system))
|
|
449
732
|
return [identityBlock];
|
|
450
|
-
const sanitized =
|
|
733
|
+
const sanitized = [];
|
|
734
|
+
for (const item of system) {
|
|
451
735
|
if (typeof item === 'string') {
|
|
452
|
-
|
|
736
|
+
const text = sanitizeSystemText(item);
|
|
737
|
+
if (text && text !== CLAUDE_CODE_IDENTITY) {
|
|
738
|
+
sanitized.push({ type: 'text', text });
|
|
739
|
+
}
|
|
740
|
+
continue;
|
|
453
741
|
}
|
|
454
742
|
if (isRecord(item) &&
|
|
455
743
|
item.type === 'text' &&
|
|
456
744
|
typeof item.text === 'string') {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
type: 'text',
|
|
460
|
-
|
|
461
|
-
};
|
|
745
|
+
const text = sanitizeSystemText(item.text);
|
|
746
|
+
if (text && text !== CLAUDE_CODE_IDENTITY) {
|
|
747
|
+
sanitized.push({ ...item, type: 'text', text });
|
|
748
|
+
}
|
|
462
749
|
}
|
|
463
|
-
return { type: 'text', text: String(item) };
|
|
464
|
-
});
|
|
465
|
-
// Idempotency: don't double-prepend if first block already has the identity
|
|
466
|
-
if (sanitized[0]?.text === CLAUDE_CODE_IDENTITY) {
|
|
467
|
-
return sanitized;
|
|
468
750
|
}
|
|
469
751
|
return [identityBlock, ...sanitized];
|
|
470
752
|
}
|
|
753
|
+
const BILLING_HEADER_PREFIX = 'x-anthropic-billing-header:';
|
|
754
|
+
function isBillingBlock(value) {
|
|
755
|
+
return (isRecord(value) &&
|
|
756
|
+
value.type === 'text' &&
|
|
757
|
+
typeof value.text === 'string' &&
|
|
758
|
+
value.text.startsWith(BILLING_HEADER_PREFIX));
|
|
759
|
+
}
|
|
760
|
+
function hasRewriteMarker(parsed) {
|
|
761
|
+
if (!Array.isArray(parsed.system))
|
|
762
|
+
return false;
|
|
763
|
+
const hasIdentity = parsed.system.some((block) => isRecord(block) && block.text === CLAUDE_CODE_IDENTITY);
|
|
764
|
+
if (!hasIdentity)
|
|
765
|
+
return false;
|
|
766
|
+
const hasUser = Array.isArray(parsed.messages) &&
|
|
767
|
+
parsed.messages.some((message) => isRecord(message) && message.role === 'user');
|
|
768
|
+
return !hasUser || parsed.system.some(isBillingBlock);
|
|
769
|
+
}
|
|
471
770
|
/**
|
|
472
771
|
* Rewrite the full request body: sanitize system prompt and prefix tool names.
|
|
473
|
-
*
|
|
474
|
-
* `version` must be the same value passed to setOAuthHeaders for the same
|
|
475
|
-
* request, otherwise the two reported versions disagree.
|
|
476
772
|
*/
|
|
477
|
-
export function rewriteRequestBody(body, version = CLAUDE_CODE_VERSION) {
|
|
773
|
+
export function rewriteRequestBody(body, version = CLAUDE_CODE_VERSION, aliases = statelessToolNameAliases()) {
|
|
774
|
+
let value;
|
|
478
775
|
try {
|
|
479
|
-
|
|
480
|
-
const billingHeader = Array.isArray(parsed.messages) &&
|
|
481
|
-
parsed.messages.some((message) => message.role === 'user')
|
|
482
|
-
? buildBillingHeaderValue(parsed.messages, version, CLAUDE_CODE_ENTRYPOINT)
|
|
483
|
-
: null;
|
|
484
|
-
// Sanitize system prompt and prepend Claude Code identity
|
|
485
|
-
parsed.system = prependClaudeCodeIdentity(parsed.system);
|
|
486
|
-
// Prepend the billing header as a separate system block so the
|
|
487
|
-
// final layout is: [billing header, identity, ...rest]
|
|
488
|
-
if (billingHeader && Array.isArray(parsed.system)) {
|
|
489
|
-
parsed.system.unshift({ type: 'text', text: billingHeader });
|
|
490
|
-
}
|
|
491
|
-
return prefixToolNames(parsed);
|
|
776
|
+
value = JSON.parse(body);
|
|
492
777
|
}
|
|
493
778
|
catch {
|
|
494
779
|
return body;
|
|
495
780
|
}
|
|
781
|
+
if (!isRecord(value))
|
|
782
|
+
return body;
|
|
783
|
+
const parsed = value;
|
|
784
|
+
const alreadyPrefixed = hasRewriteMarker(parsed);
|
|
785
|
+
const billingHeader = Array.isArray(parsed.messages) &&
|
|
786
|
+
parsed.messages.some((message) => isRecord(message) && message.role === 'user')
|
|
787
|
+
? buildBillingHeaderValue(parsed.messages, version, CLAUDE_CODE_ENTRYPOINT)
|
|
788
|
+
: null;
|
|
789
|
+
// Sanitize system prompt and prepend Claude Code identity
|
|
790
|
+
const system = prependClaudeCodeIdentity(parsed.system).filter((block) => !isBillingBlock(block));
|
|
791
|
+
// Prepend the billing header as a separate system block so the
|
|
792
|
+
// final layout is: [billing header, identity, ...rest]
|
|
793
|
+
if (billingHeader) {
|
|
794
|
+
system.unshift({ type: 'text', text: billingHeader });
|
|
795
|
+
}
|
|
796
|
+
parsed.system = system;
|
|
797
|
+
return prefixToolNames(parsed, alreadyPrefixed, aliases);
|
|
496
798
|
}
|
|
497
799
|
/**
|
|
498
800
|
* Create a streaming response that strips the tool prefix from tool names.
|
|
499
801
|
*/
|
|
500
|
-
export function createStrippedStream(response) {
|
|
802
|
+
export function createStrippedStream(response, aliases = statelessToolNameAliases(), onFinalize = () => { }) {
|
|
803
|
+
let finalized = false;
|
|
804
|
+
const finalize = () => {
|
|
805
|
+
if (finalized)
|
|
806
|
+
return;
|
|
807
|
+
finalized = true;
|
|
808
|
+
onFinalize();
|
|
809
|
+
};
|
|
810
|
+
if (!response.ok) {
|
|
811
|
+
finalize();
|
|
812
|
+
return response;
|
|
813
|
+
}
|
|
501
814
|
const mediaType = response.headers
|
|
502
815
|
.get('content-type')
|
|
503
816
|
?.split(';', 1)[0]
|
|
504
817
|
?.trim()
|
|
505
818
|
.toLowerCase();
|
|
506
|
-
if (!response.body)
|
|
819
|
+
if (!response.body) {
|
|
820
|
+
finalize();
|
|
507
821
|
return response;
|
|
822
|
+
}
|
|
508
823
|
if (mediaType === 'application/json' || mediaType?.endsWith('+json')) {
|
|
509
|
-
const stream =
|
|
824
|
+
const stream = finalizeStream(createBoundedJsonToolNameStream(response.body, TOOL_PREFIX, (alias) => aliases.decode(alias)), finalize);
|
|
510
825
|
const headers = headersAfterBodyTransform(response.headers);
|
|
511
826
|
return new Response(stream, {
|
|
512
827
|
status: response.status,
|
|
@@ -514,63 +829,11 @@ export function createStrippedStream(response) {
|
|
|
514
829
|
headers,
|
|
515
830
|
});
|
|
516
831
|
}
|
|
517
|
-
if (mediaType !== 'text/event-stream')
|
|
832
|
+
if (mediaType !== 'text/event-stream') {
|
|
833
|
+
finalize();
|
|
518
834
|
return response;
|
|
519
|
-
|
|
520
|
-
const
|
|
521
|
-
let pending = new Uint8Array(0);
|
|
522
|
-
let pendingLength = 0;
|
|
523
|
-
const appendPending = (bytes) => {
|
|
524
|
-
const requiredLength = pendingLength + bytes.byteLength;
|
|
525
|
-
if (requiredLength > MAX_SSE_LINE_BYTES) {
|
|
526
|
-
throw new Error(`SSE line exceeds ${MAX_SSE_LINE_BYTES} byte limit`);
|
|
527
|
-
}
|
|
528
|
-
if (requiredLength > pending.byteLength) {
|
|
529
|
-
let capacity = Math.max(1024, pending.byteLength);
|
|
530
|
-
while (capacity < requiredLength) {
|
|
531
|
-
capacity = Math.min(MAX_SSE_LINE_BYTES, capacity * 2);
|
|
532
|
-
}
|
|
533
|
-
const expanded = new Uint8Array(capacity);
|
|
534
|
-
expanded.set(pending.subarray(0, pendingLength));
|
|
535
|
-
pending = expanded;
|
|
536
|
-
}
|
|
537
|
-
pending.set(bytes, pendingLength);
|
|
538
|
-
pendingLength = requiredLength;
|
|
539
|
-
};
|
|
540
|
-
const stream = response.body.pipeThrough(new TransformStream({
|
|
541
|
-
transform(chunk, controller) {
|
|
542
|
-
let lastLineBreak = -1;
|
|
543
|
-
let lineLength = pendingLength;
|
|
544
|
-
for (let index = 0; index < chunk.byteLength; index++) {
|
|
545
|
-
if (chunk[index] === 0x0a || chunk[index] === 0x0d) {
|
|
546
|
-
lastLineBreak = index;
|
|
547
|
-
lineLength = 0;
|
|
548
|
-
}
|
|
549
|
-
else {
|
|
550
|
-
lineLength++;
|
|
551
|
-
if (lineLength > MAX_SSE_LINE_BYTES) {
|
|
552
|
-
throw new Error(`SSE line exceeds ${MAX_SSE_LINE_BYTES} byte limit`);
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
if (lastLineBreak < 0) {
|
|
557
|
-
appendPending(chunk);
|
|
558
|
-
return;
|
|
559
|
-
}
|
|
560
|
-
const completeLines = decoder.decode(pending.subarray(0, pendingLength), {
|
|
561
|
-
stream: true,
|
|
562
|
-
}) + decoder.decode(chunk.subarray(0, lastLineBreak + 1));
|
|
563
|
-
pendingLength = 0;
|
|
564
|
-
appendPending(chunk.subarray(lastLineBreak + 1));
|
|
565
|
-
controller.enqueue(encoder.encode(stripToolPrefix(completeLines)));
|
|
566
|
-
},
|
|
567
|
-
flush(controller) {
|
|
568
|
-
const trailing = decoder.decode(pending.subarray(0, pendingLength));
|
|
569
|
-
if (trailing) {
|
|
570
|
-
controller.enqueue(encoder.encode(stripToolPrefix(trailing)));
|
|
571
|
-
}
|
|
572
|
-
},
|
|
573
|
-
}));
|
|
835
|
+
}
|
|
836
|
+
const stream = finalizeStream(createBoundedSseToolNameStream(response.body, aliases), finalize);
|
|
574
837
|
const headers = headersAfterBodyTransform(response.headers);
|
|
575
838
|
return new Response(stream, {
|
|
576
839
|
status: response.status,
|