@khanglvm/llm-router 1.3.1 → 2.0.0-beta.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/CHANGELOG.md +39 -0
- package/README.md +337 -41
- package/package.json +19 -3
- package/src/cli/router-module.js +7331 -3805
- package/src/cli/wrangler-toml.js +1 -1
- package/src/cli-entry.js +162 -24
- package/src/node/amp-client-config.js +426 -0
- package/src/node/coding-tool-config.js +763 -0
- package/src/node/config-store.js +49 -18
- package/src/node/instance-state.js +213 -12
- package/src/node/listen-port.js +5 -37
- package/src/node/local-server-settings.js +122 -0
- package/src/node/local-server.js +3 -2
- package/src/node/provider-probe.js +13 -0
- package/src/node/start-command.js +282 -40
- package/src/node/startup-manager.js +64 -29
- package/src/node/web-command.js +106 -0
- package/src/node/web-console-assets.js +26 -0
- package/src/node/web-console-client.js +56 -0
- package/src/node/web-console-dev-assets.js +258 -0
- package/src/node/web-console-server.js +3146 -0
- package/src/node/web-console-styles.generated.js +1 -0
- package/src/node/web-console-ui/config-editor-utils.js +616 -0
- package/src/node/web-console-ui/lib/utils.js +6 -0
- package/src/node/web-console-ui/rate-limit-utils.js +144 -0
- package/src/node/web-console-ui/select-search-utils.js +36 -0
- package/src/runtime/codex-request-transformer.js +46 -5
- package/src/runtime/codex-response-transformer.js +268 -35
- package/src/runtime/config.js +1394 -35
- package/src/runtime/handler/amp-gemini.js +913 -0
- package/src/runtime/handler/amp-response.js +308 -0
- package/src/runtime/handler/amp.js +290 -0
- package/src/runtime/handler/auth.js +17 -2
- package/src/runtime/handler/provider-call.js +168 -50
- package/src/runtime/handler/provider-translation.js +937 -26
- package/src/runtime/handler/request.js +149 -6
- package/src/runtime/handler/route-debug.js +22 -1
- package/src/runtime/handler.js +449 -9
- package/src/runtime/subscription-auth.js +1 -6
- package/src/shared/local-router-defaults.js +62 -0
- package/src/translator/index.js +3 -1
- package/src/translator/request/openai-to-claude.js +217 -6
- package/src/translator/response/openai-to-claude.js +206 -58
|
@@ -30,7 +30,7 @@ function normalizeTextContent(content) {
|
|
|
30
30
|
const text = [];
|
|
31
31
|
for (const part of content) {
|
|
32
32
|
if (!part || typeof part !== "object") continue;
|
|
33
|
-
if ((part.type === "text" || part.type === "input_text") && typeof part.text === "string") {
|
|
33
|
+
if ((part.type === "text" || part.type === "input_text" || part.type === "output_text") && typeof part.text === "string") {
|
|
34
34
|
text.push(part.text);
|
|
35
35
|
}
|
|
36
36
|
}
|
|
@@ -104,6 +104,179 @@ function convertOpenAIContentToClaudeBlocks(content) {
|
|
|
104
104
|
return blocks;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
function appendClaudeMessage(messages, role, blocks) {
|
|
108
|
+
if (!Array.isArray(blocks) || blocks.length === 0) return;
|
|
109
|
+
const normalizedRole = role === "assistant" ? "assistant" : "user";
|
|
110
|
+
const lastMessage = messages[messages.length - 1];
|
|
111
|
+
if (lastMessage?.role === normalizedRole && Array.isArray(lastMessage.content)) {
|
|
112
|
+
lastMessage.content.push(...blocks);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
messages.push({
|
|
116
|
+
role: normalizedRole,
|
|
117
|
+
content: blocks
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function convertResponsesMessageContentToClaudeBlocks(content) {
|
|
122
|
+
if (typeof content === "string") {
|
|
123
|
+
return content ? [{ type: "text", text: content }] : [];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!Array.isArray(content)) {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const blocks = [];
|
|
131
|
+
for (const part of content) {
|
|
132
|
+
if (!part || typeof part !== "object") continue;
|
|
133
|
+
|
|
134
|
+
if ((part.type === "text" || part.type === "input_text" || part.type === "output_text") && typeof part.text === "string") {
|
|
135
|
+
const cacheControl = cloneCacheControl(part.cache_control);
|
|
136
|
+
blocks.push({
|
|
137
|
+
type: "text",
|
|
138
|
+
text: part.text,
|
|
139
|
+
...(cacheControl ? { cache_control: cacheControl } : {})
|
|
140
|
+
});
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const rawImageUrl = typeof part.image_url === "string"
|
|
145
|
+
? part.image_url
|
|
146
|
+
: part.image_url?.url;
|
|
147
|
+
if ((part.type === "image_url" || part.type === "input_image") && typeof rawImageUrl === "string" && rawImageUrl.trim()) {
|
|
148
|
+
const parsed = parseDataUrl(rawImageUrl.trim());
|
|
149
|
+
const cacheControl = cloneCacheControl(part.cache_control);
|
|
150
|
+
if (parsed) {
|
|
151
|
+
blocks.push({
|
|
152
|
+
type: "image",
|
|
153
|
+
source: {
|
|
154
|
+
type: "base64",
|
|
155
|
+
media_type: parsed.mediaType,
|
|
156
|
+
data: parsed.data
|
|
157
|
+
},
|
|
158
|
+
...(cacheControl ? { cache_control: cacheControl } : {})
|
|
159
|
+
});
|
|
160
|
+
} else {
|
|
161
|
+
blocks.push({
|
|
162
|
+
type: "text",
|
|
163
|
+
text: `[image_url:${rawImageUrl.trim()}]`,
|
|
164
|
+
...(cacheControl ? { cache_control: cacheControl } : {})
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const rawFileUrl = typeof part.file_url === "string"
|
|
171
|
+
? part.file_url
|
|
172
|
+
: (typeof part.url === "string" ? part.url : "");
|
|
173
|
+
if (part.type === "input_file" && rawFileUrl.trim()) {
|
|
174
|
+
const cacheControl = cloneCacheControl(part.cache_control);
|
|
175
|
+
blocks.push({
|
|
176
|
+
type: "text",
|
|
177
|
+
text: `[input_file:${rawFileUrl.trim()}]`,
|
|
178
|
+
...(cacheControl ? { cache_control: cacheControl } : {})
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return blocks;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function normalizeResponseInputArray(input) {
|
|
187
|
+
if (Array.isArray(input)) return input;
|
|
188
|
+
if (typeof input === "string" && input.trim()) {
|
|
189
|
+
return [{
|
|
190
|
+
type: "message",
|
|
191
|
+
role: "user",
|
|
192
|
+
content: [{ type: "input_text", text: input.trim() }]
|
|
193
|
+
}];
|
|
194
|
+
}
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function extractResponsesSystem(body) {
|
|
199
|
+
const blocks = [];
|
|
200
|
+
const pushText = (text) => {
|
|
201
|
+
if (typeof text !== "string" || !text.trim()) return;
|
|
202
|
+
blocks.push({ type: "text", text: text.trim() });
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
if (typeof body?.instructions === "string" && body.instructions.trim()) {
|
|
206
|
+
pushText(body.instructions);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const explicitSystem = body?.system;
|
|
210
|
+
if (typeof explicitSystem === "string" && explicitSystem.trim()) {
|
|
211
|
+
pushText(explicitSystem);
|
|
212
|
+
} else if (Array.isArray(explicitSystem)) {
|
|
213
|
+
for (const item of explicitSystem) {
|
|
214
|
+
if ((item?.type === "text" || item?.type === "input_text" || item?.type === "output_text") && typeof item.text === "string") {
|
|
215
|
+
pushText(item.text);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
for (const item of normalizeResponseInputArray(body?.input)) {
|
|
221
|
+
if (!item || typeof item !== "object") continue;
|
|
222
|
+
const role = String(item.role || "").trim().toLowerCase();
|
|
223
|
+
if (role !== "system" && role !== "developer") continue;
|
|
224
|
+
const text = normalizeTextContent(item.content);
|
|
225
|
+
if (text) pushText(text);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (blocks.length === 0) return "";
|
|
229
|
+
return blocks.map((block) => block.text).join("\n").trim();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function convertOpenAIResponsesInput(input) {
|
|
233
|
+
const messages = [];
|
|
234
|
+
const items = normalizeResponseInputArray(input);
|
|
235
|
+
let generatedToolIndex = 0;
|
|
236
|
+
|
|
237
|
+
for (const item of items) {
|
|
238
|
+
if (!item || typeof item !== "object") continue;
|
|
239
|
+
|
|
240
|
+
const itemType = String(item.type || (item.role ? "message" : "")).trim().toLowerCase();
|
|
241
|
+
if (itemType === "message") {
|
|
242
|
+
const role = String(item.role || "user").trim().toLowerCase();
|
|
243
|
+
if (role === "system" || role === "developer") {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const blocks = convertResponsesMessageContentToClaudeBlocks(item.content);
|
|
247
|
+
if (blocks.length > 0) {
|
|
248
|
+
appendClaudeMessage(messages, role === "assistant" ? "assistant" : "user", blocks);
|
|
249
|
+
}
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (itemType === "function_call") {
|
|
254
|
+
const callId = String(item.call_id || item.id || `tool_call_${generatedToolIndex += 1}`).trim();
|
|
255
|
+
const name = String(item.name || "tool").trim() || "tool";
|
|
256
|
+
appendClaudeMessage(messages, "assistant", [{
|
|
257
|
+
type: "tool_use",
|
|
258
|
+
id: callId,
|
|
259
|
+
name,
|
|
260
|
+
input: safeJsonParse(item.arguments, {})
|
|
261
|
+
}]);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (itemType === "function_call_output") {
|
|
266
|
+
const toolUseId = String(item.call_id || item.tool_call_id || item.id || "").trim();
|
|
267
|
+
if (!toolUseId) continue;
|
|
268
|
+
const content = normalizeTextContent(item.output ?? item.content);
|
|
269
|
+
appendClaudeMessage(messages, "user", [{
|
|
270
|
+
type: "tool_result",
|
|
271
|
+
tool_use_id: toolUseId,
|
|
272
|
+
content
|
|
273
|
+
}]);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return messages;
|
|
278
|
+
}
|
|
279
|
+
|
|
107
280
|
function mapToolChoice(choice) {
|
|
108
281
|
if (!choice) return undefined;
|
|
109
282
|
if (typeof choice === "string") {
|
|
@@ -251,21 +424,47 @@ function normalizeToolInputSchema(schema) {
|
|
|
251
424
|
return normalized;
|
|
252
425
|
}
|
|
253
426
|
|
|
427
|
+
function isOpenAIWebSearchToolType(type) {
|
|
428
|
+
const normalized = String(type || "").trim().toLowerCase();
|
|
429
|
+
if (!normalized) return false;
|
|
430
|
+
return normalized === "web_search" || normalized.startsWith("web_search_preview");
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function convertOpenAIWebSearchTool(tool) {
|
|
434
|
+
if (!tool || typeof tool !== "object") return null;
|
|
435
|
+
const name = typeof tool.name === "string" && tool.name.trim()
|
|
436
|
+
? tool.name.trim()
|
|
437
|
+
: "web_search";
|
|
438
|
+
const maxUses = Number(tool.max_uses);
|
|
439
|
+
return {
|
|
440
|
+
type: "web_search_20250305",
|
|
441
|
+
name,
|
|
442
|
+
...(Number.isFinite(maxUses) && maxUses > 0 ? { max_uses: Math.trunc(maxUses) } : {})
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
254
446
|
/**
|
|
255
447
|
* Convert OpenAI chat completion request to Claude messages request.
|
|
256
448
|
*/
|
|
257
449
|
export function openAIToClaudeRequest(model, body, stream = false) {
|
|
258
|
-
const
|
|
450
|
+
const isResponsesPayload = body?.input !== undefined || body?.instructions !== undefined;
|
|
451
|
+
const messages = isResponsesPayload
|
|
452
|
+
? convertOpenAIResponsesInput(body?.input)
|
|
453
|
+
: convertOpenAIMessages(Array.isArray(body?.messages) ? body.messages : []);
|
|
259
454
|
const result = {
|
|
260
455
|
model,
|
|
261
|
-
messages
|
|
456
|
+
messages,
|
|
262
457
|
stream: Boolean(stream),
|
|
263
|
-
max_tokens: Number.isFinite(body?.
|
|
458
|
+
max_tokens: Number.isFinite(body?.max_output_tokens)
|
|
459
|
+
? Number(body.max_output_tokens)
|
|
460
|
+
: (Number.isFinite(body?.max_tokens)
|
|
264
461
|
? Number(body.max_tokens)
|
|
265
|
-
: (Number.isFinite(body?.max_completion_tokens) ? Number(body.max_completion_tokens) : DEFAULT_MAX_TOKENS)
|
|
462
|
+
: (Number.isFinite(body?.max_completion_tokens) ? Number(body.max_completion_tokens) : DEFAULT_MAX_TOKENS))
|
|
266
463
|
};
|
|
267
464
|
|
|
268
|
-
const system =
|
|
465
|
+
const system = isResponsesPayload
|
|
466
|
+
? extractResponsesSystem(body)
|
|
467
|
+
: normalizeSystemText(Array.isArray(body?.messages) ? body.messages : [], body?.system);
|
|
269
468
|
if (system && ((Array.isArray(system) && system.length > 0) || (typeof system === "string" && system.trim()))) {
|
|
270
469
|
result.system = system;
|
|
271
470
|
}
|
|
@@ -278,6 +477,9 @@ export function openAIToClaudeRequest(model, body, stream = false) {
|
|
|
278
477
|
result.tools = body.tools
|
|
279
478
|
.map((tool) => {
|
|
280
479
|
if (!tool || typeof tool !== "object") return null;
|
|
480
|
+
if (isOpenAIWebSearchToolType(tool.type)) {
|
|
481
|
+
return convertOpenAIWebSearchTool(tool);
|
|
482
|
+
}
|
|
281
483
|
if (tool.type === "function" && tool.function) {
|
|
282
484
|
const cacheControl = cloneCacheControl(tool.cache_control || tool.function?.cache_control);
|
|
283
485
|
return {
|
|
@@ -287,6 +489,15 @@ export function openAIToClaudeRequest(model, body, stream = false) {
|
|
|
287
489
|
...(cacheControl ? { cache_control: cacheControl } : {})
|
|
288
490
|
};
|
|
289
491
|
}
|
|
492
|
+
if (tool.type === "function" && tool.name) {
|
|
493
|
+
const cacheControl = cloneCacheControl(tool.cache_control);
|
|
494
|
+
return {
|
|
495
|
+
name: tool.name,
|
|
496
|
+
description: tool.description,
|
|
497
|
+
input_schema: normalizeToolInputSchema(tool.parameters),
|
|
498
|
+
...(cacheControl ? { cache_control: cacheControl } : {})
|
|
499
|
+
};
|
|
500
|
+
}
|
|
290
501
|
if (tool.name) {
|
|
291
502
|
const cacheControl = cloneCacheControl(tool.cache_control);
|
|
292
503
|
return {
|
|
@@ -26,26 +26,7 @@ export function openaiToClaudeResponse(chunk, state) {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
// First chunk - send message_start
|
|
29
|
-
|
|
30
|
-
state.messageStartSent = true;
|
|
31
|
-
state.messageId = chunk.id?.replace("chatcmpl-", "") || `msg_${Date.now()}`;
|
|
32
|
-
state.model = chunk.model || "unknown";
|
|
33
|
-
state.nextBlockIndex = 0;
|
|
34
|
-
|
|
35
|
-
results.push({
|
|
36
|
-
type: "message_start",
|
|
37
|
-
message: {
|
|
38
|
-
id: state.messageId,
|
|
39
|
-
type: "message",
|
|
40
|
-
role: "assistant",
|
|
41
|
-
model: state.model,
|
|
42
|
-
content: [],
|
|
43
|
-
stop_reason: null,
|
|
44
|
-
stop_sequence: null,
|
|
45
|
-
usage: { input_tokens: 0, output_tokens: 0 }
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
}
|
|
29
|
+
ensureMessageStart(state, results, chunk);
|
|
49
30
|
|
|
50
31
|
// Handle thinking/reasoning content
|
|
51
32
|
const reasoningContent = delta?.reasoning_content || delta?.reasoning;
|
|
@@ -70,7 +51,8 @@ export function openaiToClaudeResponse(chunk, state) {
|
|
|
70
51
|
}
|
|
71
52
|
|
|
72
53
|
// Handle regular content
|
|
73
|
-
|
|
54
|
+
const textDelta = normalizeTextDelta(delta?.content);
|
|
55
|
+
if (textDelta) {
|
|
74
56
|
stopThinkingBlock(state, results);
|
|
75
57
|
|
|
76
58
|
if (!state.textBlockStarted) {
|
|
@@ -87,7 +69,7 @@ export function openaiToClaudeResponse(chunk, state) {
|
|
|
87
69
|
results.push({
|
|
88
70
|
type: "content_block_delta",
|
|
89
71
|
index: state.textBlockIndex,
|
|
90
|
-
delta: { type: "text_delta", text:
|
|
72
|
+
delta: { type: "text_delta", text: textDelta }
|
|
91
73
|
});
|
|
92
74
|
}
|
|
93
75
|
|
|
@@ -95,32 +77,12 @@ export function openaiToClaudeResponse(chunk, state) {
|
|
|
95
77
|
if (delta?.tool_calls) {
|
|
96
78
|
for (const tc of delta.tool_calls) {
|
|
97
79
|
const idx = tc.index ?? 0;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const toolBlockIndex = state.nextBlockIndex++;
|
|
104
|
-
state.toolCalls.set(idx, {
|
|
105
|
-
id: tc.id,
|
|
106
|
-
name: tc.function?.name || "",
|
|
107
|
-
blockIndex: toolBlockIndex
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
results.push({
|
|
111
|
-
type: "content_block_start",
|
|
112
|
-
index: toolBlockIndex,
|
|
113
|
-
content_block: {
|
|
114
|
-
type: "tool_use",
|
|
115
|
-
id: tc.id,
|
|
116
|
-
name: tc.function?.name || "",
|
|
117
|
-
input: {}
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
}
|
|
80
|
+
const toolInfo = ensureToolUseBlock(state, results, idx, {
|
|
81
|
+
id: tc.id,
|
|
82
|
+
name: tc.function?.name
|
|
83
|
+
});
|
|
121
84
|
|
|
122
85
|
if (tc.function?.arguments) {
|
|
123
|
-
const toolInfo = state.toolCalls.get(idx);
|
|
124
86
|
if (toolInfo) {
|
|
125
87
|
results.push({
|
|
126
88
|
type: "content_block_delta",
|
|
@@ -132,31 +94,215 @@ export function openaiToClaudeResponse(chunk, state) {
|
|
|
132
94
|
}
|
|
133
95
|
}
|
|
134
96
|
|
|
97
|
+
if (delta?.function_call && typeof delta.function_call === "object") {
|
|
98
|
+
const toolInfo = ensureToolUseBlock(state, results, 0, {
|
|
99
|
+
id: delta.function_call.id,
|
|
100
|
+
name: delta.function_call.name
|
|
101
|
+
});
|
|
102
|
+
if (toolInfo && delta.function_call.arguments) {
|
|
103
|
+
results.push({
|
|
104
|
+
type: "content_block_delta",
|
|
105
|
+
index: toolInfo.blockIndex,
|
|
106
|
+
delta: { type: "input_json_delta", partial_json: delta.function_call.arguments }
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
emitFinalChoiceMessageFallback(choice?.message, state, results);
|
|
112
|
+
|
|
135
113
|
// Finish
|
|
136
114
|
if (choice.finish_reason) {
|
|
137
|
-
|
|
138
|
-
|
|
115
|
+
state.finishReason = choice.finish_reason;
|
|
116
|
+
results.push(...finalizeOpenAIToClaudeStream(state));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return results.length > 0 ? results : null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function normalizeTextDelta(content) {
|
|
123
|
+
if (typeof content === "string") return content;
|
|
124
|
+
if (!Array.isArray(content)) return "";
|
|
125
|
+
return content
|
|
126
|
+
.map((part) => {
|
|
127
|
+
if (typeof part === "string") return part;
|
|
128
|
+
if (!part || typeof part !== "object") return "";
|
|
129
|
+
if ((part.type === "text" || part.type === "output_text") && typeof part.text === "string") {
|
|
130
|
+
return part.text;
|
|
131
|
+
}
|
|
132
|
+
return "";
|
|
133
|
+
})
|
|
134
|
+
.join("");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function ensureToolUseBlock(state, results, index, { id, name } = {}) {
|
|
138
|
+
if (!state?.toolCalls || !(state.toolCalls instanceof Map)) return null;
|
|
139
|
+
const normalizedIndex = Number.isFinite(index) ? Number(index) : 0;
|
|
140
|
+
const existing = state.toolCalls.get(normalizedIndex);
|
|
141
|
+
if (existing) return existing;
|
|
142
|
+
|
|
143
|
+
const toolName = typeof name === "string" && name.trim() ? name.trim() : "tool";
|
|
144
|
+
const toolId = typeof id === "string" && id.trim()
|
|
145
|
+
? id.trim()
|
|
146
|
+
: `tool_${state.messageId || "call"}_${normalizedIndex}`;
|
|
147
|
+
|
|
148
|
+
stopThinkingBlock(state, results);
|
|
149
|
+
stopTextBlock(state, results);
|
|
150
|
+
|
|
151
|
+
const toolBlockIndex = state.nextBlockIndex++;
|
|
152
|
+
const toolInfo = {
|
|
153
|
+
id: toolId,
|
|
154
|
+
name: toolName,
|
|
155
|
+
blockIndex: toolBlockIndex,
|
|
156
|
+
closed: false
|
|
157
|
+
};
|
|
158
|
+
state.toolCalls.set(normalizedIndex, toolInfo);
|
|
159
|
+
|
|
160
|
+
results.push({
|
|
161
|
+
type: "content_block_start",
|
|
162
|
+
index: toolBlockIndex,
|
|
163
|
+
content_block: {
|
|
164
|
+
type: "tool_use",
|
|
165
|
+
id: toolId,
|
|
166
|
+
name: toolName,
|
|
167
|
+
input: {}
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return toolInfo;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function normalizeMessageToolCalls(message) {
|
|
175
|
+
const toolCalls = Array.isArray(message?.tool_calls)
|
|
176
|
+
? message.tool_calls.filter((call) => call && typeof call === "object")
|
|
177
|
+
: [];
|
|
178
|
+
|
|
179
|
+
if (message?.function_call && typeof message.function_call === "object") {
|
|
180
|
+
toolCalls.push({
|
|
181
|
+
id: message.function_call.id,
|
|
182
|
+
function: {
|
|
183
|
+
name: message.function_call.name,
|
|
184
|
+
arguments: message.function_call.arguments
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
139
188
|
|
|
140
|
-
|
|
141
|
-
|
|
189
|
+
return toolCalls;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function emitTextDelta(text, state, results) {
|
|
193
|
+
if (!text) return;
|
|
194
|
+
stopThinkingBlock(state, results);
|
|
195
|
+
|
|
196
|
+
if (!state.textBlockStarted) {
|
|
197
|
+
state.textBlockIndex = state.nextBlockIndex++;
|
|
198
|
+
state.textBlockStarted = true;
|
|
199
|
+
state.textBlockClosed = false;
|
|
200
|
+
results.push({
|
|
201
|
+
type: "content_block_start",
|
|
202
|
+
index: state.textBlockIndex,
|
|
203
|
+
content_block: { type: "text", text: "" }
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
results.push({
|
|
208
|
+
type: "content_block_delta",
|
|
209
|
+
index: state.textBlockIndex,
|
|
210
|
+
delta: { type: "text_delta", text }
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function emitFinalChoiceMessageFallback(message, state, results) {
|
|
215
|
+
if (!message || typeof message !== "object") return;
|
|
216
|
+
|
|
217
|
+
const hasTextOutput = state.textBlockStarted || state.textBlockClosed;
|
|
218
|
+
if (!hasTextOutput) {
|
|
219
|
+
const fallbackText = normalizeTextDelta(message.content)
|
|
220
|
+
|| (typeof message.refusal === "string" ? message.refusal : "");
|
|
221
|
+
emitTextDelta(fallbackText, state, results);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const hasToolOutput = state.toolCalls instanceof Map && state.toolCalls.size > 0;
|
|
225
|
+
if (hasToolOutput) return;
|
|
226
|
+
|
|
227
|
+
const toolCalls = normalizeMessageToolCalls(message);
|
|
228
|
+
for (let index = 0; index < toolCalls.length; index += 1) {
|
|
229
|
+
const toolCall = toolCalls[index];
|
|
230
|
+
if (!toolCall || typeof toolCall !== "object") continue;
|
|
231
|
+
const toolInfo = ensureToolUseBlock(state, results, toolCall.index ?? index, {
|
|
232
|
+
id: toolCall.id,
|
|
233
|
+
name: toolCall.function?.name
|
|
234
|
+
});
|
|
235
|
+
if (toolInfo && toolCall.function?.arguments) {
|
|
142
236
|
results.push({
|
|
143
|
-
type: "
|
|
144
|
-
index: toolInfo.blockIndex
|
|
237
|
+
type: "content_block_delta",
|
|
238
|
+
index: toolInfo.blockIndex,
|
|
239
|
+
delta: { type: "input_json_delta", partial_json: toolCall.function.arguments }
|
|
145
240
|
});
|
|
146
241
|
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
147
244
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
245
|
+
function ensureMessageStart(state, results, chunk = undefined) {
|
|
246
|
+
if (state.messageStartSent) return;
|
|
247
|
+
state.messageStartSent = true;
|
|
248
|
+
state.messageId = chunk?.id?.replace("chatcmpl-", "") || state.messageId || `msg_${Date.now()}`;
|
|
249
|
+
state.model = chunk?.model || state.model || "unknown";
|
|
250
|
+
state.nextBlockIndex = Number.isFinite(state.nextBlockIndex) ? state.nextBlockIndex : 0;
|
|
251
|
+
|
|
252
|
+
results.push({
|
|
253
|
+
type: "message_start",
|
|
254
|
+
message: {
|
|
255
|
+
id: state.messageId,
|
|
256
|
+
type: "message",
|
|
257
|
+
role: "assistant",
|
|
258
|
+
model: state.model,
|
|
259
|
+
content: [],
|
|
260
|
+
stop_reason: null,
|
|
261
|
+
stop_sequence: null,
|
|
262
|
+
usage: { input_tokens: 0, output_tokens: 0 }
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function finalizeOpenAIToClaudeStream(state, { force = false } = {}) {
|
|
268
|
+
const results = [];
|
|
269
|
+
if (!state || (!state.messageStartSent && !force)) return results;
|
|
270
|
+
|
|
271
|
+
if (!state.messageStartSent) {
|
|
272
|
+
ensureMessageStart(state, results);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
stopThinkingBlock(state, results);
|
|
276
|
+
stopTextBlock(state, results);
|
|
277
|
+
|
|
278
|
+
for (const [, toolInfo] of state.toolCalls) {
|
|
279
|
+
if (toolInfo?.closed) continue;
|
|
280
|
+
results.push({
|
|
281
|
+
type: "content_block_stop",
|
|
282
|
+
index: toolInfo.blockIndex
|
|
283
|
+
});
|
|
284
|
+
toolInfo.closed = true;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (!state.messageDeltaSent) {
|
|
288
|
+
const hasToolCalls = state.toolCalls instanceof Map && state.toolCalls.size > 0;
|
|
289
|
+
const normalizedFinishReason = hasToolCalls && (!state.finishReason || state.finishReason === "stop")
|
|
290
|
+
? "tool_calls"
|
|
291
|
+
: (state.finishReason || "stop");
|
|
151
292
|
results.push({
|
|
152
293
|
type: "message_delta",
|
|
153
|
-
delta: { stop_reason: convertFinishReason(
|
|
154
|
-
usage:
|
|
294
|
+
delta: { stop_reason: convertFinishReason(normalizedFinishReason) },
|
|
295
|
+
usage: state.usage || { input_tokens: 0, output_tokens: 0 }
|
|
155
296
|
});
|
|
297
|
+
state.messageDeltaSent = true;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (!state.messageStopSent) {
|
|
156
301
|
results.push({ type: "message_stop" });
|
|
302
|
+
state.messageStopSent = true;
|
|
157
303
|
}
|
|
158
304
|
|
|
159
|
-
return results
|
|
305
|
+
return results;
|
|
160
306
|
}
|
|
161
307
|
|
|
162
308
|
/**
|
|
@@ -169,6 +315,7 @@ function stopThinkingBlock(state, results) {
|
|
|
169
315
|
index: state.thinkingBlockIndex
|
|
170
316
|
});
|
|
171
317
|
state.thinkingBlockStarted = false;
|
|
318
|
+
state.thinkingBlockIndex = null;
|
|
172
319
|
}
|
|
173
320
|
|
|
174
321
|
/**
|
|
@@ -191,6 +338,7 @@ function convertFinishReason(reason) {
|
|
|
191
338
|
switch (reason) {
|
|
192
339
|
case "stop": return "end_turn";
|
|
193
340
|
case "length": return "max_tokens";
|
|
341
|
+
case "function_call": return "tool_use";
|
|
194
342
|
case "tool_calls": return "tool_use";
|
|
195
343
|
default: return "end_turn";
|
|
196
344
|
}
|