@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.
Files changed (43) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +337 -41
  3. package/package.json +19 -3
  4. package/src/cli/router-module.js +7331 -3805
  5. package/src/cli/wrangler-toml.js +1 -1
  6. package/src/cli-entry.js +162 -24
  7. package/src/node/amp-client-config.js +426 -0
  8. package/src/node/coding-tool-config.js +763 -0
  9. package/src/node/config-store.js +49 -18
  10. package/src/node/instance-state.js +213 -12
  11. package/src/node/listen-port.js +5 -37
  12. package/src/node/local-server-settings.js +122 -0
  13. package/src/node/local-server.js +3 -2
  14. package/src/node/provider-probe.js +13 -0
  15. package/src/node/start-command.js +282 -40
  16. package/src/node/startup-manager.js +64 -29
  17. package/src/node/web-command.js +106 -0
  18. package/src/node/web-console-assets.js +26 -0
  19. package/src/node/web-console-client.js +56 -0
  20. package/src/node/web-console-dev-assets.js +258 -0
  21. package/src/node/web-console-server.js +3146 -0
  22. package/src/node/web-console-styles.generated.js +1 -0
  23. package/src/node/web-console-ui/config-editor-utils.js +616 -0
  24. package/src/node/web-console-ui/lib/utils.js +6 -0
  25. package/src/node/web-console-ui/rate-limit-utils.js +144 -0
  26. package/src/node/web-console-ui/select-search-utils.js +36 -0
  27. package/src/runtime/codex-request-transformer.js +46 -5
  28. package/src/runtime/codex-response-transformer.js +268 -35
  29. package/src/runtime/config.js +1394 -35
  30. package/src/runtime/handler/amp-gemini.js +913 -0
  31. package/src/runtime/handler/amp-response.js +308 -0
  32. package/src/runtime/handler/amp.js +290 -0
  33. package/src/runtime/handler/auth.js +17 -2
  34. package/src/runtime/handler/provider-call.js +168 -50
  35. package/src/runtime/handler/provider-translation.js +937 -26
  36. package/src/runtime/handler/request.js +149 -6
  37. package/src/runtime/handler/route-debug.js +22 -1
  38. package/src/runtime/handler.js +449 -9
  39. package/src/runtime/subscription-auth.js +1 -6
  40. package/src/shared/local-router-defaults.js +62 -0
  41. package/src/translator/index.js +3 -1
  42. package/src/translator/request/openai-to-claude.js +217 -6
  43. 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 { withCorsHeaders } from "./http.js";
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
- if (Array.isArray(message.tool_calls)) {
60
- for (let index = 0; index < message.tool_calls.length; index += 1) {
61
- const call = message.tool_calls[index];
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?.finish_reason),
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
- if (data === "[DONE]") {
115
- controller.enqueue(encoder.encode("event: message_stop\ndata: {}\n\n"));
116
- continue;
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
- try {
120
- const parsed = JSON.parse(data);
121
- const translated = translateResponse(FORMATS.OPENAI, FORMATS.CLAUDE, parsed, state);
122
- for (const event of translated) {
123
- controller.enqueue(encoder.encode(formatClaudeEvent(event)));
124
- }
125
- } catch (error) {
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
  });