@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
|
@@ -3,7 +3,8 @@ import {
|
|
|
3
3
|
claudeEventToOpenAIChunks,
|
|
4
4
|
initClaudeToOpenAIState
|
|
5
5
|
} from "../../translator/response/claude-to-openai.js";
|
|
6
|
-
import {
|
|
6
|
+
import { finalizeOpenAIToClaudeStream } from "../../translator/response/openai-to-claude.js";
|
|
7
|
+
import { passthroughResponseWithCors, withCorsHeaders } from "./http.js";
|
|
7
8
|
|
|
8
9
|
function normalizeOpenAIContent(content) {
|
|
9
10
|
if (typeof content === "string") {
|
|
@@ -39,6 +40,7 @@ function safeParseToolArguments(rawArguments) {
|
|
|
39
40
|
|
|
40
41
|
function convertOpenAIFinishReason(reason) {
|
|
41
42
|
switch (reason) {
|
|
43
|
+
case "function_call":
|
|
42
44
|
case "tool_calls":
|
|
43
45
|
return "tool_use";
|
|
44
46
|
case "length":
|
|
@@ -49,6 +51,42 @@ function convertOpenAIFinishReason(reason) {
|
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
53
|
|
|
54
|
+
function resolveOpenAINonStreamFinishReason(choice) {
|
|
55
|
+
const rawReason = String(choice?.finish_reason || "").trim();
|
|
56
|
+
if (rawReason && rawReason !== "stop") {
|
|
57
|
+
return rawReason;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (normalizeOpenAIToolCalls(choice?.message).length > 0) {
|
|
61
|
+
return "tool_calls";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return rawReason || "stop";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizeOpenAIToolCalls(message) {
|
|
68
|
+
const normalizedToolCalls = Array.isArray(message?.tool_calls)
|
|
69
|
+
? message.tool_calls.filter((call) => call && typeof call === "object")
|
|
70
|
+
: [];
|
|
71
|
+
|
|
72
|
+
const legacyFunctionCall = message?.function_call;
|
|
73
|
+
if (!legacyFunctionCall || typeof legacyFunctionCall !== "object") {
|
|
74
|
+
return normalizedToolCalls;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return [
|
|
78
|
+
...normalizedToolCalls,
|
|
79
|
+
{
|
|
80
|
+
id: String(message?.tool_call_id || message?.tool_use_id || "tool_0"),
|
|
81
|
+
type: "function",
|
|
82
|
+
function: {
|
|
83
|
+
name: String(legacyFunctionCall.name || "tool"),
|
|
84
|
+
arguments: typeof legacyFunctionCall.arguments === "string" ? legacyFunctionCall.arguments : ""
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
|
|
52
90
|
export function convertOpenAINonStreamToClaude(result, fallbackModel = "unknown") {
|
|
53
91
|
const choice = result?.choices?.[0];
|
|
54
92
|
const message = choice?.message || {};
|
|
@@ -56,9 +94,10 @@ export function convertOpenAINonStreamToClaude(result, fallbackModel = "unknown"
|
|
|
56
94
|
...normalizeOpenAIContent(message.content)
|
|
57
95
|
];
|
|
58
96
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
97
|
+
const toolCalls = normalizeOpenAIToolCalls(message);
|
|
98
|
+
if (toolCalls.length > 0) {
|
|
99
|
+
for (let index = 0; index < toolCalls.length; index += 1) {
|
|
100
|
+
const call = toolCalls[index];
|
|
62
101
|
if (!call || typeof call !== "object") continue;
|
|
63
102
|
content.push({
|
|
64
103
|
type: "tool_use",
|
|
@@ -79,7 +118,7 @@ export function convertOpenAINonStreamToClaude(result, fallbackModel = "unknown"
|
|
|
79
118
|
role: "assistant",
|
|
80
119
|
model: result?.model || fallbackModel,
|
|
81
120
|
content,
|
|
82
|
-
stop_reason: convertOpenAIFinishReason(choice
|
|
121
|
+
stop_reason: convertOpenAIFinishReason(resolveOpenAINonStreamFinishReason(choice)),
|
|
83
122
|
stop_sequence: null,
|
|
84
123
|
usage: {
|
|
85
124
|
input_tokens: result?.usage?.prompt_tokens || 0,
|
|
@@ -88,11 +127,863 @@ export function convertOpenAINonStreamToClaude(result, fallbackModel = "unknown"
|
|
|
88
127
|
};
|
|
89
128
|
}
|
|
90
129
|
|
|
130
|
+
const OPENAI_RESPONSES_ECHO_FIELDS = [
|
|
131
|
+
"instructions",
|
|
132
|
+
"max_output_tokens",
|
|
133
|
+
"max_tool_calls",
|
|
134
|
+
"model",
|
|
135
|
+
"parallel_tool_calls",
|
|
136
|
+
"previous_response_id",
|
|
137
|
+
"prompt_cache_key",
|
|
138
|
+
"reasoning",
|
|
139
|
+
"safety_identifier",
|
|
140
|
+
"service_tier",
|
|
141
|
+
"store",
|
|
142
|
+
"temperature",
|
|
143
|
+
"text",
|
|
144
|
+
"tool_choice",
|
|
145
|
+
"tools",
|
|
146
|
+
"top_logprobs",
|
|
147
|
+
"top_p",
|
|
148
|
+
"truncation",
|
|
149
|
+
"user",
|
|
150
|
+
"metadata"
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
function normalizeOpenAIResponseId(value) {
|
|
154
|
+
const raw = String(value || "").trim();
|
|
155
|
+
if (!raw) return `resp_${Date.now()}`;
|
|
156
|
+
return raw.startsWith("resp_") ? raw : `resp_${raw}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function normalizeClaudeToolArguments(value) {
|
|
160
|
+
if (typeof value === "string") return value;
|
|
161
|
+
try {
|
|
162
|
+
return JSON.stringify(value || {});
|
|
163
|
+
} catch {
|
|
164
|
+
return "{}";
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function toOpenAIResponsesUsage({ inputTokens = 0, outputTokens = 0, reasoningTokens = 0 } = {}) {
|
|
169
|
+
const normalizedInput = Number.isFinite(inputTokens) ? Number(inputTokens) : 0;
|
|
170
|
+
const normalizedOutput = Number.isFinite(outputTokens) ? Number(outputTokens) : 0;
|
|
171
|
+
const normalizedReasoning = Number.isFinite(reasoningTokens) ? Number(reasoningTokens) : 0;
|
|
172
|
+
return {
|
|
173
|
+
input_tokens: normalizedInput,
|
|
174
|
+
input_tokens_details: {
|
|
175
|
+
cached_tokens: 0
|
|
176
|
+
},
|
|
177
|
+
output_tokens: normalizedOutput,
|
|
178
|
+
output_tokens_details: normalizedReasoning > 0
|
|
179
|
+
? { reasoning_tokens: normalizedReasoning }
|
|
180
|
+
: {},
|
|
181
|
+
total_tokens: normalizedInput + normalizedOutput
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function applyOpenAIResponsesEchoFields(response, requestBody, fallbackModel = "unknown") {
|
|
186
|
+
const nextResponse = {
|
|
187
|
+
...response
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
for (const field of OPENAI_RESPONSES_ECHO_FIELDS) {
|
|
191
|
+
if (requestBody?.[field] !== undefined) {
|
|
192
|
+
nextResponse[field] = requestBody[field];
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (typeof nextResponse.model !== "string" || !nextResponse.model.trim()) {
|
|
197
|
+
nextResponse.model = fallbackModel;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return nextResponse;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function collectClaudeResponseOutputs(contentBlocks, responseId) {
|
|
204
|
+
const outputs = [];
|
|
205
|
+
const textParts = [];
|
|
206
|
+
const toolCalls = [];
|
|
207
|
+
const reasoningParts = [];
|
|
208
|
+
|
|
209
|
+
for (const block of (Array.isArray(contentBlocks) ? contentBlocks : [])) {
|
|
210
|
+
if (!block || typeof block !== "object") continue;
|
|
211
|
+
|
|
212
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
213
|
+
textParts.push(block.text);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (block.type === "tool_use") {
|
|
218
|
+
toolCalls.push({
|
|
219
|
+
id: String(block.id || `call_${toolCalls.length + 1}`),
|
|
220
|
+
name: String(block.name || "tool"),
|
|
221
|
+
arguments: normalizeClaudeToolArguments(block.input)
|
|
222
|
+
});
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if ((block.type === "thinking" || block.type === "redacted_thinking") && typeof block.thinking === "string") {
|
|
227
|
+
reasoningParts.push(block.thinking);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (reasoningParts.length > 0) {
|
|
232
|
+
outputs.push({
|
|
233
|
+
id: `rs_${responseId}_0`,
|
|
234
|
+
type: "reasoning",
|
|
235
|
+
summary: [{
|
|
236
|
+
type: "summary_text",
|
|
237
|
+
text: reasoningParts.join("")
|
|
238
|
+
}]
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (textParts.length > 0) {
|
|
243
|
+
outputs.push({
|
|
244
|
+
id: `msg_${responseId}_0`,
|
|
245
|
+
type: "message",
|
|
246
|
+
status: "completed",
|
|
247
|
+
role: "assistant",
|
|
248
|
+
content: [{
|
|
249
|
+
type: "output_text",
|
|
250
|
+
text: textParts.join("")
|
|
251
|
+
}]
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
for (const toolCall of toolCalls) {
|
|
256
|
+
outputs.push({
|
|
257
|
+
id: `fc_${toolCall.id}`,
|
|
258
|
+
type: "function_call",
|
|
259
|
+
status: "completed",
|
|
260
|
+
arguments: toolCall.arguments || "{}",
|
|
261
|
+
call_id: toolCall.id,
|
|
262
|
+
name: toolCall.name
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
outputs,
|
|
268
|
+
reasoningText: reasoningParts.join("")
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function convertClaudeNonStreamToOpenAIResponses(message, requestBody, fallbackModel = "unknown") {
|
|
273
|
+
const responseId = normalizeOpenAIResponseId(message?.id);
|
|
274
|
+
const collected = collectClaudeResponseOutputs(message?.content, responseId);
|
|
275
|
+
const reasoningTokens = collected.reasoningText
|
|
276
|
+
? Math.max(0, Math.floor(collected.reasoningText.length / 4))
|
|
277
|
+
: 0;
|
|
278
|
+
|
|
279
|
+
return applyOpenAIResponsesEchoFields({
|
|
280
|
+
id: responseId,
|
|
281
|
+
object: "response",
|
|
282
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
283
|
+
status: "completed",
|
|
284
|
+
background: false,
|
|
285
|
+
error: null,
|
|
286
|
+
incomplete_details: null,
|
|
287
|
+
output: collected.outputs,
|
|
288
|
+
usage: toOpenAIResponsesUsage({
|
|
289
|
+
inputTokens: message?.usage?.input_tokens,
|
|
290
|
+
outputTokens: message?.usage?.output_tokens,
|
|
291
|
+
reasoningTokens
|
|
292
|
+
})
|
|
293
|
+
}, requestBody, message?.model || fallbackModel);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function createOpenAIResponsesState(requestBody, fallbackModel = "unknown") {
|
|
297
|
+
return {
|
|
298
|
+
sequence: 0,
|
|
299
|
+
responseId: "",
|
|
300
|
+
createdAt: 0,
|
|
301
|
+
model: typeof requestBody?.model === "string" && requestBody.model.trim()
|
|
302
|
+
? requestBody.model.trim()
|
|
303
|
+
: fallbackModel,
|
|
304
|
+
textMessageId: "",
|
|
305
|
+
textOutputIndex: null,
|
|
306
|
+
textBuffer: "",
|
|
307
|
+
textOpened: false,
|
|
308
|
+
nextOutputIndex: 0,
|
|
309
|
+
outputItems: [],
|
|
310
|
+
activeBlocks: new Map(),
|
|
311
|
+
toolCalls: new Map(),
|
|
312
|
+
reasoningItems: new Map(),
|
|
313
|
+
inputTokens: 0,
|
|
314
|
+
outputTokens: 0,
|
|
315
|
+
requestBody
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function nextOpenAIResponsesSequence(state) {
|
|
320
|
+
state.sequence += 1;
|
|
321
|
+
return state.sequence;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function formatOpenAIResponsesEvent(eventType, payload) {
|
|
325
|
+
return `event: ${eventType}\ndata: ${JSON.stringify(payload)}\n\n`;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function enqueueOpenAIResponsesEvent(controller, eventType, payload, encoder) {
|
|
329
|
+
controller.enqueue(encoder.encode(formatOpenAIResponsesEvent(eventType, payload)));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function allocateOpenAIResponsesOutputIndex(state, itemType, key) {
|
|
333
|
+
const outputIndex = state.nextOutputIndex;
|
|
334
|
+
state.nextOutputIndex += 1;
|
|
335
|
+
state.outputItems.push({
|
|
336
|
+
itemType,
|
|
337
|
+
key,
|
|
338
|
+
outputIndex
|
|
339
|
+
});
|
|
340
|
+
return outputIndex;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function ensureOpenAIResponsesLifecycleStarted(state, controller, encoder) {
|
|
344
|
+
if (state.createdAt > 0) return;
|
|
345
|
+
state.createdAt = Math.floor(Date.now() / 1000);
|
|
346
|
+
if (!state.responseId) {
|
|
347
|
+
state.responseId = normalizeOpenAIResponseId(Date.now());
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
enqueueOpenAIResponsesEvent(controller, "response.created", {
|
|
351
|
+
type: "response.created",
|
|
352
|
+
sequence_number: nextOpenAIResponsesSequence(state),
|
|
353
|
+
response: applyOpenAIResponsesEchoFields({
|
|
354
|
+
id: state.responseId,
|
|
355
|
+
object: "response",
|
|
356
|
+
created_at: state.createdAt,
|
|
357
|
+
status: "in_progress",
|
|
358
|
+
background: false,
|
|
359
|
+
error: null,
|
|
360
|
+
output: []
|
|
361
|
+
}, state.requestBody, state.model)
|
|
362
|
+
}, encoder);
|
|
363
|
+
|
|
364
|
+
enqueueOpenAIResponsesEvent(controller, "response.in_progress", {
|
|
365
|
+
type: "response.in_progress",
|
|
366
|
+
sequence_number: nextOpenAIResponsesSequence(state),
|
|
367
|
+
response: {
|
|
368
|
+
id: state.responseId,
|
|
369
|
+
object: "response",
|
|
370
|
+
created_at: state.createdAt,
|
|
371
|
+
status: "in_progress"
|
|
372
|
+
}
|
|
373
|
+
}, encoder);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function ensureOpenAIResponsesTextItem(state, controller, encoder) {
|
|
377
|
+
if (state.textMessageId) return;
|
|
378
|
+
ensureOpenAIResponsesLifecycleStarted(state, controller, encoder);
|
|
379
|
+
state.textMessageId = `msg_${state.responseId}_0`;
|
|
380
|
+
state.textOutputIndex = allocateOpenAIResponsesOutputIndex(state, "message", "assistant");
|
|
381
|
+
|
|
382
|
+
enqueueOpenAIResponsesEvent(controller, "response.output_item.added", {
|
|
383
|
+
type: "response.output_item.added",
|
|
384
|
+
sequence_number: nextOpenAIResponsesSequence(state),
|
|
385
|
+
output_index: state.textOutputIndex,
|
|
386
|
+
item: {
|
|
387
|
+
id: state.textMessageId,
|
|
388
|
+
type: "message",
|
|
389
|
+
status: "in_progress",
|
|
390
|
+
content: [],
|
|
391
|
+
role: "assistant"
|
|
392
|
+
}
|
|
393
|
+
}, encoder);
|
|
394
|
+
|
|
395
|
+
enqueueOpenAIResponsesEvent(controller, "response.content_part.added", {
|
|
396
|
+
type: "response.content_part.added",
|
|
397
|
+
sequence_number: nextOpenAIResponsesSequence(state),
|
|
398
|
+
item_id: state.textMessageId,
|
|
399
|
+
output_index: state.textOutputIndex,
|
|
400
|
+
content_index: 0,
|
|
401
|
+
part: {
|
|
402
|
+
type: "output_text",
|
|
403
|
+
text: ""
|
|
404
|
+
}
|
|
405
|
+
}, encoder);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function ensureOpenAIResponsesToolCall(state, index, block, controller, encoder) {
|
|
409
|
+
ensureOpenAIResponsesLifecycleStarted(state, controller, encoder);
|
|
410
|
+
const normalizedIndex = Number.isFinite(index) ? Number(index) : 0;
|
|
411
|
+
const existing = state.toolCalls.get(normalizedIndex);
|
|
412
|
+
if (existing) {
|
|
413
|
+
if (typeof block?.name === "string" && block.name.trim()) {
|
|
414
|
+
existing.name = block.name.trim();
|
|
415
|
+
}
|
|
416
|
+
if (typeof block?.id === "string" && block.id.trim()) {
|
|
417
|
+
existing.id = block.id.trim();
|
|
418
|
+
}
|
|
419
|
+
return existing;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const toolCall = {
|
|
423
|
+
id: String(block?.id || `call_${normalizedIndex}`),
|
|
424
|
+
name: String(block?.name || "tool"),
|
|
425
|
+
arguments: "",
|
|
426
|
+
outputIndex: allocateOpenAIResponsesOutputIndex(state, "function_call", normalizedIndex)
|
|
427
|
+
};
|
|
428
|
+
state.toolCalls.set(normalizedIndex, toolCall);
|
|
429
|
+
|
|
430
|
+
enqueueOpenAIResponsesEvent(controller, "response.output_item.added", {
|
|
431
|
+
type: "response.output_item.added",
|
|
432
|
+
sequence_number: nextOpenAIResponsesSequence(state),
|
|
433
|
+
output_index: toolCall.outputIndex,
|
|
434
|
+
item: {
|
|
435
|
+
id: `fc_${toolCall.id}`,
|
|
436
|
+
type: "function_call",
|
|
437
|
+
status: "in_progress",
|
|
438
|
+
arguments: "",
|
|
439
|
+
call_id: toolCall.id,
|
|
440
|
+
name: toolCall.name
|
|
441
|
+
}
|
|
442
|
+
}, encoder);
|
|
443
|
+
|
|
444
|
+
return toolCall;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function ensureOpenAIResponsesReasoningItem(state, index, controller, encoder) {
|
|
448
|
+
ensureOpenAIResponsesLifecycleStarted(state, controller, encoder);
|
|
449
|
+
const normalizedIndex = Number.isFinite(index) ? Number(index) : 0;
|
|
450
|
+
const existing = state.reasoningItems.get(normalizedIndex);
|
|
451
|
+
if (existing) {
|
|
452
|
+
return existing;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const reasoningItem = {
|
|
456
|
+
id: `rs_${state.responseId}_${normalizedIndex}`,
|
|
457
|
+
outputIndex: allocateOpenAIResponsesOutputIndex(state, "reasoning", normalizedIndex),
|
|
458
|
+
text: "",
|
|
459
|
+
opened: true
|
|
460
|
+
};
|
|
461
|
+
state.reasoningItems.set(normalizedIndex, reasoningItem);
|
|
462
|
+
|
|
463
|
+
enqueueOpenAIResponsesEvent(controller, "response.output_item.added", {
|
|
464
|
+
type: "response.output_item.added",
|
|
465
|
+
sequence_number: nextOpenAIResponsesSequence(state),
|
|
466
|
+
output_index: reasoningItem.outputIndex,
|
|
467
|
+
item: {
|
|
468
|
+
id: reasoningItem.id,
|
|
469
|
+
type: "reasoning",
|
|
470
|
+
status: "in_progress",
|
|
471
|
+
summary: []
|
|
472
|
+
}
|
|
473
|
+
}, encoder);
|
|
474
|
+
|
|
475
|
+
enqueueOpenAIResponsesEvent(controller, "response.reasoning_summary_part.added", {
|
|
476
|
+
type: "response.reasoning_summary_part.added",
|
|
477
|
+
sequence_number: nextOpenAIResponsesSequence(state),
|
|
478
|
+
item_id: reasoningItem.id,
|
|
479
|
+
output_index: reasoningItem.outputIndex,
|
|
480
|
+
summary_index: 0,
|
|
481
|
+
part: {
|
|
482
|
+
type: "summary_text",
|
|
483
|
+
text: ""
|
|
484
|
+
}
|
|
485
|
+
}, encoder);
|
|
486
|
+
|
|
487
|
+
return reasoningItem;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function flushOpenAIResponsesTextItem(state, controller, encoder) {
|
|
491
|
+
if (!state.textMessageId || !state.textOpened) return;
|
|
492
|
+
enqueueOpenAIResponsesEvent(controller, "response.output_text.done", {
|
|
493
|
+
type: "response.output_text.done",
|
|
494
|
+
sequence_number: nextOpenAIResponsesSequence(state),
|
|
495
|
+
item_id: state.textMessageId,
|
|
496
|
+
output_index: state.textOutputIndex ?? 0,
|
|
497
|
+
content_index: 0,
|
|
498
|
+
text: state.textBuffer
|
|
499
|
+
}, encoder);
|
|
500
|
+
|
|
501
|
+
enqueueOpenAIResponsesEvent(controller, "response.content_part.done", {
|
|
502
|
+
type: "response.content_part.done",
|
|
503
|
+
sequence_number: nextOpenAIResponsesSequence(state),
|
|
504
|
+
item_id: state.textMessageId,
|
|
505
|
+
output_index: state.textOutputIndex ?? 0,
|
|
506
|
+
content_index: 0,
|
|
507
|
+
part: {
|
|
508
|
+
type: "output_text",
|
|
509
|
+
text: state.textBuffer
|
|
510
|
+
}
|
|
511
|
+
}, encoder);
|
|
512
|
+
|
|
513
|
+
enqueueOpenAIResponsesEvent(controller, "response.output_item.done", {
|
|
514
|
+
type: "response.output_item.done",
|
|
515
|
+
sequence_number: nextOpenAIResponsesSequence(state),
|
|
516
|
+
output_index: state.textOutputIndex ?? 0,
|
|
517
|
+
item: {
|
|
518
|
+
id: state.textMessageId,
|
|
519
|
+
type: "message",
|
|
520
|
+
status: "completed",
|
|
521
|
+
role: "assistant",
|
|
522
|
+
content: [{
|
|
523
|
+
type: "output_text",
|
|
524
|
+
text: state.textBuffer
|
|
525
|
+
}]
|
|
526
|
+
}
|
|
527
|
+
}, encoder);
|
|
528
|
+
|
|
529
|
+
state.textOpened = false;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function flushOpenAIResponsesToolCall(state, index, controller, encoder) {
|
|
533
|
+
const normalizedIndex = Number.isFinite(index) ? Number(index) : 0;
|
|
534
|
+
const toolCall = state.toolCalls.get(normalizedIndex);
|
|
535
|
+
if (!toolCall) return;
|
|
536
|
+
enqueueOpenAIResponsesEvent(controller, "response.function_call_arguments.done", {
|
|
537
|
+
type: "response.function_call_arguments.done",
|
|
538
|
+
sequence_number: nextOpenAIResponsesSequence(state),
|
|
539
|
+
item_id: `fc_${toolCall.id}`,
|
|
540
|
+
output_index: toolCall.outputIndex,
|
|
541
|
+
arguments: toolCall.arguments || "{}"
|
|
542
|
+
}, encoder);
|
|
543
|
+
|
|
544
|
+
enqueueOpenAIResponsesEvent(controller, "response.output_item.done", {
|
|
545
|
+
type: "response.output_item.done",
|
|
546
|
+
sequence_number: nextOpenAIResponsesSequence(state),
|
|
547
|
+
output_index: toolCall.outputIndex,
|
|
548
|
+
item: {
|
|
549
|
+
id: `fc_${toolCall.id}`,
|
|
550
|
+
type: "function_call",
|
|
551
|
+
status: "completed",
|
|
552
|
+
arguments: toolCall.arguments || "{}",
|
|
553
|
+
call_id: toolCall.id,
|
|
554
|
+
name: toolCall.name
|
|
555
|
+
}
|
|
556
|
+
}, encoder);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function flushOpenAIResponsesReasoningItem(state, index, controller, encoder) {
|
|
560
|
+
const normalizedIndex = Number.isFinite(index) ? Number(index) : 0;
|
|
561
|
+
const reasoningItem = state.reasoningItems.get(normalizedIndex);
|
|
562
|
+
if (!reasoningItem || !reasoningItem.opened) return;
|
|
563
|
+
|
|
564
|
+
enqueueOpenAIResponsesEvent(controller, "response.reasoning_summary_text.done", {
|
|
565
|
+
type: "response.reasoning_summary_text.done",
|
|
566
|
+
sequence_number: nextOpenAIResponsesSequence(state),
|
|
567
|
+
item_id: reasoningItem.id,
|
|
568
|
+
output_index: reasoningItem.outputIndex,
|
|
569
|
+
summary_index: 0,
|
|
570
|
+
text: reasoningItem.text
|
|
571
|
+
}, encoder);
|
|
572
|
+
|
|
573
|
+
enqueueOpenAIResponsesEvent(controller, "response.reasoning_summary_part.done", {
|
|
574
|
+
type: "response.reasoning_summary_part.done",
|
|
575
|
+
sequence_number: nextOpenAIResponsesSequence(state),
|
|
576
|
+
item_id: reasoningItem.id,
|
|
577
|
+
output_index: reasoningItem.outputIndex,
|
|
578
|
+
summary_index: 0,
|
|
579
|
+
part: {
|
|
580
|
+
type: "summary_text",
|
|
581
|
+
text: reasoningItem.text
|
|
582
|
+
}
|
|
583
|
+
}, encoder);
|
|
584
|
+
|
|
585
|
+
reasoningItem.opened = false;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function buildCompletedOpenAIResponse(state) {
|
|
589
|
+
const outputs = state.outputItems
|
|
590
|
+
.slice()
|
|
591
|
+
.sort((left, right) => left.outputIndex - right.outputIndex)
|
|
592
|
+
.map((entry) => {
|
|
593
|
+
if (entry.itemType === "reasoning") {
|
|
594
|
+
const reasoningItem = state.reasoningItems.get(entry.key);
|
|
595
|
+
if (!reasoningItem) return null;
|
|
596
|
+
return {
|
|
597
|
+
id: reasoningItem.id,
|
|
598
|
+
type: "reasoning",
|
|
599
|
+
summary: [{
|
|
600
|
+
type: "summary_text",
|
|
601
|
+
text: reasoningItem.text
|
|
602
|
+
}]
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (entry.itemType === "message") {
|
|
607
|
+
if (!state.textMessageId && !state.textBuffer) return null;
|
|
608
|
+
return {
|
|
609
|
+
id: state.textMessageId || `msg_${state.responseId}_0`,
|
|
610
|
+
type: "message",
|
|
611
|
+
status: "completed",
|
|
612
|
+
role: "assistant",
|
|
613
|
+
content: [{
|
|
614
|
+
type: "output_text",
|
|
615
|
+
text: state.textBuffer
|
|
616
|
+
}]
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (entry.itemType === "function_call") {
|
|
621
|
+
const toolCall = state.toolCalls.get(entry.key);
|
|
622
|
+
if (!toolCall) return null;
|
|
623
|
+
return {
|
|
624
|
+
id: `fc_${toolCall.id}`,
|
|
625
|
+
type: "function_call",
|
|
626
|
+
status: "completed",
|
|
627
|
+
arguments: toolCall.arguments || "{}",
|
|
628
|
+
call_id: toolCall.id,
|
|
629
|
+
name: toolCall.name
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
return null;
|
|
634
|
+
})
|
|
635
|
+
.filter(Boolean);
|
|
636
|
+
|
|
637
|
+
const reasoningText = [...state.reasoningItems.values()]
|
|
638
|
+
.map((item) => item.text)
|
|
639
|
+
.join("");
|
|
640
|
+
|
|
641
|
+
return applyOpenAIResponsesEchoFields({
|
|
642
|
+
id: state.responseId || normalizeOpenAIResponseId(Date.now()),
|
|
643
|
+
object: "response",
|
|
644
|
+
created_at: state.createdAt || Math.floor(Date.now() / 1000),
|
|
645
|
+
status: "completed",
|
|
646
|
+
background: false,
|
|
647
|
+
error: null,
|
|
648
|
+
incomplete_details: null,
|
|
649
|
+
output: outputs,
|
|
650
|
+
usage: toOpenAIResponsesUsage({
|
|
651
|
+
inputTokens: state.inputTokens,
|
|
652
|
+
outputTokens: state.outputTokens,
|
|
653
|
+
reasoningTokens: reasoningText ? Math.floor(reasoningText.length / 4) : 0
|
|
654
|
+
})
|
|
655
|
+
}, state.requestBody, state.model);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
export function handleClaudeStreamToOpenAIResponses(response, requestBody, fallbackModel = "unknown") {
|
|
659
|
+
const state = createOpenAIResponsesState(requestBody, fallbackModel);
|
|
660
|
+
const decoder = new TextDecoder();
|
|
661
|
+
const encoder = new TextEncoder();
|
|
662
|
+
let buffer = "";
|
|
663
|
+
|
|
664
|
+
function processBlock(block, controller) {
|
|
665
|
+
if (!block || !block.trim()) return;
|
|
666
|
+
const parsedBlock = parseSseBlock(block);
|
|
667
|
+
if (!parsedBlock.data || parsedBlock.data === "[DONE]") return;
|
|
668
|
+
|
|
669
|
+
let payload;
|
|
670
|
+
try {
|
|
671
|
+
payload = JSON.parse(parsedBlock.data);
|
|
672
|
+
} catch {
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const eventType = String(payload?.type || "").trim();
|
|
677
|
+
if (!eventType) return;
|
|
678
|
+
|
|
679
|
+
if (eventType === "message_start") {
|
|
680
|
+
ensureOpenAIResponsesLifecycleStarted(state, controller, encoder);
|
|
681
|
+
const message = payload.message || {};
|
|
682
|
+
state.responseId = normalizeOpenAIResponseId(message.id || state.responseId || Date.now());
|
|
683
|
+
state.model = typeof requestBody?.model === "string" && requestBody.model.trim()
|
|
684
|
+
? requestBody.model.trim()
|
|
685
|
+
: (message.model || state.model || fallbackModel);
|
|
686
|
+
if (message.usage && typeof message.usage === "object") {
|
|
687
|
+
if (Number.isFinite(message.usage.input_tokens)) state.inputTokens = Number(message.usage.input_tokens);
|
|
688
|
+
if (Number.isFinite(message.usage.output_tokens)) state.outputTokens = Number(message.usage.output_tokens);
|
|
689
|
+
}
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (eventType === "content_block_start") {
|
|
694
|
+
const index = Number(payload.index);
|
|
695
|
+
const blockInfo = payload.content_block || {};
|
|
696
|
+
state.activeBlocks.set(index, String(blockInfo.type || "").trim());
|
|
697
|
+
if (blockInfo.type === "text") {
|
|
698
|
+
ensureOpenAIResponsesTextItem(state, controller, encoder);
|
|
699
|
+
state.textOpened = true;
|
|
700
|
+
} else if (blockInfo.type === "thinking" || blockInfo.type === "redacted_thinking") {
|
|
701
|
+
ensureOpenAIResponsesReasoningItem(state, index, controller, encoder);
|
|
702
|
+
} else if (blockInfo.type === "tool_use") {
|
|
703
|
+
ensureOpenAIResponsesToolCall(state, index, blockInfo, controller, encoder);
|
|
704
|
+
}
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if (eventType === "content_block_delta") {
|
|
709
|
+
const index = Number(payload.index);
|
|
710
|
+
const delta = payload.delta || {};
|
|
711
|
+
if (delta.type === "text_delta" && typeof delta.text === "string") {
|
|
712
|
+
ensureOpenAIResponsesTextItem(state, controller, encoder);
|
|
713
|
+
state.textOpened = true;
|
|
714
|
+
state.textBuffer += delta.text;
|
|
715
|
+
enqueueOpenAIResponsesEvent(controller, "response.output_text.delta", {
|
|
716
|
+
type: "response.output_text.delta",
|
|
717
|
+
sequence_number: nextOpenAIResponsesSequence(state),
|
|
718
|
+
item_id: state.textMessageId,
|
|
719
|
+
output_index: state.textOutputIndex ?? 0,
|
|
720
|
+
content_index: 0,
|
|
721
|
+
delta: delta.text
|
|
722
|
+
}, encoder);
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
if (delta.type === "input_json_delta" && typeof delta.partial_json === "string") {
|
|
727
|
+
const toolCall = ensureOpenAIResponsesToolCall(state, index, payload.content_block, controller, encoder);
|
|
728
|
+
toolCall.arguments += delta.partial_json;
|
|
729
|
+
enqueueOpenAIResponsesEvent(controller, "response.function_call_arguments.delta", {
|
|
730
|
+
type: "response.function_call_arguments.delta",
|
|
731
|
+
sequence_number: nextOpenAIResponsesSequence(state),
|
|
732
|
+
item_id: `fc_${toolCall.id}`,
|
|
733
|
+
output_index: toolCall.outputIndex,
|
|
734
|
+
delta: delta.partial_json
|
|
735
|
+
}, encoder);
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if (delta.type === "thinking_delta" && typeof delta.thinking === "string") {
|
|
740
|
+
const reasoningItem = ensureOpenAIResponsesReasoningItem(state, index, controller, encoder);
|
|
741
|
+
reasoningItem.text += delta.thinking;
|
|
742
|
+
enqueueOpenAIResponsesEvent(controller, "response.reasoning_summary_text.delta", {
|
|
743
|
+
type: "response.reasoning_summary_text.delta",
|
|
744
|
+
sequence_number: nextOpenAIResponsesSequence(state),
|
|
745
|
+
item_id: reasoningItem.id,
|
|
746
|
+
output_index: reasoningItem.outputIndex,
|
|
747
|
+
summary_index: 0,
|
|
748
|
+
delta: delta.thinking
|
|
749
|
+
}, encoder);
|
|
750
|
+
}
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
if (eventType === "content_block_stop") {
|
|
755
|
+
const index = Number(payload.index);
|
|
756
|
+
const blockType = state.activeBlocks.get(index);
|
|
757
|
+
if (blockType === "text") {
|
|
758
|
+
flushOpenAIResponsesTextItem(state, controller, encoder);
|
|
759
|
+
} else if (blockType === "thinking" || blockType === "redacted_thinking") {
|
|
760
|
+
flushOpenAIResponsesReasoningItem(state, index, controller, encoder);
|
|
761
|
+
} else if (blockType === "tool_use") {
|
|
762
|
+
flushOpenAIResponsesToolCall(state, index, controller, encoder);
|
|
763
|
+
}
|
|
764
|
+
state.activeBlocks.delete(index);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if (eventType === "message_delta") {
|
|
769
|
+
const usage = payload.usage || {};
|
|
770
|
+
if (Number.isFinite(usage.input_tokens)) state.inputTokens = Number(usage.input_tokens);
|
|
771
|
+
if (Number.isFinite(usage.output_tokens)) state.outputTokens = Number(usage.output_tokens);
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
if (eventType === "message_stop") {
|
|
776
|
+
flushOpenAIResponsesTextItem(state, controller, encoder);
|
|
777
|
+
for (const index of [...state.activeBlocks.keys()]) {
|
|
778
|
+
if (state.activeBlocks.get(index) === "thinking" || state.activeBlocks.get(index) === "redacted_thinking") {
|
|
779
|
+
flushOpenAIResponsesReasoningItem(state, index, controller, encoder);
|
|
780
|
+
} else if (state.activeBlocks.get(index) === "tool_use") {
|
|
781
|
+
flushOpenAIResponsesToolCall(state, index, controller, encoder);
|
|
782
|
+
}
|
|
783
|
+
state.activeBlocks.delete(index);
|
|
784
|
+
}
|
|
785
|
+
ensureOpenAIResponsesLifecycleStarted(state, controller, encoder);
|
|
786
|
+
enqueueOpenAIResponsesEvent(controller, "response.completed", {
|
|
787
|
+
type: "response.completed",
|
|
788
|
+
sequence_number: nextOpenAIResponsesSequence(state),
|
|
789
|
+
response: buildCompletedOpenAIResponse(state)
|
|
790
|
+
}, encoder);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const transformStream = new TransformStream({
|
|
795
|
+
transform(chunk, controller) {
|
|
796
|
+
buffer += decoder.decode(chunk, { stream: true }).replace(/\r\n/g, "\n");
|
|
797
|
+
let boundaryIndex;
|
|
798
|
+
while ((boundaryIndex = buffer.indexOf("\n\n")) >= 0) {
|
|
799
|
+
const block = buffer.slice(0, boundaryIndex);
|
|
800
|
+
buffer = buffer.slice(boundaryIndex + 2);
|
|
801
|
+
processBlock(block, controller);
|
|
802
|
+
}
|
|
803
|
+
},
|
|
804
|
+
|
|
805
|
+
flush(controller) {
|
|
806
|
+
const remainder = buffer.trim();
|
|
807
|
+
if (remainder) {
|
|
808
|
+
processBlock(remainder, controller);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
return new Response(response.body.pipeThrough(transformStream), {
|
|
814
|
+
headers: withCorsHeaders({
|
|
815
|
+
"Content-Type": "text/event-stream",
|
|
816
|
+
"Cache-Control": "no-cache",
|
|
817
|
+
Connection: "keep-alive"
|
|
818
|
+
})
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
|
|
91
822
|
function formatClaudeEvent(event) {
|
|
92
823
|
const eventType = event.type || "message";
|
|
93
824
|
return `event: ${eventType}\ndata: ${JSON.stringify(event)}\n\n`;
|
|
94
825
|
}
|
|
95
826
|
|
|
827
|
+
function mergeClaudeUsage(state, usage) {
|
|
828
|
+
if (!usage || typeof usage !== "object") return;
|
|
829
|
+
const nextUsage = {
|
|
830
|
+
...(state.usage && typeof state.usage === "object" ? state.usage : {})
|
|
831
|
+
};
|
|
832
|
+
|
|
833
|
+
for (const [key, value] of Object.entries(usage)) {
|
|
834
|
+
if (value !== undefined) {
|
|
835
|
+
nextUsage[key] = value;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
state.usage = nextUsage;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function buildSyntheticClaudeMessageDelta(state) {
|
|
843
|
+
const usage = {
|
|
844
|
+
...(state.usage && typeof state.usage === "object" ? state.usage : {})
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
if (!Number.isFinite(usage.input_tokens)) usage.input_tokens = 0;
|
|
848
|
+
if (!Number.isFinite(usage.output_tokens)) usage.output_tokens = 0;
|
|
849
|
+
|
|
850
|
+
return {
|
|
851
|
+
type: "message_delta",
|
|
852
|
+
delta: {
|
|
853
|
+
stop_reason: state.stopReason || (state.hasToolUse ? "tool_use" : "end_turn"),
|
|
854
|
+
stop_sequence: state.stopSequence ?? null
|
|
855
|
+
},
|
|
856
|
+
usage
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
export function normalizeClaudePassthroughStream(response) {
|
|
861
|
+
const decoder = new TextDecoder();
|
|
862
|
+
const encoder = new TextEncoder();
|
|
863
|
+
const state = {
|
|
864
|
+
messageStarted: false,
|
|
865
|
+
messageStopped: false,
|
|
866
|
+
terminalDeltaSeen: false,
|
|
867
|
+
hasToolUse: false,
|
|
868
|
+
stopReason: null,
|
|
869
|
+
stopSequence: undefined,
|
|
870
|
+
usage: undefined
|
|
871
|
+
};
|
|
872
|
+
let buffer = "";
|
|
873
|
+
|
|
874
|
+
function enqueueRawBlock(controller, block) {
|
|
875
|
+
controller.enqueue(encoder.encode(`${block}\n\n`));
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function enqueueSyntheticMessageDelta(controller) {
|
|
879
|
+
controller.enqueue(encoder.encode(formatClaudeEvent(buildSyntheticClaudeMessageDelta(state))));
|
|
880
|
+
state.terminalDeltaSeen = true;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function finalizeClaudeMessage(controller) {
|
|
884
|
+
if (!state.messageStarted || state.messageStopped) return;
|
|
885
|
+
if (!state.terminalDeltaSeen) {
|
|
886
|
+
enqueueSyntheticMessageDelta(controller);
|
|
887
|
+
}
|
|
888
|
+
controller.enqueue(encoder.encode(formatClaudeEvent({ type: "message_stop" })));
|
|
889
|
+
state.messageStopped = true;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function processBlock(block, controller) {
|
|
893
|
+
if (!block || !block.trim()) return;
|
|
894
|
+
const parsedBlock = parseSseBlock(block);
|
|
895
|
+
if (!parsedBlock.data) {
|
|
896
|
+
enqueueRawBlock(controller, block);
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
if (parsedBlock.data === "[DONE]") {
|
|
901
|
+
finalizeClaudeMessage(controller);
|
|
902
|
+
enqueueRawBlock(controller, block);
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
let payload;
|
|
907
|
+
try {
|
|
908
|
+
payload = JSON.parse(parsedBlock.data);
|
|
909
|
+
} catch {
|
|
910
|
+
enqueueRawBlock(controller, block);
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
const eventType = String(payload?.type || parsedBlock.eventType || "").trim();
|
|
915
|
+
if (eventType === "message_start") {
|
|
916
|
+
state.messageStarted = true;
|
|
917
|
+
mergeClaudeUsage(state, payload.message?.usage);
|
|
918
|
+
enqueueRawBlock(controller, block);
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
if (eventType === "content_block_start") {
|
|
923
|
+
if (String(payload?.content_block?.type || "").trim() === "tool_use") {
|
|
924
|
+
state.hasToolUse = true;
|
|
925
|
+
}
|
|
926
|
+
enqueueRawBlock(controller, block);
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
if (eventType === "message_delta") {
|
|
931
|
+
mergeClaudeUsage(state, payload.usage);
|
|
932
|
+
if (typeof payload?.delta?.stop_reason === "string" && payload.delta.stop_reason.trim()) {
|
|
933
|
+
state.stopReason = payload.delta.stop_reason.trim();
|
|
934
|
+
state.terminalDeltaSeen = true;
|
|
935
|
+
}
|
|
936
|
+
if (payload?.delta && Object.hasOwn(payload.delta, "stop_sequence")) {
|
|
937
|
+
state.stopSequence = payload.delta.stop_sequence;
|
|
938
|
+
}
|
|
939
|
+
enqueueRawBlock(controller, block);
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
if (eventType === "message_stop") {
|
|
944
|
+
if (!state.terminalDeltaSeen) {
|
|
945
|
+
enqueueSyntheticMessageDelta(controller);
|
|
946
|
+
}
|
|
947
|
+
state.messageStopped = true;
|
|
948
|
+
enqueueRawBlock(controller, block);
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
enqueueRawBlock(controller, block);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
const transformStream = new TransformStream({
|
|
956
|
+
transform(chunk, controller) {
|
|
957
|
+
buffer += decoder.decode(chunk, { stream: true }).replace(/\r\n/g, "\n");
|
|
958
|
+
|
|
959
|
+
let boundaryIndex;
|
|
960
|
+
while ((boundaryIndex = buffer.indexOf("\n\n")) >= 0) {
|
|
961
|
+
const block = buffer.slice(0, boundaryIndex);
|
|
962
|
+
buffer = buffer.slice(boundaryIndex + 2);
|
|
963
|
+
processBlock(block, controller);
|
|
964
|
+
}
|
|
965
|
+
},
|
|
966
|
+
|
|
967
|
+
flush(controller) {
|
|
968
|
+
const remainder = buffer.trim();
|
|
969
|
+
if (remainder) {
|
|
970
|
+
processBlock(remainder, controller);
|
|
971
|
+
}
|
|
972
|
+
finalizeClaudeMessage(controller);
|
|
973
|
+
}
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
return passthroughResponseWithCors(new Response(response.body?.pipeThrough(transformStream), {
|
|
977
|
+
status: response.status,
|
|
978
|
+
statusText: response.statusText,
|
|
979
|
+
headers: response.headers
|
|
980
|
+
}), {
|
|
981
|
+
"Content-Type": "text/event-stream",
|
|
982
|
+
"Cache-Control": "no-cache",
|
|
983
|
+
Connection: "keep-alive"
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
|
|
96
987
|
export function handleOpenAIStreamToClaude(response) {
|
|
97
988
|
const state = initState(FORMATS.CLAUDE);
|
|
98
989
|
const decoder = new TextDecoder();
|
|
@@ -100,31 +991,51 @@ export function handleOpenAIStreamToClaude(response) {
|
|
|
100
991
|
|
|
101
992
|
let buffer = "";
|
|
102
993
|
|
|
994
|
+
function enqueueClaudeEvents(controller, events) {
|
|
995
|
+
for (const event of events || []) {
|
|
996
|
+
controller.enqueue(encoder.encode(formatClaudeEvent(event)));
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function processBlock(block, controller) {
|
|
1001
|
+
if (!block || !block.trim()) return;
|
|
1002
|
+
const parsedBlock = parseSseBlock(block);
|
|
1003
|
+
if (!parsedBlock.data) return;
|
|
1004
|
+
|
|
1005
|
+
if (parsedBlock.data === "[DONE]") {
|
|
1006
|
+
if (!state.messageStopSent) {
|
|
1007
|
+
enqueueClaudeEvents(controller, finalizeOpenAIToClaudeStream(state));
|
|
1008
|
+
}
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
try {
|
|
1013
|
+
const parsed = JSON.parse(parsedBlock.data);
|
|
1014
|
+
enqueueClaudeEvents(controller, translateResponse(FORMATS.OPENAI, FORMATS.CLAUDE, parsed, state));
|
|
1015
|
+
} catch (error) {
|
|
1016
|
+
console.error("[Stream] Failed parsing OpenAI chunk:", error instanceof Error ? error.message : String(error));
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
103
1020
|
const transformStream = new TransformStream({
|
|
104
1021
|
transform(chunk, controller) {
|
|
105
|
-
buffer += decoder.decode(chunk, { stream: true });
|
|
106
|
-
const lines = buffer.split("\n");
|
|
107
|
-
buffer = lines.pop() || "";
|
|
108
|
-
|
|
109
|
-
for (const line of lines) {
|
|
110
|
-
const trimmed = line.trim();
|
|
111
|
-
if (!trimmed || !trimmed.startsWith("data:")) continue;
|
|
112
|
-
const data = trimmed.slice(5).trim();
|
|
1022
|
+
buffer += decoder.decode(chunk, { stream: true }).replace(/\r\n/g, "\n");
|
|
113
1023
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
1024
|
+
let boundaryIndex;
|
|
1025
|
+
while ((boundaryIndex = buffer.indexOf("\n\n")) >= 0) {
|
|
1026
|
+
const block = buffer.slice(0, boundaryIndex);
|
|
1027
|
+
buffer = buffer.slice(boundaryIndex + 2);
|
|
1028
|
+
processBlock(block, controller);
|
|
1029
|
+
}
|
|
1030
|
+
},
|
|
118
1031
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
console.error("[Stream] Failed parsing OpenAI chunk:", error instanceof Error ? error.message : String(error));
|
|
127
|
-
}
|
|
1032
|
+
flush(controller) {
|
|
1033
|
+
const remainder = buffer.trim();
|
|
1034
|
+
if (remainder) {
|
|
1035
|
+
processBlock(remainder, controller);
|
|
1036
|
+
}
|
|
1037
|
+
if (!state.messageStopSent) {
|
|
1038
|
+
enqueueClaudeEvents(controller, finalizeOpenAIToClaudeStream(state, { force: state.messageStartSent }));
|
|
128
1039
|
}
|
|
129
1040
|
}
|
|
130
1041
|
});
|