@ex-machina/opencode-anthropic-auth 2.0.0-next.0 → 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 +102 -37
- 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.d.ts +39 -0
- package/dist/config.js +78 -0
- package/dist/constants.d.ts +10 -2
- package/dist/constants.js +12 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +535 -51
- 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 +42 -14
- package/dist/transform.js +595 -326
- package/package.json +4 -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
|
-
import { CLAUDE_CODE_ENTRYPOINT, CLAUDE_CODE_IDENTITY, OPENCODE_IDENTITY_PREFIX, PARAGRAPH_REMOVAL_ANCHORS, REQUIRED_BETAS, TEXT_REPLACEMENTS, TOOL_PREFIX,
|
|
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.
|
|
@@ -272,10 +227,10 @@ export function mergeBetaHeaders(headers) {
|
|
|
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
229
|
*/
|
|
275
|
-
export function setOAuthHeaders(headers, accessToken) {
|
|
230
|
+
export function setOAuthHeaders(headers, accessToken, version = CLAUDE_CODE_VERSION) {
|
|
276
231
|
headers.set('authorization', `Bearer ${accessToken}`);
|
|
277
232
|
headers.set('anthropic-beta', mergeBetaHeaders(headers));
|
|
278
|
-
headers.set('user-agent',
|
|
233
|
+
headers.set('user-agent', formatUserAgent(version));
|
|
279
234
|
headers.delete('x-api-key');
|
|
280
235
|
return headers;
|
|
281
236
|
}
|
|
@@ -283,24 +238,28 @@ export function setOAuthHeaders(headers, accessToken) {
|
|
|
283
238
|
* Add TOOL_PREFIX to tool names in the request body.
|
|
284
239
|
* Prefixes both tool definitions and tool_use blocks in messages.
|
|
285
240
|
*/
|
|
286
|
-
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);
|
|
287
245
|
if (parsed.tools && Array.isArray(parsed.tools)) {
|
|
288
|
-
parsed.tools = parsed.tools.map((tool) => (
|
|
289
|
-
...tool,
|
|
290
|
-
|
|
291
|
-
}));
|
|
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);
|
|
292
249
|
}
|
|
293
250
|
if (parsed.messages && Array.isArray(parsed.messages)) {
|
|
294
|
-
parsed.messages = parsed.messages.map((
|
|
295
|
-
if (
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
+
};
|
|
304
263
|
});
|
|
305
264
|
}
|
|
306
265
|
return JSON.stringify(parsed);
|
|
@@ -308,23 +267,324 @@ export function prefixToolNames(parsed) {
|
|
|
308
267
|
/**
|
|
309
268
|
* Strip TOOL_PREFIX from tool names in streaming response text.
|
|
310
269
|
*/
|
|
311
|
-
export function stripToolPrefix(text) {
|
|
312
|
-
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
|
+
});
|
|
313
275
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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. */
|
|
318
581
|
export function isInsecure() {
|
|
319
582
|
if (!process.env.ANTHROPIC_BASE_URL?.trim())
|
|
320
583
|
return false;
|
|
321
584
|
const raw = process.env.ANTHROPIC_INSECURE?.trim();
|
|
322
585
|
return raw === '1' || raw === 'true';
|
|
323
586
|
}
|
|
324
|
-
/**
|
|
325
|
-
* Parse ANTHROPIC_BASE_URL from the environment.
|
|
326
|
-
* Returns a valid HTTP(S) URL or null if unset/invalid.
|
|
327
|
-
*/
|
|
587
|
+
/** Resolve the configured custom endpoint without accepting embedded credentials. */
|
|
328
588
|
function resolveBaseUrl() {
|
|
329
589
|
const raw = process.env.ANTHROPIC_BASE_URL?.trim();
|
|
330
590
|
if (!raw)
|
|
@@ -336,6 +596,11 @@ function resolveBaseUrl() {
|
|
|
336
596
|
baseUrl.password) {
|
|
337
597
|
return null;
|
|
338
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;
|
|
339
604
|
return baseUrl;
|
|
340
605
|
}
|
|
341
606
|
catch {
|
|
@@ -343,10 +608,26 @@ function resolveBaseUrl() {
|
|
|
343
608
|
}
|
|
344
609
|
}
|
|
345
610
|
/**
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
|
|
349
|
-
|
|
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.
|
|
350
631
|
*/
|
|
351
632
|
export function rewriteUrl(input) {
|
|
352
633
|
let requestUrl = null;
|
|
@@ -433,74 +714,114 @@ export function prependClaudeCodeIdentity(system) {
|
|
|
433
714
|
return [identityBlock];
|
|
434
715
|
if (typeof system === 'string') {
|
|
435
716
|
const sanitized = sanitizeSystemText(system);
|
|
436
|
-
if (sanitized === CLAUDE_CODE_IDENTITY)
|
|
717
|
+
if (!sanitized || sanitized === CLAUDE_CODE_IDENTITY) {
|
|
437
718
|
return [identityBlock];
|
|
719
|
+
}
|
|
438
720
|
return [identityBlock, { type: 'text', text: sanitized }];
|
|
439
721
|
}
|
|
440
722
|
if (isRecord(system)) {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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 }];
|
|
444
730
|
}
|
|
445
731
|
if (!Array.isArray(system))
|
|
446
732
|
return [identityBlock];
|
|
447
|
-
const sanitized =
|
|
733
|
+
const sanitized = [];
|
|
734
|
+
for (const item of system) {
|
|
448
735
|
if (typeof item === 'string') {
|
|
449
|
-
|
|
736
|
+
const text = sanitizeSystemText(item);
|
|
737
|
+
if (text && text !== CLAUDE_CODE_IDENTITY) {
|
|
738
|
+
sanitized.push({ type: 'text', text });
|
|
739
|
+
}
|
|
740
|
+
continue;
|
|
450
741
|
}
|
|
451
742
|
if (isRecord(item) &&
|
|
452
743
|
item.type === 'text' &&
|
|
453
744
|
typeof item.text === 'string') {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
type: 'text',
|
|
457
|
-
|
|
458
|
-
};
|
|
745
|
+
const text = sanitizeSystemText(item.text);
|
|
746
|
+
if (text && text !== CLAUDE_CODE_IDENTITY) {
|
|
747
|
+
sanitized.push({ ...item, type: 'text', text });
|
|
748
|
+
}
|
|
459
749
|
}
|
|
460
|
-
return { type: 'text', text: String(item) };
|
|
461
|
-
});
|
|
462
|
-
// Idempotency: don't double-prepend if first block already has the identity
|
|
463
|
-
if (sanitized[0]?.text === CLAUDE_CODE_IDENTITY) {
|
|
464
|
-
return sanitized;
|
|
465
750
|
}
|
|
466
751
|
return [identityBlock, ...sanitized];
|
|
467
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
|
+
}
|
|
468
770
|
/**
|
|
469
771
|
* Rewrite the full request body: sanitize system prompt and prefix tool names.
|
|
470
772
|
*/
|
|
471
|
-
export function rewriteRequestBody(body) {
|
|
773
|
+
export function rewriteRequestBody(body, version = CLAUDE_CODE_VERSION, aliases = statelessToolNameAliases()) {
|
|
774
|
+
let value;
|
|
472
775
|
try {
|
|
473
|
-
|
|
474
|
-
const billingHeader = Array.isArray(parsed.messages) &&
|
|
475
|
-
parsed.messages.some((message) => message.role === 'user')
|
|
476
|
-
? buildBillingHeaderValue(parsed.messages, undefined, CLAUDE_CODE_ENTRYPOINT)
|
|
477
|
-
: null;
|
|
478
|
-
// Sanitize system prompt and prepend Claude Code identity
|
|
479
|
-
parsed.system = prependClaudeCodeIdentity(parsed.system);
|
|
480
|
-
// Prepend the billing header as a separate system block so the
|
|
481
|
-
// final layout is: [billing header, identity, ...rest]
|
|
482
|
-
if (billingHeader && Array.isArray(parsed.system)) {
|
|
483
|
-
parsed.system.unshift({ type: 'text', text: billingHeader });
|
|
484
|
-
}
|
|
485
|
-
return prefixToolNames(parsed);
|
|
776
|
+
value = JSON.parse(body);
|
|
486
777
|
}
|
|
487
778
|
catch {
|
|
488
779
|
return body;
|
|
489
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);
|
|
490
798
|
}
|
|
491
799
|
/**
|
|
492
800
|
* Create a streaming response that strips the tool prefix from tool names.
|
|
493
801
|
*/
|
|
494
|
-
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
|
+
}
|
|
495
814
|
const mediaType = response.headers
|
|
496
815
|
.get('content-type')
|
|
497
816
|
?.split(';', 1)[0]
|
|
498
817
|
?.trim()
|
|
499
818
|
.toLowerCase();
|
|
500
|
-
if (!response.body)
|
|
819
|
+
if (!response.body) {
|
|
820
|
+
finalize();
|
|
501
821
|
return response;
|
|
822
|
+
}
|
|
502
823
|
if (mediaType === 'application/json' || mediaType?.endsWith('+json')) {
|
|
503
|
-
const stream =
|
|
824
|
+
const stream = finalizeStream(createBoundedJsonToolNameStream(response.body, TOOL_PREFIX, (alias) => aliases.decode(alias)), finalize);
|
|
504
825
|
const headers = headersAfterBodyTransform(response.headers);
|
|
505
826
|
return new Response(stream, {
|
|
506
827
|
status: response.status,
|
|
@@ -508,63 +829,11 @@ export function createStrippedStream(response) {
|
|
|
508
829
|
headers,
|
|
509
830
|
});
|
|
510
831
|
}
|
|
511
|
-
if (mediaType !== 'text/event-stream')
|
|
832
|
+
if (mediaType !== 'text/event-stream') {
|
|
833
|
+
finalize();
|
|
512
834
|
return response;
|
|
513
|
-
|
|
514
|
-
const
|
|
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);
|
|
552
|
-
return;
|
|
553
|
-
}
|
|
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)));
|
|
560
|
-
},
|
|
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
|
-
}));
|
|
835
|
+
}
|
|
836
|
+
const stream = finalizeStream(createBoundedSseToolNameStream(response.body, aliases), finalize);
|
|
568
837
|
const headers = headersAfterBodyTransform(response.headers);
|
|
569
838
|
return new Response(stream, {
|
|
570
839
|
status: response.status,
|