codexmate 0.0.37 → 0.0.39

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 (38) hide show
  1. package/cli/analytics-export-args.js +68 -0
  2. package/cli/builtin-proxy.js +626 -207
  3. package/cli/openai-bridge.js +541 -210
  4. package/cli/session-usage.js +187 -1
  5. package/cli.js +84 -2
  6. package/package.json +1 -1
  7. package/web-ui/app.js +12 -3
  8. package/web-ui/modules/app.computed.main-tabs.mjs +37 -30
  9. package/web-ui/modules/app.methods.claude-config.mjs +111 -9
  10. package/web-ui/modules/app.methods.openclaw-editing.mjs +48 -0
  11. package/web-ui/modules/app.methods.openclaw-persist.mjs +13 -7
  12. package/web-ui/modules/app.methods.providers.mjs +36 -10
  13. package/web-ui/modules/app.methods.runtime.mjs +76 -1
  14. package/web-ui/modules/app.methods.startup-claude.mjs +1 -0
  15. package/web-ui/modules/config-mode.computed.mjs +3 -3
  16. package/web-ui/modules/i18n.dict.mjs +13 -0
  17. package/web-ui/modules/i18n.mjs +65 -16
  18. package/web-ui/modules/skills.methods.mjs +1 -1
  19. package/web-ui/partials/index/layout-header.html +16 -46
  20. package/web-ui/partials/index/modal-openclaw-config.html +135 -71
  21. package/web-ui/partials/index/modal-webhook.html +8 -8
  22. package/web-ui/partials/index/modals-basic.html +56 -16
  23. package/web-ui/partials/index/panel-config-claude.html +20 -20
  24. package/web-ui/partials/index/panel-config-codex.html +5 -5
  25. package/web-ui/partials/index/panel-config-openclaw.html +70 -64
  26. package/web-ui/partials/index/panel-dashboard.html +62 -77
  27. package/web-ui/partials/index/panel-settings.html +28 -7
  28. package/web-ui/partials/index/panel-trash.html +14 -14
  29. package/web-ui/res/web-ui-render.precompiled.js +846 -539
  30. package/web-ui/styles/controls-forms.css +6 -0
  31. package/web-ui/styles/dashboard.css +46 -14
  32. package/web-ui/styles/layout-shell.css +45 -0
  33. package/web-ui/styles/navigation-panels.css +3 -3
  34. package/web-ui/styles/openclaw-structured.css +383 -33
  35. package/web-ui/styles/responsive.css +68 -0
  36. package/web-ui/styles/sessions-usage.css +105 -9
  37. package/web-ui/styles/settings-panel.css +4 -0
  38. package/web-ui/partials/index/panel-config-codex.html.bak +0 -337
@@ -138,7 +138,7 @@ function createBuiltinProxyRuntimeController(deps = {}) {
138
138
  return false;
139
139
  }
140
140
 
141
- const TRANSIENT_RETRY_DELAYS_MS = [200, 600];
141
+ const TRANSIENT_RETRY_DELAYS_MS = [200, 600, 1200];
142
142
 
143
143
  async function retryTransientRequest(executor) {
144
144
  let lastResult = null;
@@ -158,7 +158,7 @@ function createBuiltinProxyRuntimeController(deps = {}) {
158
158
  if (result.ok) return result;
159
159
  if (result.retry) return result;
160
160
  if (result.status && result.status > 0) return result;
161
- if (!isTransientNetworkError(result.error)) return result;
161
+ if (!result.retryTransient && !isTransientNetworkError(result.error)) return result;
162
162
  }
163
163
  return lastResult;
164
164
  }
@@ -264,6 +264,17 @@ function createBuiltinProxyRuntimeController(deps = {}) {
264
264
  }
265
265
  }
266
266
 
