u1s1-cli-cn 1.17.0 → 1.19.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/dist/agents/claude.d.ts +4 -1
- package/dist/agents/claude.js +6 -3
- package/dist/agents/codex.d.ts +4 -1
- package/dist/agents/codex.js +6 -3
- package/dist/agents/custom-models.d.ts +61 -0
- package/dist/agents/custom-models.js +175 -0
- package/dist/agents/launch.d.ts +3 -0
- package/dist/agents/launch.js +17 -5
- package/dist/agents/models-list.d.ts +65 -0
- package/dist/agents/models-list.js +297 -0
- package/dist/api.d.ts +2 -0
- package/dist/chat-protocol/anthropic.d.ts +29 -0
- package/dist/chat-protocol/anthropic.js +484 -0
- package/dist/chat-protocol/json.d.ts +20 -0
- package/dist/chat-protocol/json.js +22 -0
- package/dist/chat-protocol/responses.d.ts +97 -0
- package/dist/chat-protocol/responses.js +1076 -0
- package/dist/chat-protocol/sse.d.ts +19 -0
- package/dist/chat-protocol/sse.js +67 -0
- package/dist/chat-protocol/thinking.d.ts +22 -0
- package/dist/chat-protocol/thinking.js +58 -0
- package/dist/config-command.js +5 -1
- package/dist/config.d.ts +22 -0
- package/dist/config.js +51 -0
- package/dist/custom-endpoint-proxy.d.ts +92 -0
- package/dist/custom-endpoint-proxy.js +461 -0
- package/dist/device-auth.d.ts +13 -2
- package/dist/device-auth.js +36 -2
- package/dist/index.js +9 -1
- package/dist/launcher.js +8 -0
- package/package.json +1 -1
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
// GENERATED from workers/gateway/src/chat-protocol/anthropic.ts; do not edit here. Edit the canonical file, then run: node scripts/sync-chat-protocol.mjs
|
|
2
|
+
import { asRecord } from "./json.js";
|
|
3
|
+
import { SSE_KEEPALIVE_COMMENT, SseParser } from "./sse.js";
|
|
4
|
+
import { requestedThinkingLevel } from "./thinking.js";
|
|
5
|
+
// Anthropic Messages (POST /v1/messages, Claude Code) <-> OpenAI Chat
|
|
6
|
+
// Completions. The request is converted to a Chat body; the Chat response
|
|
7
|
+
// (JSON or SSE) is converted back. Moved verbatim from the gateway's
|
|
8
|
+
// anthropic-inbound.ts; what was gateway-specific is injected: the reasoning
|
|
9
|
+
// fields (per-model thinking contract) and the usage mapping (the gateway
|
|
10
|
+
// reuses its billing parser so clients see the token split they were billed).
|
|
11
|
+
// See json.ts for the rules of this directory.
|
|
12
|
+
export const MESSAGES_PATH = "/v1/messages";
|
|
13
|
+
function count(value) {
|
|
14
|
+
const n = Number(value);
|
|
15
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Plain Chat Completions usage → Anthropic usage: cache reads
|
|
19
|
+
* (`prompt_tokens_details.cached_tokens`, or DeepSeek's
|
|
20
|
+
* `prompt_cache_hit_tokens`) are reported beside `input_tokens`, not in it.
|
|
21
|
+
*/
|
|
22
|
+
export function chatUsageToAnthropic(value) {
|
|
23
|
+
const usage = asRecord(value) ?? {};
|
|
24
|
+
const prompt = count(usage.prompt_tokens ?? usage.input_tokens);
|
|
25
|
+
const details = asRecord(usage.prompt_tokens_details) ?? asRecord(usage.input_tokens_details);
|
|
26
|
+
const cacheRead = Math.min(count(details?.cached_tokens ?? usage.prompt_cache_hit_tokens), prompt);
|
|
27
|
+
return {
|
|
28
|
+
input_tokens: prompt - cacheRead,
|
|
29
|
+
output_tokens: count(usage.completion_tokens ?? usage.output_tokens),
|
|
30
|
+
cache_read_input_tokens: cacheRead,
|
|
31
|
+
cache_creation_input_tokens: 0,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** Anthropic error body: `{type: "error", error: {type, message}}`. */
|
|
35
|
+
export function anthropicErrorBody(status, message) {
|
|
36
|
+
return { type: "error", error: { type: anthropicErrorType(status), message } };
|
|
37
|
+
}
|
|
38
|
+
// Claude Code prepends a request-metadata block to the system prompt. It is not
|
|
39
|
+
// an instruction, and forwarding it would put a client-version string at the
|
|
40
|
+
// very start of every upstream prompt (the prefix providers cache on).
|
|
41
|
+
const BILLING_HEADER_PREFIX = "x-anthropic-billing-header:";
|
|
42
|
+
function systemText(blocks) {
|
|
43
|
+
if (typeof blocks === "string")
|
|
44
|
+
return blocks;
|
|
45
|
+
if (!Array.isArray(blocks))
|
|
46
|
+
return "";
|
|
47
|
+
return blocks
|
|
48
|
+
.map((block) => {
|
|
49
|
+
const row = asRecord(block);
|
|
50
|
+
return row?.type === "text" && typeof row.text === "string" ? row.text : "";
|
|
51
|
+
})
|
|
52
|
+
.filter((text) => text && !text.startsWith(BILLING_HEADER_PREFIX))
|
|
53
|
+
.join("\n\n");
|
|
54
|
+
}
|
|
55
|
+
function imagePart(block) {
|
|
56
|
+
const source = asRecord(block.source);
|
|
57
|
+
if (source?.type === "base64" && typeof source.media_type === "string" && typeof source.data === "string") {
|
|
58
|
+
return { type: "image_url", image_url: { url: `data:${source.media_type};base64,${source.data}` } };
|
|
59
|
+
}
|
|
60
|
+
if (source?.type === "url" && typeof source.url === "string") {
|
|
61
|
+
return { type: "image_url", image_url: { url: source.url } };
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
/** text/image parts of a user turn; a text-only turn collapses to a plain string. */
|
|
66
|
+
function userContent(parts) {
|
|
67
|
+
if (parts.every((part) => part.type === "text")) {
|
|
68
|
+
return parts.map((part) => part.text).join("\n\n");
|
|
69
|
+
}
|
|
70
|
+
return parts;
|
|
71
|
+
}
|
|
72
|
+
function toolResultMessage(block, images) {
|
|
73
|
+
const content = block.content;
|
|
74
|
+
let text = typeof content === "string" ? content : "";
|
|
75
|
+
if (Array.isArray(content)) {
|
|
76
|
+
const texts = [];
|
|
77
|
+
for (const item of content) {
|
|
78
|
+
const row = asRecord(item);
|
|
79
|
+
if (row?.type === "text" && typeof row.text === "string")
|
|
80
|
+
texts.push(row.text);
|
|
81
|
+
if (row?.type === "image") {
|
|
82
|
+
const image = imagePart(row);
|
|
83
|
+
if (image)
|
|
84
|
+
images.push(image);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
text = texts.join("\n\n");
|
|
88
|
+
}
|
|
89
|
+
if (block.is_error === true)
|
|
90
|
+
text = `Error: ${text}`;
|
|
91
|
+
return {
|
|
92
|
+
role: "tool",
|
|
93
|
+
tool_call_id: typeof block.tool_use_id === "string" ? block.tool_use_id : "unknown",
|
|
94
|
+
content: text,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function convertUserTurn(content, out) {
|
|
98
|
+
if (typeof content === "string") {
|
|
99
|
+
out.push({ role: "user", content });
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const parts = [];
|
|
103
|
+
const toolImages = [];
|
|
104
|
+
for (const item of Array.isArray(content) ? content : []) {
|
|
105
|
+
const block = asRecord(item);
|
|
106
|
+
if (!block)
|
|
107
|
+
continue;
|
|
108
|
+
if (block.type === "tool_result") {
|
|
109
|
+
// OpenAI requires tool messages directly after the assistant tool_calls turn.
|
|
110
|
+
out.push(toolResultMessage(block, toolImages));
|
|
111
|
+
}
|
|
112
|
+
else if (block.type === "text" && typeof block.text === "string") {
|
|
113
|
+
parts.push({ type: "text", text: block.text });
|
|
114
|
+
}
|
|
115
|
+
else if (block.type === "image") {
|
|
116
|
+
const image = imagePart(block);
|
|
117
|
+
if (image)
|
|
118
|
+
parts.push(image);
|
|
119
|
+
}
|
|
120
|
+
else if (block.type === "document") {
|
|
121
|
+
parts.push({ type: "text", text: "[document attachment omitted: unsupported by this endpoint]" });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Chat Completions tool messages cannot carry images: replay them as a user turn.
|
|
125
|
+
if (toolImages.length)
|
|
126
|
+
parts.unshift({ type: "text", text: "Images returned by the tool call above:" }, ...toolImages);
|
|
127
|
+
if (parts.length)
|
|
128
|
+
out.push({ role: "user", content: userContent(parts) });
|
|
129
|
+
}
|
|
130
|
+
function convertAssistantTurn(content, out) {
|
|
131
|
+
if (typeof content === "string") {
|
|
132
|
+
out.push({ role: "assistant", content });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const text = [];
|
|
136
|
+
const reasoning = [];
|
|
137
|
+
const toolCalls = [];
|
|
138
|
+
for (const item of Array.isArray(content) ? content : []) {
|
|
139
|
+
const block = asRecord(item);
|
|
140
|
+
if (block?.type === "text" && typeof block.text === "string")
|
|
141
|
+
text.push(block.text);
|
|
142
|
+
if (block?.type === "thinking" && typeof block.thinking === "string")
|
|
143
|
+
reasoning.push(block.thinking);
|
|
144
|
+
if (block?.type === "tool_use" && typeof block.name === "string") {
|
|
145
|
+
toolCalls.push({
|
|
146
|
+
id: typeof block.id === "string" ? block.id : `call_${crypto.randomUUID()}`,
|
|
147
|
+
type: "function",
|
|
148
|
+
function: { name: block.name, arguments: JSON.stringify(block.input ?? {}) },
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const message = { role: "assistant", content: text.length ? text.join("") : null };
|
|
153
|
+
// DeepSeek thinking mode requires reasoning_content replay on tool-call turns.
|
|
154
|
+
if (reasoning.length)
|
|
155
|
+
message.reasoning_content = reasoning.join("");
|
|
156
|
+
if (toolCalls.length)
|
|
157
|
+
message.tool_calls = toolCalls;
|
|
158
|
+
if (message.content === null && !toolCalls.length)
|
|
159
|
+
message.content = "";
|
|
160
|
+
out.push(message);
|
|
161
|
+
}
|
|
162
|
+
function convertTools(value) {
|
|
163
|
+
if (!Array.isArray(value))
|
|
164
|
+
return undefined;
|
|
165
|
+
const tools = [];
|
|
166
|
+
for (const item of value) {
|
|
167
|
+
const tool = asRecord(item);
|
|
168
|
+
// Server tools (web_search_20250305 etc.) carry a versioned type and no schema.
|
|
169
|
+
if (typeof tool?.name !== "string" || !asRecord(tool.input_schema))
|
|
170
|
+
continue;
|
|
171
|
+
tools.push({
|
|
172
|
+
type: "function",
|
|
173
|
+
function: {
|
|
174
|
+
name: tool.name,
|
|
175
|
+
...(typeof tool.description === "string" ? { description: tool.description } : {}),
|
|
176
|
+
parameters: tool.input_schema,
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return tools.length ? tools : undefined;
|
|
181
|
+
}
|
|
182
|
+
function convertToolChoice(value) {
|
|
183
|
+
const choice = asRecord(value);
|
|
184
|
+
if (choice?.type === "auto")
|
|
185
|
+
return "auto";
|
|
186
|
+
if (choice?.type === "any")
|
|
187
|
+
return "required";
|
|
188
|
+
if (choice?.type === "none")
|
|
189
|
+
return "none";
|
|
190
|
+
if (choice?.type === "tool" && typeof choice.name === "string") {
|
|
191
|
+
return { type: "function", function: { name: choice.name } };
|
|
192
|
+
}
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Anthropic Messages request → Chat Completions body. `thinking` and
|
|
197
|
+
* `output_config.effort` become whatever `reasoningFields` returns for the
|
|
198
|
+
* requested level (thinking.ts); `metadata` is dropped on purpose.
|
|
199
|
+
*/
|
|
200
|
+
export function anthropicRequestToChat(body, reasoningFields) {
|
|
201
|
+
const messages = [];
|
|
202
|
+
const system = systemText(body.system);
|
|
203
|
+
if (system)
|
|
204
|
+
messages.push({ role: "system", content: system });
|
|
205
|
+
for (const item of Array.isArray(body.messages) ? body.messages : []) {
|
|
206
|
+
const message = asRecord(item);
|
|
207
|
+
if (message?.role === "assistant")
|
|
208
|
+
convertAssistantTurn(message.content, messages);
|
|
209
|
+
else if (message?.role === "user")
|
|
210
|
+
convertUserTurn(message.content, messages);
|
|
211
|
+
}
|
|
212
|
+
const converted = { model: body.model, messages, stream: body.stream === true };
|
|
213
|
+
if (typeof body.max_tokens === "number")
|
|
214
|
+
converted.max_tokens = body.max_tokens;
|
|
215
|
+
for (const field of ["temperature", "top_p"]) {
|
|
216
|
+
if (typeof body[field] === "number")
|
|
217
|
+
converted[field] = body[field];
|
|
218
|
+
}
|
|
219
|
+
if (Array.isArray(body.stop_sequences) && body.stop_sequences.length)
|
|
220
|
+
converted.stop = body.stop_sequences;
|
|
221
|
+
Object.assign(converted, reasoningFields(String(body.model ?? ""), requestedThinkingLevel(body)));
|
|
222
|
+
const tools = convertTools(body.tools);
|
|
223
|
+
if (tools) {
|
|
224
|
+
converted.tools = tools;
|
|
225
|
+
const toolChoice = convertToolChoice(body.tool_choice);
|
|
226
|
+
if (toolChoice !== undefined)
|
|
227
|
+
converted.tool_choice = toolChoice;
|
|
228
|
+
if (asRecord(body.tool_choice)?.disable_parallel_tool_use === true)
|
|
229
|
+
converted.parallel_tool_calls = false;
|
|
230
|
+
}
|
|
231
|
+
return converted;
|
|
232
|
+
}
|
|
233
|
+
function stopReason(reason) {
|
|
234
|
+
if (reason === "length")
|
|
235
|
+
return "max_tokens";
|
|
236
|
+
if (reason === "tool_calls" || reason === "function_call")
|
|
237
|
+
return "tool_use";
|
|
238
|
+
return "end_turn";
|
|
239
|
+
}
|
|
240
|
+
function messageId(raw) {
|
|
241
|
+
return typeof raw === "string" && raw.startsWith("msg_") ? raw : `msg_${crypto.randomUUID().replaceAll("-", "")}`;
|
|
242
|
+
}
|
|
243
|
+
export function chatCompletionToAnthropic(value, requestedModel, usage = chatUsageToAnthropic) {
|
|
244
|
+
const completion = asRecord(value) ?? {};
|
|
245
|
+
const choice = asRecord(Array.isArray(completion.choices) ? completion.choices[0] : null) ?? {};
|
|
246
|
+
const message = asRecord(choice.message) ?? {};
|
|
247
|
+
const content = [];
|
|
248
|
+
const reasoning = message.reasoning_content ?? message.reasoning;
|
|
249
|
+
if (typeof reasoning === "string" && reasoning)
|
|
250
|
+
content.push({ type: "thinking", thinking: reasoning, signature: "" });
|
|
251
|
+
if (typeof message.content === "string" && message.content)
|
|
252
|
+
content.push({ type: "text", text: message.content });
|
|
253
|
+
for (const item of Array.isArray(message.tool_calls) ? message.tool_calls : []) {
|
|
254
|
+
const call = asRecord(item);
|
|
255
|
+
const fn = asRecord(call?.function);
|
|
256
|
+
if (typeof fn?.name !== "string")
|
|
257
|
+
continue;
|
|
258
|
+
let input = {};
|
|
259
|
+
try {
|
|
260
|
+
input = typeof fn.arguments === "string" && fn.arguments.trim() ? JSON.parse(fn.arguments) : {};
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
input = { raw: fn.arguments };
|
|
264
|
+
}
|
|
265
|
+
content.push({
|
|
266
|
+
type: "tool_use",
|
|
267
|
+
id: typeof call?.id === "string" ? call.id : `toolu_${crypto.randomUUID()}`,
|
|
268
|
+
name: fn.name,
|
|
269
|
+
input,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
id: messageId(completion.id),
|
|
274
|
+
type: "message",
|
|
275
|
+
role: "assistant",
|
|
276
|
+
model: requestedModel,
|
|
277
|
+
content,
|
|
278
|
+
stop_reason: stopReason(choice.finish_reason),
|
|
279
|
+
stop_sequence: null,
|
|
280
|
+
usage: usage(completion.usage),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
/** OpenAI chat.completion.chunk SSE → Anthropic Messages SSE event sequence. */
|
|
284
|
+
export function chatSseToAnthropic(requestedModel, usage = chatUsageToAnthropic) {
|
|
285
|
+
const decoder = new TextDecoder();
|
|
286
|
+
const encoder = new TextEncoder();
|
|
287
|
+
const parser = new SseParser();
|
|
288
|
+
const state = {
|
|
289
|
+
started: false,
|
|
290
|
+
finished: false,
|
|
291
|
+
blockIndex: -1,
|
|
292
|
+
open: null,
|
|
293
|
+
openToolIndex: null,
|
|
294
|
+
stopReason: "end_turn",
|
|
295
|
+
usage: usage(null),
|
|
296
|
+
};
|
|
297
|
+
const emit = (controller, type, data) => {
|
|
298
|
+
controller.enqueue(encoder.encode(`event: ${type}\ndata: ${JSON.stringify({ type, ...data })}\n\n`));
|
|
299
|
+
};
|
|
300
|
+
const start = (controller, id) => {
|
|
301
|
+
if (state.started)
|
|
302
|
+
return;
|
|
303
|
+
state.started = true;
|
|
304
|
+
emit(controller, "message_start", {
|
|
305
|
+
message: {
|
|
306
|
+
id: messageId(id),
|
|
307
|
+
type: "message",
|
|
308
|
+
role: "assistant",
|
|
309
|
+
model: requestedModel,
|
|
310
|
+
content: [],
|
|
311
|
+
stop_reason: null,
|
|
312
|
+
stop_sequence: null,
|
|
313
|
+
usage: { input_tokens: 0, output_tokens: 0 },
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
};
|
|
317
|
+
const closeBlock = (controller) => {
|
|
318
|
+
if (!state.open)
|
|
319
|
+
return;
|
|
320
|
+
emit(controller, "content_block_stop", { index: state.blockIndex });
|
|
321
|
+
state.open = null;
|
|
322
|
+
state.openToolIndex = null;
|
|
323
|
+
};
|
|
324
|
+
const openBlock = (controller, kind, block) => {
|
|
325
|
+
closeBlock(controller);
|
|
326
|
+
state.blockIndex += 1;
|
|
327
|
+
state.open = kind;
|
|
328
|
+
emit(controller, "content_block_start", { index: state.blockIndex, content_block: block });
|
|
329
|
+
};
|
|
330
|
+
const delta = (controller, data) => {
|
|
331
|
+
emit(controller, "content_block_delta", { index: state.blockIndex, delta: data });
|
|
332
|
+
};
|
|
333
|
+
const toolDeltas = (controller, calls) => {
|
|
334
|
+
for (const item of calls) {
|
|
335
|
+
const call = asRecord(item);
|
|
336
|
+
if (!call)
|
|
337
|
+
continue;
|
|
338
|
+
const fn = asRecord(call.function) ?? {};
|
|
339
|
+
const index = Number(call.index ?? 0);
|
|
340
|
+
if (state.open !== "tool_use" || (state.openToolIndex !== index && typeof fn.name === "string")) {
|
|
341
|
+
openBlock(controller, "tool_use", {
|
|
342
|
+
type: "tool_use",
|
|
343
|
+
id: typeof call.id === "string" ? call.id : `toolu_${crypto.randomUUID()}`,
|
|
344
|
+
name: typeof fn.name === "string" ? fn.name : "",
|
|
345
|
+
input: {},
|
|
346
|
+
});
|
|
347
|
+
state.openToolIndex = index;
|
|
348
|
+
}
|
|
349
|
+
if (typeof fn.arguments === "string" && fn.arguments) {
|
|
350
|
+
delta(controller, { type: "input_json_delta", partial_json: fn.arguments });
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
const finish = (controller) => {
|
|
355
|
+
if (state.finished)
|
|
356
|
+
return;
|
|
357
|
+
start(controller, null);
|
|
358
|
+
closeBlock(controller);
|
|
359
|
+
state.finished = true;
|
|
360
|
+
emit(controller, "message_delta", {
|
|
361
|
+
delta: { stop_reason: state.stopReason, stop_sequence: null },
|
|
362
|
+
usage: state.usage,
|
|
363
|
+
});
|
|
364
|
+
emit(controller, "message_stop", {});
|
|
365
|
+
};
|
|
366
|
+
const fail = (controller, message, type = "api_error") => {
|
|
367
|
+
if (state.finished)
|
|
368
|
+
return;
|
|
369
|
+
state.finished = true;
|
|
370
|
+
emit(controller, "error", { error: { type, message } });
|
|
371
|
+
};
|
|
372
|
+
const consume = (event, controller) => {
|
|
373
|
+
if (state.finished)
|
|
374
|
+
return;
|
|
375
|
+
const data = event.data.trim();
|
|
376
|
+
if (!data) {
|
|
377
|
+
// Keepalive comment frames: forward as ping so client idle timers reset.
|
|
378
|
+
// Before message_start a ping would be out of order, so pass the comment through.
|
|
379
|
+
if (state.started)
|
|
380
|
+
emit(controller, "ping", {});
|
|
381
|
+
else
|
|
382
|
+
controller.enqueue(encoder.encode(SSE_KEEPALIVE_COMMENT));
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (data === "[DONE]") {
|
|
386
|
+
finish(controller);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
let chunk;
|
|
390
|
+
try {
|
|
391
|
+
chunk = asRecord(JSON.parse(data));
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
chunk = null;
|
|
395
|
+
}
|
|
396
|
+
if (!chunk)
|
|
397
|
+
return;
|
|
398
|
+
const error = asRecord(chunk.error);
|
|
399
|
+
if (error) {
|
|
400
|
+
// Gateway error frames sent after the keepalive headers carry the HTTP status they replaced.
|
|
401
|
+
const type = typeof error.upstream_status === "number" ? anthropicErrorType(error.upstream_status) : "api_error";
|
|
402
|
+
fail(controller, typeof error.message === "string" ? error.message : "upstream stream error", type);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
start(controller, chunk.id);
|
|
406
|
+
if (asRecord(chunk.usage))
|
|
407
|
+
state.usage = usage(chunk.usage);
|
|
408
|
+
const choice = asRecord(Array.isArray(chunk.choices) ? chunk.choices[0] : null);
|
|
409
|
+
if (!choice)
|
|
410
|
+
return;
|
|
411
|
+
const d = asRecord(choice.delta) ?? {};
|
|
412
|
+
const reasoning = d.reasoning_content ?? d.reasoning;
|
|
413
|
+
if (typeof reasoning === "string" && reasoning) {
|
|
414
|
+
if (state.open !== "thinking")
|
|
415
|
+
openBlock(controller, "thinking", { type: "thinking", thinking: "", signature: "" });
|
|
416
|
+
delta(controller, { type: "thinking_delta", thinking: reasoning });
|
|
417
|
+
}
|
|
418
|
+
if (typeof d.content === "string" && d.content) {
|
|
419
|
+
if (state.open !== "text")
|
|
420
|
+
openBlock(controller, "text", { type: "text", text: "" });
|
|
421
|
+
delta(controller, { type: "text_delta", text: d.content });
|
|
422
|
+
}
|
|
423
|
+
if (Array.isArray(d.tool_calls))
|
|
424
|
+
toolDeltas(controller, d.tool_calls);
|
|
425
|
+
if (typeof choice.finish_reason === "string")
|
|
426
|
+
state.stopReason = stopReason(choice.finish_reason);
|
|
427
|
+
};
|
|
428
|
+
const consumeAll = (events, controller) => {
|
|
429
|
+
for (const event of events)
|
|
430
|
+
consume(event, controller);
|
|
431
|
+
};
|
|
432
|
+
return new TransformStream({
|
|
433
|
+
transform(bytes, controller) {
|
|
434
|
+
try {
|
|
435
|
+
consumeAll(parser.push(decoder.decode(bytes, { stream: true })), controller);
|
|
436
|
+
}
|
|
437
|
+
catch (error) {
|
|
438
|
+
fail(controller, error instanceof Error ? error.message : "stream conversion failed");
|
|
439
|
+
}
|
|
440
|
+
},
|
|
441
|
+
flush(controller) {
|
|
442
|
+
try {
|
|
443
|
+
consumeAll(parser.finish(decoder.decode()), controller);
|
|
444
|
+
}
|
|
445
|
+
catch (error) {
|
|
446
|
+
fail(controller, error instanceof Error ? error.message : "stream conversion failed");
|
|
447
|
+
}
|
|
448
|
+
// A stream that ended without [DONE] or a terminal frame was cut off upstream.
|
|
449
|
+
if (!state.finished) {
|
|
450
|
+
if (state.started)
|
|
451
|
+
fail(controller, "stream ended unexpectedly");
|
|
452
|
+
else
|
|
453
|
+
finish(controller);
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
export function anthropicErrorType(status) {
|
|
459
|
+
if (status === 400)
|
|
460
|
+
return "invalid_request_error";
|
|
461
|
+
if (status === 401)
|
|
462
|
+
return "authentication_error";
|
|
463
|
+
if (status === 403)
|
|
464
|
+
return "permission_error";
|
|
465
|
+
if (status === 404)
|
|
466
|
+
return "not_found_error";
|
|
467
|
+
if (status === 413)
|
|
468
|
+
return "request_too_large";
|
|
469
|
+
if (status === 429)
|
|
470
|
+
return "rate_limit_error";
|
|
471
|
+
if (status === 529 || status === 503)
|
|
472
|
+
return "overloaded_error";
|
|
473
|
+
return "api_error";
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Anthropic SDKs retry 408/409/429/5xx unless the server sends
|
|
477
|
+
* `x-should-retry: false`. Quota and policy refusals are final: without the
|
|
478
|
+
* header the client would hammer the gateway with retries.
|
|
479
|
+
*/
|
|
480
|
+
export function anthropicShouldRetry(status, code) {
|
|
481
|
+
if (status === 429)
|
|
482
|
+
return code !== "quota_exceeded";
|
|
483
|
+
return status === 408 || status === 409 || status >= 500;
|
|
484
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared chat-protocol modules (this directory): pure Anthropic Messages /
|
|
3
|
+
* OpenAI Responses <-> OpenAI Chat Completions conversion, used by the gateway
|
|
4
|
+
* (/v1/messages, /v1/responses) and by the u1s1 CLI's local signing proxy
|
|
5
|
+
* (custom endpoints for Claude Code / Codex).
|
|
6
|
+
*
|
|
7
|
+
* The canonical copy lives in workers/gateway/src/chat-protocol/; the CLI keeps
|
|
8
|
+
* an identical copy (plus a generated-file header line) in
|
|
9
|
+
* packages/cli/src/chat-protocol/ (the self-hosted gateway image only sees
|
|
10
|
+
* workers/*, the npm package only sees packages/cli).
|
|
11
|
+
* Edit here, then run `node scripts/sync-chat-protocol.mjs`; tests on both
|
|
12
|
+
* sides fail when the copies differ.
|
|
13
|
+
*
|
|
14
|
+
* Rules: Web-standard globals only (fetch types, streams, TextEncoder, crypto,
|
|
15
|
+
* btoa/atob) available in Workers and Node >= 22.19; no gateway env, billing,
|
|
16
|
+
* logging or storage; relative imports with `.ts` extensions only.
|
|
17
|
+
*/
|
|
18
|
+
export type JsonRecord = Record<string, unknown>;
|
|
19
|
+
/** Plain object (not null, not an array) or null. */
|
|
20
|
+
export declare function asRecord(value: unknown): JsonRecord | null;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// GENERATED from workers/gateway/src/chat-protocol/json.ts; do not edit here. Edit the canonical file, then run: node scripts/sync-chat-protocol.mjs
|
|
2
|
+
/**
|
|
3
|
+
* Shared chat-protocol modules (this directory): pure Anthropic Messages /
|
|
4
|
+
* OpenAI Responses <-> OpenAI Chat Completions conversion, used by the gateway
|
|
5
|
+
* (/v1/messages, /v1/responses) and by the u1s1 CLI's local signing proxy
|
|
6
|
+
* (custom endpoints for Claude Code / Codex).
|
|
7
|
+
*
|
|
8
|
+
* The canonical copy lives in workers/gateway/src/chat-protocol/; the CLI keeps
|
|
9
|
+
* an identical copy (plus a generated-file header line) in
|
|
10
|
+
* packages/cli/src/chat-protocol/ (the self-hosted gateway image only sees
|
|
11
|
+
* workers/*, the npm package only sees packages/cli).
|
|
12
|
+
* Edit here, then run `node scripts/sync-chat-protocol.mjs`; tests on both
|
|
13
|
+
* sides fail when the copies differ.
|
|
14
|
+
*
|
|
15
|
+
* Rules: Web-standard globals only (fetch types, streams, TextEncoder, crypto,
|
|
16
|
+
* btoa/atob) available in Workers and Node >= 22.19; no gateway env, billing,
|
|
17
|
+
* logging or storage; relative imports with `.ts` extensions only.
|
|
18
|
+
*/
|
|
19
|
+
/** Plain object (not null, not an array) or null. */
|
|
20
|
+
export function asRecord(value) {
|
|
21
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
22
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { type JsonRecord } from "./json.ts";
|
|
2
|
+
import { type ReasoningFields } from "./thinking.ts";
|
|
3
|
+
export declare const RESPONSES_PATH = "/v1/responses";
|
|
4
|
+
/** What the request converter dropped; logged by the route, asserted by tests. */
|
|
5
|
+
export interface ResponsesConversionNotes {
|
|
6
|
+
skippedItems: string[];
|
|
7
|
+
skippedTools: string[];
|
|
8
|
+
/** custom (freeform) tool names: calls to these come back as custom_tool_call. */
|
|
9
|
+
customTools: string[];
|
|
10
|
+
/** Namespaces seen in `tools`, `additional_tools` and `tool_search_output`. */
|
|
11
|
+
namespaces: string[];
|
|
12
|
+
/** Shortened Chat names (see chatToolName) → the client's name and namespace. */
|
|
13
|
+
aliases: Record<string, {
|
|
14
|
+
name: string;
|
|
15
|
+
namespace?: string;
|
|
16
|
+
}>;
|
|
17
|
+
/** The client offered `tool_search` with client-side execution. */
|
|
18
|
+
toolSearch: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface ResponsesConversion {
|
|
21
|
+
body: JsonRecord;
|
|
22
|
+
notes: ResponsesConversionNotes;
|
|
23
|
+
}
|
|
24
|
+
/** What a gateway reasoning blob carries. */
|
|
25
|
+
export interface ReasoningPayload {
|
|
26
|
+
text: string;
|
|
27
|
+
/**
|
|
28
|
+
* Aggregator-style `reasoning_details` (signed thinking, encrypted
|
|
29
|
+
* reasoning) exactly as the upstream sent them, merged per `index` when
|
|
30
|
+
* streamed. Replayed verbatim; the upstream verifies its own signatures.
|
|
31
|
+
*/
|
|
32
|
+
details?: JsonRecord[];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Opaque reasoning blob handed to the client as `encrypted_content`. Not
|
|
36
|
+
* encrypted: the text is the user's own conversation and replaying a tampered
|
|
37
|
+
* one is no different from editing a message (signed details are verified by
|
|
38
|
+
* the upstream that issued them). `{r}` is the P0 layout; `d` was added
|
|
39
|
+
* without a version bump because older readers ignore it.
|
|
40
|
+
*/
|
|
41
|
+
export declare function encodeReasoningBlob(reasoning: string, details?: readonly JsonRecord[]): string;
|
|
42
|
+
export declare function decodeReasoningPayload(blob: unknown): ReasoningPayload | null;
|
|
43
|
+
export declare function decodeReasoningBlob(blob: unknown): string | null;
|
|
44
|
+
/**
|
|
45
|
+
* Merges streamed `reasoning_details` fragments: entries sharing an `index`
|
|
46
|
+
* are one block whose `text`/`summary`/`data` arrive in pieces and whose
|
|
47
|
+
* `signature`/`id`/`format` arrive once. Fragments without an index append.
|
|
48
|
+
*/
|
|
49
|
+
export declare function mergeReasoningDetails(into: JsonRecord[], fragments: unknown): void;
|
|
50
|
+
/**
|
|
51
|
+
* Responses request → Chat Completions body. Only the fields below are read;
|
|
52
|
+
* everything else (`store`, `include`, `client_metadata`, `text.verbosity`,
|
|
53
|
+
* `service_tier`, `truncation`, `metadata`, future fields …) is ignored.
|
|
54
|
+
* `prompt_cache_key` is forwarded (Chat whitelist field, also feeds
|
|
55
|
+
* upstream session affinity).
|
|
56
|
+
*/
|
|
57
|
+
export declare function responsesRequestToChat(body: JsonRecord, reasoningFields: ReasoningFields): ResponsesConversion;
|
|
58
|
+
/** What the response converter needs to know about the original request. */
|
|
59
|
+
export interface ResponsesContext {
|
|
60
|
+
model: string;
|
|
61
|
+
/** Set when the client relied on server-side state we do not keep. */
|
|
62
|
+
previousResponseId: string | null;
|
|
63
|
+
/** `conversation` (Conversations API): server-side state as well. */
|
|
64
|
+
conversation?: string | null;
|
|
65
|
+
customTools: readonly string[];
|
|
66
|
+
namespaces: readonly string[];
|
|
67
|
+
/** Shortened Chat tool names → client name/namespace. */
|
|
68
|
+
aliases?: Readonly<Record<string, {
|
|
69
|
+
name: string;
|
|
70
|
+
namespace?: string;
|
|
71
|
+
}>>;
|
|
72
|
+
/** Calls to `tool_search` come back as client-executed `tool_search_call`. */
|
|
73
|
+
toolSearch?: boolean;
|
|
74
|
+
/** The client asked for SSE; errors that Codex only understands in-stream use it. */
|
|
75
|
+
stream?: boolean;
|
|
76
|
+
}
|
|
77
|
+
export declare function responsesContext(body: JsonRecord, notes: ResponsesConversionNotes): ResponsesContext;
|
|
78
|
+
export declare function openAICompletionToResponses(value: unknown, ctx: ResponsesContext): JsonRecord;
|
|
79
|
+
/** OpenAI chat.completion.chunk SSE → Responses SSE event sequence. */
|
|
80
|
+
export declare function openAISseToResponses(ctx: ResponsesContext): TransformStream<Uint8Array, Uint8Array>;
|
|
81
|
+
type ErrorClass = "final" | "context" | "retry";
|
|
82
|
+
export declare function classifyError(status: number, code: unknown): ErrorClass;
|
|
83
|
+
/** `response.failed` error code for a gateway error frame. */
|
|
84
|
+
export declare function responsesStreamErrorCode(status: number, code: unknown): string;
|
|
85
|
+
/** SSE body with just `response.created` + `response.failed`, for errors Codex only understands in-stream. */
|
|
86
|
+
export declare function failedStream(ctx: ResponsesContext, code: string, message: string, headers: Headers): Response;
|
|
87
|
+
/**
|
|
88
|
+
* The one shape of a final error on /v1/responses: HTTP 400, `code: "invalid_prompt"`
|
|
89
|
+
* (or `model_not_found`, the other code Codex displays), `x-should-retry: false`, the
|
|
90
|
+
* gateway's own code and status kept as `u1s1_code` / `u1s1_status`. Codex sends the
|
|
91
|
+
* turn once and shows `message`. Used for chat-pipeline errors (errorToResponses) and
|
|
92
|
+
* auth-stage refusals (auth.ts) alike.
|
|
93
|
+
*/
|
|
94
|
+
export declare function responsesFinalError(error: JsonRecord | null, status: number, message: string, headers?: Headers): Response;
|
|
95
|
+
/** Convert the Chat Completions handler's response into a Responses API response. */
|
|
96
|
+
export declare function openAIResponseToResponses(response: Response, ctx: ResponsesContext): Promise<Response>;
|
|
97
|
+
export {};
|