@khanglvm/llm-router 1.0.5
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/.env.test-suite.example +19 -0
- package/README.md +230 -0
- package/package.json +26 -0
- package/src/cli/router-module.js +3987 -0
- package/src/cli-entry.js +144 -0
- package/src/index.js +18 -0
- package/src/node/config-store.js +74 -0
- package/src/node/config-workflows.js +245 -0
- package/src/node/instance-state.js +206 -0
- package/src/node/local-server.js +294 -0
- package/src/node/provider-probe.js +905 -0
- package/src/node/start-command.js +498 -0
- package/src/node/startup-manager.js +369 -0
- package/src/runtime/config.js +655 -0
- package/src/runtime/handler/auth.js +32 -0
- package/src/runtime/handler/config-loading.js +45 -0
- package/src/runtime/handler/fallback.js +424 -0
- package/src/runtime/handler/http.js +71 -0
- package/src/runtime/handler/network-guards.js +137 -0
- package/src/runtime/handler/provider-call.js +245 -0
- package/src/runtime/handler/provider-translation.js +232 -0
- package/src/runtime/handler/request.js +194 -0
- package/src/runtime/handler/utils.js +41 -0
- package/src/runtime/handler.js +301 -0
- package/src/translator/formats.js +7 -0
- package/src/translator/index.js +73 -0
- package/src/translator/request/claude-to-openai.js +228 -0
- package/src/translator/request/openai-to-claude.js +241 -0
- package/src/translator/response/claude-to-openai.js +204 -0
- package/src/translator/response/openai-to-claude.js +197 -0
- package/wrangler.toml +20 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI -> Claude request translator.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const DEFAULT_MAX_TOKENS = 1024;
|
|
6
|
+
|
|
7
|
+
function safeJsonParse(raw, fallback = {}) {
|
|
8
|
+
if (typeof raw !== "string" || raw.length === 0) return fallback;
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(raw);
|
|
11
|
+
} catch {
|
|
12
|
+
return fallback;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizeTextContent(content) {
|
|
17
|
+
if (typeof content === "string") return content;
|
|
18
|
+
if (Array.isArray(content)) {
|
|
19
|
+
const text = [];
|
|
20
|
+
for (const part of content) {
|
|
21
|
+
if (!part || typeof part !== "object") continue;
|
|
22
|
+
if ((part.type === "text" || part.type === "input_text") && typeof part.text === "string") {
|
|
23
|
+
text.push(part.text);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return text.join("\n");
|
|
27
|
+
}
|
|
28
|
+
return "";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseDataUrl(url) {
|
|
32
|
+
if (typeof url !== "string") return null;
|
|
33
|
+
const match = url.match(/^data:([^;]+);base64,(.+)$/);
|
|
34
|
+
if (!match) return null;
|
|
35
|
+
return {
|
|
36
|
+
mediaType: match[1],
|
|
37
|
+
data: match[2]
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function convertOpenAIContentToClaudeBlocks(content) {
|
|
42
|
+
if (typeof content === "string") {
|
|
43
|
+
return [{ type: "text", text: content }];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!Array.isArray(content)) {
|
|
47
|
+
return [{ type: "text", text: "" }];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const blocks = [];
|
|
51
|
+
for (const part of content) {
|
|
52
|
+
if (!part || typeof part !== "object") continue;
|
|
53
|
+
|
|
54
|
+
if ((part.type === "text" || part.type === "input_text") && typeof part.text === "string") {
|
|
55
|
+
blocks.push({ type: "text", text: part.text });
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (part.type === "image_url" && part.image_url?.url) {
|
|
60
|
+
const parsed = parseDataUrl(part.image_url.url);
|
|
61
|
+
if (parsed) {
|
|
62
|
+
blocks.push({
|
|
63
|
+
type: "image",
|
|
64
|
+
source: {
|
|
65
|
+
type: "base64",
|
|
66
|
+
media_type: parsed.mediaType,
|
|
67
|
+
data: parsed.data
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
} else {
|
|
71
|
+
// Claude image blocks do not accept remote URLs directly.
|
|
72
|
+
blocks.push({
|
|
73
|
+
type: "text",
|
|
74
|
+
text: `[image_url:${part.image_url.url}]`
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (blocks.length === 0) {
|
|
82
|
+
blocks.push({ type: "text", text: "" });
|
|
83
|
+
}
|
|
84
|
+
return blocks;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function mapToolChoice(choice) {
|
|
88
|
+
if (!choice) return undefined;
|
|
89
|
+
if (typeof choice === "string") {
|
|
90
|
+
if (choice === "required") return { type: "any" };
|
|
91
|
+
if (choice === "none") return { type: "auto" };
|
|
92
|
+
return { type: "auto" };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (choice.type === "function") {
|
|
96
|
+
return {
|
|
97
|
+
type: "tool",
|
|
98
|
+
name: choice.function?.name
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (choice.type === "auto") return { type: "auto" };
|
|
103
|
+
if (choice.type === "any") return { type: "any" };
|
|
104
|
+
if (choice.type === "tool") return { type: "tool", name: choice.name };
|
|
105
|
+
|
|
106
|
+
return { type: "auto" };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function normalizeSystemText(messages, explicitSystem) {
|
|
110
|
+
const parts = [];
|
|
111
|
+
if (typeof explicitSystem === "string" && explicitSystem.trim()) {
|
|
112
|
+
parts.push(explicitSystem);
|
|
113
|
+
} else if (Array.isArray(explicitSystem)) {
|
|
114
|
+
for (const item of explicitSystem) {
|
|
115
|
+
if (item?.type === "text" && typeof item.text === "string") {
|
|
116
|
+
parts.push(item.text);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const message of messages) {
|
|
122
|
+
if (message?.role !== "system") continue;
|
|
123
|
+
const text = normalizeTextContent(message.content);
|
|
124
|
+
if (text) parts.push(text);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return parts.join("\n").trim();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function convertOpenAIMessages(messages) {
|
|
131
|
+
const result = [];
|
|
132
|
+
|
|
133
|
+
for (const message of messages) {
|
|
134
|
+
if (!message || typeof message !== "object") continue;
|
|
135
|
+
|
|
136
|
+
if (message.role === "system") {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (message.role === "tool") {
|
|
141
|
+
const toolUseId = message.tool_call_id || message.tool_use_id;
|
|
142
|
+
if (!toolUseId) continue;
|
|
143
|
+
|
|
144
|
+
const content = normalizeTextContent(message.content);
|
|
145
|
+
result.push({
|
|
146
|
+
role: "user",
|
|
147
|
+
content: [
|
|
148
|
+
{
|
|
149
|
+
type: "tool_result",
|
|
150
|
+
tool_use_id: toolUseId,
|
|
151
|
+
content
|
|
152
|
+
}
|
|
153
|
+
]
|
|
154
|
+
});
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (message.role === "assistant") {
|
|
159
|
+
const contentBlocks = convertOpenAIContentToClaudeBlocks(message.content);
|
|
160
|
+
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
161
|
+
|
|
162
|
+
for (const call of toolCalls) {
|
|
163
|
+
if (!call || typeof call !== "object") continue;
|
|
164
|
+
contentBlocks.push({
|
|
165
|
+
type: "tool_use",
|
|
166
|
+
id: call.id || `tool_${Date.now()}`,
|
|
167
|
+
name: call.function?.name || "tool",
|
|
168
|
+
input: safeJsonParse(call.function?.arguments, {})
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
result.push({
|
|
173
|
+
role: "assistant",
|
|
174
|
+
content: contentBlocks
|
|
175
|
+
});
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// default user role
|
|
180
|
+
result.push({
|
|
181
|
+
role: "user",
|
|
182
|
+
content: convertOpenAIContentToClaudeBlocks(message.content)
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Convert OpenAI chat completion request to Claude messages request.
|
|
191
|
+
*/
|
|
192
|
+
export function openAIToClaudeRequest(model, body, stream = false) {
|
|
193
|
+
const messages = Array.isArray(body?.messages) ? body.messages : [];
|
|
194
|
+
const result = {
|
|
195
|
+
model,
|
|
196
|
+
messages: convertOpenAIMessages(messages),
|
|
197
|
+
stream: Boolean(stream),
|
|
198
|
+
max_tokens: Number.isFinite(body?.max_tokens)
|
|
199
|
+
? Number(body.max_tokens)
|
|
200
|
+
: (Number.isFinite(body?.max_completion_tokens) ? Number(body.max_completion_tokens) : DEFAULT_MAX_TOKENS)
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const system = normalizeSystemText(messages, body?.system);
|
|
204
|
+
if (system) {
|
|
205
|
+
result.system = system;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (body?.temperature !== undefined) {
|
|
209
|
+
result.temperature = body.temperature;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (Array.isArray(body?.tools)) {
|
|
213
|
+
result.tools = body.tools
|
|
214
|
+
.map((tool) => {
|
|
215
|
+
if (!tool || typeof tool !== "object") return null;
|
|
216
|
+
if (tool.type === "function" && tool.function) {
|
|
217
|
+
return {
|
|
218
|
+
name: tool.function.name,
|
|
219
|
+
description: tool.function.description,
|
|
220
|
+
input_schema: tool.function.parameters || { type: "object", properties: {} }
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
if (tool.name) {
|
|
224
|
+
return {
|
|
225
|
+
name: tool.name,
|
|
226
|
+
description: tool.description,
|
|
227
|
+
input_schema: tool.input_schema || { type: "object", properties: {} }
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
})
|
|
232
|
+
.filter(Boolean);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const mappedChoice = mapToolChoice(body?.tool_choice);
|
|
236
|
+
if (mappedChoice) {
|
|
237
|
+
result.tool_choice = mappedChoice;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude (Anthropic) -> OpenAI response translator (stream + non-stream helpers).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
function mapStopReason(reason) {
|
|
6
|
+
switch (reason) {
|
|
7
|
+
case "max_tokens":
|
|
8
|
+
return "length";
|
|
9
|
+
case "tool_use":
|
|
10
|
+
return "tool_calls";
|
|
11
|
+
case "end_turn":
|
|
12
|
+
case "stop_sequence":
|
|
13
|
+
default:
|
|
14
|
+
return "stop";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function toOpenAIUsage(usage) {
|
|
19
|
+
if (!usage || typeof usage !== "object") return undefined;
|
|
20
|
+
const prompt = usage.input_tokens || 0;
|
|
21
|
+
const completion = usage.output_tokens || 0;
|
|
22
|
+
return {
|
|
23
|
+
prompt_tokens: prompt,
|
|
24
|
+
completion_tokens: completion,
|
|
25
|
+
total_tokens: prompt + completion
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function stringifyToolInput(input) {
|
|
30
|
+
if (typeof input === "string") return input;
|
|
31
|
+
try {
|
|
32
|
+
return JSON.stringify(input || {});
|
|
33
|
+
} catch {
|
|
34
|
+
return "{}";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function claudeToOpenAINonStreamResponse(message) {
|
|
39
|
+
const contentBlocks = Array.isArray(message?.content) ? message.content : [];
|
|
40
|
+
const textParts = [];
|
|
41
|
+
const toolCalls = [];
|
|
42
|
+
|
|
43
|
+
for (let i = 0; i < contentBlocks.length; i += 1) {
|
|
44
|
+
const block = contentBlocks[i];
|
|
45
|
+
if (!block || typeof block !== "object") continue;
|
|
46
|
+
|
|
47
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
48
|
+
textParts.push(block.text);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (block.type === "tool_use") {
|
|
53
|
+
toolCalls.push({
|
|
54
|
+
id: block.id || `call_${i}`,
|
|
55
|
+
type: "function",
|
|
56
|
+
function: {
|
|
57
|
+
name: block.name || "tool",
|
|
58
|
+
arguments: stringifyToolInput(block.input)
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const text = textParts.join("");
|
|
65
|
+
const responseMessage = {
|
|
66
|
+
role: "assistant",
|
|
67
|
+
content: text.length > 0 ? text : null
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
if (toolCalls.length > 0) {
|
|
71
|
+
responseMessage.tool_calls = toolCalls;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
id: message?.id?.startsWith("chatcmpl_") ? message.id : `chatcmpl_${message?.id || Date.now()}`,
|
|
76
|
+
object: "chat.completion",
|
|
77
|
+
created: Math.floor(Date.now() / 1000),
|
|
78
|
+
model: message?.model || "unknown",
|
|
79
|
+
choices: [
|
|
80
|
+
{
|
|
81
|
+
index: 0,
|
|
82
|
+
message: responseMessage,
|
|
83
|
+
finish_reason: mapStopReason(message?.stop_reason)
|
|
84
|
+
}
|
|
85
|
+
],
|
|
86
|
+
usage: toOpenAIUsage(message?.usage)
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function initClaudeToOpenAIState() {
|
|
91
|
+
return {
|
|
92
|
+
chatId: `chatcmpl_${Date.now()}`,
|
|
93
|
+
created: Math.floor(Date.now() / 1000),
|
|
94
|
+
model: "unknown",
|
|
95
|
+
toolCallByBlockIndex: new Map(),
|
|
96
|
+
nextToolCallIndex: 0,
|
|
97
|
+
usage: undefined
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function makeChunk(state, delta = {}, finishReason = null, usage = undefined) {
|
|
102
|
+
const chunk = {
|
|
103
|
+
id: state.chatId,
|
|
104
|
+
object: "chat.completion.chunk",
|
|
105
|
+
created: state.created,
|
|
106
|
+
model: state.model || "unknown",
|
|
107
|
+
choices: [
|
|
108
|
+
{
|
|
109
|
+
index: 0,
|
|
110
|
+
delta,
|
|
111
|
+
finish_reason: finishReason
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
if (usage) {
|
|
117
|
+
chunk.usage = usage;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return chunk;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function claudeEventToOpenAIChunks(eventType, event, state) {
|
|
124
|
+
if (!event || typeof event !== "object") {
|
|
125
|
+
if (eventType === "message_stop") return ["[DONE]"];
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
switch (eventType) {
|
|
130
|
+
case "message_start": {
|
|
131
|
+
const message = event.message || {};
|
|
132
|
+
state.chatId = message.id?.startsWith("chatcmpl_") ? message.id : `chatcmpl_${message.id || Date.now()}`;
|
|
133
|
+
state.model = message.model || state.model || "unknown";
|
|
134
|
+
state.usage = toOpenAIUsage(message.usage);
|
|
135
|
+
return [makeChunk(state, { role: "assistant" }, null)];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
case "content_block_start": {
|
|
139
|
+
const block = event.content_block || {};
|
|
140
|
+
if (block.type !== "tool_use") return [];
|
|
141
|
+
|
|
142
|
+
const toolIndex = state.nextToolCallIndex++;
|
|
143
|
+
state.toolCallByBlockIndex.set(event.index, toolIndex);
|
|
144
|
+
|
|
145
|
+
return [
|
|
146
|
+
makeChunk(state, {
|
|
147
|
+
tool_calls: [
|
|
148
|
+
{
|
|
149
|
+
index: toolIndex,
|
|
150
|
+
id: block.id || `call_${toolIndex}`,
|
|
151
|
+
type: "function",
|
|
152
|
+
function: {
|
|
153
|
+
name: block.name || "tool",
|
|
154
|
+
arguments: ""
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
]
|
|
158
|
+
}, null)
|
|
159
|
+
];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
case "content_block_delta": {
|
|
163
|
+
const delta = event.delta || {};
|
|
164
|
+
if (delta.type === "text_delta") {
|
|
165
|
+
return [makeChunk(state, { content: delta.text || "" }, null)];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (delta.type === "input_json_delta") {
|
|
169
|
+
const toolIndex = state.toolCallByBlockIndex.get(event.index);
|
|
170
|
+
if (toolIndex === undefined) return [];
|
|
171
|
+
return [
|
|
172
|
+
makeChunk(state, {
|
|
173
|
+
tool_calls: [
|
|
174
|
+
{
|
|
175
|
+
index: toolIndex,
|
|
176
|
+
function: {
|
|
177
|
+
arguments: delta.partial_json || ""
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
]
|
|
181
|
+
}, null)
|
|
182
|
+
];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// thinking_delta and other Anthropic-specific blocks are ignored for OpenAI chat compat.
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
case "message_delta": {
|
|
190
|
+
const usage = toOpenAIUsage(event.usage);
|
|
191
|
+
if (usage) state.usage = usage;
|
|
192
|
+
const stopReason = event.delta?.stop_reason ? mapStopReason(event.delta.stop_reason) : null;
|
|
193
|
+
if (!stopReason && !usage) return [];
|
|
194
|
+
return [makeChunk(state, {}, stopReason, usage)];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
case "message_stop":
|
|
198
|
+
return ["[DONE]"];
|
|
199
|
+
|
|
200
|
+
default:
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI -> Claude (Anthropic) Response Translator
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { FORMATS } from "../formats.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Convert OpenAI stream chunk to Claude format
|
|
9
|
+
*/
|
|
10
|
+
export function openaiToClaudeResponse(chunk, state) {
|
|
11
|
+
if (!chunk || !chunk.choices?.[0]) return null;
|
|
12
|
+
|
|
13
|
+
const results = [];
|
|
14
|
+
const choice = chunk.choices[0];
|
|
15
|
+
const delta = choice.delta;
|
|
16
|
+
|
|
17
|
+
// Track usage
|
|
18
|
+
if (chunk.usage && typeof chunk.usage === "object") {
|
|
19
|
+
const promptTokens = chunk.usage.prompt_tokens || 0;
|
|
20
|
+
const outputTokens = chunk.usage.completion_tokens || 0;
|
|
21
|
+
|
|
22
|
+
state.usage = {
|
|
23
|
+
input_tokens: promptTokens,
|
|
24
|
+
output_tokens: outputTokens
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// First chunk - send message_start
|
|
29
|
+
if (!state.messageStartSent) {
|
|
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
|
+
}
|
|
49
|
+
|
|
50
|
+
// Handle thinking/reasoning content
|
|
51
|
+
const reasoningContent = delta?.reasoning_content || delta?.reasoning;
|
|
52
|
+
if (reasoningContent) {
|
|
53
|
+
stopTextBlock(state, results);
|
|
54
|
+
|
|
55
|
+
if (!state.thinkingBlockStarted) {
|
|
56
|
+
state.thinkingBlockIndex = state.nextBlockIndex++;
|
|
57
|
+
state.thinkingBlockStarted = true;
|
|
58
|
+
results.push({
|
|
59
|
+
type: "content_block_start",
|
|
60
|
+
index: state.thinkingBlockIndex,
|
|
61
|
+
content_block: { type: "thinking", thinking: "" }
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
results.push({
|
|
66
|
+
type: "content_block_delta",
|
|
67
|
+
index: state.thinkingBlockIndex,
|
|
68
|
+
delta: { type: "thinking_delta", thinking: reasoningContent }
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Handle regular content
|
|
73
|
+
if (delta?.content) {
|
|
74
|
+
stopThinkingBlock(state, results);
|
|
75
|
+
|
|
76
|
+
if (!state.textBlockStarted) {
|
|
77
|
+
state.textBlockIndex = state.nextBlockIndex++;
|
|
78
|
+
state.textBlockStarted = true;
|
|
79
|
+
state.textBlockClosed = false;
|
|
80
|
+
results.push({
|
|
81
|
+
type: "content_block_start",
|
|
82
|
+
index: state.textBlockIndex,
|
|
83
|
+
content_block: { type: "text", text: "" }
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
results.push({
|
|
88
|
+
type: "content_block_delta",
|
|
89
|
+
index: state.textBlockIndex,
|
|
90
|
+
delta: { type: "text_delta", text: delta.content }
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Tool calls
|
|
95
|
+
if (delta?.tool_calls) {
|
|
96
|
+
for (const tc of delta.tool_calls) {
|
|
97
|
+
const idx = tc.index ?? 0;
|
|
98
|
+
|
|
99
|
+
if (tc.id) {
|
|
100
|
+
stopThinkingBlock(state, results);
|
|
101
|
+
stopTextBlock(state, results);
|
|
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
|
+
}
|
|
121
|
+
|
|
122
|
+
if (tc.function?.arguments) {
|
|
123
|
+
const toolInfo = state.toolCalls.get(idx);
|
|
124
|
+
if (toolInfo) {
|
|
125
|
+
results.push({
|
|
126
|
+
type: "content_block_delta",
|
|
127
|
+
index: toolInfo.blockIndex,
|
|
128
|
+
delta: { type: "input_json_delta", partial_json: tc.function.arguments }
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Finish
|
|
136
|
+
if (choice.finish_reason) {
|
|
137
|
+
stopThinkingBlock(state, results);
|
|
138
|
+
stopTextBlock(state, results);
|
|
139
|
+
|
|
140
|
+
// Stop all tool blocks
|
|
141
|
+
for (const [, toolInfo] of state.toolCalls) {
|
|
142
|
+
results.push({
|
|
143
|
+
type: "content_block_stop",
|
|
144
|
+
index: toolInfo.blockIndex
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
state.finishReason = choice.finish_reason;
|
|
149
|
+
|
|
150
|
+
const finalUsage = state.usage || { input_tokens: 0, output_tokens: 0 };
|
|
151
|
+
results.push({
|
|
152
|
+
type: "message_delta",
|
|
153
|
+
delta: { stop_reason: convertFinishReason(choice.finish_reason) },
|
|
154
|
+
usage: finalUsage
|
|
155
|
+
});
|
|
156
|
+
results.push({ type: "message_stop" });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return results.length > 0 ? results : null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Stop thinking block
|
|
164
|
+
*/
|
|
165
|
+
function stopThinkingBlock(state, results) {
|
|
166
|
+
if (!state.thinkingBlockStarted) return;
|
|
167
|
+
results.push({
|
|
168
|
+
type: "content_block_stop",
|
|
169
|
+
index: state.thinkingBlockIndex
|
|
170
|
+
});
|
|
171
|
+
state.thinkingBlockStarted = false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Stop text block
|
|
176
|
+
*/
|
|
177
|
+
function stopTextBlock(state, results) {
|
|
178
|
+
if (!state.textBlockStarted || state.textBlockClosed) return;
|
|
179
|
+
state.textBlockClosed = true;
|
|
180
|
+
results.push({
|
|
181
|
+
type: "content_block_stop",
|
|
182
|
+
index: state.textBlockIndex
|
|
183
|
+
});
|
|
184
|
+
state.textBlockStarted = false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Convert finish reason
|
|
189
|
+
*/
|
|
190
|
+
function convertFinishReason(reason) {
|
|
191
|
+
switch (reason) {
|
|
192
|
+
case "stop": return "end_turn";
|
|
193
|
+
case "length": return "max_tokens";
|
|
194
|
+
case "tool_calls": return "tool_use";
|
|
195
|
+
default: return "end_turn";
|
|
196
|
+
}
|
|
197
|
+
}
|
package/wrangler.toml
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name = "llm-router-route"
|
|
2
|
+
main = "src/index.js"
|
|
3
|
+
compatibility_date = "2024-01-01"
|
|
4
|
+
workers_dev = false
|
|
5
|
+
preview_urls = false
|
|
6
|
+
|
|
7
|
+
# No custom routes are defined by default.
|
|
8
|
+
# Configure routes/domains per environment in your own deployment setup.
|
|
9
|
+
|
|
10
|
+
# Optional non-secret vars
|
|
11
|
+
[vars]
|
|
12
|
+
ENVIRONMENT = "production"
|
|
13
|
+
|
|
14
|
+
# Required secret(s):
|
|
15
|
+
# LLM_ROUTER_CONFIG_JSON - All-in-one JSON config exported by:
|
|
16
|
+
# llm-router deploy --export-only=true --out=.llm-router.worker.json
|
|
17
|
+
# wrangler secret put LLM_ROUTER_CONFIG_JSON < .llm-router.worker.json
|
|
18
|
+
#
|
|
19
|
+
# Optional override:
|
|
20
|
+
# LLM_ROUTER_MASTER_KEY - Overrides config.masterKey at runtime
|