@1outres/dsh-plugin-claude-subscription 0.0.0-stage → 0.4.0
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/LICENSE +22 -0
- package/README.md +146 -2
- package/cordis.patch.yml +6 -0
- package/lib/index.js +3773 -0
- package/lib/inert-mcp.js +33 -0
- package/package.json +52 -3
package/lib/index.js
ADDED
|
@@ -0,0 +1,3773 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
|
|
4
|
+
// src/errors.ts
|
|
5
|
+
var FailureCode = {
|
|
6
|
+
aborted: "ABORTED",
|
|
7
|
+
contextWindowExceeded: "CONTEXT_WINDOW_EXCEEDED",
|
|
8
|
+
emptyResponse: "EMPTY_RESPONSE",
|
|
9
|
+
imageOffloadRequired: "IMAGE_OFFLOAD_REQUIRED",
|
|
10
|
+
invalidCredential: "INVALID_CREDENTIAL",
|
|
11
|
+
quota: "QUOTA",
|
|
12
|
+
rateLimit: "RATE_LIMIT",
|
|
13
|
+
timeout: "TIMEOUT",
|
|
14
|
+
server: "SERVER",
|
|
15
|
+
transport: "TRANSPORT",
|
|
16
|
+
claudeCodeMissing: "CLAUDE_CODE_MISSING",
|
|
17
|
+
conflictingEnvironment: "CONFLICTING_ENVIRONMENT",
|
|
18
|
+
invalidRequest: "INVALID_REQUEST",
|
|
19
|
+
nativeProtocol: "NATIVE_PROTOCOL",
|
|
20
|
+
upstreamError: "UPSTREAM_ERROR",
|
|
21
|
+
relayError: "RELAY_ERROR"
|
|
22
|
+
};
|
|
23
|
+
var REQUEST_ID = /^req_[A-Za-z0-9_-]{1,124}$/;
|
|
24
|
+
var NativeFailure = class extends Error {
|
|
25
|
+
code;
|
|
26
|
+
failure;
|
|
27
|
+
constructor(message, code, facts = {}) {
|
|
28
|
+
const requestId = facts.requestId !== void 0 && REQUEST_ID.test(facts.requestId) ? facts.requestId : void 0;
|
|
29
|
+
const text = requestId === void 0 ? message : `${message} (request_id=${requestId})`;
|
|
30
|
+
super(text, facts.cause === void 0 ? void 0 : { cause: facts.cause });
|
|
31
|
+
this.name = "NativeFailure";
|
|
32
|
+
this.code = code;
|
|
33
|
+
this.failure = {
|
|
34
|
+
message: text,
|
|
35
|
+
code,
|
|
36
|
+
...facts.status === void 0 ? {} : { status: facts.status },
|
|
37
|
+
...facts.providerRetryAfterMs === void 0 ? {} : { providerRetryAfterMs: facts.providerRetryAfterMs },
|
|
38
|
+
...requestId === void 0 ? {} : { requestId },
|
|
39
|
+
...facts.offloadImages === void 0 ? {} : { offloadImages: facts.offloadImages }
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// src/response/finish.ts
|
|
45
|
+
var CONTENT_FILTER_CODE = "CONTENT_FILTER";
|
|
46
|
+
var CUT_OFF_STOP_REASONS = /* @__PURE__ */ new Set(["max_tokens", "model_context_window_exceeded"]);
|
|
47
|
+
function isRefusal(assistants) {
|
|
48
|
+
return assistants.some((message) => message.stop_reason === "refusal");
|
|
49
|
+
}
|
|
50
|
+
function isCutOff(assistants) {
|
|
51
|
+
return assistants.some((message) => CUT_OFF_STOP_REASONS.has(message.stop_reason ?? ""));
|
|
52
|
+
}
|
|
53
|
+
function refusalFailure(assistants) {
|
|
54
|
+
const details = assistants.find((message) => message.stop_details !== void 0)?.stop_details;
|
|
55
|
+
return { code: CONTENT_FILTER_CODE, message: refusalMessage(details) };
|
|
56
|
+
}
|
|
57
|
+
function refusalMessage(details) {
|
|
58
|
+
const explanation = details?.explanation;
|
|
59
|
+
if (typeof explanation === "string" && explanation.trim() !== "") return explanation.trim();
|
|
60
|
+
const category = details?.category;
|
|
61
|
+
if (typeof category === "string" && category !== "") {
|
|
62
|
+
return `provider refusal category: ${category}`;
|
|
63
|
+
}
|
|
64
|
+
return "The provider refused this request.";
|
|
65
|
+
}
|
|
66
|
+
function usageOf(usage) {
|
|
67
|
+
const cacheRead = usage.cache_read_input_tokens;
|
|
68
|
+
const cacheWrite = usage.cache_creation_input_tokens;
|
|
69
|
+
const reasoning = usage.output_tokens_details?.thinking_tokens;
|
|
70
|
+
return {
|
|
71
|
+
inputTokens: usage.input_tokens,
|
|
72
|
+
outputTokens: usage.output_tokens,
|
|
73
|
+
totalTokens: usage.input_tokens + usage.output_tokens + (cacheRead ?? 0) + (cacheWrite ?? 0),
|
|
74
|
+
...cacheRead === void 0 ? {} : { cacheReadTokens: cacheRead },
|
|
75
|
+
...cacheWrite === void 0 ? {} : { cacheWriteTokens: cacheWrite },
|
|
76
|
+
...reasoning === void 0 ? {} : { reasoningTokens: reasoning }
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/response/lossless.ts
|
|
81
|
+
var OBJECT_PROTOTYPE = Object.prototype;
|
|
82
|
+
var ARRAY_PROTOTYPE = Array.prototype;
|
|
83
|
+
function isLosslessNumber(value) {
|
|
84
|
+
return Number.isFinite(value) && !Object.is(value, -0);
|
|
85
|
+
}
|
|
86
|
+
function isLosslessArray(value, ancestors) {
|
|
87
|
+
if (Object.getPrototypeOf(value) !== ARRAY_PROTOTYPE) return false;
|
|
88
|
+
if (Reflect.ownKeys(value).length !== value.length + 1) return false;
|
|
89
|
+
return value.every(
|
|
90
|
+
(_item, index) => Object.hasOwn(value, index) && isLossless(value[index], ancestors)
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
function isLosslessObject(value, ancestors) {
|
|
94
|
+
const prototype = Object.getPrototypeOf(value);
|
|
95
|
+
if (prototype !== OBJECT_PROTOTYPE && prototype !== null) return false;
|
|
96
|
+
const keys = Reflect.ownKeys(value);
|
|
97
|
+
return keys.every(
|
|
98
|
+
(key) => typeof key === "string" && Object.prototype.propertyIsEnumerable.call(value, key) && isLossless(value[key], ancestors)
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
function isLossless(value, ancestors) {
|
|
102
|
+
if (value === null || typeof value === "boolean" || typeof value === "string") return true;
|
|
103
|
+
if (typeof value === "number") return isLosslessNumber(value);
|
|
104
|
+
if (typeof value !== "object" || ancestors.has(value)) return false;
|
|
105
|
+
ancestors.add(value);
|
|
106
|
+
const lossless = Array.isArray(value) ? isLosslessArray(value, ancestors) : isLosslessObject(value, ancestors);
|
|
107
|
+
ancestors.delete(value);
|
|
108
|
+
return lossless;
|
|
109
|
+
}
|
|
110
|
+
function isLosslessJson(value) {
|
|
111
|
+
return isLossless(value, /* @__PURE__ */ new Set());
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// src/contract.ts
|
|
115
|
+
var PROVIDER_ID = "claude-subscription";
|
|
116
|
+
var MCP_SERVER_NAME = "dsh";
|
|
117
|
+
var TOOL_PREFIX = `mcp__${MCP_SERVER_NAME}__`;
|
|
118
|
+
|
|
119
|
+
// src/response/tool-names.ts
|
|
120
|
+
function invertToolNames(toolNames) {
|
|
121
|
+
return new Map([...toolNames].map(([dshName, nativeName]) => [nativeName, dshName]));
|
|
122
|
+
}
|
|
123
|
+
function dshToolName(nativeName, names) {
|
|
124
|
+
const bare = nativeName.startsWith(TOOL_PREFIX) ? nativeName.slice(TOOL_PREFIX.length) : nativeName;
|
|
125
|
+
return names.get(bare) ?? bare;
|
|
126
|
+
}
|
|
127
|
+
function withOfferedToolNames(message, names) {
|
|
128
|
+
return {
|
|
129
|
+
...message,
|
|
130
|
+
content: message.content.map(
|
|
131
|
+
(block) => block.type === "tool_use" && typeof block.name === "string" && names.has(block.name) ? { ...block, name: `${TOOL_PREFIX}${block.name}` } : block
|
|
132
|
+
)
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/response/chunks.ts
|
|
137
|
+
var ChunkTranslator = class {
|
|
138
|
+
names;
|
|
139
|
+
projectionOf;
|
|
140
|
+
streamed = /* @__PURE__ */ new Map();
|
|
141
|
+
settled = /* @__PURE__ */ new Map();
|
|
142
|
+
nextIndex = 0;
|
|
143
|
+
constructor(options) {
|
|
144
|
+
this.names = invertToolNames(options.toolNames);
|
|
145
|
+
this.projectionOf = options.projectionOf;
|
|
146
|
+
}
|
|
147
|
+
event(event) {
|
|
148
|
+
switch (event.type) {
|
|
149
|
+
case "content_block_start":
|
|
150
|
+
return this.startBlock(event);
|
|
151
|
+
case "content_block_delta":
|
|
152
|
+
return this.applyDelta(event);
|
|
153
|
+
case "content_block_stop":
|
|
154
|
+
return this.stopBlock(event);
|
|
155
|
+
default:
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/** @throws NativeFailure when the final message contradicts what was streamed. */
|
|
160
|
+
complete(completion) {
|
|
161
|
+
const { assistants } = completion;
|
|
162
|
+
const usage = { type: "usage", usage: usageOf(completion.usage) };
|
|
163
|
+
if (isRefusal(assistants)) {
|
|
164
|
+
return [usage, finishChunk({ kind: "error", failure: refusalFailure(assistants) })];
|
|
165
|
+
}
|
|
166
|
+
const blocks = assistants.flatMap((message) => message.content);
|
|
167
|
+
const hasToolUse = blocks.some(isToolUse);
|
|
168
|
+
const display = this.settleDisplay(blocks);
|
|
169
|
+
if (isCutOff(assistants)) {
|
|
170
|
+
const replayState = hasToolUse ? void 0 : this.replayState(completion);
|
|
171
|
+
return [...display, usage, finishChunk({ kind: "max-tokens" }, replayState)];
|
|
172
|
+
}
|
|
173
|
+
const toolCalls = this.settleToolCalls(blocks);
|
|
174
|
+
if (!hasToolUse && !hasVisibleText(blocks)) {
|
|
175
|
+
return [...display, usage, finishChunk({ kind: "error", failure: EMPTY_RESPONSE })];
|
|
176
|
+
}
|
|
177
|
+
const reason = { kind: hasToolUse ? "tool-calls" : "stop" };
|
|
178
|
+
return [...display, ...toolCalls, usage, finishChunk(reason, this.replayState(completion))];
|
|
179
|
+
}
|
|
180
|
+
startBlock(event) {
|
|
181
|
+
const nativeIndex = blockIndexOf(event);
|
|
182
|
+
if (this.streamed.has(nativeIndex)) malformed(`block ${nativeIndex} started twice`);
|
|
183
|
+
const block = objectField(event, "content_block");
|
|
184
|
+
switch (block.type) {
|
|
185
|
+
case "text":
|
|
186
|
+
return this.startDisplay(nativeIndex, "text", block.text);
|
|
187
|
+
case "thinking":
|
|
188
|
+
return this.startDisplay(nativeIndex, "reasoning", block.thinking);
|
|
189
|
+
case "tool_use":
|
|
190
|
+
return this.startToolCall(nativeIndex, block);
|
|
191
|
+
default:
|
|
192
|
+
this.streamed.set(nativeIndex, { kind: "hidden" });
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
startDisplay(nativeIndex, kind, initial) {
|
|
197
|
+
const block = newDisplayBlock(kind);
|
|
198
|
+
this.streamed.set(nativeIndex, block);
|
|
199
|
+
return typeof initial === "string" ? this.appendDisplay(block, initial) : [];
|
|
200
|
+
}
|
|
201
|
+
startToolCall(nativeIndex, block) {
|
|
202
|
+
const id = stringField(block, "id");
|
|
203
|
+
const name2 = dshToolName(stringField(block, "name"), this.names);
|
|
204
|
+
const index = this.takeIndex();
|
|
205
|
+
this.streamed.set(nativeIndex, { kind: "tool-call", index, id, name: name2 });
|
|
206
|
+
return [
|
|
207
|
+
{ type: "block-start", index, blockType: "tool-call" },
|
|
208
|
+
{ type: "tool-call-delta", index, id: toolCallId(id), name: name2, argumentsDelta: "" }
|
|
209
|
+
];
|
|
210
|
+
}
|
|
211
|
+
applyDelta(event) {
|
|
212
|
+
const nativeIndex = blockIndexOf(event);
|
|
213
|
+
const block = this.streamed.get(nativeIndex);
|
|
214
|
+
if (block === void 0) malformed(`delta for block ${nativeIndex}, which never started`);
|
|
215
|
+
if (block.kind === "hidden") return [];
|
|
216
|
+
const delta = objectField(event, "delta");
|
|
217
|
+
switch (delta.type) {
|
|
218
|
+
case "text_delta":
|
|
219
|
+
if (block.kind !== "text") malformed(`text delta for a ${block.kind} block`);
|
|
220
|
+
return this.appendDisplay(block, stringField(delta, "text"));
|
|
221
|
+
case "thinking_delta":
|
|
222
|
+
if (block.kind !== "reasoning") malformed(`thinking delta for a ${block.kind} block`);
|
|
223
|
+
return this.appendDisplay(block, stringField(delta, "thinking"));
|
|
224
|
+
case "input_json_delta":
|
|
225
|
+
if (block.kind !== "tool-call") malformed(`tool input delta for a ${block.kind} block`);
|
|
226
|
+
return [
|
|
227
|
+
{
|
|
228
|
+
type: "tool-call-delta",
|
|
229
|
+
index: block.index,
|
|
230
|
+
id: toolCallId(block.id),
|
|
231
|
+
argumentsDelta: stringField(delta, "partial_json")
|
|
232
|
+
}
|
|
233
|
+
];
|
|
234
|
+
default:
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
appendDisplay(block, text) {
|
|
239
|
+
if (text === "") return [];
|
|
240
|
+
const chunks = [];
|
|
241
|
+
if (block.index === void 0) {
|
|
242
|
+
block.index = this.takeIndex();
|
|
243
|
+
chunks.push({ type: "block-start", index: block.index, blockType: block.kind });
|
|
244
|
+
}
|
|
245
|
+
block.text += text;
|
|
246
|
+
chunks.push(
|
|
247
|
+
block.kind === "text" ? { type: "text-delta", index: block.index, text } : { type: "reasoning-delta", index: block.index, text }
|
|
248
|
+
);
|
|
249
|
+
return chunks;
|
|
250
|
+
}
|
|
251
|
+
stopBlock(event) {
|
|
252
|
+
const block = this.streamed.get(blockIndexOf(event));
|
|
253
|
+
if (block === void 0) malformed("stop for a block that never started");
|
|
254
|
+
return block.kind === "text" || block.kind === "reasoning" ? this.closeDisplay(block) : [];
|
|
255
|
+
}
|
|
256
|
+
closeDisplay(block) {
|
|
257
|
+
if (block.index === void 0 || block.closed) return [];
|
|
258
|
+
block.closed = true;
|
|
259
|
+
const content = displayContent(block.kind, block.text);
|
|
260
|
+
this.settled.set(block.index, content);
|
|
261
|
+
return [{ type: "block-end", index: block.index, block: content }];
|
|
262
|
+
}
|
|
263
|
+
settleDisplay(blocks) {
|
|
264
|
+
const finals = blocks.flatMap(displayOf);
|
|
265
|
+
const streamed = [...this.streamed.values()].filter(isDisplayBlock);
|
|
266
|
+
const lost = streamed.slice(finals.length).find((block) => block.text !== "");
|
|
267
|
+
if (lost !== void 0) malformed(`the final message lost streamed ${lost.kind}`);
|
|
268
|
+
return finals.flatMap((final, position) => {
|
|
269
|
+
const block = streamed[position] ?? newDisplayBlock(final.kind);
|
|
270
|
+
if (block.kind !== final.kind) malformed("final block kinds differ from the stream");
|
|
271
|
+
return this.settleOneDisplay(block, final.text);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
settleOneDisplay(block, final) {
|
|
275
|
+
const matches = block.closed ? block.text === final : final.startsWith(block.text);
|
|
276
|
+
if (!matches && block.kind === "text") {
|
|
277
|
+
malformed("the final text differs from the incremental stream");
|
|
278
|
+
}
|
|
279
|
+
if (block.closed) return [];
|
|
280
|
+
const chunks = matches ? this.appendDisplay(block, final.slice(block.text.length)) : [];
|
|
281
|
+
if (!matches) block.text = final;
|
|
282
|
+
return [...chunks, ...this.closeDisplay(block)];
|
|
283
|
+
}
|
|
284
|
+
settleToolCalls(blocks) {
|
|
285
|
+
const calls = blocks.filter(isToolUse);
|
|
286
|
+
const finalIds = new Set(calls.map((call) => stringField(call, "id")));
|
|
287
|
+
const streamed = [...this.streamed.values()].filter(isToolCallStream);
|
|
288
|
+
const lost = streamed.find((call) => !finalIds.has(call.id));
|
|
289
|
+
if (lost !== void 0) malformed(`the tool call to ${lost.name} has no complete arguments`);
|
|
290
|
+
return calls.flatMap((call) => {
|
|
291
|
+
const id = stringField(call, "id");
|
|
292
|
+
const name2 = dshToolName(stringField(call, "name"), this.names);
|
|
293
|
+
const args = JSON.stringify(inputObjectOf(call));
|
|
294
|
+
const open = streamed.find((candidate) => candidate.id === id);
|
|
295
|
+
const index = open?.index ?? this.takeIndex();
|
|
296
|
+
const content = {
|
|
297
|
+
type: "tool-call",
|
|
298
|
+
id: toolCallId(id),
|
|
299
|
+
name: name2,
|
|
300
|
+
arguments: args
|
|
301
|
+
};
|
|
302
|
+
this.settled.set(index, content);
|
|
303
|
+
const opening = open !== void 0 ? [] : [
|
|
304
|
+
{ type: "block-start", index, blockType: "tool-call" },
|
|
305
|
+
{ type: "tool-call-delta", index, id: toolCallId(id), name: name2, argumentsDelta: args }
|
|
306
|
+
];
|
|
307
|
+
return [...opening, { type: "block-end", index, block: content }];
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
replayState(completion) {
|
|
311
|
+
const messages = completion.assistants.map(
|
|
312
|
+
(message) => withOfferedToolNames(message, this.names)
|
|
313
|
+
);
|
|
314
|
+
if (!isReplayable(messages)) return void 0;
|
|
315
|
+
const carrier = {
|
|
316
|
+
kind: "claude-subscription",
|
|
317
|
+
version: 1,
|
|
318
|
+
messages,
|
|
319
|
+
projection: this.projectionOf(this.settledInOrder())
|
|
320
|
+
};
|
|
321
|
+
return isLosslessJson(carrier) ? { response: carrier } : void 0;
|
|
322
|
+
}
|
|
323
|
+
settledInOrder() {
|
|
324
|
+
return [...this.settled.entries()].sort(([a], [b]) => a - b).map(([, block]) => block);
|
|
325
|
+
}
|
|
326
|
+
takeIndex() {
|
|
327
|
+
const index = this.nextIndex;
|
|
328
|
+
this.nextIndex += 1;
|
|
329
|
+
return index;
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
function failureChunk(failure, signal) {
|
|
333
|
+
const aborted = failure.code === FailureCode.aborted || signal?.aborted === true;
|
|
334
|
+
return {
|
|
335
|
+
type: "finish",
|
|
336
|
+
reason: { kind: aborted ? "aborted" : "error", failure: failure.failure }
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
var EMPTY_RESPONSE = {
|
|
340
|
+
code: FailureCode.emptyResponse,
|
|
341
|
+
message: "The model finished with no text and no tool call."
|
|
342
|
+
};
|
|
343
|
+
function finishChunk(reason, replayState) {
|
|
344
|
+
return { type: "finish", reason, ...replayState === void 0 ? {} : { replayState } };
|
|
345
|
+
}
|
|
346
|
+
function newDisplayBlock(kind) {
|
|
347
|
+
return { kind, index: void 0, text: "", closed: false };
|
|
348
|
+
}
|
|
349
|
+
function displayContent(kind, text) {
|
|
350
|
+
return { type: kind, text };
|
|
351
|
+
}
|
|
352
|
+
function displayOf(block) {
|
|
353
|
+
if (block.type === "text") return [{ kind: "text", text: stringOrEmpty(block.text) }];
|
|
354
|
+
if (block.type === "thinking") return [{ kind: "reasoning", text: stringOrEmpty(block.thinking) }];
|
|
355
|
+
return [];
|
|
356
|
+
}
|
|
357
|
+
function isReplayable(messages) {
|
|
358
|
+
const blocks = messages.flatMap((message) => message.content);
|
|
359
|
+
const hasAnswer = blocks.some(
|
|
360
|
+
(block) => isToolUse(block) || block.type === "text" && stringOrEmpty(block.text) !== ""
|
|
361
|
+
);
|
|
362
|
+
const unsigned = blocks.some(
|
|
363
|
+
(block) => block.type === "thinking" && stringOrEmpty(block.signature) === ""
|
|
364
|
+
);
|
|
365
|
+
return hasAnswer && !unsigned;
|
|
366
|
+
}
|
|
367
|
+
function stringOrEmpty(value) {
|
|
368
|
+
return typeof value === "string" ? value : "";
|
|
369
|
+
}
|
|
370
|
+
function hasVisibleText(blocks) {
|
|
371
|
+
return blocks.some((block) => block.type === "text" && stringOrEmpty(block.text) !== "");
|
|
372
|
+
}
|
|
373
|
+
function inputObjectOf(call) {
|
|
374
|
+
const { input } = call;
|
|
375
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
376
|
+
malformed(`the tool call to ${String(call.name)} has no input object`);
|
|
377
|
+
}
|
|
378
|
+
return input;
|
|
379
|
+
}
|
|
380
|
+
function isToolUse(block) {
|
|
381
|
+
return block.type === "tool_use";
|
|
382
|
+
}
|
|
383
|
+
function isDisplayBlock(block) {
|
|
384
|
+
return block.kind === "text" || block.kind === "reasoning";
|
|
385
|
+
}
|
|
386
|
+
function isToolCallStream(block) {
|
|
387
|
+
return block.kind === "tool-call";
|
|
388
|
+
}
|
|
389
|
+
function toolCallId(id) {
|
|
390
|
+
return id;
|
|
391
|
+
}
|
|
392
|
+
function blockIndexOf(event) {
|
|
393
|
+
const index = event.index;
|
|
394
|
+
if (typeof index !== "number" || !Number.isInteger(index)) malformed("event with no block index");
|
|
395
|
+
return index;
|
|
396
|
+
}
|
|
397
|
+
function objectField(source, field2) {
|
|
398
|
+
const value = source[field2];
|
|
399
|
+
if (typeof value !== "object" || value === null) malformed(`event with no ${field2}`);
|
|
400
|
+
return value;
|
|
401
|
+
}
|
|
402
|
+
function stringField(source, field2) {
|
|
403
|
+
const value = source[field2];
|
|
404
|
+
if (typeof value !== "string") malformed(`block with no ${field2}`);
|
|
405
|
+
return value;
|
|
406
|
+
}
|
|
407
|
+
function malformed(detail) {
|
|
408
|
+
throw new NativeFailure(`Malformed Claude Code stream: ${detail}`, FailureCode.nativeProtocol);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// src/adapter.ts
|
|
412
|
+
var ClaudeSubscriptionAdapter = class {
|
|
413
|
+
constructor(deps) {
|
|
414
|
+
this.deps = deps;
|
|
415
|
+
}
|
|
416
|
+
deps;
|
|
417
|
+
discovery;
|
|
418
|
+
providerInfo(provider) {
|
|
419
|
+
return { id: provider, name: "Claude Subscription" };
|
|
420
|
+
}
|
|
421
|
+
providerRetryPolicy(_provider) {
|
|
422
|
+
return void 0;
|
|
423
|
+
}
|
|
424
|
+
imageRequestPricing(_provider, _model) {
|
|
425
|
+
return void 0;
|
|
426
|
+
}
|
|
427
|
+
imageRequestTarget(_provider, _model, image) {
|
|
428
|
+
return this.deps.imageTarget(image);
|
|
429
|
+
}
|
|
430
|
+
async listModels(provider) {
|
|
431
|
+
const models = await this.discovered();
|
|
432
|
+
return models.map((model) => this.modelRow(provider, model));
|
|
433
|
+
}
|
|
434
|
+
resolveModel(provider, model, _signal) {
|
|
435
|
+
return Promise.resolve(this.deps.modelInfo(provider, model));
|
|
436
|
+
}
|
|
437
|
+
async prepareCall(provider, model, signal) {
|
|
438
|
+
return {
|
|
439
|
+
model: await this.resolveModel(provider, model, signal),
|
|
440
|
+
stream: (options) => this.stream(options)
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
async *stream(options) {
|
|
444
|
+
try {
|
|
445
|
+
const prepared = await this.deps.prepare(options);
|
|
446
|
+
yield* this.runPrepared(prepared, options.signal);
|
|
447
|
+
} catch (error) {
|
|
448
|
+
if (!(error instanceof NativeFailure)) throw error;
|
|
449
|
+
yield failureChunk(error, options.signal);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
async *runPrepared(prepared, signal) {
|
|
453
|
+
const translator = new ChunkTranslator({
|
|
454
|
+
toolNames: prepared.toolNames,
|
|
455
|
+
projectionOf: this.deps.projectionOf
|
|
456
|
+
});
|
|
457
|
+
const events = this.deps.run(prepared, signal, this.deps.native);
|
|
458
|
+
let completion;
|
|
459
|
+
try {
|
|
460
|
+
while (completion === void 0) {
|
|
461
|
+
const next = await events.next();
|
|
462
|
+
if (next.done) completion = next.value;
|
|
463
|
+
else yield* translator.event(next.value);
|
|
464
|
+
}
|
|
465
|
+
} finally {
|
|
466
|
+
if (completion === void 0) await events.return(void 0);
|
|
467
|
+
}
|
|
468
|
+
this.warnIfUnrestored(completion);
|
|
469
|
+
yield* translator.complete(completion);
|
|
470
|
+
}
|
|
471
|
+
warnIfUnrestored(completion) {
|
|
472
|
+
if (completion.unrestored === null) return;
|
|
473
|
+
this.deps.warn(`The queried turn stayed as Claude Code built it: ${completion.unrestored}`);
|
|
474
|
+
}
|
|
475
|
+
modelRow(provider, model) {
|
|
476
|
+
const { inputModalities } = this.deps.modelInfo(provider, model.id);
|
|
477
|
+
return {
|
|
478
|
+
provider,
|
|
479
|
+
id: model.id,
|
|
480
|
+
name: model.name,
|
|
481
|
+
...model.description === void 0 ? {} : { description: model.description },
|
|
482
|
+
...inputModalities === void 0 ? {} : { inputModalities }
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
discovered() {
|
|
486
|
+
const key = JSON.stringify(this.deps.native.settings());
|
|
487
|
+
if (this.discovery?.key === key) return this.discovery.models;
|
|
488
|
+
const models = Promise.resolve().then(() => this.discover(key));
|
|
489
|
+
this.discovery = { key, models };
|
|
490
|
+
return models;
|
|
491
|
+
}
|
|
492
|
+
async discover(key) {
|
|
493
|
+
try {
|
|
494
|
+
const models = await this.deps.discover(this.deps.native);
|
|
495
|
+
if (this.discovery?.key === key) this.deps.reportDiscovery(void 0);
|
|
496
|
+
return models;
|
|
497
|
+
} catch (error) {
|
|
498
|
+
if (this.discovery?.key === key) {
|
|
499
|
+
this.discovery = void 0;
|
|
500
|
+
this.deps.reportDiscovery(error instanceof Error ? error.message : String(error));
|
|
501
|
+
}
|
|
502
|
+
throw error;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
// src/config.ts
|
|
508
|
+
import z from "@deepseek-ai/schemastery";
|
|
509
|
+
var MAX_TIMER_SECONDS = 2147483;
|
|
510
|
+
var Config = z.object({
|
|
511
|
+
command: z.string().pattern(/\S/).default("claude").description("Name or absolute path of the Claude Code executable.").volatile(),
|
|
512
|
+
claudeConfigDir: z.string().default("").description("Claude Code config directory. Empty means the CLI's own default lookup.").volatile(),
|
|
513
|
+
claudeCodeTelemetry: z.boolean().default(true).description("Let Claude Code send telemetry and fetch feature flags. Flags unlock models.").volatile(),
|
|
514
|
+
readIdleTimeoutSeconds: z.number().min(1).max(MAX_TIMER_SECONDS).default(1800).description("Seconds of silence from Claude Code before a call fails.").volatile()
|
|
515
|
+
});
|
|
516
|
+
function readSettings(config) {
|
|
517
|
+
return {
|
|
518
|
+
command: config.command.get(),
|
|
519
|
+
claudeConfigDir: config.claudeConfigDir.get(),
|
|
520
|
+
claudeCodeTelemetry: config.claudeCodeTelemetry.get(),
|
|
521
|
+
readIdleTimeoutSeconds: config.readIdleTimeoutSeconds.get()
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// src/native/argv.ts
|
|
526
|
+
function inertMcpConfig(deps, files) {
|
|
527
|
+
return JSON.stringify({
|
|
528
|
+
mcpServers: {
|
|
529
|
+
[MCP_SERVER_NAME]: { command: deps.nodePath, args: [deps.inertMcpPath, files.tools] }
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
function turnArgv(executable, request, files, deps) {
|
|
534
|
+
return [
|
|
535
|
+
executable,
|
|
536
|
+
"-p",
|
|
537
|
+
"--model",
|
|
538
|
+
request.nativeModel,
|
|
539
|
+
"--input-format",
|
|
540
|
+
"stream-json",
|
|
541
|
+
"--output-format",
|
|
542
|
+
"stream-json",
|
|
543
|
+
"--verbose",
|
|
544
|
+
"--include-partial-messages",
|
|
545
|
+
"--tools",
|
|
546
|
+
"",
|
|
547
|
+
"--system-prompt-file",
|
|
548
|
+
files.system,
|
|
549
|
+
"--settings",
|
|
550
|
+
files.settings,
|
|
551
|
+
"--setting-sources",
|
|
552
|
+
"",
|
|
553
|
+
"--strict-mcp-config",
|
|
554
|
+
"--disable-slash-commands",
|
|
555
|
+
"--max-turns",
|
|
556
|
+
"1",
|
|
557
|
+
"--permission-mode",
|
|
558
|
+
"dontAsk",
|
|
559
|
+
"--no-session-persistence",
|
|
560
|
+
"--mcp-config",
|
|
561
|
+
inertMcpConfig(deps, files),
|
|
562
|
+
// The CLI adds a per-turn effort message at its own level unless told, and that message
|
|
563
|
+
// overrides `output_config.effort` from the extra body.
|
|
564
|
+
...request.effort === void 0 ? [] : ["--effort", request.effort],
|
|
565
|
+
// One CLI session per conversation: the session id names a scratchpad directory near the top
|
|
566
|
+
// of every request, so a new id per spawn would move the cache prefix.
|
|
567
|
+
"--session-id",
|
|
568
|
+
request.sessionId
|
|
569
|
+
];
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// src/native/failures.ts
|
|
573
|
+
var INSTALL_HINT = "Claude Code is not installed. Install it with `npm install -g @anthropic-ai/claude-code`, or set the `command` plugin setting to the binary.";
|
|
574
|
+
var LOGIN_HINT = "Claude Code is installed but not logged in. Run `claude auth login`, then select this provider again.";
|
|
575
|
+
var LOGGED_OUT_HINT = "Claude Code is installed but has no usable login where DSH runs it. Run `claude auth login` as the user DSH runs as, or point the `claudeConfigDir` plugin setting at a logged-in config directory, then try again.";
|
|
576
|
+
var STDERR_LINES = 40;
|
|
577
|
+
var STDERR_CHARS = 400;
|
|
578
|
+
var UPSTREAM_TEXT_CHARS = 500;
|
|
579
|
+
var NATIVE_ANSWERS = {
|
|
580
|
+
rate_limit: { code: FailureCode.rateLimit, status: 429 },
|
|
581
|
+
billing_error: { code: FailureCode.quota, status: 402 },
|
|
582
|
+
authentication_failed: { code: FailureCode.invalidCredential, status: 401 },
|
|
583
|
+
overloaded: { code: FailureCode.server, status: 529 },
|
|
584
|
+
// The CLI uses this code for capacity, 5xx and login-refresh races. All of them are transient.
|
|
585
|
+
server_error: { code: FailureCode.server, status: 503 }
|
|
586
|
+
};
|
|
587
|
+
var STREAM_ERRORS = /* @__PURE__ */ new Map([
|
|
588
|
+
["overloaded_error", { code: FailureCode.server, status: 529 }],
|
|
589
|
+
["rate_limit_error", { code: FailureCode.rateLimit, status: 429 }],
|
|
590
|
+
["api_error", { code: FailureCode.server, status: 500 }],
|
|
591
|
+
["authentication_error", { code: FailureCode.invalidCredential, status: 401 }]
|
|
592
|
+
]);
|
|
593
|
+
var CONTEXT_WINDOW_MESSAGES = [
|
|
594
|
+
/prompt is too long/i,
|
|
595
|
+
/\bexceed(?:s|ed)?\s+(?:the\s+)?(?:model(?:'s)?\s+)?context\s+(?:limit|length|window)\b/i,
|
|
596
|
+
/\bcontext[\s_-](?:length|window)[\s_-](?:exceed(?:ed|s)?|overflow(?:ed)?|limit[\s_-]exceeded)\b/i,
|
|
597
|
+
/\b(?:maximum|max)(?:\s+(?:allowed|supported))?\s+context\s+(?:length|window)\b/i,
|
|
598
|
+
/\btoo\s+(?:large|long)\s+for\s+(?:(?:this|the)\s+)?(?:model(?:'s)?\s+)?context\b/i
|
|
599
|
+
];
|
|
600
|
+
function isContextWindowMessage(text) {
|
|
601
|
+
return CONTEXT_WINDOW_MESSAGES.some((pattern) => pattern.test(text));
|
|
602
|
+
}
|
|
603
|
+
function withoutRelayRoute(text, relayUrl) {
|
|
604
|
+
return text.replaceAll(relayUrl, "<relay>");
|
|
605
|
+
}
|
|
606
|
+
function abortedFailure(cause) {
|
|
607
|
+
return new NativeFailure(
|
|
608
|
+
"Claude request cancelled",
|
|
609
|
+
FailureCode.aborted,
|
|
610
|
+
cause === void 0 ? {} : { cause }
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
function throwIfAborted(signal) {
|
|
614
|
+
if (signal?.aborted === true) throw abortedFailure();
|
|
615
|
+
}
|
|
616
|
+
function timeoutFailure() {
|
|
617
|
+
return new NativeFailure("Claude request timed out", FailureCode.timeout);
|
|
618
|
+
}
|
|
619
|
+
function stderrSuffix(stderr) {
|
|
620
|
+
const lines = stderr.split("\n").map((line) => line.trim()).filter((line) => line !== "");
|
|
621
|
+
const text = lines.slice(-STDERR_LINES).join(" ").slice(0, STDERR_CHARS);
|
|
622
|
+
return text === "" ? "" : ` (native stderr: ${text})`;
|
|
623
|
+
}
|
|
624
|
+
function nativeAnswerFailure(code, text, stderrTail) {
|
|
625
|
+
const message = `Native API error: ${text}${stderrTail}`;
|
|
626
|
+
if (isContextWindowMessage(text)) {
|
|
627
|
+
return new NativeFailure(message, FailureCode.contextWindowExceeded);
|
|
628
|
+
}
|
|
629
|
+
const shape = code === void 0 ? void 0 : NATIVE_ANSWERS[code];
|
|
630
|
+
if (shape === void 0) return new NativeFailure(message, FailureCode.nativeProtocol);
|
|
631
|
+
const facts = shape.status === void 0 ? {} : { status: shape.status };
|
|
632
|
+
return new NativeFailure(message, shape.code, facts);
|
|
633
|
+
}
|
|
634
|
+
function loggedOutFailure(detail, stderrTail) {
|
|
635
|
+
const suffix = detail === "" ? "" : ` (native: ${detail})`;
|
|
636
|
+
return new NativeFailure(
|
|
637
|
+
`${LOGGED_OUT_HINT}${suffix}${stderrTail}`,
|
|
638
|
+
FailureCode.invalidCredential,
|
|
639
|
+
{
|
|
640
|
+
status: 401
|
|
641
|
+
}
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
function codeForStatus(status, upstreamText) {
|
|
645
|
+
if (isContextWindowMessage(upstreamText)) return FailureCode.contextWindowExceeded;
|
|
646
|
+
if (status === 429) return FailureCode.rateLimit;
|
|
647
|
+
if (status === 401 || status === 403) return FailureCode.invalidCredential;
|
|
648
|
+
if (status === 402) return FailureCode.quota;
|
|
649
|
+
if (status >= 500) return FailureCode.server;
|
|
650
|
+
return FailureCode.upstreamError;
|
|
651
|
+
}
|
|
652
|
+
function firstAttemptSummary(relay, upstreamText) {
|
|
653
|
+
const capture = relay.capture.complete ? "complete" : "incomplete";
|
|
654
|
+
const parts = [`status ${relay.status ?? "none"}`, `capture ${capture}`];
|
|
655
|
+
if (relay.failure !== null) parts.push(`relay failure ${relay.failure}`);
|
|
656
|
+
parts.push(`native retries denied: ${relay.denied}`);
|
|
657
|
+
if (upstreamText !== "") parts.push(`upstream said: ${upstreamText}`);
|
|
658
|
+
return `first upstream attempt: ${parts.join(", ")}`;
|
|
659
|
+
}
|
|
660
|
+
function streamErrorShape(type, text) {
|
|
661
|
+
if (type === "invalid_request_error") {
|
|
662
|
+
const code = isContextWindowMessage(text) ? FailureCode.contextWindowExceeded : FailureCode.upstreamError;
|
|
663
|
+
return { code, status: 400 };
|
|
664
|
+
}
|
|
665
|
+
return STREAM_ERRORS.get(type) ?? { code: FailureCode.transport };
|
|
666
|
+
}
|
|
667
|
+
function upstreamFailure(relay, nativeError) {
|
|
668
|
+
const requestId = relay.requestId === null ? {} : { requestId: relay.requestId };
|
|
669
|
+
const retryAfter = relay.retryAfterMs === null ? {} : { providerRetryAfterMs: relay.retryAfterMs };
|
|
670
|
+
const streamError = relay.capture.error;
|
|
671
|
+
if (streamError !== null) {
|
|
672
|
+
const text = streamError.message.slice(0, UPSTREAM_TEXT_CHARS);
|
|
673
|
+
const { code, status } = streamErrorShape(streamError.type, text);
|
|
674
|
+
return new NativeFailure(text, code, {
|
|
675
|
+
...status === void 0 ? {} : { status },
|
|
676
|
+
...requestId,
|
|
677
|
+
...retryAfter
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
const upstreamText = relay.errorText().slice(0, UPSTREAM_TEXT_CHARS);
|
|
681
|
+
const suffix = nativeError === null || nativeError === "" ? "" : `: ${nativeError}`;
|
|
682
|
+
const message = `Incomplete upstream response (${firstAttemptSummary(relay, upstreamText)})${suffix}`;
|
|
683
|
+
if (relay.status === null || relay.status === 200) {
|
|
684
|
+
return new NativeFailure(message, FailureCode.transport, { ...requestId, ...retryAfter });
|
|
685
|
+
}
|
|
686
|
+
return new NativeFailure(message, codeForStatus(relay.status, upstreamText), {
|
|
687
|
+
status: relay.status,
|
|
688
|
+
...requestId,
|
|
689
|
+
...retryAfter
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// src/native/channel.ts
|
|
694
|
+
var SHOWN_LINE_CHARS = 300;
|
|
695
|
+
function parseLine(line) {
|
|
696
|
+
try {
|
|
697
|
+
const parsed = JSON.parse(line);
|
|
698
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
699
|
+
return { kind: "event", event: parsed };
|
|
700
|
+
}
|
|
701
|
+
} catch {
|
|
702
|
+
return { kind: "malformed", line };
|
|
703
|
+
}
|
|
704
|
+
return { kind: "malformed", line };
|
|
705
|
+
}
|
|
706
|
+
function endOf(stream) {
|
|
707
|
+
if (stream === void 0) return Promise.resolve();
|
|
708
|
+
return new Promise((resolve) => {
|
|
709
|
+
stream.once("end", resolve);
|
|
710
|
+
stream.once("close", resolve);
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
var NativeChannel = class {
|
|
714
|
+
#handle;
|
|
715
|
+
#idleMs;
|
|
716
|
+
#signal;
|
|
717
|
+
#redact;
|
|
718
|
+
#items = [];
|
|
719
|
+
#waiter = null;
|
|
720
|
+
#partialLine = "";
|
|
721
|
+
#discarding = false;
|
|
722
|
+
#outcome = null;
|
|
723
|
+
constructor(handle, options) {
|
|
724
|
+
this.#handle = handle;
|
|
725
|
+
this.#idleMs = options.idleMs;
|
|
726
|
+
this.#signal = options.signal;
|
|
727
|
+
this.#redact = options.redact;
|
|
728
|
+
handle.stdin?.on("error", () => {
|
|
729
|
+
});
|
|
730
|
+
handle.stdout?.setEncoding("utf8");
|
|
731
|
+
handle.stdout?.on("data", (chunk) => this.#onOutput(chunk));
|
|
732
|
+
handle.stdout?.on("error", () => {
|
|
733
|
+
});
|
|
734
|
+
this.#watchEnd();
|
|
735
|
+
}
|
|
736
|
+
/** The exit code, known once `receive` has returned null. */
|
|
737
|
+
get exitCode() {
|
|
738
|
+
return this.#outcome?.exitCode ?? null;
|
|
739
|
+
}
|
|
740
|
+
/** Suffix for a failure message, from the CLI's stderr. */
|
|
741
|
+
stderrTail() {
|
|
742
|
+
return stderrSuffix(this.#redact(this.#handle.collected.stderr?.readFrom(0).text ?? ""));
|
|
743
|
+
}
|
|
744
|
+
/** The next stream-json object, or null when the process and its stdout have ended. */
|
|
745
|
+
async receive() {
|
|
746
|
+
if (this.#signal?.aborted === true) throw abortedFailure();
|
|
747
|
+
const item = await this.#bounded(this.#nextItem());
|
|
748
|
+
switch (item.kind) {
|
|
749
|
+
case "event":
|
|
750
|
+
return item.event;
|
|
751
|
+
case "ended":
|
|
752
|
+
return null;
|
|
753
|
+
case "malformed":
|
|
754
|
+
throw new NativeFailure(
|
|
755
|
+
`Invalid native stream-json output: ${JSON.stringify(this.#redact(item.line).slice(0, SHOWN_LINE_CHARS))}${this.stderrTail()}`,
|
|
756
|
+
FailureCode.nativeProtocol
|
|
757
|
+
);
|
|
758
|
+
case "failed":
|
|
759
|
+
throw new NativeFailure(
|
|
760
|
+
`Native process failed: ${item.error instanceof Error ? item.error.message : String(item.error)}${this.stderrTail()}`,
|
|
761
|
+
FailureCode.nativeProtocol,
|
|
762
|
+
{ cause: item.error }
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Writes one line to stdin and waits until the pipe took it. A child that is gone leaves
|
|
768
|
+
* nothing to write to: what it printed, read by `receive`, is the better diagnosis.
|
|
769
|
+
*/
|
|
770
|
+
async write(line) {
|
|
771
|
+
const { stdin } = this.#handle;
|
|
772
|
+
if (stdin === void 0 || stdin.destroyed) return;
|
|
773
|
+
await this.#bounded(new Promise((resolve) => stdin.write(line, () => resolve())));
|
|
774
|
+
}
|
|
775
|
+
async endInput() {
|
|
776
|
+
const { stdin } = this.#handle;
|
|
777
|
+
if (stdin === void 0 || stdin.destroyed) return;
|
|
778
|
+
await this.#bounded(new Promise((resolve) => stdin.end(() => resolve())));
|
|
779
|
+
}
|
|
780
|
+
/** Keeps stdout flowing with nobody reading, so a draining child never blocks on it. */
|
|
781
|
+
discardOutput() {
|
|
782
|
+
this.#discarding = true;
|
|
783
|
+
this.#items.length = 0;
|
|
784
|
+
this.#waiter = null;
|
|
785
|
+
}
|
|
786
|
+
/** Releases the pipes. Call once the process has gone. */
|
|
787
|
+
close() {
|
|
788
|
+
this.#handle.stdin?.destroy();
|
|
789
|
+
this.#handle.stdout?.destroy();
|
|
790
|
+
}
|
|
791
|
+
#watchEnd() {
|
|
792
|
+
Promise.all([endOf(this.#handle.stdout), this.#handle.done]).then(
|
|
793
|
+
([, outcome]) => {
|
|
794
|
+
this.#outcome = outcome;
|
|
795
|
+
this.#flushPartialLine();
|
|
796
|
+
this.#push({ kind: "ended" });
|
|
797
|
+
},
|
|
798
|
+
(error) => this.#push({ kind: "failed", error })
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
#onOutput(chunk) {
|
|
802
|
+
if (this.#discarding) return;
|
|
803
|
+
const lines = `${this.#partialLine}${chunk}`.split("\n");
|
|
804
|
+
this.#partialLine = lines.pop() ?? "";
|
|
805
|
+
for (const line of lines) this.#pushLine(line);
|
|
806
|
+
}
|
|
807
|
+
#flushPartialLine() {
|
|
808
|
+
this.#pushLine(this.#partialLine);
|
|
809
|
+
this.#partialLine = "";
|
|
810
|
+
}
|
|
811
|
+
#pushLine(line) {
|
|
812
|
+
if (line.trim() !== "") this.#push(parseLine(line));
|
|
813
|
+
}
|
|
814
|
+
#push(item) {
|
|
815
|
+
if (this.#discarding) return;
|
|
816
|
+
if (this.#waiter === null) {
|
|
817
|
+
this.#items.push(item);
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
const waiter = this.#waiter;
|
|
821
|
+
this.#waiter = null;
|
|
822
|
+
waiter(item);
|
|
823
|
+
}
|
|
824
|
+
#nextItem() {
|
|
825
|
+
const queued = this.#items.shift();
|
|
826
|
+
if (queued !== void 0) return Promise.resolve(queued);
|
|
827
|
+
return new Promise((resolve) => {
|
|
828
|
+
this.#waiter = resolve;
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
#bounded(work) {
|
|
832
|
+
const signal = this.#signal;
|
|
833
|
+
return new Promise((resolve, reject) => {
|
|
834
|
+
const onAbort = () => finish(() => reject(abortedFailure()));
|
|
835
|
+
const timer = setTimeout(() => finish(() => reject(timeoutFailure())), this.#idleMs);
|
|
836
|
+
const finish = (settle) => {
|
|
837
|
+
clearTimeout(timer);
|
|
838
|
+
signal?.removeEventListener("abort", onAbort);
|
|
839
|
+
settle();
|
|
840
|
+
};
|
|
841
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
842
|
+
work.then(
|
|
843
|
+
(value) => finish(() => resolve(value)),
|
|
844
|
+
(error) => finish(() => reject(error))
|
|
845
|
+
);
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
// src/native/drain.ts
|
|
851
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
852
|
+
var DRAIN_GRACE_MS = 1e4;
|
|
853
|
+
var TERMINATE_WAIT_MS = 5e3;
|
|
854
|
+
async function settledWithin(work, limitMs) {
|
|
855
|
+
const limit = new AbortController();
|
|
856
|
+
const settled = work.then(
|
|
857
|
+
() => void 0,
|
|
858
|
+
() => void 0
|
|
859
|
+
);
|
|
860
|
+
const timer = sleep(limitMs, void 0, { signal: limit.signal, ref: false }).catch(
|
|
861
|
+
() => void 0
|
|
862
|
+
);
|
|
863
|
+
await Promise.race([settled, timer]);
|
|
864
|
+
limit.abort();
|
|
865
|
+
}
|
|
866
|
+
async function drainAfterAnswer(handle, relay, graceMs = DRAIN_GRACE_MS) {
|
|
867
|
+
await settledWithin(handle.done, graceMs);
|
|
868
|
+
handle.terminate();
|
|
869
|
+
await settledWithin(handle.done, TERMINATE_WAIT_MS);
|
|
870
|
+
await relay.close();
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// src/native/env.ts
|
|
874
|
+
import { homedir, userInfo } from "node:os";
|
|
875
|
+
import { join } from "node:path";
|
|
876
|
+
var BACKEND_URL = "ANTHROPIC_BASE_URL";
|
|
877
|
+
var LOOPBACK = "127.0.0.1";
|
|
878
|
+
var BACKEND_SWITCHES = [
|
|
879
|
+
"CLAUDE_CODE_USE_BEDROCK",
|
|
880
|
+
"CLAUDE_CODE_USE_VERTEX",
|
|
881
|
+
"CLAUDE_CODE_USE_FOUNDRY"
|
|
882
|
+
];
|
|
883
|
+
var SWITCHED_OFF = /* @__PURE__ */ new Set(["", "0", "false", "no", "off"]);
|
|
884
|
+
var FOREIGN_CREDENTIALS = [
|
|
885
|
+
"ANTHROPIC_API_KEY",
|
|
886
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
887
|
+
"ANTHROPIC_FOUNDRY_API_KEY"
|
|
888
|
+
];
|
|
889
|
+
var INHERITED_REQUEST_SETTINGS = ["CLAUDE_CODE_EXTRA_BODY", "CLAUDE_CODE_EFFORT_LEVEL"];
|
|
890
|
+
var QUIET_TRAFFIC = {
|
|
891
|
+
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1",
|
|
892
|
+
DISABLE_TELEMETRY: "1",
|
|
893
|
+
DISABLE_ERROR_REPORTING: "1"
|
|
894
|
+
};
|
|
895
|
+
var ALWAYS_QUIET = {
|
|
896
|
+
DISABLE_AUTOUPDATER: "1",
|
|
897
|
+
DISABLE_FEEDBACK_COMMAND: "1",
|
|
898
|
+
// This also stops the CLI's own session-title request, which would race the turn for the one
|
|
899
|
+
// upstream request that the relay admits (upstream #116).
|
|
900
|
+
CLAUDE_CODE_DISABLE_TERMINAL_TITLE: "1"
|
|
901
|
+
};
|
|
902
|
+
var CALLER_IDENTITY = {
|
|
903
|
+
CLAUDE_CODE_ENTRYPOINT: "deepseek-harness",
|
|
904
|
+
CLAUDE_CODE_DISABLE_ARTIFACT: "1"
|
|
905
|
+
};
|
|
906
|
+
var TURN_POLICY = {
|
|
907
|
+
ENABLE_TOOL_SEARCH: "false",
|
|
908
|
+
CLAUDE_CODE_MAX_RETRIES: "0",
|
|
909
|
+
DISABLE_AUTO_COMPACT: "1",
|
|
910
|
+
DISABLE_COMPACT: "1",
|
|
911
|
+
// DSH owns the budgets, and the CLI's replayed reminder would break the cached history.
|
|
912
|
+
CLAUDE_CODE_TOTAL_TOKENS_REMINDER: "off",
|
|
913
|
+
// The CLI adds this note to the history after quiet tool turns. A DSH turn did not ask for it, and
|
|
914
|
+
// its time-based form could move the cached prefix.
|
|
915
|
+
CLAUDE_CODE_SILENT_TURN_REMINDER: "0",
|
|
916
|
+
// DSH owns memory and instructions. `--setting-sources ""` does not stop these two, and the CLI
|
|
917
|
+
// would put them inside the user's turn (upstream #195).
|
|
918
|
+
CLAUDE_CODE_DISABLE_AUTO_MEMORY: "1",
|
|
919
|
+
CLAUDE_CODE_DISABLE_CLAUDE_MDS: "1"
|
|
920
|
+
};
|
|
921
|
+
function isSwitchedOn(value) {
|
|
922
|
+
return value !== void 0 && !SWITCHED_OFF.has(value.trim().toLowerCase());
|
|
923
|
+
}
|
|
924
|
+
function backendOverrides(processEnv) {
|
|
925
|
+
const conflicts = processEnv[BACKEND_URL] ? [BACKEND_URL] : [];
|
|
926
|
+
return [...conflicts, ...BACKEND_SWITCHES.filter((name2) => isSwitchedOn(processEnv[name2]))];
|
|
927
|
+
}
|
|
928
|
+
function assertNoBackendOverride(processEnv) {
|
|
929
|
+
const conflicts = backendOverrides(processEnv);
|
|
930
|
+
if (conflicts.length === 0) return;
|
|
931
|
+
throw new NativeFailure(
|
|
932
|
+
`The Claude subscription provider refuses these inherited backend overrides: ${conflicts.join(", ")}. Unset them where DSH runs.`,
|
|
933
|
+
FailureCode.conflictingEnvironment
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
function expandHome(path, processEnv) {
|
|
937
|
+
if (path !== "~" && !path.startsWith("~/")) return path;
|
|
938
|
+
return join(processEnv.HOME || homedir(), path.slice(1));
|
|
939
|
+
}
|
|
940
|
+
function configDirEntry(settings, processEnv) {
|
|
941
|
+
const chosen = settings.claudeConfigDir.trim() || processEnv.CLAUDE_CONFIG_DIR;
|
|
942
|
+
return chosen ? { CLAUDE_CONFIG_DIR: expandHome(chosen, processEnv) } : {};
|
|
943
|
+
}
|
|
944
|
+
function osUsername() {
|
|
945
|
+
try {
|
|
946
|
+
return userInfo().username;
|
|
947
|
+
} catch {
|
|
948
|
+
return void 0;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
function loginIdentity(processEnv) {
|
|
952
|
+
const user = processEnv.USER || processEnv.LOGNAME || osUsername();
|
|
953
|
+
if (user === void 0) return {};
|
|
954
|
+
return { USER: processEnv.USER || user, LOGNAME: processEnv.LOGNAME || user };
|
|
955
|
+
}
|
|
956
|
+
function loopbackProxyBypass(processEnv) {
|
|
957
|
+
const inherited = processEnv.no_proxy || processEnv.NO_PROXY;
|
|
958
|
+
const exceptions = inherited ? `${inherited},${LOOPBACK}` : LOOPBACK;
|
|
959
|
+
return { NO_PROXY: exceptions, no_proxy: exceptions };
|
|
960
|
+
}
|
|
961
|
+
function removed(names) {
|
|
962
|
+
return Object.fromEntries(names.map((name2) => [name2, void 0]));
|
|
963
|
+
}
|
|
964
|
+
function probeEnv(settings, processEnv) {
|
|
965
|
+
return {
|
|
966
|
+
...removed([...FOREIGN_CREDENTIALS, BACKEND_URL]),
|
|
967
|
+
...loginIdentity(processEnv),
|
|
968
|
+
...configDirEntry(settings, processEnv),
|
|
969
|
+
...loopbackProxyBypass(processEnv),
|
|
970
|
+
...ALWAYS_QUIET,
|
|
971
|
+
...CALLER_IDENTITY,
|
|
972
|
+
...settings.claudeCodeTelemetry ? {} : QUIET_TRAFFIC
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
function nativeEnv(settings, processEnv) {
|
|
976
|
+
return {
|
|
977
|
+
...probeEnv(settings, processEnv),
|
|
978
|
+
...removed(INHERITED_REQUEST_SETTINGS),
|
|
979
|
+
...TURN_POLICY
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// src/native/executable.ts
|
|
984
|
+
var NOT_FOUND_ERROR = "SubprocessExecutableNotFoundError";
|
|
985
|
+
async function resolveClaude(deps, signal) {
|
|
986
|
+
const { command } = deps.settings();
|
|
987
|
+
try {
|
|
988
|
+
return await deps.subprocess.resolveExecutable(command, void 0, signal);
|
|
989
|
+
} catch (error) {
|
|
990
|
+
if (signal?.aborted === true) throw abortedFailure(error);
|
|
991
|
+
if (error instanceof Error && error.name === NOT_FOUND_ERROR) {
|
|
992
|
+
throw new NativeFailure(INSTALL_HINT, FailureCode.claudeCodeMissing, {
|
|
993
|
+
status: 503,
|
|
994
|
+
cause: error
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
998
|
+
throw new NativeFailure(
|
|
999
|
+
`The \`command\` plugin setting cannot be used: ${detail}`,
|
|
1000
|
+
FailureCode.nativeProtocol,
|
|
1001
|
+
{ cause: error }
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// src/native/request-files.ts
|
|
1007
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
1008
|
+
import { tmpdir } from "node:os";
|
|
1009
|
+
import { join as join2 } from "node:path";
|
|
1010
|
+
async function writeRequestFiles(request) {
|
|
1011
|
+
const directory = await mkdtemp(join2(tmpdir(), "claude-subscription-"));
|
|
1012
|
+
const files = {
|
|
1013
|
+
directory,
|
|
1014
|
+
system: join2(directory, "system.md"),
|
|
1015
|
+
settings: join2(directory, "settings.json"),
|
|
1016
|
+
tools: join2(directory, "tools.json")
|
|
1017
|
+
};
|
|
1018
|
+
const settings = { env: { CLAUDE_CODE_EXTRA_BODY: JSON.stringify(request.extraBody) } };
|
|
1019
|
+
const writes = await Promise.allSettled([
|
|
1020
|
+
writeFile(files.system, request.system),
|
|
1021
|
+
writeFile(files.settings, JSON.stringify(settings)),
|
|
1022
|
+
writeFile(files.tools, JSON.stringify(request.manifest))
|
|
1023
|
+
]);
|
|
1024
|
+
const failed = writes.find((write) => write.status === "rejected");
|
|
1025
|
+
if (failed !== void 0) {
|
|
1026
|
+
await removeRequestFiles(files).catch(() => {
|
|
1027
|
+
});
|
|
1028
|
+
throw failed.reason;
|
|
1029
|
+
}
|
|
1030
|
+
return files;
|
|
1031
|
+
}
|
|
1032
|
+
async function removeRequestFiles(files) {
|
|
1033
|
+
await rm(files.directory, { recursive: true, force: true });
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// src/native/turn.ts
|
|
1037
|
+
function newTurnState() {
|
|
1038
|
+
return { assistants: [], results: [], stopped: false, streamedText: "", nativeError: null };
|
|
1039
|
+
}
|
|
1040
|
+
function isObject(value) {
|
|
1041
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1042
|
+
}
|
|
1043
|
+
function protocolFailure(what, stderrTail) {
|
|
1044
|
+
return new NativeFailure(`${what}${stderrTail}`, FailureCode.nativeProtocol);
|
|
1045
|
+
}
|
|
1046
|
+
function blocksOf(message) {
|
|
1047
|
+
return Array.isArray(message.content) ? message.content : [];
|
|
1048
|
+
}
|
|
1049
|
+
function textOf(blocks) {
|
|
1050
|
+
return blocks.filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join("");
|
|
1051
|
+
}
|
|
1052
|
+
function recordAssistant(state, event, stderrTail) {
|
|
1053
|
+
const { message } = event;
|
|
1054
|
+
if (!isObject(message) || !Array.isArray(message.content)) {
|
|
1055
|
+
throw protocolFailure("Native assistant event holds no message", stderrTail());
|
|
1056
|
+
}
|
|
1057
|
+
if (event.error || message.error) {
|
|
1058
|
+
const code = typeof event.error === "string" ? event.error : void 0;
|
|
1059
|
+
state.nativeError = {
|
|
1060
|
+
code,
|
|
1061
|
+
text: blocksOf(message).filter((block) => block.type === "text").map((block) => String(block.text ?? "")).join("\n")
|
|
1062
|
+
};
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
state.assistants.push(message);
|
|
1066
|
+
}
|
|
1067
|
+
function recordStreamEvent(state, event, stderrTail) {
|
|
1068
|
+
const inner = event.event;
|
|
1069
|
+
if (!isObject(inner) || typeof inner.type !== "string") {
|
|
1070
|
+
throw protocolFailure("Native stream_event holds no event", stderrTail());
|
|
1071
|
+
}
|
|
1072
|
+
if (inner.type === "message_stop") state.stopped = true;
|
|
1073
|
+
const { delta } = inner;
|
|
1074
|
+
if (isObject(delta) && delta.type === "text_delta" && typeof delta.text === "string") {
|
|
1075
|
+
state.streamedText += delta.text;
|
|
1076
|
+
}
|
|
1077
|
+
return inner;
|
|
1078
|
+
}
|
|
1079
|
+
function recordEvent(state, event, stderrTail) {
|
|
1080
|
+
switch (event.type) {
|
|
1081
|
+
case "assistant":
|
|
1082
|
+
recordAssistant(state, event, stderrTail);
|
|
1083
|
+
return null;
|
|
1084
|
+
case "result":
|
|
1085
|
+
state.results.push(event);
|
|
1086
|
+
return null;
|
|
1087
|
+
case "stream_event":
|
|
1088
|
+
return recordStreamEvent(state, event, stderrTail);
|
|
1089
|
+
default:
|
|
1090
|
+
return null;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
function endedWithToolCall(result, blocks) {
|
|
1094
|
+
return result.subtype === "error_max_turns" && blocks.some((block) => block.type === "tool_use");
|
|
1095
|
+
}
|
|
1096
|
+
function fastEligible(result, relay, state) {
|
|
1097
|
+
const message = relay.capture.message;
|
|
1098
|
+
if (message === null) return false;
|
|
1099
|
+
const admittedCleanly = relay.used && relay.status === 200 && relay.capture.complete && relay.denied === 0;
|
|
1100
|
+
if (!admittedCleanly || !state.stopped || state.nativeError !== null) return false;
|
|
1101
|
+
if (message.stop_reason === "refusal") return false;
|
|
1102
|
+
const succeeded = result.subtype === "success" && !result.is_error;
|
|
1103
|
+
return succeeded || endedWithToolCall(result, message.content);
|
|
1104
|
+
}
|
|
1105
|
+
function isUsage(value) {
|
|
1106
|
+
return isObject(value) && typeof value.input_tokens === "number" && typeof value.output_tokens === "number";
|
|
1107
|
+
}
|
|
1108
|
+
function assertTextMatches(streamed, blocks, tail) {
|
|
1109
|
+
if (!textOf(blocks).startsWith(streamed)) {
|
|
1110
|
+
throw protocolFailure("Native final text differs from the incremental stream", tail);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
function asCaptured(message, usage) {
|
|
1114
|
+
return {
|
|
1115
|
+
...message,
|
|
1116
|
+
role: "assistant",
|
|
1117
|
+
content: blocksOf(message),
|
|
1118
|
+
usage: isUsage(message.usage) ? message.usage : usage
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
function finalAnswer(state, relay, nativeError) {
|
|
1122
|
+
if (!relay.used) {
|
|
1123
|
+
return {
|
|
1124
|
+
messages: state.assistants,
|
|
1125
|
+
stopped: state.stopped,
|
|
1126
|
+
usageOf: (result) => result.usage
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
const message = relay.capture.message;
|
|
1130
|
+
if (relay.status !== 200 || !relay.capture.complete || message === null) {
|
|
1131
|
+
throw upstreamFailure(relay, nativeError);
|
|
1132
|
+
}
|
|
1133
|
+
return { messages: [message], stopped: true, usageOf: () => message.usage };
|
|
1134
|
+
}
|
|
1135
|
+
function failureOfNativeError(error, relay, stderrTail) {
|
|
1136
|
+
if (error.code === "authentication_failed" && !relay.used) {
|
|
1137
|
+
return loggedOutFailure(error.text, stderrTail);
|
|
1138
|
+
}
|
|
1139
|
+
return nativeAnswerFailure(error.code, error.text, stderrTail);
|
|
1140
|
+
}
|
|
1141
|
+
function assertProcessSucceeded(result, blocks, ending, stderrTail) {
|
|
1142
|
+
const exitCode = ending.fast ? 0 : ending.exitCode;
|
|
1143
|
+
const atTurnLimit = endedWithToolCall(result, blocks) && (ending.fast || ending.exitCode === 1);
|
|
1144
|
+
if (atTurnLimit) return;
|
|
1145
|
+
if (exitCode !== 0 || result.is_error || result.subtype !== "success") {
|
|
1146
|
+
throw protocolFailure(`Native request failed: ${String(result.subtype)}`, stderrTail);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
function finishTurn(state, relay, ending, stderrTail) {
|
|
1150
|
+
const nativeError = state.nativeError === null ? null : { ...state.nativeError, text: withoutRelayRoute(state.nativeError.text, relay.url) };
|
|
1151
|
+
const answer = finalAnswer(state, relay, nativeError?.text ?? null);
|
|
1152
|
+
const refused2 = relay.used && answer.messages[0]?.stop_reason === "refusal";
|
|
1153
|
+
const relayAnswerStands = relay.denied > 0 || refused2;
|
|
1154
|
+
if (nativeError !== null && !relayAnswerStands) {
|
|
1155
|
+
throw failureOfNativeError(nativeError, relay, stderrTail);
|
|
1156
|
+
}
|
|
1157
|
+
const [result] = state.results;
|
|
1158
|
+
if (state.results.length !== 1 || result === void 0 || answer.messages.length === 0 || !answer.stopped) {
|
|
1159
|
+
throw protocolFailure(
|
|
1160
|
+
"Incomplete native response: assistant, message_stop and one result required",
|
|
1161
|
+
stderrTail
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
const blocks = answer.messages.flatMap(blocksOf);
|
|
1165
|
+
if (!relayAnswerStands) assertProcessSucceeded(result, blocks, ending, stderrTail);
|
|
1166
|
+
const usage = answer.usageOf(result);
|
|
1167
|
+
if (!isUsage(usage))
|
|
1168
|
+
throw protocolFailure("Native result missing complete token usage", stderrTail);
|
|
1169
|
+
assertTextMatches(state.streamedText, blocks, stderrTail);
|
|
1170
|
+
return {
|
|
1171
|
+
assistants: answer.messages.map((message) => asCaptured(message, usage)),
|
|
1172
|
+
usage,
|
|
1173
|
+
requestId: relay.requestId,
|
|
1174
|
+
blockedRequests: relay.denied,
|
|
1175
|
+
unrestored: relay.unrestored
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// src/native/workdir.ts
|
|
1180
|
+
import {
|
|
1181
|
+
closeSync,
|
|
1182
|
+
constants,
|
|
1183
|
+
fchmodSync,
|
|
1184
|
+
lstatSync,
|
|
1185
|
+
mkdirSync,
|
|
1186
|
+
openSync,
|
|
1187
|
+
statSync,
|
|
1188
|
+
utimesSync
|
|
1189
|
+
} from "node:fs";
|
|
1190
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
1191
|
+
import { join as join3 } from "node:path";
|
|
1192
|
+
var OPEN_TO_OTHERS = 63;
|
|
1193
|
+
var WRITABLE_BY_OTHERS = 18;
|
|
1194
|
+
var STICKY_BIT = 512;
|
|
1195
|
+
function refused(path, why, cause) {
|
|
1196
|
+
return new NativeFailure(
|
|
1197
|
+
`The working directory ${path} was refused: ${why}.`,
|
|
1198
|
+
FailureCode.nativeProtocol,
|
|
1199
|
+
cause === void 0 ? {} : { cause }
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
function assertSafeParent(parent, path) {
|
|
1203
|
+
const { mode } = statSync(parent);
|
|
1204
|
+
if ((mode & WRITABLE_BY_OTHERS) !== 0 && (mode & STICKY_BIT) === 0) {
|
|
1205
|
+
throw refused(path, "others can rename it out of its parent directory");
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
function isExisting(error) {
|
|
1209
|
+
return error instanceof Error && "code" in error && error.code === "EEXIST";
|
|
1210
|
+
}
|
|
1211
|
+
function tighten(path) {
|
|
1212
|
+
const descriptor = openSync(
|
|
1213
|
+
path,
|
|
1214
|
+
constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW
|
|
1215
|
+
);
|
|
1216
|
+
try {
|
|
1217
|
+
fchmodSync(descriptor, 448);
|
|
1218
|
+
} finally {
|
|
1219
|
+
closeSync(descriptor);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
function createPrivate(path) {
|
|
1223
|
+
try {
|
|
1224
|
+
mkdirSync(path, { mode: 448 });
|
|
1225
|
+
} catch (error) {
|
|
1226
|
+
if (isExisting(error)) return;
|
|
1227
|
+
throw error;
|
|
1228
|
+
}
|
|
1229
|
+
tighten(path);
|
|
1230
|
+
}
|
|
1231
|
+
function assertPrivate(path, uid) {
|
|
1232
|
+
const info = lstatSync(path);
|
|
1233
|
+
if (!info.isDirectory()) throw refused(path, "it is not a real directory");
|
|
1234
|
+
if (info.uid !== uid) throw refused(path, "another user owns it");
|
|
1235
|
+
if ((info.mode & OPEN_TO_OTHERS) !== 0) throw refused(path, "it is open to other users");
|
|
1236
|
+
}
|
|
1237
|
+
function sharedWorkdir() {
|
|
1238
|
+
const uid = process.getuid?.();
|
|
1239
|
+
const parent = tmpdir2();
|
|
1240
|
+
const path = join3(parent, `claude-subscription-cwd-${uid}`);
|
|
1241
|
+
if (uid === void 0) throw refused(path, "this platform has no user id");
|
|
1242
|
+
try {
|
|
1243
|
+
assertSafeParent(parent, path);
|
|
1244
|
+
createPrivate(path);
|
|
1245
|
+
assertPrivate(path, uid);
|
|
1246
|
+
const now = /* @__PURE__ */ new Date();
|
|
1247
|
+
utimesSync(path, now, now);
|
|
1248
|
+
} catch (error) {
|
|
1249
|
+
if (error instanceof NativeFailure) throw error;
|
|
1250
|
+
throw refused(path, error instanceof Error ? error.message : String(error), error);
|
|
1251
|
+
}
|
|
1252
|
+
return path;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// src/native/run.ts
|
|
1256
|
+
var ANTHROPIC_API = "https://api.anthropic.com";
|
|
1257
|
+
var GRACE_MS = 2e3;
|
|
1258
|
+
var STDERR_BYTES = 16384;
|
|
1259
|
+
var STOP_WAIT_MS = GRACE_MS + 3e3;
|
|
1260
|
+
function queriedBlocks(request) {
|
|
1261
|
+
const last = request.frames.at(-1);
|
|
1262
|
+
if (last === void 0) {
|
|
1263
|
+
throw new NativeFailure("The request holds no frame to query", FailureCode.invalidRequest);
|
|
1264
|
+
}
|
|
1265
|
+
return last.message.content;
|
|
1266
|
+
}
|
|
1267
|
+
async function openRelay(request, context) {
|
|
1268
|
+
try {
|
|
1269
|
+
return await context.deps.createRelay({
|
|
1270
|
+
upstream: ANTHROPIC_API,
|
|
1271
|
+
timeoutMs: context.idleMs,
|
|
1272
|
+
queried: context.queried,
|
|
1273
|
+
oneShot: request.oneShot,
|
|
1274
|
+
env: context.processEnv
|
|
1275
|
+
});
|
|
1276
|
+
} catch (error) {
|
|
1277
|
+
if (error instanceof NativeFailure) throw error;
|
|
1278
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1279
|
+
throw new NativeFailure(`The relay could not open: ${detail}`, FailureCode.relayError, {
|
|
1280
|
+
cause: error
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
var ProcessAbort = class {
|
|
1285
|
+
#controller = new AbortController();
|
|
1286
|
+
#source;
|
|
1287
|
+
#forward = () => this.#controller.abort();
|
|
1288
|
+
constructor(source) {
|
|
1289
|
+
this.#source = source;
|
|
1290
|
+
if (source?.aborted === true) this.#controller.abort();
|
|
1291
|
+
source?.addEventListener("abort", this.#forward, { once: true });
|
|
1292
|
+
}
|
|
1293
|
+
get signal() {
|
|
1294
|
+
return this.#controller.signal;
|
|
1295
|
+
}
|
|
1296
|
+
release() {
|
|
1297
|
+
this.#source?.removeEventListener("abort", this.#forward);
|
|
1298
|
+
}
|
|
1299
|
+
};
|
|
1300
|
+
function spawnClaude(context, env, signal) {
|
|
1301
|
+
const { request, deps, executable, files, cwd } = context;
|
|
1302
|
+
try {
|
|
1303
|
+
return deps.subprocess.spawn({
|
|
1304
|
+
argv: turnArgv(executable, request, files, deps),
|
|
1305
|
+
cwd,
|
|
1306
|
+
env,
|
|
1307
|
+
graceMs: GRACE_MS,
|
|
1308
|
+
signal,
|
|
1309
|
+
stdio: { stdin: "pipe", stdout: "pipe", stderr: { maxBytes: STDERR_BYTES } }
|
|
1310
|
+
});
|
|
1311
|
+
} catch (error) {
|
|
1312
|
+
if (signal.aborted) throw abortedFailure(error);
|
|
1313
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1314
|
+
throw new NativeFailure(`Claude Code could not start: ${detail}`, FailureCode.nativeProtocol, {
|
|
1315
|
+
cause: error
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
function frameLine(frame, replayed) {
|
|
1320
|
+
return `${JSON.stringify(replayed ? { ...frame, shouldQuery: false } : frame)}
|
|
1321
|
+
`;
|
|
1322
|
+
}
|
|
1323
|
+
async function awaitReplayAcknowledgment(channel, stderrTail) {
|
|
1324
|
+
while (true) {
|
|
1325
|
+
const event = await channel.receive();
|
|
1326
|
+
if (event === null) {
|
|
1327
|
+
throw new NativeFailure(
|
|
1328
|
+
`Native exited before replay acknowledgment (exit=${channel.exitCode})${stderrTail()}`,
|
|
1329
|
+
FailureCode.nativeProtocol
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
if (event.type !== "result") continue;
|
|
1333
|
+
if (event.num_turns !== 0 || event.is_error) {
|
|
1334
|
+
throw new NativeFailure(
|
|
1335
|
+
`Native history replay not supported: expected zero-turn acknowledgment${stderrTail()}`,
|
|
1336
|
+
FailureCode.nativeProtocol
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
async function sendFrames(channel, frames, stderrTail) {
|
|
1343
|
+
for (const [index, frame] of frames.entries()) {
|
|
1344
|
+
const replayed = frame.type === "user" && index < frames.length - 1;
|
|
1345
|
+
await channel.write(frameLine(frame, replayed));
|
|
1346
|
+
if (replayed) await awaitReplayAcknowledgment(channel, stderrTail);
|
|
1347
|
+
}
|
|
1348
|
+
await channel.endInput();
|
|
1349
|
+
}
|
|
1350
|
+
function startDrain(handle, relay, channel) {
|
|
1351
|
+
channel.discardOutput();
|
|
1352
|
+
void drainAfterAnswer(handle, relay).catch(() => {
|
|
1353
|
+
}).finally(() => channel.close());
|
|
1354
|
+
}
|
|
1355
|
+
async function stopProcess(handle, relay, channel) {
|
|
1356
|
+
handle.terminate();
|
|
1357
|
+
relay.abort();
|
|
1358
|
+
await settledWithin(handle.done, STOP_WAIT_MS);
|
|
1359
|
+
channel.close();
|
|
1360
|
+
await relay.close().catch(() => {
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
function childEnv(context, relay) {
|
|
1364
|
+
const { settings, processEnv, request } = context;
|
|
1365
|
+
return {
|
|
1366
|
+
...nativeEnv(settings, processEnv),
|
|
1367
|
+
ANTHROPIC_BASE_URL: relay.url,
|
|
1368
|
+
...request.maxOutputTokens === void 0 ? {} : { CLAUDE_CODE_MAX_OUTPUT_TOKENS: String(request.maxOutputTokens) }
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
async function* runProcess(context, relay) {
|
|
1372
|
+
const { request, signal } = context;
|
|
1373
|
+
const processAbort = new ProcessAbort(signal);
|
|
1374
|
+
const abortRelay = () => relay.abort();
|
|
1375
|
+
processAbort.signal.addEventListener("abort", abortRelay, { once: true });
|
|
1376
|
+
const stopWatching = () => {
|
|
1377
|
+
processAbort.release();
|
|
1378
|
+
processAbort.signal.removeEventListener("abort", abortRelay);
|
|
1379
|
+
};
|
|
1380
|
+
let handle;
|
|
1381
|
+
try {
|
|
1382
|
+
handle = spawnClaude(context, childEnv(context, relay), processAbort.signal);
|
|
1383
|
+
} catch (error) {
|
|
1384
|
+
stopWatching();
|
|
1385
|
+
await relay.close().catch(() => {
|
|
1386
|
+
});
|
|
1387
|
+
throw error;
|
|
1388
|
+
}
|
|
1389
|
+
const channel = new NativeChannel(handle, {
|
|
1390
|
+
idleMs: context.idleMs,
|
|
1391
|
+
signal,
|
|
1392
|
+
redact: (text) => withoutRelayRoute(text, relay.url)
|
|
1393
|
+
});
|
|
1394
|
+
const stderrTail = () => channel.stderrTail();
|
|
1395
|
+
let handedOff = false;
|
|
1396
|
+
try {
|
|
1397
|
+
await sendFrames(channel, request.frames, stderrTail);
|
|
1398
|
+
const state = newTurnState();
|
|
1399
|
+
let ending = null;
|
|
1400
|
+
while (ending === null) {
|
|
1401
|
+
const event = await channel.receive();
|
|
1402
|
+
if (event === null) {
|
|
1403
|
+
ending = { fast: false, exitCode: channel.exitCode };
|
|
1404
|
+
break;
|
|
1405
|
+
}
|
|
1406
|
+
const streamEvent = recordEvent(state, event, stderrTail);
|
|
1407
|
+
if (streamEvent !== null) yield streamEvent;
|
|
1408
|
+
if (event.type === "result" && fastEligible(event, relay, state)) ending = { fast: true };
|
|
1409
|
+
}
|
|
1410
|
+
if (ending.fast) {
|
|
1411
|
+
stopWatching();
|
|
1412
|
+
handedOff = true;
|
|
1413
|
+
startDrain(handle, relay, channel);
|
|
1414
|
+
} else {
|
|
1415
|
+
throwIfAborted(signal);
|
|
1416
|
+
}
|
|
1417
|
+
return finishTurn(state, relay, ending, stderrTail());
|
|
1418
|
+
} finally {
|
|
1419
|
+
stopWatching();
|
|
1420
|
+
if (!handedOff) await stopProcess(handle, relay, channel);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
var runNative = async function* (request, signal, deps) {
|
|
1424
|
+
throwIfAborted(signal);
|
|
1425
|
+
const processEnv = deps.processEnv();
|
|
1426
|
+
assertNoBackendOverride(processEnv);
|
|
1427
|
+
const settings = deps.settings();
|
|
1428
|
+
const queried = queriedBlocks(request);
|
|
1429
|
+
const cwd = sharedWorkdir();
|
|
1430
|
+
const executable = await resolveClaude(deps, signal);
|
|
1431
|
+
const files = await writeRequestFiles(request);
|
|
1432
|
+
const idleMs = settings.readIdleTimeoutSeconds * 1e3;
|
|
1433
|
+
const context = {
|
|
1434
|
+
request,
|
|
1435
|
+
signal,
|
|
1436
|
+
deps,
|
|
1437
|
+
settings,
|
|
1438
|
+
processEnv,
|
|
1439
|
+
idleMs,
|
|
1440
|
+
cwd,
|
|
1441
|
+
executable,
|
|
1442
|
+
files,
|
|
1443
|
+
queried
|
|
1444
|
+
};
|
|
1445
|
+
try {
|
|
1446
|
+
const relay = await openRelay(request, context);
|
|
1447
|
+
if (signal?.aborted === true) {
|
|
1448
|
+
await relay.close().catch(() => {
|
|
1449
|
+
});
|
|
1450
|
+
throw abortedFailure();
|
|
1451
|
+
}
|
|
1452
|
+
return yield* runProcess(context, relay);
|
|
1453
|
+
} finally {
|
|
1454
|
+
await removeRequestFiles(files).catch(() => {
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
};
|
|
1458
|
+
|
|
1459
|
+
// src/catalog.ts
|
|
1460
|
+
var LONG_CONTEXT_SUFFIX = "[1m]";
|
|
1461
|
+
var LONG_CONTEXT_WINDOW = 1e6;
|
|
1462
|
+
var DEFAULT_WINDOW = 2e5;
|
|
1463
|
+
var CONTEXT_WINDOWS = {
|
|
1464
|
+
"claude-sonnet-5-5": LONG_CONTEXT_WINDOW,
|
|
1465
|
+
"claude-sonnet-5": LONG_CONTEXT_WINDOW,
|
|
1466
|
+
"claude-haiku-5-5": LONG_CONTEXT_WINDOW,
|
|
1467
|
+
"claude-haiku-4-5-20251001": DEFAULT_WINDOW,
|
|
1468
|
+
"claude-opus-5-5": LONG_CONTEXT_WINDOW,
|
|
1469
|
+
"claude-opus-5": LONG_CONTEXT_WINDOW,
|
|
1470
|
+
"claude-opus-4-8": LONG_CONTEXT_WINDOW,
|
|
1471
|
+
"claude-fable-5-1": LONG_CONTEXT_WINDOW,
|
|
1472
|
+
"claude-mythos-5-1": LONG_CONTEXT_WINDOW
|
|
1473
|
+
};
|
|
1474
|
+
var ACCESS_GATED = /* @__PURE__ */ new Set(["claude-mythos-5-1"]);
|
|
1475
|
+
var ALIASES = {
|
|
1476
|
+
sonnet: "claude-sonnet-5-5",
|
|
1477
|
+
haiku: "claude-haiku-5-5",
|
|
1478
|
+
"claude-haiku-4-5": "claude-haiku-4-5-20251001",
|
|
1479
|
+
opus: "claude-opus-5-5",
|
|
1480
|
+
fable: "claude-fable-5-1"
|
|
1481
|
+
};
|
|
1482
|
+
var MANDATORY_THINKING_PREFIXES = [
|
|
1483
|
+
"claude-fable",
|
|
1484
|
+
"claude-opus-5-5",
|
|
1485
|
+
"claude-sonnet-5-5",
|
|
1486
|
+
"claude-haiku-5-5",
|
|
1487
|
+
"claude-mythos"
|
|
1488
|
+
];
|
|
1489
|
+
var NO_ADAPTIVE_THINKING = /* @__PURE__ */ new Set(["claude-haiku-4-5-20251001"]);
|
|
1490
|
+
function lookup(table, key) {
|
|
1491
|
+
return Object.hasOwn(table, key) ? table[key] : void 0;
|
|
1492
|
+
}
|
|
1493
|
+
function isCatalogued(name2) {
|
|
1494
|
+
return lookup(ALIASES, name2) !== void 0 || lookup(CONTEXT_WINDOWS, name2) !== void 0;
|
|
1495
|
+
}
|
|
1496
|
+
function routeBase(name2) {
|
|
1497
|
+
if (isCatalogued(name2)) return name2;
|
|
1498
|
+
const dotted = name2.replaceAll(".", "-");
|
|
1499
|
+
if (dotted !== name2) return routeBase(dotted);
|
|
1500
|
+
const vendored = `claude-${name2.startsWith("claude-") ? name2.slice("claude-".length) : name2}`;
|
|
1501
|
+
return isCatalogued(vendored) ? vendored : name2;
|
|
1502
|
+
}
|
|
1503
|
+
function resolveRoute(model) {
|
|
1504
|
+
const longContextRequested = model.endsWith(LONG_CONTEXT_SUFFIX);
|
|
1505
|
+
const bare = longContextRequested ? model.slice(0, -LONG_CONTEXT_SUFFIX.length) : model;
|
|
1506
|
+
const base = routeBase(bare);
|
|
1507
|
+
return { canonical: lookup(ALIASES, base) ?? base, longContextRequested };
|
|
1508
|
+
}
|
|
1509
|
+
function nativeModel(model) {
|
|
1510
|
+
const { canonical, longContextRequested } = resolveRoute(model);
|
|
1511
|
+
const window = lookup(CONTEXT_WINDOWS, canonical);
|
|
1512
|
+
if (window === LONG_CONTEXT_WINDOW) return `${canonical}${LONG_CONTEXT_SUFFIX}`;
|
|
1513
|
+
if (window === void 0) return model;
|
|
1514
|
+
if (longContextRequested) {
|
|
1515
|
+
throw new NativeFailure(
|
|
1516
|
+
"Haiku 4.5 does not support a 1M context window",
|
|
1517
|
+
FailureCode.invalidRequest
|
|
1518
|
+
);
|
|
1519
|
+
}
|
|
1520
|
+
return canonical;
|
|
1521
|
+
}
|
|
1522
|
+
function contextWindowOf(model) {
|
|
1523
|
+
const { canonical, longContextRequested } = resolveRoute(model);
|
|
1524
|
+
const pinned = lookup(CONTEXT_WINDOWS, canonical);
|
|
1525
|
+
if (longContextRequested) return pinned === LONG_CONTEXT_WINDOW ? pinned : void 0;
|
|
1526
|
+
return pinned ?? DEFAULT_WINDOW;
|
|
1527
|
+
}
|
|
1528
|
+
function canonicalOf(model) {
|
|
1529
|
+
return resolveRoute(model).canonical;
|
|
1530
|
+
}
|
|
1531
|
+
function acceptsThinkingDisable(model) {
|
|
1532
|
+
const canonical = canonicalOf(model);
|
|
1533
|
+
return !MANDATORY_THINKING_PREFIXES.some((prefix) => canonical.startsWith(prefix));
|
|
1534
|
+
}
|
|
1535
|
+
function supportsAdaptiveThinking(model) {
|
|
1536
|
+
return !NO_ADAPTIVE_THINKING.has(canonicalOf(model));
|
|
1537
|
+
}
|
|
1538
|
+
function pickerRoutes() {
|
|
1539
|
+
return Object.keys(CONTEXT_WINDOWS).filter((canonical) => !ACCESS_GATED.has(canonical)).map(nativeModel);
|
|
1540
|
+
}
|
|
1541
|
+
var PICKER_ROUTES = pickerRoutes();
|
|
1542
|
+
|
|
1543
|
+
// src/native/picker.ts
|
|
1544
|
+
var LONG_CONTEXT_SUFFIX2 = "[1m]";
|
|
1545
|
+
var USAGE_CREDITS = "usage credits";
|
|
1546
|
+
var UNPINNED = "unpinned";
|
|
1547
|
+
var SEPARATOR = " \xB7 ";
|
|
1548
|
+
var CREDIT_BILLED_MODELS = /* @__PURE__ */ new Set(["claude-fable-5", "claude-fable-5-1"]);
|
|
1549
|
+
function billingRuleOf(plan) {
|
|
1550
|
+
const lower = plan.toLowerCase();
|
|
1551
|
+
if (lower.includes("pro")) return { models: CREDIT_BILLED_MODELS, note: USAGE_CREDITS };
|
|
1552
|
+
if (lower.includes("team") || lower.includes("enterprise")) {
|
|
1553
|
+
return { models: CREDIT_BILLED_MODELS, note: `${USAGE_CREDITS} on standard seats` };
|
|
1554
|
+
}
|
|
1555
|
+
return { models: /* @__PURE__ */ new Set(), note: USAGE_CREDITS };
|
|
1556
|
+
}
|
|
1557
|
+
function textOf2(value) {
|
|
1558
|
+
return typeof value === "string" ? value : "";
|
|
1559
|
+
}
|
|
1560
|
+
function withoutLongContext(model) {
|
|
1561
|
+
return model.endsWith(LONG_CONTEXT_SUFFIX2) ? model.slice(0, -LONG_CONTEXT_SUFFIX2.length) : model;
|
|
1562
|
+
}
|
|
1563
|
+
function isPinned(route) {
|
|
1564
|
+
return Object.hasOwn(CONTEXT_WINDOWS, withoutLongContext(route));
|
|
1565
|
+
}
|
|
1566
|
+
function announcedModel(row) {
|
|
1567
|
+
return textOf2(row.resolvedModel) || textOf2(row.value);
|
|
1568
|
+
}
|
|
1569
|
+
function nameOf(row, route) {
|
|
1570
|
+
const display = textOf2(row.displayName).trim();
|
|
1571
|
+
const beforeParenthesis = display.split("(")[0] ?? "";
|
|
1572
|
+
const namesVersion = /\d/.test(beforeParenthesis) && !display.toLowerCase().startsWith("default");
|
|
1573
|
+
if (namesVersion) return display;
|
|
1574
|
+
return textOf2(row.description).split("\xB7")[0]?.trim() || route;
|
|
1575
|
+
}
|
|
1576
|
+
function catalogName(canonical) {
|
|
1577
|
+
const [family = "", ...rest] = canonical.replace(/^claude-/, "").split("-");
|
|
1578
|
+
const version = rest.filter((part) => /^\d{1,7}$/.test(part)).join(".");
|
|
1579
|
+
const title = `${family.charAt(0).toUpperCase()}${family.slice(1)}`;
|
|
1580
|
+
return version === "" ? title : `${title} ${version}`;
|
|
1581
|
+
}
|
|
1582
|
+
function routeOf(row, base, longContext) {
|
|
1583
|
+
const route = nativeModel(base);
|
|
1584
|
+
if (isPinned(route)) return route;
|
|
1585
|
+
if (textOf2(row.resolvedModel) === "") return null;
|
|
1586
|
+
return longContext.has(base) ? `${base}${LONG_CONTEXT_SUFFIX2}` : route;
|
|
1587
|
+
}
|
|
1588
|
+
function isDefaultRow(row) {
|
|
1589
|
+
return textOf2(row.value) === "default";
|
|
1590
|
+
}
|
|
1591
|
+
function newEntry(row, route) {
|
|
1592
|
+
return {
|
|
1593
|
+
id: route,
|
|
1594
|
+
name: nameOf(row, route),
|
|
1595
|
+
note: isPinned(route) ? "" : UNPINNED,
|
|
1596
|
+
namedByDefaultRow: isDefaultRow(row)
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
function applyRowName(entry, row) {
|
|
1600
|
+
if (!entry.namedByDefaultRow || isDefaultRow(row)) return;
|
|
1601
|
+
entry.name = nameOf(row, entry.id);
|
|
1602
|
+
entry.namedByDefaultRow = false;
|
|
1603
|
+
}
|
|
1604
|
+
function applyBillingNote(entry, row, base, rule) {
|
|
1605
|
+
const described = textOf2(row.description).toLowerCase().includes("usage credit");
|
|
1606
|
+
if (!described && !rule.models.has(base)) return;
|
|
1607
|
+
const warning = described ? USAGE_CREDITS : rule.note;
|
|
1608
|
+
entry.note = isPinned(entry.id) ? warning : `${warning}${SEPARATOR}${UNPINNED}`;
|
|
1609
|
+
}
|
|
1610
|
+
function advertisedEntries(rows, rule) {
|
|
1611
|
+
const announced = rows.map(announcedModel);
|
|
1612
|
+
const longContext = new Set(
|
|
1613
|
+
announced.filter((model) => model.endsWith(LONG_CONTEXT_SUFFIX2)).map(withoutLongContext)
|
|
1614
|
+
);
|
|
1615
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1616
|
+
for (const row of rows) {
|
|
1617
|
+
const base = withoutLongContext(announcedModel(row));
|
|
1618
|
+
if (base === "") continue;
|
|
1619
|
+
const route = routeOf(row, base, longContext);
|
|
1620
|
+
if (route === null) continue;
|
|
1621
|
+
const entry = entries.get(route) ?? newEntry(row, route);
|
|
1622
|
+
entries.set(route, entry);
|
|
1623
|
+
applyRowName(entry, row);
|
|
1624
|
+
applyBillingNote(entry, row, base, rule);
|
|
1625
|
+
}
|
|
1626
|
+
return entries;
|
|
1627
|
+
}
|
|
1628
|
+
function appendPinnedRoutes(entries, rule) {
|
|
1629
|
+
for (const route of PICKER_ROUTES) {
|
|
1630
|
+
if (entries.has(route)) continue;
|
|
1631
|
+
const canonical = withoutLongContext(route);
|
|
1632
|
+
entries.set(route, {
|
|
1633
|
+
id: route,
|
|
1634
|
+
name: catalogName(canonical),
|
|
1635
|
+
note: rule.models.has(canonical) ? rule.note : "",
|
|
1636
|
+
namedByDefaultRow: false
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
function toDiscovered({ id, name: name2, note }) {
|
|
1641
|
+
return note === "" ? { id, name: name2 } : { id, name: name2, description: note };
|
|
1642
|
+
}
|
|
1643
|
+
function mergePicker(rows, plan) {
|
|
1644
|
+
const rule = billingRuleOf(plan);
|
|
1645
|
+
const entries = advertisedEntries(rows, rule);
|
|
1646
|
+
if (entries.size === 0) return [];
|
|
1647
|
+
appendPinnedRoutes(entries, rule);
|
|
1648
|
+
return [...entries.values()].map(toDiscovered);
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
// src/native/probe.ts
|
|
1652
|
+
import { mkdtemp as mkdtemp2, rm as rm2 } from "node:fs/promises";
|
|
1653
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
1654
|
+
import { join as join4 } from "node:path";
|
|
1655
|
+
var GRACE_MS2 = 2e3;
|
|
1656
|
+
var OUTPUT_BYTES = 8 * 1024 * 1024;
|
|
1657
|
+
var STDERR_BYTES2 = 16 * 1024;
|
|
1658
|
+
function startFailure(error) {
|
|
1659
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1660
|
+
return new NativeFailure(`Claude Code could not start: ${detail}`, FailureCode.nativeProtocol, {
|
|
1661
|
+
cause: error
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1664
|
+
function collectedText(handle, stream) {
|
|
1665
|
+
return handle.collected[stream]?.readFrom(0).text ?? "";
|
|
1666
|
+
}
|
|
1667
|
+
async function runProbe(deps, spec) {
|
|
1668
|
+
throwIfAborted(spec.signal);
|
|
1669
|
+
const deadline = AbortSignal.timeout(spec.timeoutMs);
|
|
1670
|
+
const classify = (error) => {
|
|
1671
|
+
if (spec.signal?.aborted === true) return abortedFailure(error);
|
|
1672
|
+
if (deadline.aborted) return timeoutFailure();
|
|
1673
|
+
return startFailure(error);
|
|
1674
|
+
};
|
|
1675
|
+
let handle;
|
|
1676
|
+
try {
|
|
1677
|
+
handle = deps.subprocess.spawn({
|
|
1678
|
+
argv: spec.argv,
|
|
1679
|
+
cwd: spec.cwd,
|
|
1680
|
+
env: spec.env,
|
|
1681
|
+
graceMs: GRACE_MS2,
|
|
1682
|
+
signal: AbortSignal.any(spec.signal === void 0 ? [deadline] : [deadline, spec.signal]),
|
|
1683
|
+
stdio: {
|
|
1684
|
+
stdin: spec.stdin,
|
|
1685
|
+
stdout: { maxBytes: OUTPUT_BYTES },
|
|
1686
|
+
stderr: { maxBytes: STDERR_BYTES2 }
|
|
1687
|
+
}
|
|
1688
|
+
});
|
|
1689
|
+
} catch (error) {
|
|
1690
|
+
throw classify(error);
|
|
1691
|
+
}
|
|
1692
|
+
try {
|
|
1693
|
+
const { exitCode } = await handle.done;
|
|
1694
|
+
throwIfAborted(spec.signal);
|
|
1695
|
+
if (deadline.aborted) throw timeoutFailure();
|
|
1696
|
+
return {
|
|
1697
|
+
stdout: collectedText(handle, "stdout"),
|
|
1698
|
+
stderr: collectedText(handle, "stderr"),
|
|
1699
|
+
exitCode
|
|
1700
|
+
};
|
|
1701
|
+
} catch (error) {
|
|
1702
|
+
if (error instanceof NativeFailure) throw error;
|
|
1703
|
+
throw classify(error);
|
|
1704
|
+
} finally {
|
|
1705
|
+
handle.terminate();
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
async function withProbeDirectory(work) {
|
|
1709
|
+
const cwd = await mkdtemp2(join4(tmpdir3(), "claude-subscription-probe-"));
|
|
1710
|
+
try {
|
|
1711
|
+
return await work(cwd);
|
|
1712
|
+
} finally {
|
|
1713
|
+
await rm2(cwd, { recursive: true, force: true });
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
// src/native/setup.ts
|
|
1718
|
+
var AUTH_STATUS_TIMEOUT_MS = 2e4;
|
|
1719
|
+
var HANDSHAKE_TIMEOUT_MS = 4e4;
|
|
1720
|
+
var ANTHROPIC_API2 = "https://api.anthropic.com";
|
|
1721
|
+
var HANDSHAKE_REQUEST = `${JSON.stringify({
|
|
1722
|
+
type: "control_request",
|
|
1723
|
+
request_id: "dsh-picker",
|
|
1724
|
+
request: { subtype: "initialize" }
|
|
1725
|
+
})}
|
|
1726
|
+
`;
|
|
1727
|
+
var HANDSHAKE_ARGS = [
|
|
1728
|
+
"-p",
|
|
1729
|
+
"--model",
|
|
1730
|
+
"sonnet",
|
|
1731
|
+
"--input-format",
|
|
1732
|
+
"stream-json",
|
|
1733
|
+
"--output-format",
|
|
1734
|
+
"stream-json",
|
|
1735
|
+
"--verbose",
|
|
1736
|
+
"--tools",
|
|
1737
|
+
"",
|
|
1738
|
+
"--setting-sources",
|
|
1739
|
+
"",
|
|
1740
|
+
"--strict-mcp-config",
|
|
1741
|
+
"--mcp-config",
|
|
1742
|
+
'{"mcpServers":{}}',
|
|
1743
|
+
"--disable-slash-commands",
|
|
1744
|
+
"--no-session-persistence"
|
|
1745
|
+
];
|
|
1746
|
+
function isJsonObject(value) {
|
|
1747
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1748
|
+
}
|
|
1749
|
+
function protocolFailure2(what, output, cause) {
|
|
1750
|
+
return new NativeFailure(
|
|
1751
|
+
`${what}${stderrSuffix(output.stderr)}`,
|
|
1752
|
+
FailureCode.nativeProtocol,
|
|
1753
|
+
cause === void 0 ? {} : { cause }
|
|
1754
|
+
);
|
|
1755
|
+
}
|
|
1756
|
+
function planLabel(raw) {
|
|
1757
|
+
if (raw.toLowerCase().startsWith("claude")) return raw;
|
|
1758
|
+
const words = raw.split("_").map((word) => `${word.charAt(0).toUpperCase()}${word.slice(1)}`);
|
|
1759
|
+
return `Claude ${words.join(" ")}`;
|
|
1760
|
+
}
|
|
1761
|
+
function parseAuthStatus(output) {
|
|
1762
|
+
const text = output.stdout.trim();
|
|
1763
|
+
try {
|
|
1764
|
+
const parsed = JSON.parse(text);
|
|
1765
|
+
if (isJsonObject(parsed)) return parsed;
|
|
1766
|
+
} catch (error) {
|
|
1767
|
+
throw protocolFailure2(unreadableAuth(text), output, error);
|
|
1768
|
+
}
|
|
1769
|
+
throw protocolFailure2(unreadableAuth(text), output);
|
|
1770
|
+
}
|
|
1771
|
+
function unreadableAuth(text) {
|
|
1772
|
+
return `Could not read \`claude auth status\`: ${JSON.stringify(text.slice(0, 300))}`;
|
|
1773
|
+
}
|
|
1774
|
+
function statusFrom(auth) {
|
|
1775
|
+
const plan = typeof auth.subscriptionType === "string" ? auth.subscriptionType.trim() : "";
|
|
1776
|
+
const loggedIn = auth.loggedIn === true;
|
|
1777
|
+
return {
|
|
1778
|
+
installed: true,
|
|
1779
|
+
loggedIn,
|
|
1780
|
+
...plan === "" ? {} : { subscriptionType: planLabel(plan) },
|
|
1781
|
+
...loggedIn ? {} : { detail: LOGIN_HINT }
|
|
1782
|
+
};
|
|
1783
|
+
}
|
|
1784
|
+
async function resolveOrMissing(deps, signal) {
|
|
1785
|
+
try {
|
|
1786
|
+
return await resolveClaude(deps, signal);
|
|
1787
|
+
} catch (error) {
|
|
1788
|
+
if (error instanceof NativeFailure && error.code === FailureCode.claudeCodeMissing) return null;
|
|
1789
|
+
throw error;
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
async function setupStatus(deps, signal) {
|
|
1793
|
+
throwIfAborted(signal);
|
|
1794
|
+
const executable = await resolveOrMissing(deps, signal);
|
|
1795
|
+
if (executable === null) return { installed: false, loggedIn: false, detail: INSTALL_HINT };
|
|
1796
|
+
const output = await withProbeDirectory(
|
|
1797
|
+
(cwd) => runProbe(deps, {
|
|
1798
|
+
argv: [executable, "auth", "status"],
|
|
1799
|
+
stdin: "ignore",
|
|
1800
|
+
env: probeEnv(deps.settings(), deps.processEnv()),
|
|
1801
|
+
cwd,
|
|
1802
|
+
timeoutMs: AUTH_STATUS_TIMEOUT_MS,
|
|
1803
|
+
...signal === void 0 ? {} : { signal }
|
|
1804
|
+
})
|
|
1805
|
+
);
|
|
1806
|
+
return statusFrom(parseAuthStatus(output));
|
|
1807
|
+
}
|
|
1808
|
+
async function assertLoggedIn(deps, signal) {
|
|
1809
|
+
const status = await setupStatus(deps, signal);
|
|
1810
|
+
if (!status.installed) {
|
|
1811
|
+
throw new NativeFailure(INSTALL_HINT, FailureCode.claudeCodeMissing, { status: 503 });
|
|
1812
|
+
}
|
|
1813
|
+
if (!status.loggedIn) {
|
|
1814
|
+
throw new NativeFailure(LOGGED_OUT_HINT, FailureCode.invalidCredential, { status: 401 });
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
function parseRows(output) {
|
|
1818
|
+
const lines = output.stdout.split("\n").filter((line) => line.startsWith("{"));
|
|
1819
|
+
try {
|
|
1820
|
+
return lines.map((line) => JSON.parse(line));
|
|
1821
|
+
} catch (error) {
|
|
1822
|
+
throw protocolFailure2("Invalid output from the Claude Code handshake", output, error);
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
function field(value, name2) {
|
|
1826
|
+
return isJsonObject(value) ? value[name2] : void 0;
|
|
1827
|
+
}
|
|
1828
|
+
function parseHandshake(output) {
|
|
1829
|
+
const answer = parseRows(output).find((row) => row.type === "control_response");
|
|
1830
|
+
if (answer === void 0) {
|
|
1831
|
+
throw protocolFailure2("Claude Code did not answer the model handshake", output);
|
|
1832
|
+
}
|
|
1833
|
+
const body = field(answer.response, "response");
|
|
1834
|
+
const rows = field(body, "models");
|
|
1835
|
+
if (!Array.isArray(rows) || !rows.every(isJsonObject)) {
|
|
1836
|
+
throw protocolFailure2("The model handshake holds no list of models", output);
|
|
1837
|
+
}
|
|
1838
|
+
const plan = field(field(body, "account"), "subscriptionType");
|
|
1839
|
+
return { rows, plan: typeof plan === "string" ? plan : "", output };
|
|
1840
|
+
}
|
|
1841
|
+
function handshakeRelay(deps, processEnv) {
|
|
1842
|
+
if (backendOverrides(processEnv).length === 0) return Promise.resolve(null);
|
|
1843
|
+
return deps.createRelay({
|
|
1844
|
+
upstream: ANTHROPIC_API2,
|
|
1845
|
+
timeoutMs: HANDSHAKE_TIMEOUT_MS,
|
|
1846
|
+
queried: [],
|
|
1847
|
+
oneShot: false,
|
|
1848
|
+
env: processEnv
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
async function runHandshake(deps, executable, signal) {
|
|
1852
|
+
const processEnv = deps.processEnv();
|
|
1853
|
+
const relay = await handshakeRelay(deps, processEnv);
|
|
1854
|
+
try {
|
|
1855
|
+
const env = probeEnv(deps.settings(), processEnv);
|
|
1856
|
+
if (relay !== null) env.ANTHROPIC_BASE_URL = relay.url;
|
|
1857
|
+
const raw = await withProbeDirectory(
|
|
1858
|
+
(cwd) => runProbe(deps, {
|
|
1859
|
+
argv: [executable, ...HANDSHAKE_ARGS],
|
|
1860
|
+
stdin: { data: HANDSHAKE_REQUEST },
|
|
1861
|
+
env,
|
|
1862
|
+
cwd,
|
|
1863
|
+
timeoutMs: HANDSHAKE_TIMEOUT_MS,
|
|
1864
|
+
...signal === void 0 ? {} : { signal }
|
|
1865
|
+
})
|
|
1866
|
+
);
|
|
1867
|
+
const output = relay === null ? raw : { ...raw, stderr: withoutRelayRoute(raw.stderr, relay.url) };
|
|
1868
|
+
if (relay?.used === true) {
|
|
1869
|
+
throw protocolFailure2("The model handshake sent a model request", output);
|
|
1870
|
+
}
|
|
1871
|
+
return parseHandshake(output);
|
|
1872
|
+
} finally {
|
|
1873
|
+
await relay?.close();
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
async function discoverModels(deps, signal) {
|
|
1877
|
+
await assertLoggedIn(deps, signal);
|
|
1878
|
+
const executable = await resolveClaude(deps, signal);
|
|
1879
|
+
const answer = await runHandshake(deps, executable, signal);
|
|
1880
|
+
const models = mergePicker(answer.rows, answer.plan);
|
|
1881
|
+
if (models.length === 0) {
|
|
1882
|
+
throw protocolFailure2("The model handshake advertised no model", answer.output);
|
|
1883
|
+
}
|
|
1884
|
+
return models;
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
// src/relay/relay.ts
|
|
1888
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
1889
|
+
import { once as once2 } from "node:events";
|
|
1890
|
+
import { createServer } from "node:http";
|
|
1891
|
+
|
|
1892
|
+
// src/relay/rewrite/payload.ts
|
|
1893
|
+
var utf8 = new TextDecoder("utf-8", { fatal: true });
|
|
1894
|
+
function isObject2(value) {
|
|
1895
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1896
|
+
}
|
|
1897
|
+
function parseJsonObject(payload) {
|
|
1898
|
+
try {
|
|
1899
|
+
const value = JSON.parse(utf8.decode(payload));
|
|
1900
|
+
return isObject2(value) ? value : void 0;
|
|
1901
|
+
} catch {
|
|
1902
|
+
return void 0;
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
function serializeBody(body) {
|
|
1906
|
+
return Buffer.from(JSON.stringify(body), "utf8");
|
|
1907
|
+
}
|
|
1908
|
+
function readMessages(body) {
|
|
1909
|
+
const { messages } = body;
|
|
1910
|
+
return Array.isArray(messages) && messages.every(isObject2) ? messages : void 0;
|
|
1911
|
+
}
|
|
1912
|
+
function jsonEqual(left, right) {
|
|
1913
|
+
if (left === right) return true;
|
|
1914
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
1915
|
+
return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((item, index) => jsonEqual(item, right[index]));
|
|
1916
|
+
}
|
|
1917
|
+
if (!isObject2(left) || !isObject2(right)) return false;
|
|
1918
|
+
const keys = Object.keys(left);
|
|
1919
|
+
return keys.length === Object.keys(right).length && keys.every((key) => Object.hasOwn(right, key) && jsonEqual(left[key], right[key]));
|
|
1920
|
+
}
|
|
1921
|
+
function lastAssistantIndex(messages) {
|
|
1922
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
1923
|
+
if (messages[index]?.role === "assistant") return index;
|
|
1924
|
+
}
|
|
1925
|
+
return -1;
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
// src/relay/capture.ts
|
|
1929
|
+
var CaptureError = class extends Error {
|
|
1930
|
+
constructor(message, options) {
|
|
1931
|
+
super(message, options);
|
|
1932
|
+
this.name = "CaptureError";
|
|
1933
|
+
}
|
|
1934
|
+
};
|
|
1935
|
+
var FRAME_END = /\r?\n\r?\n/;
|
|
1936
|
+
var LINE_BREAK = /\r\n|\n|\r/;
|
|
1937
|
+
var LEADING_SPACES = /^ +/;
|
|
1938
|
+
var TEXT_FIELDS = /* @__PURE__ */ new Map([
|
|
1939
|
+
["text_delta", "text"],
|
|
1940
|
+
["thinking_delta", "thinking"],
|
|
1941
|
+
["signature_delta", "signature"]
|
|
1942
|
+
]);
|
|
1943
|
+
var BLANK_CHARACTERS = new Set(
|
|
1944
|
+
" \n\v\f\r \x85\xA0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u2028\u2029\u202F\u205F\u3000"
|
|
1945
|
+
);
|
|
1946
|
+
function isBlank(text) {
|
|
1947
|
+
return [...text].every((character) => BLANK_CHARACTERS.has(character));
|
|
1948
|
+
}
|
|
1949
|
+
function storableNumber(_key, value) {
|
|
1950
|
+
if (typeof value !== "number") return value;
|
|
1951
|
+
if (!Number.isFinite(value))
|
|
1952
|
+
throw new RangeError("a tool input holds a number that is not finite");
|
|
1953
|
+
return Object.is(value, -0) ? 0 : value;
|
|
1954
|
+
}
|
|
1955
|
+
function hasType(value) {
|
|
1956
|
+
return isObject2(value) && typeof value.type === "string";
|
|
1957
|
+
}
|
|
1958
|
+
function isCapturedMessage(value) {
|
|
1959
|
+
return isObject2(value) && value.role === "assistant" && Array.isArray(value.content) && isObject2(value.usage) && typeof value.usage.input_tokens === "number" && typeof value.usage.output_tokens === "number";
|
|
1960
|
+
}
|
|
1961
|
+
function readIndex(event) {
|
|
1962
|
+
const { index } = event;
|
|
1963
|
+
if (typeof index !== "number" || !Number.isInteger(index) || index < 0) {
|
|
1964
|
+
throw new CaptureError(`${event.type} has no valid index`);
|
|
1965
|
+
}
|
|
1966
|
+
return index;
|
|
1967
|
+
}
|
|
1968
|
+
function readObject(event, field2) {
|
|
1969
|
+
const value = event[field2];
|
|
1970
|
+
if (!isObject2(value)) throw new CaptureError(`${event.type} has no ${field2} object`);
|
|
1971
|
+
return value;
|
|
1972
|
+
}
|
|
1973
|
+
function mergeFields(target, source) {
|
|
1974
|
+
for (const [key, value] of Object.entries(source)) {
|
|
1975
|
+
Object.defineProperty(target, key, {
|
|
1976
|
+
value,
|
|
1977
|
+
writable: true,
|
|
1978
|
+
enumerable: true,
|
|
1979
|
+
configurable: true
|
|
1980
|
+
});
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
function appendText(block, field2, addition) {
|
|
1984
|
+
const current = block[field2] ?? "";
|
|
1985
|
+
if (typeof current !== "string" || typeof addition !== "string") {
|
|
1986
|
+
throw new CaptureError(`a ${block.type} block cannot take a text delta for ${field2}`);
|
|
1987
|
+
}
|
|
1988
|
+
block[field2] = current + addition;
|
|
1989
|
+
}
|
|
1990
|
+
function addCitation(block, citation) {
|
|
1991
|
+
const citations = block.citations ?? [];
|
|
1992
|
+
if (!Array.isArray(citations) || citation === void 0) {
|
|
1993
|
+
throw new CaptureError(`a ${block.type} block cannot take a citation`);
|
|
1994
|
+
}
|
|
1995
|
+
block.citations = [...citations, structuredClone(citation)];
|
|
1996
|
+
}
|
|
1997
|
+
var EventStreamParser = class {
|
|
1998
|
+
#pending = "";
|
|
1999
|
+
#decoder = new TextDecoder("utf-8", { fatal: true });
|
|
2000
|
+
parse(chunk) {
|
|
2001
|
+
const frames = (this.#pending + this.#decode(chunk)).split(FRAME_END);
|
|
2002
|
+
this.#pending = frames.pop() ?? "";
|
|
2003
|
+
return frames.map(frameData).filter((data) => data !== "").map(parseEvent);
|
|
2004
|
+
}
|
|
2005
|
+
#decode(chunk) {
|
|
2006
|
+
try {
|
|
2007
|
+
return this.#decoder.decode(chunk, { stream: true });
|
|
2008
|
+
} catch (cause) {
|
|
2009
|
+
throw new CaptureError("the event stream is not valid UTF-8", { cause });
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
};
|
|
2013
|
+
function frameData(frame) {
|
|
2014
|
+
return frame.split(LINE_BREAK).filter((line) => line.startsWith("data:")).map((line) => line.slice("data:".length).replace(LEADING_SPACES, "")).join("\n");
|
|
2015
|
+
}
|
|
2016
|
+
function parseEvent(data) {
|
|
2017
|
+
let value;
|
|
2018
|
+
try {
|
|
2019
|
+
value = JSON.parse(data);
|
|
2020
|
+
} catch (cause) {
|
|
2021
|
+
throw new CaptureError("an event does not hold valid JSON", { cause });
|
|
2022
|
+
}
|
|
2023
|
+
if (!hasType(value)) throw new CaptureError("an event has no type");
|
|
2024
|
+
return value;
|
|
2025
|
+
}
|
|
2026
|
+
function readError(event) {
|
|
2027
|
+
const { type, message } = readObject(event, "error");
|
|
2028
|
+
if (typeof type !== "string" || typeof message !== "string") {
|
|
2029
|
+
throw new CaptureError("error has no type and message");
|
|
2030
|
+
}
|
|
2031
|
+
return { type, message };
|
|
2032
|
+
}
|
|
2033
|
+
var MessageAssembler = class {
|
|
2034
|
+
#message = null;
|
|
2035
|
+
#complete = false;
|
|
2036
|
+
#error = null;
|
|
2037
|
+
#partialInputs = /* @__PURE__ */ new Map();
|
|
2038
|
+
#unparsedInputs = /* @__PURE__ */ new Set();
|
|
2039
|
+
get message() {
|
|
2040
|
+
return this.#message;
|
|
2041
|
+
}
|
|
2042
|
+
get complete() {
|
|
2043
|
+
return this.#complete;
|
|
2044
|
+
}
|
|
2045
|
+
get error() {
|
|
2046
|
+
return this.#error;
|
|
2047
|
+
}
|
|
2048
|
+
apply(event) {
|
|
2049
|
+
switch (event.type) {
|
|
2050
|
+
case "message_start":
|
|
2051
|
+
this.#start(event);
|
|
2052
|
+
break;
|
|
2053
|
+
case "content_block_start":
|
|
2054
|
+
this.#startBlock(event);
|
|
2055
|
+
break;
|
|
2056
|
+
case "content_block_delta":
|
|
2057
|
+
this.#applyDelta(event);
|
|
2058
|
+
break;
|
|
2059
|
+
case "content_block_stop":
|
|
2060
|
+
this.#stopBlock(event);
|
|
2061
|
+
break;
|
|
2062
|
+
case "message_delta":
|
|
2063
|
+
this.#updateMessage(event);
|
|
2064
|
+
break;
|
|
2065
|
+
case "message_stop":
|
|
2066
|
+
this.#stop(event);
|
|
2067
|
+
break;
|
|
2068
|
+
case "error":
|
|
2069
|
+
this.#error ??= readError(event);
|
|
2070
|
+
break;
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
#started(event) {
|
|
2074
|
+
if (this.#message === null) throw new CaptureError(`${event.type} came before message_start`);
|
|
2075
|
+
return this.#message;
|
|
2076
|
+
}
|
|
2077
|
+
#blockAt(event, index) {
|
|
2078
|
+
const block = this.#started(event).content[index];
|
|
2079
|
+
if (block === void 0) throw new CaptureError(`${event.type} points at a missing block`);
|
|
2080
|
+
return block;
|
|
2081
|
+
}
|
|
2082
|
+
#start(event) {
|
|
2083
|
+
const { message } = event;
|
|
2084
|
+
if (!isCapturedMessage(message)) throw new CaptureError("message_start has no valid message");
|
|
2085
|
+
this.#message = structuredClone(message);
|
|
2086
|
+
}
|
|
2087
|
+
#startBlock(event) {
|
|
2088
|
+
const block = event.content_block;
|
|
2089
|
+
if (!hasType(block)) throw new CaptureError("content_block_start has no typed block");
|
|
2090
|
+
this.#started(event).content.push(structuredClone(block));
|
|
2091
|
+
}
|
|
2092
|
+
#applyDelta(event) {
|
|
2093
|
+
const index = readIndex(event);
|
|
2094
|
+
const block = this.#blockAt(event, index);
|
|
2095
|
+
const delta = event.delta;
|
|
2096
|
+
if (!hasType(delta)) throw new CaptureError("content_block_delta has no typed delta");
|
|
2097
|
+
const field2 = TEXT_FIELDS.get(delta.type);
|
|
2098
|
+
if (field2 !== void 0) {
|
|
2099
|
+
appendText(block, field2, delta[field2]);
|
|
2100
|
+
} else if (delta.type === "input_json_delta") {
|
|
2101
|
+
this.#appendInput(index, delta.partial_json);
|
|
2102
|
+
} else if (delta.type === "citations_delta") {
|
|
2103
|
+
addCitation(block, delta.citation);
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
#appendInput(index, partialJson) {
|
|
2107
|
+
if (typeof partialJson !== "string") throw new CaptureError("input_json_delta has no text");
|
|
2108
|
+
this.#partialInputs.set(index, (this.#partialInputs.get(index) ?? "") + partialJson);
|
|
2109
|
+
}
|
|
2110
|
+
#stopBlock(event) {
|
|
2111
|
+
const index = readIndex(event);
|
|
2112
|
+
const raw = this.#partialInputs.get(index);
|
|
2113
|
+
if (raw === void 0) return;
|
|
2114
|
+
this.#partialInputs.delete(index);
|
|
2115
|
+
const block = this.#blockAt(event, index);
|
|
2116
|
+
if (isBlank(raw)) {
|
|
2117
|
+
block.input = {};
|
|
2118
|
+
return;
|
|
2119
|
+
}
|
|
2120
|
+
try {
|
|
2121
|
+
block.input = JSON.parse(raw, storableNumber);
|
|
2122
|
+
} catch {
|
|
2123
|
+
this.#unparsedInputs.add(index);
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
#updateMessage(event) {
|
|
2127
|
+
const message = this.#started(event);
|
|
2128
|
+
if (event.delta !== void 0) mergeFields(message, readObject(event, "delta"));
|
|
2129
|
+
if (event.usage !== void 0) mergeFields(message.usage, readObject(event, "usage"));
|
|
2130
|
+
}
|
|
2131
|
+
#stop(event) {
|
|
2132
|
+
const message = this.#started(event);
|
|
2133
|
+
for (const index of [...this.#unparsedInputs].sort((a, b) => b - a)) {
|
|
2134
|
+
message.content.splice(index, 1);
|
|
2135
|
+
}
|
|
2136
|
+
this.#unparsedInputs.clear();
|
|
2137
|
+
this.#complete = Boolean(message.stop_reason) && this.#partialInputs.size === 0;
|
|
2138
|
+
}
|
|
2139
|
+
};
|
|
2140
|
+
var SseCapture = class {
|
|
2141
|
+
#parser = new EventStreamParser();
|
|
2142
|
+
#assembler = new MessageAssembler();
|
|
2143
|
+
get message() {
|
|
2144
|
+
return this.#assembler.message;
|
|
2145
|
+
}
|
|
2146
|
+
get complete() {
|
|
2147
|
+
return this.#assembler.complete;
|
|
2148
|
+
}
|
|
2149
|
+
get error() {
|
|
2150
|
+
return this.#assembler.error;
|
|
2151
|
+
}
|
|
2152
|
+
feed(chunk) {
|
|
2153
|
+
for (const event of this.#parser.parse(chunk)) this.#assembler.apply(event);
|
|
2154
|
+
}
|
|
2155
|
+
};
|
|
2156
|
+
|
|
2157
|
+
// src/relay/retry-after.ts
|
|
2158
|
+
var SECONDS = /^\d+$/;
|
|
2159
|
+
function retryAfterMs(header, now) {
|
|
2160
|
+
if (typeof header !== "string") return null;
|
|
2161
|
+
const value = header.trim();
|
|
2162
|
+
const wait = SECONDS.test(value) ? Number(value) * 1e3 : Date.parse(value) - now;
|
|
2163
|
+
return Number.isSafeInteger(wait) && wait > 0 ? wait : null;
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
// src/relay/rewrite/blocks.ts
|
|
2167
|
+
var MARK = "cache_control";
|
|
2168
|
+
var PLAIN_TEXT_KEYS = /* @__PURE__ */ new Set(["type", "text", MARK]);
|
|
2169
|
+
var UNCACHEABLE_TYPES = /* @__PURE__ */ new Set(["thinking", "redacted_thinking"]);
|
|
2170
|
+
function hasMark(block) {
|
|
2171
|
+
return Object.hasOwn(block, MARK);
|
|
2172
|
+
}
|
|
2173
|
+
function takeMark(block) {
|
|
2174
|
+
const mark = block[MARK];
|
|
2175
|
+
delete block[MARK];
|
|
2176
|
+
return mark;
|
|
2177
|
+
}
|
|
2178
|
+
function setMarkIfAbsent(block, mark) {
|
|
2179
|
+
if (!hasMark(block)) block[MARK] = mark;
|
|
2180
|
+
}
|
|
2181
|
+
function isCacheable(block) {
|
|
2182
|
+
return typeof block.type !== "string" || !UNCACHEABLE_TYPES.has(block.type);
|
|
2183
|
+
}
|
|
2184
|
+
function isUncacheable(value) {
|
|
2185
|
+
return isObject2(value) && !isCacheable(value);
|
|
2186
|
+
}
|
|
2187
|
+
function withoutMark(block) {
|
|
2188
|
+
if (!isObject2(block)) return block;
|
|
2189
|
+
const { [MARK]: _mark, ...rest } = block;
|
|
2190
|
+
return rest;
|
|
2191
|
+
}
|
|
2192
|
+
function countMarks(value) {
|
|
2193
|
+
if (Array.isArray(value)) return value.reduce((sum, item) => sum + countMarks(item), 0);
|
|
2194
|
+
if (!isObject2(value)) return 0;
|
|
2195
|
+
return Object.entries(value).reduce(
|
|
2196
|
+
(sum, [key, item]) => sum + (key === MARK ? 1 : countMarks(item)),
|
|
2197
|
+
0
|
|
2198
|
+
);
|
|
2199
|
+
}
|
|
2200
|
+
function isPlainText(block) {
|
|
2201
|
+
return isObject2(block) && block.type === "text" && Object.keys(block).every((key) => PLAIN_TEXT_KEYS.has(key));
|
|
2202
|
+
}
|
|
2203
|
+
function leadingLength(blocks, frame) {
|
|
2204
|
+
const wanted = frame.map(withoutMark);
|
|
2205
|
+
const starts = [];
|
|
2206
|
+
for (let start = 0; start + wanted.length <= blocks.length; start += 1) {
|
|
2207
|
+
const window = blocks.slice(start, start + wanted.length).map(withoutMark);
|
|
2208
|
+
if (jsonEqual(window, wanted)) starts.push(start);
|
|
2209
|
+
}
|
|
2210
|
+
return starts.length === 1 ? starts[0] ?? 0 : 0;
|
|
2211
|
+
}
|
|
2212
|
+
function dropLeadingText(blocks, count) {
|
|
2213
|
+
const leading = blocks.slice(0, count);
|
|
2214
|
+
if (!leading.every(isPlainText)) return false;
|
|
2215
|
+
const mark = leading.find(hasMark)?.[MARK];
|
|
2216
|
+
blocks.splice(0, count);
|
|
2217
|
+
const head = blocks[0];
|
|
2218
|
+
if (mark !== void 0 && mark !== null && isObject2(head)) setMarkIfAbsent(head, mark);
|
|
2219
|
+
return true;
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
// src/relay/rewrite/breakpoints.ts
|
|
2223
|
+
var LOOKBACK = 20;
|
|
2224
|
+
var MAX_BREAKPOINTS = 4;
|
|
2225
|
+
function isBefore(left, right) {
|
|
2226
|
+
return left.message < right.message || left.message === right.message && left.index < right.index;
|
|
2227
|
+
}
|
|
2228
|
+
function placedBlocks(messages) {
|
|
2229
|
+
return messages.flatMap(
|
|
2230
|
+
(message, messageIndex) => Array.isArray(message.content) ? message.content.flatMap(
|
|
2231
|
+
(block, index) => isObject2(block) ? [{ message: messageIndex, index, block }] : []
|
|
2232
|
+
) : []
|
|
2233
|
+
);
|
|
2234
|
+
}
|
|
2235
|
+
function markedBlocks(messages) {
|
|
2236
|
+
return placedBlocks(messages).filter(({ block }) => hasMark(block));
|
|
2237
|
+
}
|
|
2238
|
+
function lastCacheable(blocks) {
|
|
2239
|
+
for (let index = blocks.length - 1; index >= 0; index -= 1) {
|
|
2240
|
+
const placed = blocks[index];
|
|
2241
|
+
if (placed !== void 0 && isCacheable(placed.block)) return placed;
|
|
2242
|
+
}
|
|
2243
|
+
return void 0;
|
|
2244
|
+
}
|
|
2245
|
+
function dropOpeningContext(messages, queried) {
|
|
2246
|
+
if (messages.some((message) => message.role === "assistant")) return false;
|
|
2247
|
+
const first = messages.find((message) => message.role === "user");
|
|
2248
|
+
if (first === void 0 || !Array.isArray(first.content)) return false;
|
|
2249
|
+
const count = leadingLength(first.content, queried);
|
|
2250
|
+
return count > 0 && dropLeadingText(first.content, count);
|
|
2251
|
+
}
|
|
2252
|
+
function queriedPrefix(messages, turn, queried) {
|
|
2253
|
+
const newest = messages[turn];
|
|
2254
|
+
if (newest?.role !== "user" || !Array.isArray(newest.content)) return [];
|
|
2255
|
+
const prefix = [];
|
|
2256
|
+
for (let index = 0; index < Math.min(newest.content.length, queried.length); index += 1) {
|
|
2257
|
+
const sent = newest.content[index];
|
|
2258
|
+
if (!isObject2(sent) || !jsonEqual(withoutMark(sent), withoutMark(queried[index]))) break;
|
|
2259
|
+
prefix.push({ message: turn, index, block: sent });
|
|
2260
|
+
}
|
|
2261
|
+
const next = newest.content[prefix.length];
|
|
2262
|
+
return isObject2(next) && next.type === "tool_result" ? [] : prefix;
|
|
2263
|
+
}
|
|
2264
|
+
function foldMarks(target, later) {
|
|
2265
|
+
const [moved] = later.map(({ block }) => takeMark(block));
|
|
2266
|
+
setMarkIfAbsent(target.block, moved);
|
|
2267
|
+
}
|
|
2268
|
+
function opensTurn(message) {
|
|
2269
|
+
if (message.role !== "user") return false;
|
|
2270
|
+
const { content } = message;
|
|
2271
|
+
if (typeof content === "string") return true;
|
|
2272
|
+
return Array.isArray(content) && content.some((block) => !isObject2(block) || block.type !== "tool_result");
|
|
2273
|
+
}
|
|
2274
|
+
function holdsThinking(message) {
|
|
2275
|
+
return message.role === "assistant" && Array.isArray(message.content) && message.content.some(isUncacheable);
|
|
2276
|
+
}
|
|
2277
|
+
function lastIndexWhere(items, from, test) {
|
|
2278
|
+
for (let index = from; index >= 0; index -= 1) {
|
|
2279
|
+
if (test(items[index])) return index;
|
|
2280
|
+
}
|
|
2281
|
+
return void 0;
|
|
2282
|
+
}
|
|
2283
|
+
function firstIndexWhere(items, from, to, test) {
|
|
2284
|
+
for (let index = from; index <= to; index += 1) {
|
|
2285
|
+
if (test(items[index])) return index;
|
|
2286
|
+
}
|
|
2287
|
+
return void 0;
|
|
2288
|
+
}
|
|
2289
|
+
function countBlocks(messages) {
|
|
2290
|
+
return messages.reduce(
|
|
2291
|
+
(sum, message) => sum + (Array.isArray(message.content) ? message.content.length : 1),
|
|
2292
|
+
0
|
|
2293
|
+
);
|
|
2294
|
+
}
|
|
2295
|
+
function turnStartSpot(messages, lastAssistant) {
|
|
2296
|
+
const opening = lastIndexWhere(messages, lastAssistant - 1, opensTurn);
|
|
2297
|
+
if (opening === void 0) return void 0;
|
|
2298
|
+
const thought = firstIndexWhere(messages, opening + 1, lastAssistant, holdsThinking);
|
|
2299
|
+
if (thought === void 0) return void 0;
|
|
2300
|
+
const frame = lastIndexWhere(messages, thought - 1, (message) => message.role === "user");
|
|
2301
|
+
const content = frame === void 0 ? void 0 : messages[frame]?.content;
|
|
2302
|
+
if (frame === void 0 || !Array.isArray(content)) return void 0;
|
|
2303
|
+
const index = lastIndexWhere(
|
|
2304
|
+
content,
|
|
2305
|
+
content.length - 1,
|
|
2306
|
+
(block2) => isObject2(block2) && isCacheable(block2)
|
|
2307
|
+
);
|
|
2308
|
+
const block = index === void 0 ? void 0 : content[index];
|
|
2309
|
+
return index !== void 0 && isObject2(block) ? { message: frame, index, block } : void 0;
|
|
2310
|
+
}
|
|
2311
|
+
function keepTurnMark(body, messages, lastAssistant, target) {
|
|
2312
|
+
const spot = turnStartSpot(messages, lastAssistant);
|
|
2313
|
+
if (spot === void 0 || hasMark(spot.block) || !isBefore(spot, target)) return false;
|
|
2314
|
+
const span = countBlocks(messages.slice(spot.message, target.message));
|
|
2315
|
+
if (span - spot.index + target.index + 1 <= LOOKBACK) return false;
|
|
2316
|
+
const later = markedBlocks(messages).filter((marked) => isBefore(spot, marked));
|
|
2317
|
+
const [firstLater] = later;
|
|
2318
|
+
if (firstLater === void 0) return false;
|
|
2319
|
+
if (countMarks(body) < MAX_BREAKPOINTS) {
|
|
2320
|
+
spot.block.cache_control = structuredClone(firstLater.block.cache_control);
|
|
2321
|
+
return true;
|
|
2322
|
+
}
|
|
2323
|
+
const spare = later.find((marked) => isBefore(marked, target));
|
|
2324
|
+
if (spare === void 0 || !hasMark(target.block)) return false;
|
|
2325
|
+
spot.block.cache_control = takeMark(spare.block);
|
|
2326
|
+
return true;
|
|
2327
|
+
}
|
|
2328
|
+
function pinMessageBreakpoint(payload, queried) {
|
|
2329
|
+
if (queried.length === 0) return payload;
|
|
2330
|
+
const body = parseJsonObject(payload);
|
|
2331
|
+
const messages = body === void 0 ? void 0 : readMessages(body);
|
|
2332
|
+
if (body === void 0 || messages === void 0) return payload;
|
|
2333
|
+
const trimmedOpening = dropOpeningContext(messages, queried);
|
|
2334
|
+
const marked = markedBlocks(messages);
|
|
2335
|
+
if (marked.length === 0 && !trimmedOpening) return payload;
|
|
2336
|
+
const lastAssistant = lastAssistantIndex(messages);
|
|
2337
|
+
const recurring = [
|
|
2338
|
+
...placedBlocks(messages).filter(({ message }) => message <= lastAssistant),
|
|
2339
|
+
...queriedPrefix(messages, lastAssistant + 1, queried)
|
|
2340
|
+
];
|
|
2341
|
+
const target = lastCacheable(recurring);
|
|
2342
|
+
const later = target === void 0 ? [] : marked.filter((mark) => isBefore(target, mark));
|
|
2343
|
+
const folded = target !== void 0 && later.length > 0;
|
|
2344
|
+
if (folded) foldMarks(target, later);
|
|
2345
|
+
const pinnedTurn = target !== void 0 && keepTurnMark(body, messages, lastAssistant, target);
|
|
2346
|
+
return trimmedOpening || folded || pinnedTurn ? serializeBody(body) : payload;
|
|
2347
|
+
}
|
|
2348
|
+
function dropMessageBreakpoints(payload) {
|
|
2349
|
+
const body = parseJsonObject(payload);
|
|
2350
|
+
const messages = body === void 0 ? void 0 : readMessages(body);
|
|
2351
|
+
if (body === void 0 || messages === void 0) return payload;
|
|
2352
|
+
let dropped = false;
|
|
2353
|
+
for (const { content } of messages) {
|
|
2354
|
+
if (!Array.isArray(content)) continue;
|
|
2355
|
+
for (const block of content) {
|
|
2356
|
+
if (!isObject2(block)) continue;
|
|
2357
|
+
const mark = takeMark(block);
|
|
2358
|
+
if (mark !== void 0 && mark !== null) dropped = true;
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
return dropped ? serializeBody(body) : payload;
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
// src/relay/rewrite/restore.ts
|
|
2365
|
+
var QUERIED_TURN_MISMATCH = "Native request does not uniquely match the DSH tool results";
|
|
2366
|
+
var QueriedTurnMismatch = class extends Error {
|
|
2367
|
+
constructor() {
|
|
2368
|
+
super(QUERIED_TURN_MISMATCH);
|
|
2369
|
+
this.name = "QueriedTurnMismatch";
|
|
2370
|
+
}
|
|
2371
|
+
};
|
|
2372
|
+
var IGNORED_RESULT_KEYS = /* @__PURE__ */ new Set(["content", "is_error", "cache_control"]);
|
|
2373
|
+
function isToolResult(block) {
|
|
2374
|
+
return isObject2(block) && block.type === "tool_result";
|
|
2375
|
+
}
|
|
2376
|
+
function isBareText(block) {
|
|
2377
|
+
return isObject2(block) && block.type === "text" && typeof block.text === "string" && Object.keys(block).length === 2;
|
|
2378
|
+
}
|
|
2379
|
+
function joinText(blocks) {
|
|
2380
|
+
return blocks.map((block) => block.text).join("");
|
|
2381
|
+
}
|
|
2382
|
+
function leadingBareText(blocks, start) {
|
|
2383
|
+
const run = [];
|
|
2384
|
+
for (let index = start; index < blocks.length; index += 1) {
|
|
2385
|
+
const block = blocks[index];
|
|
2386
|
+
if (!isBareText(block)) break;
|
|
2387
|
+
run.push(block);
|
|
2388
|
+
}
|
|
2389
|
+
return run;
|
|
2390
|
+
}
|
|
2391
|
+
function without(block, keys) {
|
|
2392
|
+
return Object.fromEntries(Object.entries(block).filter(([key]) => !keys.has(key)));
|
|
2393
|
+
}
|
|
2394
|
+
function flatText(value) {
|
|
2395
|
+
if (typeof value === "string") return value;
|
|
2396
|
+
if (Array.isArray(value) && value.every(isBareText)) return joinText(value);
|
|
2397
|
+
return void 0;
|
|
2398
|
+
}
|
|
2399
|
+
function errorFlag(block) {
|
|
2400
|
+
return Object.hasOwn(block, "is_error") ? block.is_error : false;
|
|
2401
|
+
}
|
|
2402
|
+
function restoreContent(sent, host) {
|
|
2403
|
+
if (typeof host === "string" || typeof sent === "string") {
|
|
2404
|
+
const sentText = flatText(sent);
|
|
2405
|
+
const hostText = flatText(host);
|
|
2406
|
+
if (sentText === void 0 || hostText === void 0 || !sentText.startsWith(hostText)) {
|
|
2407
|
+
throw new QueriedTurnMismatch();
|
|
2408
|
+
}
|
|
2409
|
+
return structuredClone(host);
|
|
2410
|
+
}
|
|
2411
|
+
if (Array.isArray(sent) && Array.isArray(host)) return restoreSequence(sent, host);
|
|
2412
|
+
if (!jsonEqual(sent, host)) throw new QueriedTurnMismatch();
|
|
2413
|
+
return structuredClone(host);
|
|
2414
|
+
}
|
|
2415
|
+
function restoreToolResult(sent, host, restored) {
|
|
2416
|
+
const sentError = errorFlag(sent);
|
|
2417
|
+
const hostError = errorFlag(host);
|
|
2418
|
+
if (typeof sentError !== "boolean" || typeof hostError !== "boolean" || sentError !== hostError) {
|
|
2419
|
+
throw new QueriedTurnMismatch();
|
|
2420
|
+
}
|
|
2421
|
+
if (!jsonEqual(without(sent, IGNORED_RESULT_KEYS), without(host, IGNORED_RESULT_KEYS))) {
|
|
2422
|
+
throw new QueriedTurnMismatch();
|
|
2423
|
+
}
|
|
2424
|
+
if (Object.hasOwn(sent, "content") !== Object.hasOwn(host, "content")) {
|
|
2425
|
+
throw new QueriedTurnMismatch();
|
|
2426
|
+
}
|
|
2427
|
+
if (Object.hasOwn(host, "content")) restored.content = restoreContent(sent.content, host.content);
|
|
2428
|
+
}
|
|
2429
|
+
function restoreBlock(sent, host) {
|
|
2430
|
+
if (!isObject2(sent) || !isObject2(host)) throw new QueriedTurnMismatch();
|
|
2431
|
+
const restored = structuredClone(host);
|
|
2432
|
+
if (host.type === "tool_result") {
|
|
2433
|
+
restoreToolResult(sent, host, restored);
|
|
2434
|
+
} else if (!jsonEqual(withoutMark(sent), withoutMark(host))) {
|
|
2435
|
+
throw new QueriedTurnMismatch();
|
|
2436
|
+
}
|
|
2437
|
+
if (hasMark(sent)) {
|
|
2438
|
+
if (hasMark(host) && !jsonEqual(sent.cache_control, host.cache_control)) {
|
|
2439
|
+
throw new QueriedTurnMismatch();
|
|
2440
|
+
}
|
|
2441
|
+
restored.cache_control = structuredClone(sent.cache_control);
|
|
2442
|
+
}
|
|
2443
|
+
return restored;
|
|
2444
|
+
}
|
|
2445
|
+
function restoreSequence(sent, host) {
|
|
2446
|
+
const restored = [];
|
|
2447
|
+
let hostAt = 0;
|
|
2448
|
+
let sentAt = 0;
|
|
2449
|
+
while (hostAt < host.length) {
|
|
2450
|
+
if (sentAt >= sent.length) throw new QueriedTurnMismatch();
|
|
2451
|
+
const hostRun = leadingBareText(host, hostAt);
|
|
2452
|
+
const sentRun = leadingBareText(sent, sentAt);
|
|
2453
|
+
if (hostRun.length > 0 && sentRun.length > 0) {
|
|
2454
|
+
const original = joinText(hostRun);
|
|
2455
|
+
const actual = joinText(sentRun);
|
|
2456
|
+
const reachesEnd = hostAt + hostRun.length === host.length;
|
|
2457
|
+
if (!(reachesEnd ? actual.startsWith(original) : actual === original)) {
|
|
2458
|
+
throw new QueriedTurnMismatch();
|
|
2459
|
+
}
|
|
2460
|
+
restored.push(...structuredClone(hostRun));
|
|
2461
|
+
hostAt += hostRun.length;
|
|
2462
|
+
sentAt += sentRun.length;
|
|
2463
|
+
} else {
|
|
2464
|
+
restored.push(restoreBlock(sent[sentAt], host[hostAt]));
|
|
2465
|
+
hostAt += 1;
|
|
2466
|
+
sentAt += 1;
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
if (!sent.slice(sentAt).every(isBareText)) throw new QueriedTurnMismatch();
|
|
2470
|
+
return restored;
|
|
2471
|
+
}
|
|
2472
|
+
function resultIds(blocks) {
|
|
2473
|
+
return blocks.filter(isToolResult).map((block) => block.tool_use_id);
|
|
2474
|
+
}
|
|
2475
|
+
function assertSameResultIds(native, queried) {
|
|
2476
|
+
const queriedIds = resultIds(queried);
|
|
2477
|
+
const usable = queriedIds.every((id) => typeof id === "string" && id !== "");
|
|
2478
|
+
if (!usable || new Set(queriedIds).size !== queriedIds.length) throw new QueriedTurnMismatch();
|
|
2479
|
+
if (!jsonEqual(queriedIds, resultIds(native))) throw new QueriedTurnMismatch();
|
|
2480
|
+
}
|
|
2481
|
+
function restoreQueriedTurn(payload, queried) {
|
|
2482
|
+
if (!queried.some(isToolResult)) return payload;
|
|
2483
|
+
const body = parseJsonObject(payload);
|
|
2484
|
+
const messages = body === void 0 ? void 0 : readMessages(body);
|
|
2485
|
+
if (body === void 0 || messages === void 0) throw new QueriedTurnMismatch();
|
|
2486
|
+
const newest = messages.at(lastAssistantIndex(messages) + 1);
|
|
2487
|
+
const native = newest?.role === "user" ? newest.content : void 0;
|
|
2488
|
+
if (newest === void 0 || !Array.isArray(native)) throw new QueriedTurnMismatch();
|
|
2489
|
+
assertSameResultIds(native, queried);
|
|
2490
|
+
const restored = restoreSequence(native, queried);
|
|
2491
|
+
if (jsonEqual(native, restored)) return payload;
|
|
2492
|
+
newest.content = restored;
|
|
2493
|
+
return serializeBody(body);
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
// src/relay/rewrite/stable-context.ts
|
|
2497
|
+
var DATE_LINE = /^Today's date is [0-9]{4}-[0-9]{2}-[0-9]{2}\.$/;
|
|
2498
|
+
var ROLES = /* @__PURE__ */ new Set(["system", "user", "assistant"]);
|
|
2499
|
+
function isTextBlock(block) {
|
|
2500
|
+
return block.type === "text" && typeof block.text === "string";
|
|
2501
|
+
}
|
|
2502
|
+
function isWellFormedBlock(block) {
|
|
2503
|
+
return isObject2(block) && typeof block.type === "string" && (block.type !== "text" || typeof block.text === "string");
|
|
2504
|
+
}
|
|
2505
|
+
function isMessage(message) {
|
|
2506
|
+
const { role, content } = message;
|
|
2507
|
+
return typeof role === "string" && ROLES.has(role) && (typeof content === "string" || Array.isArray(content) && content.every(isWellFormedBlock));
|
|
2508
|
+
}
|
|
2509
|
+
function soleText(content) {
|
|
2510
|
+
if (typeof content === "string") return { type: "text", text: content };
|
|
2511
|
+
const [only] = content;
|
|
2512
|
+
return content.length === 1 && only !== void 0 && isTextBlock(only) ? only : void 0;
|
|
2513
|
+
}
|
|
2514
|
+
function appendToEnvironment(environment, date) {
|
|
2515
|
+
const addition = `
|
|
2516
|
+
|
|
2517
|
+
${date}`;
|
|
2518
|
+
const { content } = environment;
|
|
2519
|
+
if (typeof content === "string") {
|
|
2520
|
+
if (content.endsWith(date)) return false;
|
|
2521
|
+
environment.content = content + addition;
|
|
2522
|
+
return true;
|
|
2523
|
+
}
|
|
2524
|
+
const last = content[content.length - 1];
|
|
2525
|
+
if (last === void 0 || !isTextBlock(last) || last.text.endsWith(date)) return false;
|
|
2526
|
+
last.text += addition;
|
|
2527
|
+
return true;
|
|
2528
|
+
}
|
|
2529
|
+
function returnDateLine(messages) {
|
|
2530
|
+
const last = messages[messages.length - 1];
|
|
2531
|
+
const environment = messages.slice(0, -1).find((message) => message.role === "system");
|
|
2532
|
+
if (last === void 0 || last.role !== "system" || environment === void 0) return false;
|
|
2533
|
+
const date = soleText(last.content);
|
|
2534
|
+
if (date === void 0 || !DATE_LINE.test(date.text)) return false;
|
|
2535
|
+
if (!appendToEnvironment(environment, date.text)) return false;
|
|
2536
|
+
const mark = Object.hasOwn(date, "cache_control") ? date.cache_control : last.cache_control;
|
|
2537
|
+
messages.pop();
|
|
2538
|
+
const previous = messages[messages.length - 1]?.content;
|
|
2539
|
+
if (mark !== void 0 && mark !== null && Array.isArray(previous) && previous.length > 0) {
|
|
2540
|
+
setMarkIfAbsent(previous[previous.length - 1], mark);
|
|
2541
|
+
}
|
|
2542
|
+
return true;
|
|
2543
|
+
}
|
|
2544
|
+
function newestUser(messages) {
|
|
2545
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
2546
|
+
if (messages[index]?.role === "user") return messages[index];
|
|
2547
|
+
}
|
|
2548
|
+
return void 0;
|
|
2549
|
+
}
|
|
2550
|
+
function dropNewestTurnContext(messages, queried) {
|
|
2551
|
+
if (!queried.some((block) => block.type !== "tool_result")) return false;
|
|
2552
|
+
if (!messages.some((message) => message.role === "assistant")) return false;
|
|
2553
|
+
const newest = newestUser(messages);
|
|
2554
|
+
if (newest === void 0 || !Array.isArray(newest.content)) return false;
|
|
2555
|
+
const count = leadingLength(newest.content, queried);
|
|
2556
|
+
return count > 0 && dropLeadingText(newest.content, count);
|
|
2557
|
+
}
|
|
2558
|
+
function stableRequestContext(payload, queried) {
|
|
2559
|
+
const body = parseJsonObject(payload);
|
|
2560
|
+
const messages = body === void 0 ? void 0 : readMessages(body);
|
|
2561
|
+
if (body === void 0 || messages === void 0 || !messages.every(isMessage)) return payload;
|
|
2562
|
+
const movedDate = returnDateLine(messages);
|
|
2563
|
+
const droppedContext = dropNewestTurnContext(messages, queried);
|
|
2564
|
+
return movedDate || droppedContext ? serializeBody(body) : payload;
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
// src/relay/rewrite/tool-input.ts
|
|
2568
|
+
var FLAG = "eager_input_streaming";
|
|
2569
|
+
function isUnflaggedTool(tool) {
|
|
2570
|
+
return isObject2(tool) && !Object.hasOwn(tool, FLAG);
|
|
2571
|
+
}
|
|
2572
|
+
function streamToolInput(payload) {
|
|
2573
|
+
const body = parseJsonObject(payload);
|
|
2574
|
+
if (body === void 0 || !Array.isArray(body.tools)) return payload;
|
|
2575
|
+
const unflagged = body.tools.filter(isUnflaggedTool);
|
|
2576
|
+
if (unflagged.length === 0) return payload;
|
|
2577
|
+
for (const tool of unflagged) tool[FLAG] = true;
|
|
2578
|
+
return serializeBody(body);
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
// src/relay/upstream.ts
|
|
2582
|
+
import { randomBytes, randomInt } from "node:crypto";
|
|
2583
|
+
import { once } from "node:events";
|
|
2584
|
+
import { request as httpRequest } from "node:http";
|
|
2585
|
+
import { isIP, connect as netConnect } from "node:net";
|
|
2586
|
+
import { connect as tlsConnect } from "node:tls";
|
|
2587
|
+
import { promisify } from "node:util";
|
|
2588
|
+
import { gzip } from "node:zlib";
|
|
2589
|
+
var UPSTREAM_IDLE_LIMIT_MS = 18e4;
|
|
2590
|
+
var KEEPALIVE_INITIAL_DELAY_MS = 15e3;
|
|
2591
|
+
var GZIP_MIN_BYTES = 4096;
|
|
2592
|
+
var GZIP_MAX_PAD_BYTES = 256;
|
|
2593
|
+
var GZIP_OFF_VALUES = /* @__PURE__ */ new Set(["0", "false", "no", "off"]);
|
|
2594
|
+
var CUSTOM_CA_VARIABLES = [
|
|
2595
|
+
"SSL_CERT_FILE",
|
|
2596
|
+
"SSL_CERT_DIR",
|
|
2597
|
+
"REQUESTS_CA_BUNDLE",
|
|
2598
|
+
"CURL_CA_BUNDLE",
|
|
2599
|
+
"NODE_EXTRA_CA_CERTS"
|
|
2600
|
+
];
|
|
2601
|
+
var REQUEST_FIELDS_OWNED_BY_THIS_HOP = /* @__PURE__ */ new Set([
|
|
2602
|
+
"host",
|
|
2603
|
+
"connection",
|
|
2604
|
+
"content-length",
|
|
2605
|
+
"transfer-encoding",
|
|
2606
|
+
"proxy-authorization",
|
|
2607
|
+
"proxy-connection",
|
|
2608
|
+
"accept-encoding"
|
|
2609
|
+
]);
|
|
2610
|
+
var RESPONSE_FIELDS_OWNED_BY_THIS_HOP = /* @__PURE__ */ new Set([
|
|
2611
|
+
"connection",
|
|
2612
|
+
"transfer-encoding",
|
|
2613
|
+
"server",
|
|
2614
|
+
"date"
|
|
2615
|
+
]);
|
|
2616
|
+
var INVALID_UPSTREAM = "Native upstream must be HTTPS or a loopback HTTP fixture";
|
|
2617
|
+
var UNSUPPORTED_PROXY = "Unusable proxy for the relay: only an http://host[:port] CONNECT proxy is supported, not SOCKS or TLS to the proxy";
|
|
2618
|
+
var DEFAULT_UPSTREAM_POLICY = {
|
|
2619
|
+
gzipOrigins: ["https://api.anthropic.com"],
|
|
2620
|
+
tls: {}
|
|
2621
|
+
};
|
|
2622
|
+
var LocalFailure = class extends Error {
|
|
2623
|
+
constructor(name2) {
|
|
2624
|
+
super(name2);
|
|
2625
|
+
this.name = name2;
|
|
2626
|
+
}
|
|
2627
|
+
};
|
|
2628
|
+
function idleLimitMs(timeoutMs) {
|
|
2629
|
+
return Math.min(timeoutMs, UPSTREAM_IDLE_LIMIT_MS);
|
|
2630
|
+
}
|
|
2631
|
+
function parseUrl(raw) {
|
|
2632
|
+
return URL.canParse(raw) ? new URL(raw) : null;
|
|
2633
|
+
}
|
|
2634
|
+
function withoutBrackets(hostname) {
|
|
2635
|
+
return hostname.startsWith("[") ? hostname.slice(1, -1) : hostname;
|
|
2636
|
+
}
|
|
2637
|
+
function isLoopback(hostname) {
|
|
2638
|
+
if (hostname === "localhost") return true;
|
|
2639
|
+
const version = isIP(hostname);
|
|
2640
|
+
if (version === 4) return hostname.startsWith("127.");
|
|
2641
|
+
return version === 6 && hostname === "::1";
|
|
2642
|
+
}
|
|
2643
|
+
function hasOnlySchemeHostAndPath(url) {
|
|
2644
|
+
return url.username === "" && url.password === "" && url.search === "" && url.hash === "";
|
|
2645
|
+
}
|
|
2646
|
+
function parseUpstreamUrl(raw) {
|
|
2647
|
+
const url = parseUrl(raw);
|
|
2648
|
+
if (url === null || !hasOnlySchemeHostAndPath(url)) {
|
|
2649
|
+
throw new NativeFailure(INVALID_UPSTREAM, FailureCode.relayError);
|
|
2650
|
+
}
|
|
2651
|
+
const hostname = withoutBrackets(url.hostname);
|
|
2652
|
+
const secure = url.protocol === "https:";
|
|
2653
|
+
if (!secure && !(url.protocol === "http:" && isLoopback(hostname))) {
|
|
2654
|
+
throw new NativeFailure(INVALID_UPSTREAM, FailureCode.relayError);
|
|
2655
|
+
}
|
|
2656
|
+
const port = url.port === "" ? secure ? 443 : 80 : Number(url.port);
|
|
2657
|
+
return {
|
|
2658
|
+
secure,
|
|
2659
|
+
origin: url.origin,
|
|
2660
|
+
hostname,
|
|
2661
|
+
port,
|
|
2662
|
+
host: url.host,
|
|
2663
|
+
tunnelAuthority: `${url.hostname}:${port}`,
|
|
2664
|
+
basePath: url.pathname.replace(/\/+$/, "")
|
|
2665
|
+
};
|
|
2666
|
+
}
|
|
2667
|
+
function proxyVariables(env) {
|
|
2668
|
+
const entries = Object.entries(env).filter(
|
|
2669
|
+
(entry) => entry[1] !== void 0 && entry[0].length > 6 && entry[0].slice(-6).toLowerCase() === "_proxy"
|
|
2670
|
+
);
|
|
2671
|
+
const proxies = /* @__PURE__ */ new Map();
|
|
2672
|
+
for (const [name2, value] of entries) {
|
|
2673
|
+
if (value !== "") proxies.set(name2.slice(0, -6).toLowerCase(), value);
|
|
2674
|
+
}
|
|
2675
|
+
if (env.REQUEST_METHOD !== void 0) proxies.delete("http");
|
|
2676
|
+
for (const [name2, value] of entries) {
|
|
2677
|
+
if (!name2.endsWith("_proxy")) continue;
|
|
2678
|
+
const key = name2.slice(0, -6).toLowerCase();
|
|
2679
|
+
if (value === "") proxies.delete(key);
|
|
2680
|
+
else proxies.set(key, value);
|
|
2681
|
+
}
|
|
2682
|
+
return proxies;
|
|
2683
|
+
}
|
|
2684
|
+
function bypassesProxy(target, noProxy) {
|
|
2685
|
+
if (noProxy === void 0) return false;
|
|
2686
|
+
const list = noProxy.replaceAll(" ", ",");
|
|
2687
|
+
if (list === "*") return true;
|
|
2688
|
+
const hostname = target.hostname.toLowerCase();
|
|
2689
|
+
const hostAndPort = `${hostname}:${target.port}`;
|
|
2690
|
+
return list.split(",").map((entry) => entry.trim().replace(/^\.+/, "").toLowerCase()).filter((name2) => name2 !== "").some(
|
|
2691
|
+
(name2) => hostname === name2 || hostAndPort === name2 || hostname.endsWith(`.${name2}`) || hostAndPort.endsWith(`.${name2}`)
|
|
2692
|
+
);
|
|
2693
|
+
}
|
|
2694
|
+
function percentDecode(text) {
|
|
2695
|
+
return text.replace(
|
|
2696
|
+
/(?:%[0-9a-f]{2})+/gi,
|
|
2697
|
+
(run) => Buffer.from(run.replaceAll("%", ""), "hex").toString("utf8")
|
|
2698
|
+
);
|
|
2699
|
+
}
|
|
2700
|
+
function proxyAuthorization(proxy) {
|
|
2701
|
+
if (proxy.username === "" && proxy.password === "") return void 0;
|
|
2702
|
+
const credentials = `${percentDecode(proxy.username)}:${percentDecode(proxy.password)}`;
|
|
2703
|
+
return `Basic ${Buffer.from(credentials).toString("base64")}`;
|
|
2704
|
+
}
|
|
2705
|
+
function parseProxy(raw) {
|
|
2706
|
+
const url = parseUrl(raw.includes("://") ? raw : `http://${raw}`);
|
|
2707
|
+
if (url === null || url.protocol !== "http:" || url.hostname === "") {
|
|
2708
|
+
throw new NativeFailure(UNSUPPORTED_PROXY, FailureCode.relayError);
|
|
2709
|
+
}
|
|
2710
|
+
const authorization = proxyAuthorization(url);
|
|
2711
|
+
return {
|
|
2712
|
+
host: withoutBrackets(url.hostname),
|
|
2713
|
+
port: url.port === "" ? 80 : Number(url.port),
|
|
2714
|
+
...authorization === void 0 ? {} : { authorization }
|
|
2715
|
+
};
|
|
2716
|
+
}
|
|
2717
|
+
function selectProxy(target, env) {
|
|
2718
|
+
if (!target.secure) return null;
|
|
2719
|
+
const proxies = proxyVariables(env);
|
|
2720
|
+
if (bypassesProxy(target, proxies.get("no"))) return null;
|
|
2721
|
+
const raw = proxies.get("https") ?? proxies.get("http");
|
|
2722
|
+
return raw === void 0 ? null : parseProxy(raw);
|
|
2723
|
+
}
|
|
2724
|
+
function gzipEligible(facts) {
|
|
2725
|
+
const setting = facts.env.CLAUDE_CODE_GZIP_REQUEST_BODIES?.toLowerCase() ?? "";
|
|
2726
|
+
return facts.gzipOrigins.includes(facts.target.origin) && facts.proxy === null && CUSTOM_CA_VARIABLES.every((name2) => facts.env[name2] === void 0) && !GZIP_OFF_VALUES.has(setting) && facts.payloadBytes >= GZIP_MIN_BYTES && facts.contentEncoding === void 0;
|
|
2727
|
+
}
|
|
2728
|
+
function gzipPadding(padBytes) {
|
|
2729
|
+
const noise = randomBytes(padBytes);
|
|
2730
|
+
const pad = Buffer.alloc(padBytes + 1);
|
|
2731
|
+
pad[0] = 10;
|
|
2732
|
+
noise.forEach((byte, index) => {
|
|
2733
|
+
pad[index + 1] = byte % 2 === 0 ? 32 : 9;
|
|
2734
|
+
});
|
|
2735
|
+
return pad;
|
|
2736
|
+
}
|
|
2737
|
+
var gzipAsync = promisify(gzip);
|
|
2738
|
+
async function gzipRequestBody(payload, padBytes = randomInt(GZIP_MAX_PAD_BYTES + 1)) {
|
|
2739
|
+
return gzipAsync(Buffer.concat([payload, gzipPadding(padBytes)]), { level: 3 });
|
|
2740
|
+
}
|
|
2741
|
+
function rawHeaderPairs(rawHeaders) {
|
|
2742
|
+
const pairs = [];
|
|
2743
|
+
for (let index = 0; index + 1 < rawHeaders.length; index += 2) {
|
|
2744
|
+
pairs.push([rawHeaders[index], rawHeaders[index + 1]]);
|
|
2745
|
+
}
|
|
2746
|
+
return pairs;
|
|
2747
|
+
}
|
|
2748
|
+
function upstreamRequestHeaders(rawHeaders, facts) {
|
|
2749
|
+
const fields = /* @__PURE__ */ new Map();
|
|
2750
|
+
for (const [name2, value] of rawHeaderPairs(rawHeaders)) {
|
|
2751
|
+
const key = name2.toLowerCase();
|
|
2752
|
+
if (REQUEST_FIELDS_OWNED_BY_THIS_HOP.has(key)) continue;
|
|
2753
|
+
const field2 = fields.get(key);
|
|
2754
|
+
if (field2 === void 0) fields.set(key, { name: name2, values: [value] });
|
|
2755
|
+
else field2.values.push(value);
|
|
2756
|
+
}
|
|
2757
|
+
const headers = {};
|
|
2758
|
+
for (const { name: name2, values } of fields.values()) {
|
|
2759
|
+
headers[name2] = values.length === 1 ? values[0] : values;
|
|
2760
|
+
}
|
|
2761
|
+
headers.Host = facts.target.host;
|
|
2762
|
+
headers["Accept-Encoding"] = "identity";
|
|
2763
|
+
headers["Content-Length"] = String(facts.bodyBytes);
|
|
2764
|
+
if (facts.gzip) headers["Content-Encoding"] = "gzip";
|
|
2765
|
+
return headers;
|
|
2766
|
+
}
|
|
2767
|
+
function clientResponseHeaders(rawHeaders) {
|
|
2768
|
+
const kept = rawHeaderPairs(rawHeaders).filter(
|
|
2769
|
+
([name2]) => !RESPONSE_FIELDS_OWNED_BY_THIS_HOP.has(name2.toLowerCase())
|
|
2770
|
+
);
|
|
2771
|
+
return [...kept.flat(), "Connection", "close"];
|
|
2772
|
+
}
|
|
2773
|
+
function ignoreError() {
|
|
2774
|
+
}
|
|
2775
|
+
function tieToCall(socket, control) {
|
|
2776
|
+
socket.on("error", (error) => control.abort(error));
|
|
2777
|
+
control.signal.addEventListener("abort", () => socket.destroy(), { once: true });
|
|
2778
|
+
}
|
|
2779
|
+
async function openTcp(host, port, control) {
|
|
2780
|
+
const socket = netConnect({ host, port });
|
|
2781
|
+
tieToCall(socket, control);
|
|
2782
|
+
socket.setKeepAlive(true, KEEPALIVE_INITIAL_DELAY_MS);
|
|
2783
|
+
await once(socket, "connect", { signal: control.signal });
|
|
2784
|
+
return socket;
|
|
2785
|
+
}
|
|
2786
|
+
async function openTunnel(socket, proxy, target, signal) {
|
|
2787
|
+
const request = httpRequest({
|
|
2788
|
+
method: "CONNECT",
|
|
2789
|
+
path: target.tunnelAuthority,
|
|
2790
|
+
headers: {
|
|
2791
|
+
Host: target.tunnelAuthority,
|
|
2792
|
+
// Without an agent Node sends "close", which is wrong for a tunnel that must stay open.
|
|
2793
|
+
Connection: "keep-alive",
|
|
2794
|
+
...proxy.authorization === void 0 ? {} : { "Proxy-Authorization": proxy.authorization }
|
|
2795
|
+
},
|
|
2796
|
+
createConnection: () => socket
|
|
2797
|
+
});
|
|
2798
|
+
request.on("error", ignoreError);
|
|
2799
|
+
request.end();
|
|
2800
|
+
const [response, tunnel, head] = await once(request, "connect", { signal });
|
|
2801
|
+
if (response.statusCode !== 200) throw new LocalFailure("ProxyTunnelRefused");
|
|
2802
|
+
if (head.length > 0) tunnel.unshift(head);
|
|
2803
|
+
return tunnel;
|
|
2804
|
+
}
|
|
2805
|
+
async function startTls(socket, route, control) {
|
|
2806
|
+
const { hostname } = route.target;
|
|
2807
|
+
const secure = tlsConnect({
|
|
2808
|
+
socket,
|
|
2809
|
+
host: hostname,
|
|
2810
|
+
// TLS does not allow an IP address as a server name.
|
|
2811
|
+
...isIP(hostname) === 0 ? { servername: hostname } : {},
|
|
2812
|
+
ALPNProtocols: ["http/1.1"],
|
|
2813
|
+
...route.tls
|
|
2814
|
+
});
|
|
2815
|
+
tieToCall(secure, control);
|
|
2816
|
+
await once(secure, "secureConnect", { signal: control.signal });
|
|
2817
|
+
return secure;
|
|
2818
|
+
}
|
|
2819
|
+
async function connectToUpstream(route, control) {
|
|
2820
|
+
const deadline = setTimeout(
|
|
2821
|
+
() => control.abort(new LocalFailure("UpstreamTimeout")),
|
|
2822
|
+
route.timeoutMs
|
|
2823
|
+
);
|
|
2824
|
+
try {
|
|
2825
|
+
const { proxy, target } = route;
|
|
2826
|
+
const first = proxy === null ? await openTcp(target.hostname, target.port, control) : await openTcp(proxy.host, proxy.port, control);
|
|
2827
|
+
const path = proxy === null ? first : await openTunnel(first, proxy, target, control.signal);
|
|
2828
|
+
return target.secure ? await startTls(path, route, control) : path;
|
|
2829
|
+
} finally {
|
|
2830
|
+
clearTimeout(deadline);
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
var SilenceLimit = class {
|
|
2834
|
+
#socket;
|
|
2835
|
+
#limitMs = 0;
|
|
2836
|
+
#failure = "";
|
|
2837
|
+
constructor(socket, control) {
|
|
2838
|
+
this.#socket = socket;
|
|
2839
|
+
socket.on("timeout", () => control.abort(new LocalFailure(this.#failure)));
|
|
2840
|
+
}
|
|
2841
|
+
start(limitMs, failure) {
|
|
2842
|
+
this.#limitMs = limitMs;
|
|
2843
|
+
this.#failure = failure;
|
|
2844
|
+
this.resume();
|
|
2845
|
+
}
|
|
2846
|
+
/** While the client holds back the stream, upstream silence means nothing. */
|
|
2847
|
+
suspend() {
|
|
2848
|
+
this.#socket.setTimeout(0);
|
|
2849
|
+
}
|
|
2850
|
+
resume() {
|
|
2851
|
+
if (!this.#socket.destroyed) this.#socket.setTimeout(this.#limitMs);
|
|
2852
|
+
}
|
|
2853
|
+
};
|
|
2854
|
+
async function exchangeHeaders(socket, upstream, signal) {
|
|
2855
|
+
const request = httpRequest({
|
|
2856
|
+
method: "POST",
|
|
2857
|
+
path: upstream.path,
|
|
2858
|
+
headers: upstream.headers,
|
|
2859
|
+
createConnection: () => socket
|
|
2860
|
+
});
|
|
2861
|
+
request.on("error", ignoreError);
|
|
2862
|
+
request.end(upstream.body);
|
|
2863
|
+
const [response] = await once(request, "response", { signal });
|
|
2864
|
+
response.on("error", ignoreError);
|
|
2865
|
+
return response;
|
|
2866
|
+
}
|
|
2867
|
+
async function openUpstream(route, upstream, control) {
|
|
2868
|
+
control.signal.throwIfAborted();
|
|
2869
|
+
const socket = await connectToUpstream(route, control);
|
|
2870
|
+
const silence = new SilenceLimit(socket, control);
|
|
2871
|
+
silence.start(route.timeoutMs, "UpstreamTimeout");
|
|
2872
|
+
const response = await exchangeHeaders(socket, upstream, control.signal);
|
|
2873
|
+
silence.start(idleLimitMs(route.timeoutMs), "UpstreamIdle");
|
|
2874
|
+
return { response, silence };
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
// src/relay/relay.ts
|
|
2878
|
+
var MESSAGES_PATH = "/v1/messages";
|
|
2879
|
+
var ERROR_BODY_LIMIT = 65536;
|
|
2880
|
+
var DENIAL_BODY = JSON.stringify({
|
|
2881
|
+
type: "error",
|
|
2882
|
+
error: { type: "invalid_request_error", message: "DSH_MODEL_ADMISSION_CONSUMED" }
|
|
2883
|
+
});
|
|
2884
|
+
var RelayRecord = class {
|
|
2885
|
+
used = false;
|
|
2886
|
+
denied = 0;
|
|
2887
|
+
cancelled = false;
|
|
2888
|
+
status = null;
|
|
2889
|
+
requestId = null;
|
|
2890
|
+
retryAfterMs = null;
|
|
2891
|
+
failure = null;
|
|
2892
|
+
unrestored = null;
|
|
2893
|
+
errorBody = Buffer.alloc(0);
|
|
2894
|
+
keepErrorBytes(chunk) {
|
|
2895
|
+
const room = ERROR_BODY_LIMIT - this.errorBody.length;
|
|
2896
|
+
if (room > 0) this.errorBody = Buffer.concat([this.errorBody, chunk.subarray(0, room)]);
|
|
2897
|
+
}
|
|
2898
|
+
};
|
|
2899
|
+
function restoreQueried(payload, options) {
|
|
2900
|
+
try {
|
|
2901
|
+
return { payload: restoreQueriedTurn(payload, options.queried), unrestored: null };
|
|
2902
|
+
} catch (error) {
|
|
2903
|
+
if (!(error instanceof QueriedTurnMismatch)) throw error;
|
|
2904
|
+
return { payload, unrestored: error.message };
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
function rewriteAdmitted(received, options) {
|
|
2908
|
+
const start = options.oneShot ? dropMessageBreakpoints(received) : received;
|
|
2909
|
+
const { payload: restored, unrestored } = restoreQueried(start, options);
|
|
2910
|
+
const stable = stableRequestContext(restored, options.queried);
|
|
2911
|
+
const pinned = pinMessageBreakpoint(stable, options.queried);
|
|
2912
|
+
return { payload: streamToolInput(pinned), unrestored };
|
|
2913
|
+
}
|
|
2914
|
+
function upstreamErrorText(body) {
|
|
2915
|
+
const text = body.toString("utf8");
|
|
2916
|
+
let parsed;
|
|
2917
|
+
try {
|
|
2918
|
+
parsed = JSON.parse(text);
|
|
2919
|
+
} catch {
|
|
2920
|
+
return text;
|
|
2921
|
+
}
|
|
2922
|
+
const message = parsed?.error?.message;
|
|
2923
|
+
return typeof message === "string" ? message : text;
|
|
2924
|
+
}
|
|
2925
|
+
function failureName(error) {
|
|
2926
|
+
if (error instanceof LocalFailure) return error.name;
|
|
2927
|
+
const code = error?.code;
|
|
2928
|
+
if (typeof code === "string" && /^[A-Z][A-Z0-9_]*$/.test(code)) return code;
|
|
2929
|
+
return error instanceof Error && /^\w+$/.test(error.name) ? error.name : "UnknownRelayFailure";
|
|
2930
|
+
}
|
|
2931
|
+
function splitRequestTarget(requestUrl = "") {
|
|
2932
|
+
const start = requestUrl.indexOf("?");
|
|
2933
|
+
return start < 0 ? { path: requestUrl, query: "" } : { path: requestUrl.slice(0, start), query: requestUrl.slice(start) };
|
|
2934
|
+
}
|
|
2935
|
+
function classifyRequest(request, messagesRoute) {
|
|
2936
|
+
if (request.method !== "POST") return "unsupportedMethod";
|
|
2937
|
+
const { path } = splitRequestTarget(request.url);
|
|
2938
|
+
return path === messagesRoute && !request.headers.origin ? "messages" : "unknownRoute";
|
|
2939
|
+
}
|
|
2940
|
+
function ignoreError2() {
|
|
2941
|
+
}
|
|
2942
|
+
function answerLocally(request, response, status, body = "") {
|
|
2943
|
+
request.once("end", () => {
|
|
2944
|
+
response.writeHead(status, {
|
|
2945
|
+
...body === "" ? {} : { "Content-Type": "application/json" },
|
|
2946
|
+
"Content-Length": Buffer.byteLength(body),
|
|
2947
|
+
Connection: "close"
|
|
2948
|
+
});
|
|
2949
|
+
response.end(body);
|
|
2950
|
+
});
|
|
2951
|
+
request.resume();
|
|
2952
|
+
}
|
|
2953
|
+
async function readBody(request, control, stallMs) {
|
|
2954
|
+
const onStall = () => {
|
|
2955
|
+
control.abort(new LocalFailure("ClientStalled"));
|
|
2956
|
+
request.destroy();
|
|
2957
|
+
};
|
|
2958
|
+
request.socket.setTimeout(stallMs, onStall);
|
|
2959
|
+
try {
|
|
2960
|
+
const chunks = [];
|
|
2961
|
+
for await (const chunk of request) chunks.push(chunk);
|
|
2962
|
+
return Buffer.concat(chunks);
|
|
2963
|
+
} finally {
|
|
2964
|
+
request.socket.setTimeout(0);
|
|
2965
|
+
request.socket.off("timeout", onStall);
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
async function waitForDrain(response, silence, control, stallMs) {
|
|
2969
|
+
silence.suspend();
|
|
2970
|
+
const stall = setTimeout(() => control.abort(new LocalFailure("ClientStalled")), stallMs);
|
|
2971
|
+
try {
|
|
2972
|
+
await once2(response, "drain", { signal: control.signal });
|
|
2973
|
+
} finally {
|
|
2974
|
+
clearTimeout(stall);
|
|
2975
|
+
}
|
|
2976
|
+
silence.resume();
|
|
2977
|
+
}
|
|
2978
|
+
var LoopbackRelay = class {
|
|
2979
|
+
#capture = new SseCapture();
|
|
2980
|
+
#record = new RelayRecord();
|
|
2981
|
+
#connections = /* @__PURE__ */ new Set();
|
|
2982
|
+
#options;
|
|
2983
|
+
#route;
|
|
2984
|
+
#gzipOrigins;
|
|
2985
|
+
#prefix = `/admit/${randomBytes2(32).toString("base64url")}`;
|
|
2986
|
+
#server;
|
|
2987
|
+
#url = "";
|
|
2988
|
+
#call = null;
|
|
2989
|
+
#exchange = null;
|
|
2990
|
+
#closing = null;
|
|
2991
|
+
constructor(options, route, gzipOrigins) {
|
|
2992
|
+
this.#options = options;
|
|
2993
|
+
this.#route = route;
|
|
2994
|
+
this.#gzipOrigins = gzipOrigins;
|
|
2995
|
+
const receiveLimitMs = Math.ceil(options.timeoutMs);
|
|
2996
|
+
this.#server = createServer(
|
|
2997
|
+
{ headersTimeout: receiveLimitMs, requestTimeout: receiveLimitMs },
|
|
2998
|
+
(request, response) => this.#onRequest(request, response)
|
|
2999
|
+
);
|
|
3000
|
+
this.#server.on("connection", (socket) => this.#onConnection(socket));
|
|
3001
|
+
this.#server.on("clientError", (_error, socket) => socket.destroy());
|
|
3002
|
+
}
|
|
3003
|
+
async listen() {
|
|
3004
|
+
this.#server.listen(0, "127.0.0.1");
|
|
3005
|
+
await once2(this.#server, "listening");
|
|
3006
|
+
this.#server.on("error", ignoreError2);
|
|
3007
|
+
const { port } = this.#server.address();
|
|
3008
|
+
this.#url = `http://127.0.0.1:${port}${this.#prefix}`;
|
|
3009
|
+
}
|
|
3010
|
+
get url() {
|
|
3011
|
+
return this.#url;
|
|
3012
|
+
}
|
|
3013
|
+
get used() {
|
|
3014
|
+
return this.#record.used;
|
|
3015
|
+
}
|
|
3016
|
+
get denied() {
|
|
3017
|
+
return this.#record.denied;
|
|
3018
|
+
}
|
|
3019
|
+
get status() {
|
|
3020
|
+
return this.#record.status;
|
|
3021
|
+
}
|
|
3022
|
+
get failure() {
|
|
3023
|
+
return this.#record.failure;
|
|
3024
|
+
}
|
|
3025
|
+
get requestId() {
|
|
3026
|
+
return this.#record.requestId;
|
|
3027
|
+
}
|
|
3028
|
+
get retryAfterMs() {
|
|
3029
|
+
return this.#record.retryAfterMs;
|
|
3030
|
+
}
|
|
3031
|
+
get unrestored() {
|
|
3032
|
+
return this.#record.unrestored;
|
|
3033
|
+
}
|
|
3034
|
+
get capture() {
|
|
3035
|
+
return this.#capture;
|
|
3036
|
+
}
|
|
3037
|
+
errorText() {
|
|
3038
|
+
return upstreamErrorText(this.#record.errorBody);
|
|
3039
|
+
}
|
|
3040
|
+
abort() {
|
|
3041
|
+
this.#record.cancelled = true;
|
|
3042
|
+
this.#call?.abort(new LocalFailure("RelayAborted"));
|
|
3043
|
+
for (const socket of this.#connections) socket.destroy();
|
|
3044
|
+
}
|
|
3045
|
+
close() {
|
|
3046
|
+
this.#closing ??= this.#shutDown();
|
|
3047
|
+
return this.#closing;
|
|
3048
|
+
}
|
|
3049
|
+
async #shutDown() {
|
|
3050
|
+
this.abort();
|
|
3051
|
+
await new Promise((resolve) => {
|
|
3052
|
+
this.#server.close(() => resolve());
|
|
3053
|
+
this.#server.closeAllConnections();
|
|
3054
|
+
});
|
|
3055
|
+
await this.#exchange;
|
|
3056
|
+
}
|
|
3057
|
+
#onConnection(socket) {
|
|
3058
|
+
socket.on("error", ignoreError2);
|
|
3059
|
+
if (this.#record.cancelled) {
|
|
3060
|
+
socket.destroy();
|
|
3061
|
+
return;
|
|
3062
|
+
}
|
|
3063
|
+
this.#connections.add(socket);
|
|
3064
|
+
socket.once("close", () => this.#connections.delete(socket));
|
|
3065
|
+
}
|
|
3066
|
+
#onRequest(request, response) {
|
|
3067
|
+
request.on("error", ignoreError2);
|
|
3068
|
+
response.on("error", ignoreError2);
|
|
3069
|
+
const kind = classifyRequest(request, `${this.#prefix}${MESSAGES_PATH}`);
|
|
3070
|
+
if (kind === "unsupportedMethod") {
|
|
3071
|
+
answerLocally(request, response, 501);
|
|
3072
|
+
} else if (kind === "unknownRoute") {
|
|
3073
|
+
answerLocally(request, response, 404);
|
|
3074
|
+
} else if (this.#record.cancelled || this.#record.used) {
|
|
3075
|
+
this.#record.denied += 1;
|
|
3076
|
+
answerLocally(request, response, 400, DENIAL_BODY);
|
|
3077
|
+
} else {
|
|
3078
|
+
this.#record.used = true;
|
|
3079
|
+
this.#exchange = this.#serveAdmitted(request, response);
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
/** Never rejects: a rejection here would crash the host process. */
|
|
3083
|
+
async #serveAdmitted(request, response) {
|
|
3084
|
+
const control = new AbortController();
|
|
3085
|
+
this.#call = control;
|
|
3086
|
+
response.once("close", () => {
|
|
3087
|
+
if (!response.writableFinished) control.abort(new LocalFailure("ClientDisconnected"));
|
|
3088
|
+
});
|
|
3089
|
+
try {
|
|
3090
|
+
const received = await readBody(request, control, this.#options.timeoutMs);
|
|
3091
|
+
const upstream = await this.#buildUpstreamRequest(request, received);
|
|
3092
|
+
const call = await openUpstream(this.#route, upstream, control);
|
|
3093
|
+
await this.#forwardResponse(call, response, control);
|
|
3094
|
+
control.signal.throwIfAborted();
|
|
3095
|
+
response.end();
|
|
3096
|
+
} catch (error) {
|
|
3097
|
+
this.#recordFailure(control.signal.aborted ? control.signal.reason : error);
|
|
3098
|
+
response.destroy();
|
|
3099
|
+
} finally {
|
|
3100
|
+
control.abort();
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
async #buildUpstreamRequest(request, received) {
|
|
3104
|
+
const { payload, unrestored } = rewriteAdmitted(received, this.#options);
|
|
3105
|
+
this.#record.unrestored = unrestored;
|
|
3106
|
+
const { target, proxy } = this.#route;
|
|
3107
|
+
const gzip2 = gzipEligible({
|
|
3108
|
+
target,
|
|
3109
|
+
proxy,
|
|
3110
|
+
env: this.#options.env,
|
|
3111
|
+
gzipOrigins: this.#gzipOrigins,
|
|
3112
|
+
payloadBytes: payload.length,
|
|
3113
|
+
contentEncoding: request.headers["content-encoding"]
|
|
3114
|
+
});
|
|
3115
|
+
const body = gzip2 ? await gzipRequestBody(payload) : payload;
|
|
3116
|
+
return {
|
|
3117
|
+
path: `${target.basePath}${MESSAGES_PATH}${splitRequestTarget(request.url).query}`,
|
|
3118
|
+
headers: upstreamRequestHeaders(request.rawHeaders, {
|
|
3119
|
+
target,
|
|
3120
|
+
bodyBytes: body.length,
|
|
3121
|
+
gzip: gzip2
|
|
3122
|
+
}),
|
|
3123
|
+
body
|
|
3124
|
+
};
|
|
3125
|
+
}
|
|
3126
|
+
async #forwardResponse(call, response, control) {
|
|
3127
|
+
const upstream = call.response;
|
|
3128
|
+
const status = upstream.statusCode;
|
|
3129
|
+
const requestId = upstream.headers["request-id"] || upstream.headers["x-request-id"];
|
|
3130
|
+
this.#record.status = status;
|
|
3131
|
+
this.#record.requestId = typeof requestId === "string" ? requestId : null;
|
|
3132
|
+
this.#record.retryAfterMs = retryAfterMs(upstream.headers["retry-after"], Date.now());
|
|
3133
|
+
response.writeHead(status, upstream.statusMessage, clientResponseHeaders(upstream.rawHeaders));
|
|
3134
|
+
response.flushHeaders();
|
|
3135
|
+
for await (const chunk of upstream) {
|
|
3136
|
+
if (status === 200) this.#capture.feed(chunk);
|
|
3137
|
+
else this.#record.keepErrorBytes(chunk);
|
|
3138
|
+
if (!response.write(chunk)) {
|
|
3139
|
+
await waitForDrain(response, call.silence, control, this.#options.timeoutMs);
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
#recordFailure(reason) {
|
|
3144
|
+
if (!this.#capture.complete) this.#record.failure ??= failureName(reason);
|
|
3145
|
+
}
|
|
3146
|
+
};
|
|
3147
|
+
async function startRelay(options, policy) {
|
|
3148
|
+
const target = parseUpstreamUrl(options.upstream);
|
|
3149
|
+
const proxy = selectProxy(target, options.env);
|
|
3150
|
+
const route = { target, proxy, tls: policy.tls, timeoutMs: options.timeoutMs };
|
|
3151
|
+
const relay = new LoopbackRelay(options, route, policy.gzipOrigins);
|
|
3152
|
+
try {
|
|
3153
|
+
await relay.listen();
|
|
3154
|
+
} catch (error) {
|
|
3155
|
+
throw new NativeFailure("Could not start the local relay", FailureCode.relayError, {
|
|
3156
|
+
cause: error
|
|
3157
|
+
});
|
|
3158
|
+
}
|
|
3159
|
+
return relay;
|
|
3160
|
+
}
|
|
3161
|
+
var createRelay = (options) => startRelay(options, DEFAULT_UPSTREAM_POLICY);
|
|
3162
|
+
|
|
3163
|
+
// src/request/images.ts
|
|
3164
|
+
var MAX_REQUEST_IMAGE_PIXELS = 2048 * 2048;
|
|
3165
|
+
var MAX_REQUEST_IMAGE_BYTES = 1024 * 1024;
|
|
3166
|
+
var MAX_REQUEST_IMAGES_BASE64_BYTES = 20 * 1024 * 1024;
|
|
3167
|
+
var EXTENSIONS = {
|
|
3168
|
+
"image/png": ".png",
|
|
3169
|
+
"image/jpeg": ".jpg",
|
|
3170
|
+
"image/webp": ".webp",
|
|
3171
|
+
"image/gif": ".gif"
|
|
3172
|
+
};
|
|
3173
|
+
function scaledDimensions(width, height) {
|
|
3174
|
+
const scale = Math.min(1, Math.sqrt(MAX_REQUEST_IMAGE_PIXELS / (width * height)));
|
|
3175
|
+
if (scale === 1) return { width, height };
|
|
3176
|
+
const longSide = Math.max(width, height);
|
|
3177
|
+
const shortSide = Math.min(width, height);
|
|
3178
|
+
let long = Math.max(1, Math.floor(longSide * scale));
|
|
3179
|
+
let short = Math.max(1, Math.round(long * shortSide / longSide));
|
|
3180
|
+
while (long * short > MAX_REQUEST_IMAGE_PIXELS && long > 1) {
|
|
3181
|
+
long -= 1;
|
|
3182
|
+
short = Math.max(1, Math.round(long * shortSide / longSide));
|
|
3183
|
+
}
|
|
3184
|
+
return width >= height ? { width: long, height: short } : { width: short, height: long };
|
|
3185
|
+
}
|
|
3186
|
+
function imageRequestTarget(ref) {
|
|
3187
|
+
return { ...scaledDimensions(ref.width, ref.height), maxBytes: MAX_REQUEST_IMAGE_BYTES };
|
|
3188
|
+
}
|
|
3189
|
+
function imageIdentity(ref) {
|
|
3190
|
+
return ref.name === void 0 ? String(ref.attachmentId) : `${JSON.stringify(ref.name)} (${ref.attachmentId})`;
|
|
3191
|
+
}
|
|
3192
|
+
function accessText(ref, access) {
|
|
3193
|
+
return ` Normalized copy (read-only; may be resized or re-encoded): ${JSON.stringify(access.readonlyPath)} (${ref.width}x${ref.height}px, ${ref.mediaType}). Source dimensions, format, and byte size may differ. Copy to a writable path ending in ${EXTENSIONS[ref.mediaType]} before editing.`;
|
|
3194
|
+
}
|
|
3195
|
+
function imageHandleText(ref, version, access) {
|
|
3196
|
+
const preview = `Image ${imageIdentity(ref)}; request preview ${version.width}x${version.height}px.`;
|
|
3197
|
+
if (access !== void 0) return preview + accessText(ref, access);
|
|
3198
|
+
return `${preview} It may be resized or re-encoded; source dimensions, format, and byte size may differ.`;
|
|
3199
|
+
}
|
|
3200
|
+
function offloadedImageText(ref, access) {
|
|
3201
|
+
const identity = `image omitted to fit request image limits; ${imageIdentity(ref)}.`;
|
|
3202
|
+
if (access !== void 0) return `[${identity}${accessText(ref, access)}]`;
|
|
3203
|
+
return `[${identity} No local normalized image path is available; ask the user to attach it again if needed.]`;
|
|
3204
|
+
}
|
|
3205
|
+
function requestVersionOf(block, images) {
|
|
3206
|
+
const version = images.get(String(block.attachment.attachmentId));
|
|
3207
|
+
if (version === void 0) {
|
|
3208
|
+
throw new NativeFailure("An image of the request was not read", FailureCode.invalidRequest);
|
|
3209
|
+
}
|
|
3210
|
+
return version;
|
|
3211
|
+
}
|
|
3212
|
+
function imageBlocksOf(message) {
|
|
3213
|
+
return message.content.filter((block) => block.type === "image");
|
|
3214
|
+
}
|
|
3215
|
+
function retainedImageBlocks(messages) {
|
|
3216
|
+
return messages.filter((message) => message.role === "user" || message.role === "tool").flatMap(imageBlocksOf).filter((block) => block.offloaded !== true);
|
|
3217
|
+
}
|
|
3218
|
+
async function readRequestImages(messages, reader, signal) {
|
|
3219
|
+
const blocks = messages.flatMap(imageBlocksOf);
|
|
3220
|
+
if (blocks.length === 0) return /* @__PURE__ */ new Map();
|
|
3221
|
+
if (reader === void 0) {
|
|
3222
|
+
throw new NativeFailure(
|
|
3223
|
+
"Images need the attachment service, which is not available",
|
|
3224
|
+
FailureCode.invalidRequest
|
|
3225
|
+
);
|
|
3226
|
+
}
|
|
3227
|
+
const refs = /* @__PURE__ */ new Map();
|
|
3228
|
+
for (const block of retainedImageBlocks(messages)) {
|
|
3229
|
+
refs.set(String(block.attachment.attachmentId), block.attachment);
|
|
3230
|
+
}
|
|
3231
|
+
const entries = await Promise.all(
|
|
3232
|
+
[...refs].map(async ([id, ref]) => {
|
|
3233
|
+
const version = await reader.readImageRequest(ref, imageRequestTarget(ref), signal);
|
|
3234
|
+
return [id, version];
|
|
3235
|
+
})
|
|
3236
|
+
);
|
|
3237
|
+
return new Map(entries);
|
|
3238
|
+
}
|
|
3239
|
+
function base64Length(bytes) {
|
|
3240
|
+
return Math.ceil(bytes / 3) * 4;
|
|
3241
|
+
}
|
|
3242
|
+
function requiredImageOffload(messages, images) {
|
|
3243
|
+
const lengths = retainedImageBlocks(messages).map(
|
|
3244
|
+
(block) => base64Length(requestVersionOf(block, images).data.byteLength)
|
|
3245
|
+
);
|
|
3246
|
+
const excess = lengths.reduce((sum, length) => sum + length, 0) - MAX_REQUEST_IMAGES_BASE64_BYTES;
|
|
3247
|
+
if (excess <= 0) return 0;
|
|
3248
|
+
let removed2 = 0;
|
|
3249
|
+
let count = 0;
|
|
3250
|
+
for (const length of lengths) {
|
|
3251
|
+
if (removed2 >= excess) break;
|
|
3252
|
+
removed2 += length;
|
|
3253
|
+
count += 1;
|
|
3254
|
+
}
|
|
3255
|
+
return count;
|
|
3256
|
+
}
|
|
3257
|
+
function assertImagesFit(messages, images) {
|
|
3258
|
+
const offloadImages = requiredImageOffload(messages, images);
|
|
3259
|
+
if (offloadImages === 0) return;
|
|
3260
|
+
throw new NativeFailure(
|
|
3261
|
+
`Request images exceed the ${MAX_REQUEST_IMAGES_BASE64_BYTES}-byte base64 bound; ${offloadImages} more oldest occurrence(s) must be offloaded.`,
|
|
3262
|
+
FailureCode.imageOffloadRequired,
|
|
3263
|
+
{ offloadImages }
|
|
3264
|
+
);
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3267
|
+
// src/request/generation.ts
|
|
3268
|
+
var OFF_EFFORT = "off";
|
|
3269
|
+
var REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
3270
|
+
var LOWEST_EFFORT = REASONING_EFFORTS[0];
|
|
3271
|
+
function invalid(message) {
|
|
3272
|
+
return new NativeFailure(message, FailureCode.invalidRequest);
|
|
3273
|
+
}
|
|
3274
|
+
function isKnownEffort(effort) {
|
|
3275
|
+
return effort === OFF_EFFORT || REASONING_EFFORTS.includes(effort);
|
|
3276
|
+
}
|
|
3277
|
+
function reasoningOff(model) {
|
|
3278
|
+
if (acceptsThinkingDisable(model)) {
|
|
3279
|
+
return {
|
|
3280
|
+
body: { thinking: { type: "disabled" }, context_management: { edits: [] } }
|
|
3281
|
+
};
|
|
3282
|
+
}
|
|
3283
|
+
return { body: { output_config: { effort: LOWEST_EFFORT } }, effort: LOWEST_EFFORT };
|
|
3284
|
+
}
|
|
3285
|
+
function reasoningOn(model, effort) {
|
|
3286
|
+
const body = {};
|
|
3287
|
+
if (supportsAdaptiveThinking(model)) {
|
|
3288
|
+
body.thinking = { type: "adaptive", display: "summarized" };
|
|
3289
|
+
}
|
|
3290
|
+
if (effort !== void 0) body.output_config = { effort };
|
|
3291
|
+
return effort === void 0 ? { body } : { body, effort };
|
|
3292
|
+
}
|
|
3293
|
+
function reasoningOf(model, effort) {
|
|
3294
|
+
if (effort !== void 0 && !isKnownEffort(effort)) {
|
|
3295
|
+
throw invalid(`Unsupported reasoning effort: ${effort}`);
|
|
3296
|
+
}
|
|
3297
|
+
return effort === OFF_EFFORT ? reasoningOff(model) : reasoningOn(model, effort);
|
|
3298
|
+
}
|
|
3299
|
+
function checkTemperature(temperature) {
|
|
3300
|
+
if (!Number.isFinite(temperature) || temperature < 0 || temperature > 1) {
|
|
3301
|
+
throw invalid("temperature must be finite and between zero and one");
|
|
3302
|
+
}
|
|
3303
|
+
}
|
|
3304
|
+
function checkMaxTokens(maxTokens) {
|
|
3305
|
+
if (!Number.isInteger(maxTokens) || maxTokens < 1) {
|
|
3306
|
+
throw invalid("maxTokens must be a positive integer");
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
3309
|
+
function checkStop(stop) {
|
|
3310
|
+
if (stop.some((sequence) => sequence === "")) {
|
|
3311
|
+
throw invalid("stop must be a list of nonempty strings");
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
function generationOf(input) {
|
|
3315
|
+
const reasoning = reasoningOf(input.model, input.reasoningEffort);
|
|
3316
|
+
const body = { ...reasoning.body };
|
|
3317
|
+
if (input.temperature !== void 0) checkTemperature(input.temperature);
|
|
3318
|
+
if (input.maxTokens !== void 0) {
|
|
3319
|
+
checkMaxTokens(input.maxTokens);
|
|
3320
|
+
body.max_tokens = input.maxTokens;
|
|
3321
|
+
}
|
|
3322
|
+
if (input.stop !== void 0) {
|
|
3323
|
+
checkStop(input.stop);
|
|
3324
|
+
body.stop_sequences = [...input.stop];
|
|
3325
|
+
}
|
|
3326
|
+
return {
|
|
3327
|
+
body,
|
|
3328
|
+
...reasoning.effort === void 0 ? {} : { effort: reasoning.effort },
|
|
3329
|
+
...input.maxTokens === void 0 ? {} : { maxOutputTokens: input.maxTokens }
|
|
3330
|
+
};
|
|
3331
|
+
}
|
|
3332
|
+
|
|
3333
|
+
// src/request/model-info.ts
|
|
3334
|
+
var EFFORT_NAMES = {
|
|
3335
|
+
[OFF_EFFORT]: "Off",
|
|
3336
|
+
low: "Low",
|
|
3337
|
+
medium: "Medium",
|
|
3338
|
+
high: "High",
|
|
3339
|
+
xhigh: "Extra high",
|
|
3340
|
+
max: "Max"
|
|
3341
|
+
};
|
|
3342
|
+
function effortInfo(id) {
|
|
3343
|
+
return { id, name: EFFORT_NAMES[id] ?? id };
|
|
3344
|
+
}
|
|
3345
|
+
function effortsOf(model) {
|
|
3346
|
+
const ids = acceptsThinkingDisable(model) ? [OFF_EFFORT, ...REASONING_EFFORTS] : REASONING_EFFORTS;
|
|
3347
|
+
return ids.map(effortInfo);
|
|
3348
|
+
}
|
|
3349
|
+
function modelInfo(provider, model) {
|
|
3350
|
+
const contextWindow = contextWindowOf(model);
|
|
3351
|
+
return {
|
|
3352
|
+
provider,
|
|
3353
|
+
id: model,
|
|
3354
|
+
name: model,
|
|
3355
|
+
inputModalities: ["text", "image"],
|
|
3356
|
+
...contextWindow === void 0 ? {} : { context: { contextWindow } },
|
|
3357
|
+
reasoning: { efforts: effortsOf(model) }
|
|
3358
|
+
};
|
|
3359
|
+
}
|
|
3360
|
+
|
|
3361
|
+
// src/request/replay.ts
|
|
3362
|
+
import { isDeepStrictEqual } from "node:util";
|
|
3363
|
+
var CARRIER_KIND = "claude-subscription";
|
|
3364
|
+
var CARRIER_VERSION = 1;
|
|
3365
|
+
function parseToolArguments(toolName, raw) {
|
|
3366
|
+
try {
|
|
3367
|
+
return JSON.parse(raw);
|
|
3368
|
+
} catch (cause) {
|
|
3369
|
+
throw new NativeFailure(
|
|
3370
|
+
`Arguments of the tool call to ${toolName} are not valid JSON`,
|
|
3371
|
+
FailureCode.invalidRequest,
|
|
3372
|
+
{ cause }
|
|
3373
|
+
);
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
function projectionOf(content) {
|
|
3377
|
+
const text = content.flatMap((block) => block.type === "text" ? [block.text] : []);
|
|
3378
|
+
const toolCalls = content.flatMap(
|
|
3379
|
+
(block) => block.type === "tool-call" ? [{ id: block.id, name: block.name, input: parseToolArguments(block.name, block.arguments) }] : []
|
|
3380
|
+
);
|
|
3381
|
+
return { content: text.join("").trim(), toolCalls };
|
|
3382
|
+
}
|
|
3383
|
+
function isObject3(value) {
|
|
3384
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3385
|
+
}
|
|
3386
|
+
function isReplayCarrier(value) {
|
|
3387
|
+
return isObject3(value) && value.kind === CARRIER_KIND && value.version === CARRIER_VERSION && Array.isArray(value.messages) && isObject3(value.projection);
|
|
3388
|
+
}
|
|
3389
|
+
function replayableTurns(message, requestModel) {
|
|
3390
|
+
const state = message.source.replayState;
|
|
3391
|
+
const carrier = isObject3(state) ? state.response : void 0;
|
|
3392
|
+
if (!isReplayCarrier(carrier)) return void 0;
|
|
3393
|
+
if (nativeModel(message.source.model) !== requestModel) return void 0;
|
|
3394
|
+
return isDeepStrictEqual(carrier.projection, projectionOf(message.content)) ? carrier.messages : void 0;
|
|
3395
|
+
}
|
|
3396
|
+
|
|
3397
|
+
// src/request/history.ts
|
|
3398
|
+
function invalid2(message) {
|
|
3399
|
+
return new NativeFailure(message, FailureCode.invalidRequest);
|
|
3400
|
+
}
|
|
3401
|
+
function textBlock(text) {
|
|
3402
|
+
return { type: "text", text };
|
|
3403
|
+
}
|
|
3404
|
+
function imageBlocks(block, ctx) {
|
|
3405
|
+
const access = ctx.imageAccess(block.attachment);
|
|
3406
|
+
if (block.offloaded === true) return [textBlock(offloadedImageText(block.attachment, access))];
|
|
3407
|
+
const version = requestVersionOf(block, ctx.images);
|
|
3408
|
+
return [
|
|
3409
|
+
textBlock(imageHandleText(block.attachment, version, access)),
|
|
3410
|
+
{
|
|
3411
|
+
type: "image",
|
|
3412
|
+
source: {
|
|
3413
|
+
type: "base64",
|
|
3414
|
+
media_type: version.mediaType,
|
|
3415
|
+
data: Buffer.from(version.data).toString("base64")
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
];
|
|
3419
|
+
}
|
|
3420
|
+
function inputBlocks(content, ctx) {
|
|
3421
|
+
return content.flatMap((block) => {
|
|
3422
|
+
if (block.type === "text") return block.text.length > 0 ? [textBlock(block.text)] : [];
|
|
3423
|
+
if (block.type === "image") return imageBlocks(block, ctx);
|
|
3424
|
+
throw invalid2(`Content of type ${block.type} cannot be sent`);
|
|
3425
|
+
});
|
|
3426
|
+
}
|
|
3427
|
+
function developerBlocks(message) {
|
|
3428
|
+
return message.content.flatMap((block) => {
|
|
3429
|
+
if (block.type === "text") return block.text.length > 0 ? [textBlock(block.text)] : [];
|
|
3430
|
+
if (block.type === "tool-addition" || block.type === "tool-removal") return [];
|
|
3431
|
+
throw invalid2(`Developer content of type ${block.type} cannot be sent`);
|
|
3432
|
+
});
|
|
3433
|
+
}
|
|
3434
|
+
function toolResultBlock(message, ctx) {
|
|
3435
|
+
const parts = inputBlocks(message.content, ctx);
|
|
3436
|
+
const onlyText = parts.every((part) => part.type === "text");
|
|
3437
|
+
const content = onlyText ? parts.map((part) => String(part.text)).join("") : parts;
|
|
3438
|
+
return {
|
|
3439
|
+
type: "tool_result",
|
|
3440
|
+
tool_use_id: message.toolCallId,
|
|
3441
|
+
content,
|
|
3442
|
+
...message.isError === true ? { is_error: true } : {}
|
|
3443
|
+
};
|
|
3444
|
+
}
|
|
3445
|
+
function nativeNameOf(name2, toolNames) {
|
|
3446
|
+
const nativeName = toolNames.get(name2);
|
|
3447
|
+
return nativeName === void 0 ? name2 : `${TOOL_PREFIX}${nativeName}`;
|
|
3448
|
+
}
|
|
3449
|
+
function canonicalBlocks(message, toolNames) {
|
|
3450
|
+
return message.content.flatMap((block) => {
|
|
3451
|
+
switch (block.type) {
|
|
3452
|
+
case "text":
|
|
3453
|
+
return block.text.length > 0 ? [textBlock(block.text)] : [];
|
|
3454
|
+
case "tool-call":
|
|
3455
|
+
return [
|
|
3456
|
+
{
|
|
3457
|
+
type: "tool_use",
|
|
3458
|
+
id: block.id,
|
|
3459
|
+
name: nativeNameOf(block.name, toolNames),
|
|
3460
|
+
input: parseToolArguments(block.name, block.arguments)
|
|
3461
|
+
}
|
|
3462
|
+
];
|
|
3463
|
+
case "image":
|
|
3464
|
+
throw invalid2("An image in an assistant message cannot be sent");
|
|
3465
|
+
default:
|
|
3466
|
+
return [];
|
|
3467
|
+
}
|
|
3468
|
+
});
|
|
3469
|
+
}
|
|
3470
|
+
function replayedFrame(turn) {
|
|
3471
|
+
const { usage: _usage, ...message } = structuredClone(turn);
|
|
3472
|
+
return { type: "assistant", message };
|
|
3473
|
+
}
|
|
3474
|
+
function assistantFrames(message, ctx) {
|
|
3475
|
+
const turns = replayableTurns(message, ctx.nativeModel);
|
|
3476
|
+
if (turns !== void 0) return turns.map(replayedFrame);
|
|
3477
|
+
const content = canonicalBlocks(message, ctx.toolNames);
|
|
3478
|
+
return content.length === 0 ? [] : [{ type: "assistant", message: { role: "assistant", content } }];
|
|
3479
|
+
}
|
|
3480
|
+
function appendUserBlocks(frames, blocks) {
|
|
3481
|
+
if (blocks.length === 0) return;
|
|
3482
|
+
const last = frames.at(-1);
|
|
3483
|
+
if (last?.type === "user") last.message.content.push(...blocks);
|
|
3484
|
+
else frames.push({ type: "user", message: { role: "user", content: blocks } });
|
|
3485
|
+
}
|
|
3486
|
+
function shieldFromCommandParsing(frame) {
|
|
3487
|
+
if (frame.type !== "user") return;
|
|
3488
|
+
const { content } = frame.message;
|
|
3489
|
+
const last = content.at(-1);
|
|
3490
|
+
if (last?.type !== "text" || !String(last.text).startsWith("/")) return;
|
|
3491
|
+
content[content.length - 1] = { ...last, text: ` ${last.text}` };
|
|
3492
|
+
}
|
|
3493
|
+
function framesOf(messages, ctx) {
|
|
3494
|
+
const frames = [];
|
|
3495
|
+
for (const message of messages) {
|
|
3496
|
+
switch (message.role) {
|
|
3497
|
+
case "assistant":
|
|
3498
|
+
frames.push(...assistantFrames(message, ctx));
|
|
3499
|
+
break;
|
|
3500
|
+
case "tool":
|
|
3501
|
+
appendUserBlocks(frames, [toolResultBlock(message, ctx)]);
|
|
3502
|
+
break;
|
|
3503
|
+
case "developer":
|
|
3504
|
+
appendUserBlocks(frames, developerBlocks(message));
|
|
3505
|
+
break;
|
|
3506
|
+
case "user":
|
|
3507
|
+
appendUserBlocks(frames, inputBlocks(message.content, ctx));
|
|
3508
|
+
break;
|
|
3509
|
+
case "system":
|
|
3510
|
+
throw invalid2("System messages must come before the conversation history");
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
frames.forEach(shieldFromCommandParsing);
|
|
3514
|
+
return frames;
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3517
|
+
// src/request/session-id.ts
|
|
3518
|
+
import { createHash } from "node:crypto";
|
|
3519
|
+
var VERSION_4 = 64;
|
|
3520
|
+
var VARIANT_RFC_4122 = 128;
|
|
3521
|
+
function canonicalJson(value) {
|
|
3522
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
3523
|
+
if (typeof value === "object" && value !== null) {
|
|
3524
|
+
const entries = Object.entries(value).filter(([, entry]) => entry !== void 0).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`);
|
|
3525
|
+
return `{${entries.join(",")}}`;
|
|
3526
|
+
}
|
|
3527
|
+
return JSON.stringify(value) ?? "null";
|
|
3528
|
+
}
|
|
3529
|
+
function formatUuid(bytes) {
|
|
3530
|
+
const hex = Buffer.from(bytes).toString("hex");
|
|
3531
|
+
return [
|
|
3532
|
+
hex.slice(0, 8),
|
|
3533
|
+
hex.slice(8, 12),
|
|
3534
|
+
hex.slice(12, 16),
|
|
3535
|
+
hex.slice(16, 20),
|
|
3536
|
+
hex.slice(20)
|
|
3537
|
+
].join("-");
|
|
3538
|
+
}
|
|
3539
|
+
function conversationSessionId(system, frames) {
|
|
3540
|
+
const first = frames[0]?.message ?? null;
|
|
3541
|
+
const digest = createHash("sha256").update(canonicalJson([system, first]), "utf8").digest();
|
|
3542
|
+
const bytes = digest.subarray(0, 16);
|
|
3543
|
+
bytes[6] = (bytes[6] ?? 0) & 15 | VERSION_4;
|
|
3544
|
+
bytes[8] = (bytes[8] ?? 0) & 63 | VARIANT_RFC_4122;
|
|
3545
|
+
return formatUuid(bytes);
|
|
3546
|
+
}
|
|
3547
|
+
|
|
3548
|
+
// src/request/tools.ts
|
|
3549
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
3550
|
+
|
|
3551
|
+
// src/request/schema.ts
|
|
3552
|
+
var UNION_KEYS = ["anyOf", "oneOf"];
|
|
3553
|
+
var UNION_META_KEYS = ["title", "description", "default", "examples"];
|
|
3554
|
+
var BANNED_TOP_LEVEL_KEYS = ["oneOf", "allOf", "anyOf"];
|
|
3555
|
+
function isObject4(value) {
|
|
3556
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3557
|
+
}
|
|
3558
|
+
function rewrite(node, visit) {
|
|
3559
|
+
if (Array.isArray(node)) return node.map((item) => rewrite(item, visit));
|
|
3560
|
+
if (!isObject4(node)) return node;
|
|
3561
|
+
const children = Object.fromEntries(
|
|
3562
|
+
Object.entries(node).map(([key, value]) => [key, rewrite(value, visit)])
|
|
3563
|
+
);
|
|
3564
|
+
return visit(children);
|
|
3565
|
+
}
|
|
3566
|
+
function isNullBranch(branch) {
|
|
3567
|
+
return isObject4(branch) && branch.type === "null";
|
|
3568
|
+
}
|
|
3569
|
+
function carryUnionMeta(outer, replacement) {
|
|
3570
|
+
for (const key of UNION_META_KEYS) {
|
|
3571
|
+
if (!Object.hasOwn(outer, key) || Object.hasOwn(replacement, key)) continue;
|
|
3572
|
+
if (key === "default" && Object.hasOwn(replacement, "$ref")) continue;
|
|
3573
|
+
replacement[key] = outer[key];
|
|
3574
|
+
}
|
|
3575
|
+
}
|
|
3576
|
+
function collapseNullableUnion(union) {
|
|
3577
|
+
for (const key of UNION_KEYS) {
|
|
3578
|
+
const variants = union[key];
|
|
3579
|
+
if (!Array.isArray(variants)) continue;
|
|
3580
|
+
const nonNull = variants.filter((branch) => !isNullBranch(branch));
|
|
3581
|
+
if (nonNull.length !== 1 || nonNull.length === variants.length) continue;
|
|
3582
|
+
const survivor = nonNull[0];
|
|
3583
|
+
const replacement = isObject4(survivor) ? { ...survivor } : {};
|
|
3584
|
+
carryUnionMeta(union, replacement);
|
|
3585
|
+
return rewrite(replacement, collapseNullableUnion);
|
|
3586
|
+
}
|
|
3587
|
+
return union;
|
|
3588
|
+
}
|
|
3589
|
+
function stripNullableUnions(schema) {
|
|
3590
|
+
return rewrite(schema, collapseNullableUnion);
|
|
3591
|
+
}
|
|
3592
|
+
function normalizeInputSchema(schema) {
|
|
3593
|
+
let normalized = stripNullableUnions(schema);
|
|
3594
|
+
if (BANNED_TOP_LEVEL_KEYS.some((key) => key in normalized)) {
|
|
3595
|
+
normalized = Object.fromEntries(
|
|
3596
|
+
Object.entries(normalized).filter(
|
|
3597
|
+
([key]) => !BANNED_TOP_LEVEL_KEYS.includes(key)
|
|
3598
|
+
)
|
|
3599
|
+
);
|
|
3600
|
+
if (!("type" in normalized)) normalized.type = "object";
|
|
3601
|
+
}
|
|
3602
|
+
if (normalized.type === "object" && !isObject4(normalized.properties)) {
|
|
3603
|
+
normalized = { ...normalized, properties: {} };
|
|
3604
|
+
}
|
|
3605
|
+
return normalized;
|
|
3606
|
+
}
|
|
3607
|
+
|
|
3608
|
+
// src/request/tools.ts
|
|
3609
|
+
var VALID_NAME = /^[A-Za-z0-9_-]{1,64}$/;
|
|
3610
|
+
var MAX_NATIVE_NAME_LENGTH = 50;
|
|
3611
|
+
var ALIAS_STEM_LENGTH = 41;
|
|
3612
|
+
var ALIAS_DIGEST_LENGTH = 8;
|
|
3613
|
+
function nativeNameOf2(name2) {
|
|
3614
|
+
if (name2.length <= MAX_NATIVE_NAME_LENGTH) return name2;
|
|
3615
|
+
const digest = createHash2("sha256").update(name2).digest("hex").slice(0, ALIAS_DIGEST_LENGTH);
|
|
3616
|
+
return `${name2.slice(0, ALIAS_STEM_LENGTH)}_${digest}`;
|
|
3617
|
+
}
|
|
3618
|
+
function declareTools(tools) {
|
|
3619
|
+
const names = /* @__PURE__ */ new Map();
|
|
3620
|
+
const owners = /* @__PURE__ */ new Map();
|
|
3621
|
+
const manifest = [];
|
|
3622
|
+
const wire = [];
|
|
3623
|
+
for (const tool of tools) {
|
|
3624
|
+
if (!VALID_NAME.test(tool.name) || names.has(tool.name)) {
|
|
3625
|
+
throw new NativeFailure(
|
|
3626
|
+
"Tool names must be unique ASCII identifiers of at most 64 characters",
|
|
3627
|
+
FailureCode.invalidRequest
|
|
3628
|
+
);
|
|
3629
|
+
}
|
|
3630
|
+
const nativeName = nativeNameOf2(tool.name);
|
|
3631
|
+
const owner = owners.get(nativeName);
|
|
3632
|
+
if (owner !== void 0) {
|
|
3633
|
+
throw new NativeFailure(
|
|
3634
|
+
`Tool name collision: ${JSON.stringify(tool.name)} and ${JSON.stringify(owner)} map to ${JSON.stringify(nativeName)}`,
|
|
3635
|
+
FailureCode.invalidRequest
|
|
3636
|
+
);
|
|
3637
|
+
}
|
|
3638
|
+
names.set(tool.name, nativeName);
|
|
3639
|
+
owners.set(nativeName, tool.name);
|
|
3640
|
+
const inputSchema = normalizeInputSchema(tool.parameters);
|
|
3641
|
+
manifest.push({ name: nativeName, description: tool.description, inputSchema });
|
|
3642
|
+
wire.push({
|
|
3643
|
+
name: `${TOOL_PREFIX}${nativeName}`,
|
|
3644
|
+
description: tool.description,
|
|
3645
|
+
input_schema: inputSchema
|
|
3646
|
+
});
|
|
3647
|
+
}
|
|
3648
|
+
return { manifest, wire, names };
|
|
3649
|
+
}
|
|
3650
|
+
|
|
3651
|
+
// src/request/prepare.ts
|
|
3652
|
+
function invalid3(message) {
|
|
3653
|
+
return new NativeFailure(message, FailureCode.invalidRequest);
|
|
3654
|
+
}
|
|
3655
|
+
function systemText(message) {
|
|
3656
|
+
return message.content.map((block) => {
|
|
3657
|
+
if (block.type !== "text") throw invalid3("System messages must contain text only");
|
|
3658
|
+
return block.text;
|
|
3659
|
+
}).join("");
|
|
3660
|
+
}
|
|
3661
|
+
function splitSystemPrompt(options) {
|
|
3662
|
+
const texts = [options.system];
|
|
3663
|
+
let end = 0;
|
|
3664
|
+
for (const message of options.messages) {
|
|
3665
|
+
if (message.role !== "system") break;
|
|
3666
|
+
texts.push(systemText(message));
|
|
3667
|
+
end += 1;
|
|
3668
|
+
}
|
|
3669
|
+
return {
|
|
3670
|
+
system: texts.filter((text) => text !== void 0 && text !== "").join("\n\n"),
|
|
3671
|
+
history: options.messages.slice(end)
|
|
3672
|
+
};
|
|
3673
|
+
}
|
|
3674
|
+
function assertEndsWithUserTurn(frames) {
|
|
3675
|
+
const last = frames.at(-1);
|
|
3676
|
+
if (last?.type !== "user" || last.message.content.length === 0) {
|
|
3677
|
+
throw invalid3("History must end in a nonempty user or tool-result message");
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
async function prepareRequest(options, deps) {
|
|
3681
|
+
if (options.model === "") throw invalid3("A model is required");
|
|
3682
|
+
const model = nativeModel(options.model);
|
|
3683
|
+
const generation = generationOf(options);
|
|
3684
|
+
const { system, history } = splitSystemPrompt(options);
|
|
3685
|
+
const declarations = declareTools(options.tools ?? []);
|
|
3686
|
+
const images = await readRequestImages(history, deps.attachments(), options.signal);
|
|
3687
|
+
assertImagesFit(history, images);
|
|
3688
|
+
const frames = framesOf(history, {
|
|
3689
|
+
nativeModel: model,
|
|
3690
|
+
toolNames: declarations.names,
|
|
3691
|
+
images,
|
|
3692
|
+
imageAccess: deps.imageAccess
|
|
3693
|
+
});
|
|
3694
|
+
assertEndsWithUserTurn(frames);
|
|
3695
|
+
return {
|
|
3696
|
+
nativeModel: model,
|
|
3697
|
+
system,
|
|
3698
|
+
frames,
|
|
3699
|
+
extraBody: { ...generation.body, tools: declarations.wire },
|
|
3700
|
+
...generation.effort === void 0 ? {} : { effort: generation.effort },
|
|
3701
|
+
...generation.maxOutputTokens === void 0 ? {} : { maxOutputTokens: generation.maxOutputTokens },
|
|
3702
|
+
manifest: declarations.manifest,
|
|
3703
|
+
toolNames: declarations.names,
|
|
3704
|
+
sessionId: conversationSessionId(system, frames),
|
|
3705
|
+
oneShot: options.purpose !== void 0 && declarations.manifest.length === 0 && frames.length === 1
|
|
3706
|
+
};
|
|
3707
|
+
}
|
|
3708
|
+
|
|
3709
|
+
// src/index.ts
|
|
3710
|
+
var name = PROVIDER_ID;
|
|
3711
|
+
var inject = ["llm", "subprocess"];
|
|
3712
|
+
var DISPLAY_NAME = "Claude Subscription";
|
|
3713
|
+
function settingsNamespace(ctx) {
|
|
3714
|
+
const id = ctx.fiber.entry?.options?.id;
|
|
3715
|
+
if (typeof id !== "string" || id === "") {
|
|
3716
|
+
throw new Error(
|
|
3717
|
+
`${PROVIDER_ID}: the loader gave this plugin no entry id, so its settings row cannot be found`
|
|
3718
|
+
);
|
|
3719
|
+
}
|
|
3720
|
+
return id;
|
|
3721
|
+
}
|
|
3722
|
+
function imageAccessOf(ctx, ref) {
|
|
3723
|
+
const hostPath = ctx.get("attachments")?.imageHostPath(ref);
|
|
3724
|
+
if (hostPath === void 0) return void 0;
|
|
3725
|
+
const mapper = ctx.get("fs");
|
|
3726
|
+
const readonlyPath = mapper?.processPathFromHostPath(hostPath);
|
|
3727
|
+
return readonlyPath === void 0 ? void 0 : { readonlyPath };
|
|
3728
|
+
}
|
|
3729
|
+
function apply(ctx, config) {
|
|
3730
|
+
const native = {
|
|
3731
|
+
subprocess: ctx.subprocess,
|
|
3732
|
+
createRelay,
|
|
3733
|
+
settings: () => readSettings(config),
|
|
3734
|
+
processEnv: () => process.env,
|
|
3735
|
+
inertMcpPath: fileURLToPath(new URL("./inert-mcp.js", import.meta.url)),
|
|
3736
|
+
nodePath: process.execPath
|
|
3737
|
+
};
|
|
3738
|
+
const entry = {
|
|
3739
|
+
provider: PROVIDER_ID,
|
|
3740
|
+
displayName: DISPLAY_NAME,
|
|
3741
|
+
settingsNs: settingsNamespace(ctx),
|
|
3742
|
+
settingsPath: []
|
|
3743
|
+
};
|
|
3744
|
+
const directory = ctx.llm.registerConfigurableProviders([entry]);
|
|
3745
|
+
let reported;
|
|
3746
|
+
ctx.llm.registerAdapter(
|
|
3747
|
+
[PROVIDER_ID],
|
|
3748
|
+
new ClaudeSubscriptionAdapter({
|
|
3749
|
+
prepare: (options) => prepareRequest(options, {
|
|
3750
|
+
attachments: () => ctx.get("attachments"),
|
|
3751
|
+
imageAccess: (ref) => imageAccessOf(ctx, ref)
|
|
3752
|
+
}),
|
|
3753
|
+
run: runNative,
|
|
3754
|
+
discover: discoverModels,
|
|
3755
|
+
modelInfo,
|
|
3756
|
+
imageTarget: imageRequestTarget,
|
|
3757
|
+
projectionOf,
|
|
3758
|
+
native,
|
|
3759
|
+
reportDiscovery: (error) => {
|
|
3760
|
+
if (error === reported) return;
|
|
3761
|
+
reported = error;
|
|
3762
|
+
directory.replace([error === void 0 ? entry : { ...entry, error }]);
|
|
3763
|
+
},
|
|
3764
|
+
warn: (message) => ctx.logger.warn(message)
|
|
3765
|
+
})
|
|
3766
|
+
);
|
|
3767
|
+
}
|
|
3768
|
+
export {
|
|
3769
|
+
Config,
|
|
3770
|
+
apply,
|
|
3771
|
+
inject,
|
|
3772
|
+
name
|
|
3773
|
+
};
|