267
+ function parseJsonValueOrNull(value) {
268
+ if (typeof value !== 'string') return null;
269
+ const text = value.trim();
270
+ if (!text) return null;
271
+ try {
272
+ return JSON.parse(text);
273
+ } catch (_) {
274
+ return null;
275
+ }
276
+ }
277
+
267
278
  function normalizeChatUsageToResponsesUsage(usage) {
268
279
  if (!usage || typeof usage !== 'object' || Array.isArray(usage)) return undefined;
269
280
  const pickNumber = (...keys) => {
@@ -338,7 +349,7 @@ function createBuiltinProxyRuntimeController(deps = {}) {
338
349
  return blocks;
339
350
  }
340
351
 
341
- function buildResponsesPayloadFromChatCompletion(payload, fallbackModel = '') {
352
+ function buildResponsesPayloadFromChatCompletion(payload, fallbackModel = '', options = {}) {
342
353
  const base = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {};
343
354
  const choice = Array.isArray(base.choices) ? base.choices[0] : null;
344
355
  const message = choice && typeof choice === 'object' && choice.message && typeof choice.message === 'object'
@@ -355,16 +366,8 @@ function createBuiltinProxyRuntimeController(deps = {}) {
355
366
  }
356
367
  if (Array.isArray(message.tool_calls)) {
357
368
  for (const toolCall of message.tool_calls) {
358
- if (!toolCall || typeof toolCall !== 'object') continue;
359
- const fn = toolCall.function && typeof toolCall.function === 'object' ? toolCall.function : {};
360
- const name = typeof fn.name === 'string' ? fn.name : '';
361
- if (!name) continue;
362
- output.push({
363
- type: 'function_call',
364
- call_id: typeof toolCall.id === 'string' && toolCall.id ? toolCall.id : `call_${crypto.randomBytes(8).toString('hex')}`,
365
- name,
366
- arguments: stringifyJsonValue(fn.arguments, '{}')
367
- });
369
+ const item = buildResponsesToolCallItemFromChatToolCall(toolCall, options.toolTypesByName || {});
370
+ if (item) output.push(item);
368
371
  }
369
372
  }
370
373
  const finish = mapChatFinishReasonToResponses(choice);
@@ -378,151 +381,499 @@ function createBuiltinProxyRuntimeController(deps = {}) {
378
381
  });
379
382
  }
380
383
 
384
+ function isRecord(value) {
385
+ return !!value && typeof value === 'object' && !Array.isArray(value);
386
+ }
387
+
388
+ function asTrimmedString(value) {
389
+ return typeof value === 'string' ? value.trim() : '';
390
+ }
391
+
392
+ function cloneJsonValue(value) {
393
+ if (Array.isArray(value)) return value.map((item) => cloneJsonValue(item));
394
+ if (isRecord(value)) {
395
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, cloneJsonValue(item)]));
396
+ }
397
+ return value;
398
+ }
399
+
400
+ function normalizeResponsesToolOutput(value) {
401
+ if (typeof value === 'string') return value;
402
+ if (value == null) return '';
403
+ return stringifyJsonValue(value, '');
404
+ }
405
+
406
+ function normalizeOpenAiToolArguments(value) {
407
+ if (typeof value === 'string') return value;
408
+ if (value == null) return '{}';
409
+ return stringifyJsonValue(value, '{}');
410
+ }
411
+
412
+ function normalizeInputFileBlock(item) {
413
+ if (!isRecord(item)) return null;
414
+ const file = isRecord(item.file) ? item.file : item;
415
+ const out = {};
416
+ const fileId = asTrimmedString(file.file_id || file.id);
417
+ const filename = asTrimmedString(file.filename || file.name);
418
+ const fileData = asTrimmedString(file.file_data || file.data);
419
+ const mimeType = asTrimmedString(file.mime_type || file.media_type);
420
+ if (fileId) out.file_id = fileId;
421
+ if (filename) out.filename = filename;
422
+ if (fileData) out.file_data = fileData;
423
+ if (mimeType) out.mime_type = mimeType;
424
+ return Object.keys(out).length > 0 ? out : null;
425
+ }
426
+
427
+ function normalizeResponsesContentBlockForChat(item) {
428
+ if (typeof item === 'string') return item.trim() ? item : null;
429
+ if (!isRecord(item)) return null;
430
+
431
+ const type = asTrimmedString(item.type).toLowerCase();
432
+ if (!type) {
433
+ const text = asTrimmedString(item.text || item.content || item.output_text);
434
+ return text ? { type: 'text', text } : null;
435
+ }
436
+
437
+ if (type === 'input_text' || type === 'output_text' || type === 'text' || type === 'summary_text' || type === 'reasoning_text') {
438
+ const text = typeof item.text === 'string' ? item.text : asTrimmedString(item.content || item.output_text);
439
+ return text ? { type: 'text', text } : null;
440
+ }
441
+
442
+ if (type === 'refusal' && typeof item.refusal === 'string') {
443
+ return item.refusal ? { type: 'text', text: item.refusal } : null;
444
+ }
445
+
446
+ if (type === 'input_image') {
447
+ const raw = item.image_url != null ? item.image_url : (item.url != null ? item.url : item.imageUrl);
448
+ if (raw === undefined) return null;
449
+ return {
450
+ type: 'image_url',
451
+ image_url: typeof raw === 'string' ? { url: raw } : cloneJsonValue(raw)
452
+ };
453
+ }
454
+
455
+ if (type === 'image_url' && item.image_url !== undefined) {
456
+ return { type: 'image_url', image_url: item.image_url };
457
+ }
458
+
459
+ if (type === 'input_audio') {
460
+ if (item.input_audio !== undefined) return { type: 'input_audio', input_audio: item.input_audio };
461
+ if (item.data !== undefined || item.format !== undefined) {
462
+ return { type: 'input_audio', input_audio: { data: item.data, format: item.format } };
463
+ }
464
+ return null;
465
+ }
466
+
467
+ if (type === 'input_file' || type === 'file') {
468
+ const file = normalizeInputFileBlock(item);
469
+ return file ? { type: 'file', file } : null;
470
+ }
471
+
472
+ if (type === 'reasoning' || type === 'thinking' || type === 'redacted_reasoning') {
473
+ const text = asTrimmedString(item.text || item.content);
474
+ return text ? { type: 'text', text } : null;
475
+ }
476
+
477
+ return cloneJsonValue(item);
478
+ }
479
+
480
+ function toOpenAiMessageContent(content) {
481
+ if (typeof content === 'string') return content;
482
+ if (!Array.isArray(content)) {
483
+ if (isRecord(content)) {
484
+ const single = normalizeResponsesContentBlockForChat(content);
485
+ if (!single) return '';
486
+ return typeof single === 'string' ? single : [single];
487
+ }
488
+ return '';
489
+ }
490
+
491
+ const blocks = content
492
+ .map((item) => normalizeResponsesContentBlockForChat(item))
493
+ .filter((item) => !!item);
494
+
495
+ if (blocks.length === 0) return '';
496
+ if (blocks.length === 1 && typeof blocks[0] === 'string') return blocks[0];
497
+ return blocks;
498
+ }
499
+
500
+ const RESPONSES_TOOL_CALL_INPUT_TYPES = new Set(['function_call', 'custom_tool_call', 'mcp_tool_call', 'local_shell_call']);
501
+ const RESPONSES_TOOL_CALL_OUTPUT_TYPES = new Set(['function_call_output', 'custom_tool_call_output', 'mcp_tool_call_output', 'tool_search_output', 'local_shell_call_output']);
502
+
503
+ function stripOrphanedResponsesToolOutputs(input) {
504
+ if (!Array.isArray(input)) return input;
505
+ const seenToolCallIds = new Set();
506
+ const sanitized = [];
507
+ for (const item of input) {
508
+ if (!isRecord(item)) {
509
+ sanitized.push(item);
510
+ continue;
511
+ }
512
+ const type = asTrimmedString(item.type).toLowerCase();
513
+ if (RESPONSES_TOOL_CALL_INPUT_TYPES.has(type)) {
514
+ const callId = asTrimmedString(item.call_id || item.id);
515
+ if (callId) seenToolCallIds.add(callId);
516
+ sanitized.push(item);
517
+ continue;
518
+ }
519
+ if (RESPONSES_TOOL_CALL_OUTPUT_TYPES.has(type)) {
520
+ const callId = asTrimmedString(item.call_id || item.id);
521
+ if (!callId || !seenToolCallIds.has(callId)) continue;
522
+ sanitized.push(item);
523
+ continue;
524
+ }
525
+ sanitized.push(item);
526
+ }
527
+ return sanitized;
528
+ }
529
+
530
+ function normalizeFreeformToolArguments(value) {
531
+ if (typeof value === 'string') return stringifyJsonValue({ input: value }, '{"input":""}');
532
+ if (value == null) return '{"input":""}';
533
+ if (isRecord(value) && Object.prototype.hasOwnProperty.call(value, 'input')) {
534
+ return stringifyJsonValue(value, '{"input":""}');
535
+ }
536
+ return stringifyJsonValue({ input: normalizeResponsesToolOutput(value) }, '{"input":""}');
537
+ }
538
+
539
+ function toOpenAiToolCall(item, fallbackIndex) {
540
+ if (!isRecord(item)) return null;
541
+ const callId = asTrimmedString(item.call_id || item.id) || `call_${crypto.randomBytes(8).toString('hex')}_${fallbackIndex}`;
542
+ const name = asTrimmedString(item.name) || asTrimmedString(item.server_label);
543
+ if (!name) return null;
544
+ const type = asTrimmedString(item.type).toLowerCase();
545
+ const rawArguments = item.arguments != null ? item.arguments : item.input;
546
+ const args = type === 'custom_tool_call' && item.arguments == null
547
+ ? normalizeFreeformToolArguments(rawArguments)
548
+ : normalizeOpenAiToolArguments(rawArguments);
549
+ return {
550
+ id: callId,
551
+ type: 'function',
552
+ function: {
553
+ name,
554
+ arguments: args
555
+ }
556
+ };
557
+ }
558
+
559
+ function hasOpenAiMessageContent(content) {
560
+ return typeof content === 'string'
561
+ ? content.trim().length > 0
562
+ : Array.isArray(content) && content.length > 0;
563
+ }
564
+
381
565
  function normalizeResponsesInputToChatMessages(input) {
382
- // 参考 cc-switch 的 Responses 转换形态:message content 保持为消息,function_call /
383
- // function_call_output 提升为 OpenAI Chat assistant tool_calls / tool 消息。
384
- const toChatContent = (blocks) => {
385
- if (!Array.isArray(blocks)) return '';
386
- const out = [];
387
- for (const block of blocks) {
388
- if (!block || typeof block !== 'object') continue;
389
- const type = typeof block.type === 'string' ? block.type : '';
390
- if ((type === 'input_text' || type === 'output_text' || type === 'text') && typeof block.text === 'string') {
391
- out.push({ type: 'text', text: block.text });
392
- continue;
393
- }
394
- if (type === 'refusal' && typeof block.refusal === 'string') {
395
- out.push({ type: 'text', text: block.refusal });
396
- continue;
397
- }
398
- if (type === 'input_image') {
399
- const raw = block.image_url != null ? block.image_url : block.imageUrl;
400
- const url = typeof raw === 'string'
401
- ? raw
402
- : (raw && typeof raw === 'object' && typeof raw.url === 'string' ? raw.url : '');
403
- if (url) {
404
- out.push({ type: 'image_url', image_url: { url } });
405
- }
406
- continue;
407
- }
408
- if (type === 'image_url' && block.image_url) {
409
- out.push({ type: 'image_url', image_url: block.image_url });
410
- }
566
+ // 参考 metapi 的 Responses Chat 桥接:聚合连续 tool calls、丢弃孤儿 tool outputs,
567
+ // 并保留 reasoning / richer content blocks / developer-role compatibility。
568
+ const messages = [];
569
+ const normalizedInput = stripOrphanedResponsesToolOutputs(input);
570
+ let functionCallIndex = 0;
571
+ let pendingToolCalls = [];
572
+ const emittedToolCallIds = new Set();
573
+
574
+ const flushPendingToolCalls = () => {
575
+ if (pendingToolCalls.length <= 0) return;
576
+ for (const toolCall of pendingToolCalls) {
577
+ const callId = asTrimmedString(toolCall.id);
578
+ if (callId) emittedToolCallIds.add(callId);
411
579
  }
412
- if (out.length === 0) return '';
413
- return out;
580
+ messages.push({
581
+ role: 'assistant',
582
+ content: null,
583
+ tool_calls: pendingToolCalls
584
+ });
585
+ pendingToolCalls = [];
414
586
  };
415
587
 
416
- const messageFromResponsesItem = (item) => {
417
- if (!item || typeof item !== 'object') return null;
418
- const type = typeof item.type === 'string' ? item.type : '';
419
- if (type === 'function_call') {
420
- const name = typeof item.name === 'string' ? item.name : '';
421
- if (!name) return null;
422
- return {
423
- role: 'assistant',
424
- content: null,
425
- tool_calls: [{
426
- id: typeof item.call_id === 'string' && item.call_id ? item.call_id : (typeof item.id === 'string' ? item.id : `call_${crypto.randomBytes(8).toString('hex')}`),
427
- type: 'function',
428
- function: {
429
- name,
430
- arguments: stringifyJsonValue(item.arguments, '{}')
431
- }
432
- }]
433
- };
588
+ const pushToolOutputMessage = (callIdRaw, outputRaw) => {
589
+ const toolCallId = asTrimmedString(callIdRaw);
590
+ if (!toolCallId) return;
591
+ messages.push({
592
+ role: 'tool',
593
+ tool_call_id: toolCallId,
594
+ content: normalizeResponsesToolOutput(outputRaw)
595
+ });
596
+ };
597
+
598
+ const processInputItem = (item) => {
599
+ if (typeof item === 'string') {
600
+ flushPendingToolCalls();
601
+ const text = item.trim();
602
+ if (text) messages.push({ role: 'user', content: text });
603
+ return;
434
604
  }
435
- if (type === 'function_call_output') {
436
- const callId = typeof item.call_id === 'string' ? item.call_id : '';
437
- return {
438
- role: 'tool',
439
- tool_call_id: callId,
440
- content: stringifyJsonValue(item.output, '')
441
- };
605
+ if (!isRecord(item)) return;
606
+
607
+ const itemType = asTrimmedString(item.type).toLowerCase();
608
+ if (itemType === 'function_call' || itemType === 'custom_tool_call') {
609
+ const toolCall = toOpenAiToolCall(item, functionCallIndex);
610
+ functionCallIndex += 1;
611
+ if (toolCall) pendingToolCalls.push(toolCall);
612
+ return;
442
613
  }
443
- if (typeof item.role === 'string' && item.content != null) {
444
- const role = item.role.trim() || 'user';
445
- const content = Array.isArray(item.content)
446
- ? toChatContent(item.content)
447
- : item.content;
448
- return content || content === null ? { role, content } : null;
614
+
615
+ if (itemType === 'function_call_output' || itemType === 'custom_tool_call_output') {
616
+ flushPendingToolCalls();
617
+ const toolCallId = asTrimmedString(item.call_id || item.id);
618
+ if (!toolCallId || !emittedToolCallIds.has(toolCallId)) return;
619
+ pushToolOutputMessage(toolCallId, item.output != null ? item.output : item.content);
620
+ return;
449
621
  }
450
- if (type) {
451
- const content = toChatContent([item]);
452
- return content ? { role: 'user', content } : null;
622
+
623
+ if (itemType === 'reasoning') {
624
+ // Any non-tool-call item is a sequence boundary: keep only consecutive
625
+ // tool calls in the same assistant `tool_calls` message.
626
+ flushPendingToolCalls();
627
+ const reasoningContent = toOpenAiMessageContent(item.summary != null ? item.summary : (item.content != null ? item.content : item));
628
+ const reasoningSignature = asTrimmedString(item.encrypted_content || item.reasoning_signature);
629
+ if (!hasOpenAiMessageContent(reasoningContent) && !reasoningSignature) return;
630
+ const message = { role: 'assistant', content: reasoningContent };
631
+ if (reasoningSignature) message.reasoning_signature = reasoningSignature;
632
+ messages.push(message);
633
+ return;
634
+ }
635
+
636
+ flushPendingToolCalls();
637
+ const role = asTrimmedString(item.role).toLowerCase() || 'user';
638
+ const normalizedRole = role === 'developer' ? 'system' : role;
639
+ const content = toOpenAiMessageContent(item.content != null ? item.content : (item.input != null ? item.input : item));
640
+
641
+ if (normalizedRole === 'tool') {
642
+ const toolCallId = asTrimmedString(item.tool_call_id || item.call_id || item.id);
643
+ if (!toolCallId || !emittedToolCallIds.has(toolCallId)) return;
644
+ pushToolOutputMessage(toolCallId, item.content);
645
+ return;
646
+ }
647
+
648
+ if (!hasOpenAiMessageContent(content)) return;
649
+ const message = { role: normalizedRole, content };
650
+ const phase = asTrimmedString(item.phase);
651
+ if (phase) message.phase = phase;
652
+ messages.push(message);
653
+ };
654
+
655
+ if (typeof normalizedInput === 'string') {
656
+ const text = normalizedInput.trim();
657
+ if (text) messages.push({ role: 'user', content: text });
658
+ } else if (Array.isArray(normalizedInput)) {
659
+ for (const item of normalizedInput) processInputItem(item);
660
+ } else if (isRecord(normalizedInput)) {
661
+ processInputItem(normalizedInput);
662
+ }
663
+ flushPendingToolCalls();
664
+ return messages;
665
+ }
666
+
667
+ function normalizeFunctionToolForChat(tool) {
668
+ if (!isRecord(tool)) return null;
669
+ const sourceFn = isRecord(tool.function) ? tool.function : tool;
670
+ const name = asTrimmedString(sourceFn.name) || asTrimmedString(tool.name);
671
+ if (!name) return null;
672
+ const fn = { name };
673
+ const description = asTrimmedString(sourceFn.description) || asTrimmedString(tool.description);
674
+ if (description) fn.description = description;
675
+ if (sourceFn.parameters !== undefined) {
676
+ fn.parameters = cloneJsonValue(sourceFn.parameters);
677
+ } else if (tool.parameters !== undefined) {
678
+ fn.parameters = cloneJsonValue(tool.parameters);
679
+ }
680
+ if (typeof sourceFn.strict === 'boolean') {
681
+ fn.strict = sourceFn.strict;
682
+ } else if (typeof tool.strict === 'boolean') {
683
+ fn.strict = tool.strict;
684
+ }
685
+ return { type: 'function', function: fn };
686
+ }
687
+
688
+ function buildLocalShellToolForChat(tool) {
689
+ return {
690
+ type: 'function',
691
+ function: {
692
+ name: asTrimmedString(tool && tool.name) || 'local_shell',
693
+ description: asTrimmedString(tool && tool.description) || 'Run a local shell command and return its output.',
694
+ parameters: {
695
+ type: 'object',
696
+ properties: {
697
+ cmd: { type: 'string', description: 'Shell command to execute.' },
698
+ yield_time_ms: { type: 'number', description: 'Milliseconds to wait before yielding partial output.' },
699
+ max_output_tokens: { type: 'number', description: 'Maximum output tokens to return.' }
700
+ },
701
+ required: ['cmd'],
702
+ additionalProperties: true
703
+ }
453
704
  }
454
- return null;
455
705
  };
706
+ }
456
707
 
457
- if (typeof input === 'string') {
458
- return [{ role: 'user', content: input }];
708
+ function buildFreeformToolForChat(tool, fallbackName = 'custom_tool') {
709
+ return {
710
+ type: 'function',
711
+ function: {
712
+ name: asTrimmedString(tool && tool.name) || fallbackName,
713
+ description: asTrimmedString(tool && tool.description) || 'Pass raw freeform input to the local tool.',
714
+ parameters: {
715
+ type: 'object',
716
+ properties: {
717
+ input: { type: 'string', description: 'Raw tool input.' }
718
+ },
719
+ required: ['input'],
720
+ additionalProperties: false
721
+ }
722
+ }
723
+ };
724
+ }
725
+
726
+ const MAX_RESPONSES_TOOL_NAMESPACE_DEPTH = 5;
727
+
728
+ function rememberResponsesToolType(tool, target, depth = 0) {
729
+ if (!isRecord(tool) || !target || depth > MAX_RESPONSES_TOOL_NAMESPACE_DEPTH) return;
730
+ const type = asTrimmedString(tool.type).toLowerCase();
731
+ if (type === 'namespace' && Array.isArray(tool.tools)) {
732
+ for (const inner of tool.tools) rememberResponsesToolType(inner, target, depth + 1);
733
+ return;
459
734
  }
460
- if (input && typeof input === 'object' && !Array.isArray(input)) {
461
- const message = messageFromResponsesItem(input);
462
- return message ? [message] : [];
735
+ const sourceFn = isRecord(tool.function) ? tool.function : tool;
736
+ const name = asTrimmedString(sourceFn.name) || asTrimmedString(tool.name);
737
+ if (!name) return;
738
+ if (type === 'local_shell') {
739
+ target[name] = 'local_shell_call';
740
+ return;
463
741
  }
464
- if (!Array.isArray(input)) {
465
- return [];
742
+ if (type === 'custom' || type === 'custom_tool' || (!type && name === 'apply_patch')) {
743
+ target[name] = 'custom_tool_call';
744
+ return;
745
+ }
746
+ if (type === 'function') {
747
+ target[name] = 'function_call';
466
748
  }
749
+ }
467
750
 
468
- const messages = [];
469
- for (const item of input) {
470
- const message = messageFromResponsesItem(item);
471
- if (message) messages.push(message);
751
+ function collectResponsesToolTypesByName(tools) {
752
+ const result = {};
753
+ if (!Array.isArray(tools)) return result;
754
+ for (const tool of tools) rememberResponsesToolType(tool, result);
755
+ return result;
756
+ }
757
+
758
+ function extractFreeformInputFromChatArguments(argumentsText) {
759
+ if (typeof argumentsText !== 'string') return '';
760
+ const parsed = parseJsonValueOrNull(argumentsText);
761
+ if (isRecord(parsed) && Object.prototype.hasOwnProperty.call(parsed, 'input')) {
762
+ return typeof parsed.input === 'string' ? parsed.input : normalizeResponsesToolOutput(parsed.input);
763
+ }
764
+ return argumentsText;
765
+ }
766
+
767
+ function extractLocalShellActionFromChatArguments(argumentsText) {
768
+ const parsed = parseJsonValueOrNull(argumentsText);
769
+ if (isRecord(parsed)) return cloneJsonValue(parsed);
770
+ return { cmd: typeof argumentsText === 'string' ? argumentsText : '' };
771
+ }
772
+
773
+ function buildResponsesToolCallItemFromChatToolCall(toolCall, toolTypesByName = {}) {
774
+ if (!isRecord(toolCall)) return null;
775
+ const fn = isRecord(toolCall.function) ? toolCall.function : {};
776
+ const name = asTrimmedString(fn.name);
777
+ if (!name) return null;
778
+ const callId = asTrimmedString(toolCall.id) || `call_${crypto.randomBytes(8).toString('hex')}`;
779
+ const argumentsText = typeof fn.arguments === 'string' ? fn.arguments : '';
780
+ const responseType = toolTypesByName && toolTypesByName[name] ? toolTypesByName[name] : 'function_call';
781
+ if (responseType === 'custom_tool_call') {
782
+ return {
783
+ type: 'custom_tool_call',
784
+ call_id: callId,
785
+ name,
786
+ input: extractFreeformInputFromChatArguments(argumentsText)
787
+ };
472
788
  }
473
- if (messages.length > 0) {
474
- return messages;
789
+ if (responseType === 'local_shell_call') {
790
+ return {
791
+ type: 'local_shell_call',
792
+ call_id: callId,
793
+ name,
794
+ action: extractLocalShellActionFromChatArguments(argumentsText)
795
+ };
475
796
  }
797
+ return {
798
+ type: 'function_call',
799
+ call_id: callId,
800
+ name,
801
+ arguments: argumentsText
802
+ };
803
+ }
476
804
 
477
- const fallbackContent = toChatContent(input);
478
- if (fallbackContent) {
479
- return [{ role: 'user', content: fallbackContent }];
805
+ function normalizeSingleResponsesToolToChatTools(tool, depth = 0) {
806
+ if (!isRecord(tool) || depth > MAX_RESPONSES_TOOL_NAMESPACE_DEPTH) return [];
807
+ const type = asTrimmedString(tool.type).toLowerCase();
808
+ if (type === 'namespace' && Array.isArray(tool.tools)) {
809
+ return tool.tools.flatMap((inner) => normalizeSingleResponsesToolToChatTools(inner, depth + 1));
810
+ }
811
+ if (type === 'function') {
812
+ const converted = normalizeFunctionToolForChat(tool);
813
+ return converted ? [converted] : [];
814
+ }
815
+ if (type === 'local_shell') {
816
+ return [buildLocalShellToolForChat(tool)];
817
+ }
818
+ const name = asTrimmedString(tool.name);
819
+ if (type === 'custom' || type === 'custom_tool' || (!type && name === 'apply_patch')) {
820
+ return [buildFreeformToolForChat(tool, name || 'custom_tool')];
480
821
  }
822
+ // Hosted Responses tools such as web_search/image_generation/computer_use
823
+ // do not have a safe Chat Completions representation. Passing them through
824
+ // as-is makes OpenAI-compatible chat gateways reject the request, so drop
825
+ // them instead of pretending the shapes are compatible.
481
826
  return [];
482
827
  }
483
828
 
484
829
  function normalizeResponsesToolsToChatTools(tools) {
485
830
  if (!Array.isArray(tools)) return tools;
486
- return tools
487
- .map((tool) => {
488
- if (!tool || typeof tool !== 'object') return null;
489
- if (tool.type !== 'function') return tool;
490
- const sourceFn = tool.function && typeof tool.function === 'object' && !Array.isArray(tool.function)
491
- ? tool.function
492
- : {};
493
- const name = typeof sourceFn.name === 'string' && sourceFn.name.trim()
494
- ? sourceFn.name.trim()
495
- : (typeof tool.name === 'string' ? tool.name.trim() : '');
496
- if (!name) return null;
497
- const description = typeof sourceFn.description === 'string'
498
- ? sourceFn.description
499
- : (typeof tool.description === 'string' ? tool.description : undefined);
500
- const parameters = sourceFn.parameters && typeof sourceFn.parameters === 'object' && !Array.isArray(sourceFn.parameters)
501
- ? sourceFn.parameters
502
- : (tool.parameters && typeof tool.parameters === 'object' && !Array.isArray(tool.parameters) ? tool.parameters : {});
503
- const strict = typeof sourceFn.strict === 'boolean'
504
- ? sourceFn.strict
505
- : (typeof tool.strict === 'boolean' ? tool.strict : undefined);
506
- const fn = { name, parameters };
507
- if (description !== undefined) fn.description = description;
508
- if (strict !== undefined) fn.strict = strict;
509
- return { type: 'function', function: fn };
510
- })
511
- .filter(Boolean);
831
+ return tools.flatMap((tool) => normalizeSingleResponsesToolToChatTools(tool));
512
832
  }
513
833
 
514
834
  function normalizeResponsesToolChoiceToChatToolChoice(toolChoice) {
515
- if (!toolChoice || typeof toolChoice !== 'object' || Array.isArray(toolChoice)) return toolChoice;
516
- if (toolChoice.type === 'function' && typeof toolChoice.name === 'string') {
517
- return { type: 'function', function: { name: toolChoice.name } };
835
+ if (toolChoice === undefined) return undefined;
836
+ if (typeof toolChoice === 'string') return toolChoice;
837
+ if (!isRecord(toolChoice)) return toolChoice;
838
+
839
+ const type = asTrimmedString(toolChoice.type).toLowerCase();
840
+ if (type === 'tool' || type === 'function' || type === 'custom' || type === 'custom_tool' || type === 'local_shell') {
841
+ if (isRecord(toolChoice.function) && asTrimmedString(toolChoice.function.name)) return cloneJsonValue(toolChoice);
842
+ const name = asTrimmedString(toolChoice.name) || asTrimmedString(toolChoice.server_label);
843
+ if (!name) return 'required';
844
+ return { type: 'function', function: { name } };
845
+ }
846
+ if (type === 'auto' || type === 'none' || type === 'required') return type;
847
+ return 'auto';
848
+ }
849
+
850
+ function getChatToolChoiceName(toolChoice) {
851
+ if (!isRecord(toolChoice)) return '';
852
+ if (isRecord(toolChoice.function)) return asTrimmedString(toolChoice.function.name);
853
+ return '';
854
+ }
855
+
856
+ function pruneInvalidChatToolChoice(chatBody) {
857
+ if (!isRecord(chatBody) || !Array.isArray(chatBody.tools)) return;
858
+ if (chatBody.tools.length === 0) {
859
+ delete chatBody.tools;
860
+ delete chatBody.tool_choice;
861
+ return;
862
+ }
863
+ const chosenName = getChatToolChoiceName(chatBody.tool_choice);
864
+ if (!chosenName) return;
865
+ const toolNames = new Set(chatBody.tools
866
+ .map((tool) => isRecord(tool) && isRecord(tool.function) ? asTrimmedString(tool.function.name) : '')
867
+ .filter(Boolean));
868
+ if (!toolNames.has(chosenName)) {
869
+ delete chatBody.tool_choice;
518
870
  }
519
- return toolChoice;
520
871
  }
521
872
 
522
873
  function buildChatCompletionsBodyFromResponsesPayload(payload) {
523
- const source = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {};
874
+ const source = isRecord(payload) ? payload : {};
524
875
  const messages = normalizeResponsesInputToChatMessages(source.input);
525
- const instructions = typeof source.instructions === 'string' ? source.instructions.trim() : '';
876
+ const instructions = asTrimmedString(source.instructions);
526
877
  if (instructions) {
527
878
  messages.unshift({ role: 'system', content: instructions });
528
879
  }
@@ -536,12 +887,12 @@ function createBuiltinProxyRuntimeController(deps = {}) {
536
887
  const passthroughKeys = [
537
888
  'frequency_penalty',
538
889
  'presence_penalty',
539
- 'response_format',
540
890
  'stop',
541
891
  'temperature',
542
892
  'top_p',
543
893
  'tools',
544
894
  'tool_choice',
895
+ 'parallel_tool_calls',
545
896
  'logprobs',
546
897
  'top_logprobs',
547
898
  'kbs',
@@ -551,7 +902,9 @@ function createBuiltinProxyRuntimeController(deps = {}) {
551
902
  'n',
552
903
  'modalities',
553
904
  'audio',
554
- 'reasoning_effort'
905
+ 'reasoning',
906
+ 'reasoning_effort',
907
+ 'service_tier'
555
908
  ];
556
909
  for (const key of passthroughKeys) {
557
910
  if (Object.prototype.hasOwnProperty.call(source, key)) {
@@ -560,11 +913,22 @@ function createBuiltinProxyRuntimeController(deps = {}) {
560
913
  } else if (key === 'tool_choice') {
561
914
  chatBody[key] = normalizeResponsesToolChoiceToChatToolChoice(source[key]);
562
915
  } else {
563
- chatBody[key] = source[key];
916
+ chatBody[key] = cloneJsonValue(source[key]);
564
917
  }
565
918
  }
566
919
  }
567
920
 
921
+ if (Object.prototype.hasOwnProperty.call(source, 'response_format')) {
922
+ chatBody.response_format = cloneJsonValue(source.response_format);
923
+ } else if (isRecord(source.text) && source.text.format !== undefined) {
924
+ chatBody.response_format = cloneJsonValue(source.text.format);
925
+ }
926
+ if (isRecord(source.text) && asTrimmedString(source.text.verbosity)) {
927
+ chatBody.verbosity = asTrimmedString(source.text.verbosity);
928
+ }
929
+
930
+ pruneInvalidChatToolChoice(chatBody);
931
+
568
932
  if (Object.prototype.hasOwnProperty.call(source, 'max_tokens')) {
569
933
  chatBody.max_tokens = source.max_tokens;
570
934
  } else if (source.max_output_tokens != null) {
@@ -712,6 +1076,8 @@ function createBuiltinProxyRuntimeController(deps = {}) {
712
1076
  content: [{ type: 'output_text', text: '' }]
713
1077
  };
714
1078
  state.output.push(state.messageItem);
1079
+ state.outputStarted = true;
1080
+ beginChatStreamResponsesSse(state);
715
1081
  writeSse(state.res, 'response.output_item.added', {
716
1082
  type: 'response.output_item.added',
717
1083
  output_index: state.output.length - 1,
@@ -764,6 +1130,7 @@ function createBuiltinProxyRuntimeController(deps = {}) {
764
1130
 
765
1131
  function finishChatStreamResponsesSse(state) {
766
1132
  if (state.finished) return;
1133
+ beginChatStreamResponsesSse(state);
767
1134
  state.finished = true;
768
1135
  stopChatStreamHeartbeat(state);
769
1136
 
@@ -787,14 +1154,11 @@ function createBuiltinProxyRuntimeController(deps = {}) {
787
1154
 
788
1155
  for (const toolCall of state.toolCalls) {
789
1156
  if (!toolCall) continue;
790
- const item = {
791
- type: 'function_call',
792
- call_id: toolCall.id || `call_${crypto.randomBytes(8).toString('hex')}`,
793
- name: toolCall.function && typeof toolCall.function.name === 'string' ? toolCall.function.name : '',
794
- arguments: toolCall.function && typeof toolCall.function.arguments === 'string' ? toolCall.function.arguments : ''
795
- };
1157
+ const item = buildResponsesToolCallItemFromChatToolCall(toolCall, state.toolTypesByName || {});
1158
+ if (!item) continue;
796
1159
  const outputIndex = state.output.length;
797
1160
  state.output.push(item);
1161
+ state.outputStarted = true;
798
1162
  writeSse(state.res, 'response.output_item.added', {
799
1163
  type: 'response.output_item.added',
800
1164
  output_index: outputIndex,
@@ -829,13 +1193,70 @@ function createBuiltinProxyRuntimeController(deps = {}) {
829
1193
  } catch (_) {}
830
1194
  }
831
1195
 
1196
+ function beginChatStreamResponsesSse(state) {
1197
+ if (!state || state.started) return;
1198
+ state.started = true;
1199
+ const res = state.res;
1200
+ if (!res.headersSent) {
1201
+ res.writeHead(200, {
1202
+ 'Content-Type': 'text/event-stream; charset=utf-8',
1203
+ 'Cache-Control': 'no-cache',
1204
+ 'Connection': 'keep-alive',
1205
+ 'X-Accel-Buffering': 'no'
1206
+ });
1207
+ }
1208
+ startChatStreamHeartbeat(state);
1209
+ if (typeof res.on === 'function' && !state.closeListenerAttached) {
1210
+ state.closeListenerAttached = true;
1211
+ res.on('close', () => {
1212
+ stopChatStreamHeartbeat(state);
1213
+ if (!state.finished && state.upstreamReq) {
1214
+ try { state.upstreamReq.destroy(new Error('client aborted')); } catch (_) {}
1215
+ }
1216
+ });
1217
+ }
1218
+ writeSse(res, 'response.created', {
1219
+ type: 'response.created',
1220
+ response: {
1221
+ id: state.responseId,
1222
+ model: state.model,
1223
+ created_at: state.createdAt
1224
+ }
1225
+ });
1226
+ }
1227
+
832
1228
  function failChatStreamResponsesSse(state, message) {
833
1229
  if (!state || state.finished) return;
1230
+ beginChatStreamResponsesSse(state);
834
1231
  state.finished = true;
835
1232
  stopChatStreamHeartbeat(state);
836
1233
  failResponsesSseRaw(state.res, message);
837
1234
  }
838
1235
 
1236
+ function createChatStreamResponsesSseState(res, model, options = {}) {
1237
+ let sequence = 0;
1238
+ return {
1239
+ res,
1240
+ upstreamReq: null,
1241
+ responseId: `resp_${crypto.randomBytes(10).toString('hex')}`,
1242
+ model: typeof model === 'string' ? model : '',
1243
+ createdAt: Math.floor(Date.now() / 1000),
1244
+ output: [],
1245
+ messageItem: null,
1246
+ messageText: '',
1247
+ toolCalls: [],
1248
+ toolTypesByName: options.toolTypesByName || {},
1249
+ finished: false,
1250
+ started: false,
1251
+ outputStarted: false,
1252
+ closeListenerAttached: false,
1253
+ nextSeq: () => {
1254
+ sequence += 1;
1255
+ return sequence;
1256
+ }
1257
+ };
1258
+ }
1259
+
839
1260
  function streamChatCompletionsAsResponsesSse(targetUrl, options = {}) {
840
1261
  const parsed = new URL(targetUrl);
841
1262
  const transport = parsed.protocol === 'https:' ? https : http;
@@ -853,9 +1274,13 @@ function createBuiltinProxyRuntimeController(deps = {}) {
853
1274
  : 30000;
854
1275
  const res = options.res;
855
1276
  const model = typeof options.model === 'string' ? options.model : '';
1277
+ const sharedState = options.streamState || createChatStreamResponsesSseState(res, model, {
1278
+ toolTypesByName: options.toolTypesByName || {}
1279
+ });
856
1280
 
857
1281
  return new Promise((resolve) => {
858
1282
  let settled = false;
1283
+ let streamAccepted = false;
859
1284
  const finish = (value) => {
860
1285
  if (settled) return;
861
1286
  settled = true;
@@ -873,13 +1298,21 @@ function createBuiltinProxyRuntimeController(deps = {}) {
873
1298
  const status = upstreamRes.statusCode || 0;
874
1299
  const chunks = [];
875
1300
  const contentType = String(upstreamRes.headers && upstreamRes.headers['content-type'] || '');
1301
+ streamAccepted = status >= 200 && status < 300 && /text\/event-stream/i.test(contentType);
1302
+ if (streamAccepted) {
1303
+ req.setTimeout(0);
1304
+ }
876
1305
  let streamState = null;
877
1306
 
878
1307
  const handleAbort = (reason) => {
879
1308
  if (settled) return;
880
1309
  if (streamState) {
881
- failChatStreamResponsesSse(streamState, reason);
882
- finish({ ok: true });
1310
+ if (streamState.outputStarted) {
1311
+ failChatStreamResponsesSse(streamState, reason);
1312
+ finish({ ok: true });
1313
+ return;
1314
+ }
1315
+ finish({ ok: false, retryTransient: true, error: reason || 'upstream stream failed' });
883
1316
  return;
884
1317
  }
885
1318
  if (res.headersSent) {
@@ -887,11 +1320,14 @@ function createBuiltinProxyRuntimeController(deps = {}) {
887
1320
  finish({ ok: true });
888
1321
  return;
889
1322
  }
1323
+ const bodyText = chunks.length ? Buffer.concat(chunks).toString('utf-8') : '';
1324
+ const transient = isTransientNetworkError(reason) || /aborted|stream aborted/i.test(String(reason || ''));
890
1325
  finish({
891
1326
  ok: false,
892
- status,
1327
+ ...(transient ? {} : { status }),
1328
+ ...(transient ? { retryTransient: true } : {}),
893
1329
  error: reason,
894
- bodyText: chunks.length ? Buffer.concat(chunks).toString('utf-8') : ''
1330
+ bodyText
895
1331
  });
896
1332
  };
897
1333
  upstreamRes.on('error', (err) => handleAbort(err && err.message ? err.message : 'upstream stream failed'));
@@ -909,18 +1345,17 @@ function createBuiltinProxyRuntimeController(deps = {}) {
909
1345
  return;
910
1346
  }
911
1347
 
912
- res.writeHead(200, {
913
- 'Content-Type': 'text/event-stream; charset=utf-8',
914
- 'Cache-Control': 'no-cache',
915
- 'Connection': 'keep-alive',
916
- 'X-Accel-Buffering': 'no'
917
- });
918
-
919
1348
  if (!/text\/event-stream/i.test(contentType)) {
920
1349
  upstreamRes.on('data', (chunk) => chunk && chunks.push(chunk));
921
1350
  upstreamRes.on('end', () => {
922
1351
  const text = chunks.length ? Buffer.concat(chunks).toString('utf-8') : '';
923
1352
  const parsedJson = parseJsonOrError(text);
1353
+ res.writeHead(200, {
1354
+ 'Content-Type': 'text/event-stream; charset=utf-8',
1355
+ 'Cache-Control': 'no-cache',
1356
+ 'Connection': 'keep-alive',
1357
+ 'X-Accel-Buffering': 'no'
1358
+ });
924
1359
  if (parsedJson.error) {
925
1360
  writeSse(res, 'response.failed', { type: 'response.failed', error: `invalid upstream response: ${parsedJson.error}` });
926
1361
  writeSse(res, 'done', '[DONE]');
@@ -928,42 +1363,20 @@ function createBuiltinProxyRuntimeController(deps = {}) {
928
1363
  finish({ ok: true });
929
1364
  return;
930
1365
  }
931
- sendResponsesSse(res, buildResponsesPayloadFromChatCompletion(parsedJson.value, model));
1366
+ sendResponsesSse(res, buildResponsesPayloadFromChatCompletion(parsedJson.value, model, {
1367
+ toolTypesByName: options.toolTypesByName || {}
1368
+ }));
932
1369
  res.end();
933
1370
  finish({ ok: true });
934
1371
  });
935
1372
  return;
936
1373
  }
937
1374
 
938
- let sequence = 0;
939
- const state = {
940
- res,
941
- responseId: `resp_${crypto.randomBytes(10).toString('hex')}`,
942
- model,
943
- createdAt: Math.floor(Date.now() / 1000),
944
- output: [],
945
- messageItem: null,
946
- messageText: '',
947
- toolCalls: [],
948
- finished: false,
949
- nextSeq: () => {
950
- sequence += 1;
951
- return sequence;
952
- }
953
- };
1375
+ const state = sharedState;
1376
+ state.upstreamReq = req;
1377
+ if (!state.model && model) state.model = model;
954
1378
  streamState = state;
955
- startChatStreamHeartbeat(state);
956
- if (typeof res.on === 'function') {
957
- res.on('close', () => stopChatStreamHeartbeat(state));
958
- }
959
- writeSse(res, 'response.created', {
960
- type: 'response.created',
961
- response: {
962
- id: state.responseId,
963
- model: state.model,
964
- created_at: state.createdAt
965
- }
966
- });
1379
+ beginChatStreamResponsesSse(state);
967
1380
 
968
1381
  let buffer = '';
969
1382
  const handleEventBlock = (block) => {
@@ -981,6 +1394,7 @@ function createBuiltinProxyRuntimeController(deps = {}) {
981
1394
  }
982
1395
  const parsedChunk = parseJsonOrError(data);
983
1396
  if (!parsedChunk.error) {
1397
+ beginChatStreamResponsesSse(state);
984
1398
  writeChatCompletionChunkAsResponsesSse(state, parsedChunk.value);
985
1399
  }
986
1400
  };
@@ -1003,6 +1417,7 @@ function createBuiltinProxyRuntimeController(deps = {}) {
1003
1417
  });
1004
1418
  });
1005
1419
  req.setTimeout(timeoutMs, () => {
1420
+ if (streamAccepted) return;
1006
1421
  try { req.destroy(new Error('timeout')); } catch (_) {}
1007
1422
  finish({ ok: false, error: 'timeout' });
1008
1423
  });
@@ -1018,8 +1433,11 @@ function createBuiltinProxyRuntimeController(deps = {}) {
1018
1433
  return { ok: false, error: 'failed to build upstream URL' };
1019
1434
  }
1020
1435
  let lastResult = null;
1436
+ const streamState = options.streamState || createChatStreamResponsesSseState(options.res, options.model, {
1437
+ toolTypesByName: options.toolTypesByName || {}
1438
+ });
1021
1439
  for (const url of urls) {
1022
- const result = await retryTransientRequest(() => streamChatCompletionsAsResponsesSse(url, options));
1440
+ const result = await retryTransientRequest(() => streamChatCompletionsAsResponsesSse(url, { ...options, streamState }));
1023
1441
  lastResult = result;
1024
1442
  if (result && result.retry) continue;
1025
1443
  return result;
@@ -1303,7 +1721,6 @@ function createBuiltinProxyRuntimeController(deps = {}) {
1303
1721
  function createBuiltinProxyServer(settings, upstream) {
1304
1722
  const connections = new Set();
1305
1723
  const timeoutMs = settings.timeoutMs;
1306
-
1307
1724
  const server = http.createServer((req, res) => {
1308
1725
  let parsedIncoming;
1309
1726
  try {
@@ -1376,8 +1793,8 @@ function createBuiltinProxyRuntimeController(deps = {}) {
1376
1793
 
1377
1794
  // Responses shim:
1378
1795
  // - Codex CLI 默认走 /v1/responses(含 SSE)
1379
- // - 某些上游只支持 /v1/chat/completions
1380
- // 因此这里优先尝试 /v1/responses(stream=false),失败再转换到 chat/completions 并回包为 responses。
1796
+ // - SSE/streaming 任务优先走 chat/completions fallback,避免卡在会接收但不产出 Responses 的兼容网关
1797
+ // - 非流式请求仍优先尝试 /v1/responses(stream=false),失败再转换到 chat/completions 并回包为 responses。
1381
1798
  if ((incomingPath === '/v1/responses' || incomingPath === '/v1/responses/') && (req.method || 'GET').toUpperCase() === 'POST') {
1382
1799
  void (async () => {
1383
1800
  const { body, error } = await readRequestBody(req, 10 * 1024 * 1024);
@@ -1401,6 +1818,34 @@ function createBuiltinProxyRuntimeController(deps = {}) {
1401
1818
  'X-Codexmate-Proxy': '1'
1402
1819
  };
1403
1820
 
1821
+ const model = typeof payload.model === 'string' ? payload.model : '';
1822
+ const chatBody = buildChatCompletionsBodyFromResponsesPayload(payload);
1823
+ const toolTypesByName = collectResponsesToolTypesByName(payload.tools);
1824
+
1825
+ if (wantsStream) {
1826
+ const streamingChatBody = { ...chatBody, stream: true };
1827
+ const streamed = await streamChatCompletionsAsResponsesSseWithFallbackUrls(upstream.baseUrl, 'chat/completions', {
1828
+ method: 'POST',
1829
+ headers: commonHeaders,
1830
+ timeoutMs,
1831
+ body: streamingChatBody,
1832
+ res,
1833
+ model,
1834
+ toolTypesByName
1835
+ });
1836
+ if (!streamed.ok) {
1837
+ if (!res.headersSent) {
1838
+ res.writeHead(streamed.status && streamed.status >= 400 ? streamed.status : 502, { 'Content-Type': 'application/json; charset=utf-8' });
1839
+ res.end(streamed.bodyText || JSON.stringify({ error: streamed.error || 'proxy request failed' }));
1840
+ } else if (!res.writableEnded) {
1841
+ writeSse(res, 'response.failed', { type: 'response.failed', error: streamed.error || streamed.bodyText || 'proxy request failed' });
1842
+ writeSse(res, 'done', '[DONE]');
1843
+ res.end();
1844
+ }
1845
+ }
1846
+ return;
1847
+ }
1848
+
1404
1849
  const upstreamResponses = await proxyRequestJsonWithFallbackUrls(upstream.baseUrl, 'responses', {
1405
1850
  method: 'POST',
1406
1851
  headers: commonHeaders,
@@ -1452,32 +1897,6 @@ function createBuiltinProxyRuntimeController(deps = {}) {
1452
1897
  // Treat that as an unsupported Responses endpoint and try the chat fallback.
1453
1898
  }
1454
1899
 
1455
- const model = typeof payload.model === 'string' ? payload.model : '';
1456
- const chatBody = buildChatCompletionsBodyFromResponsesPayload(payload);
1457
-
1458
- if (wantsStream) {
1459
- const streamingChatBody = { ...chatBody, stream: true };
1460
- const streamed = await streamChatCompletionsAsResponsesSseWithFallbackUrls(upstream.baseUrl, 'chat/completions', {
1461
- method: 'POST',
1462
- headers: commonHeaders,
1463
- timeoutMs,
1464
- body: streamingChatBody,
1465
- res,
1466
- model
1467
- });
1468
- if (!streamed.ok) {
1469
- if (!res.headersSent) {
1470
- res.writeHead(streamed.status && streamed.status >= 400 ? streamed.status : 502, { 'Content-Type': 'application/json; charset=utf-8' });
1471
- res.end(streamed.bodyText || JSON.stringify({ error: streamed.error || 'proxy request failed' }));
1472
- } else if (!res.writableEnded) {
1473
- writeSse(res, 'response.failed', { type: 'response.failed', error: streamed.error || streamed.bodyText || 'proxy request failed' });
1474
- writeSse(res, 'done', '[DONE]');
1475
- res.end();
1476
- }
1477
- }
1478
- return;
1479
- }
1480
-
1481
1900
  const upstreamChat = await proxyRequestJsonWithFallbackUrls(upstream.baseUrl, 'chat/completions', {
1482
1901
  method: 'POST',
1483
1902
  headers: commonHeaders,
@@ -1503,7 +1922,7 @@ function createBuiltinProxyRuntimeController(deps = {}) {
1503
1922
  return;
1504
1923
  }
1505
1924
 
1506
- const responsesPayload = buildResponsesPayloadFromChatCompletion(chatJson.value, model);
1925
+ const responsesPayload = buildResponsesPayloadFromChatCompletion(chatJson.value, model, { toolTypesByName });
1507
1926
 
1508
1927
  if (wantsStream) {
1509
1928
  res.writeHead(200, {