@mmmbuto/zai-codex-bridge 0.4.4 → 0.4.6

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 CHANGED
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.4.6] - 2026-01-16
9
+
10
+ ### Fixed
11
+ - Avoid empty output_text items in tool-only streaming responses
12
+ - Only emit output_text.done/content_part.done when output text exists
13
+
14
+ ## [0.4.5] - 2026-01-16
15
+
16
+ ### Fixed
17
+ - Handle streaming tool_calls without `index` by assigning a stable fallback index
18
+ - Improve tool name logging when tools define top-level `name`
19
+
8
20
  ## [0.4.4] - 2026-01-16
9
21
 
10
22
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmmbuto/zai-codex-bridge",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "description": "Local proxy that translates OpenAI Responses API format to Z.AI Chat Completions format for Codex",
5
5
  "main": "src/server.js",
6
6
  "bin": {
package/src/server.js CHANGED
@@ -130,7 +130,7 @@ function summarizeTools(tools, limit = 8) {
130
130
  types[type] = (types[type] || 0) + 1;
131
131
  if (names.length < limit) {
132
132
  if (type === 'function') {
133
- names.push(tool?.function?.name || '(missing_name)');
133
+ names.push(tool?.function?.name || tool?.name || '(missing_name)');
134
134
  } else {
135
135
  names.push(type);
136
136
  }
@@ -359,7 +359,9 @@ function translateChatToResponses(chatResponse, responsesRequest, ids, allowTool
359
359
  if (reasoningText) {
360
360
  content.push({ type: 'reasoning_text', text: reasoningText, annotations: [] });
361
361
  }
362
- content.push({ type: 'output_text', text: outputText, annotations: [] });
362
+ if (outputText) {
363
+ content.push({ type: 'output_text', text: outputText, annotations: [] });
364
+ }
363
365
 
364
366
  const msgItem = {
365
367
  id: msgId,
@@ -369,11 +371,16 @@ function translateChatToResponses(chatResponse, responsesRequest, ids, allowTool
369
371
  content,
370
372
  };
371
373
 
372
- // Build output array: message item + any function_call items
373
- const finalOutput = [msgItem];
374
+ // Build output array: message item (if any) + any function_call items
375
+ const finalOutput = [];
376
+
377
+ const hasToolCalls = allowTools && msg.tool_calls && Array.isArray(msg.tool_calls);
378
+ if (content.length > 0 || !hasToolCalls) {
379
+ finalOutput.push(msgItem);
380
+ }
374
381
 
375
382
  // Handle tool_calls (only if allowTools)
376
- if (allowTools && msg.tool_calls && Array.isArray(msg.tool_calls)) {
383
+ if (hasToolCalls) {
377
384
  for (const tc of msg.tool_calls) {
378
385
  const callId = tc.id || `call_${randomUUID().replace(/-/g, '')}`;
379
386
  const name = tc.function?.name || '';
@@ -517,20 +524,17 @@ async function streamChatToResponses(upstreamBody, res, responsesRequest, ids, a
517
524
  item: msgItemInProgress,
518
525
  });
519
526
 
520
- sse({
521
- type: 'response.content_part.added',
522
- item_id: msgId,
523
- output_index: OUTPUT_INDEX,
524
- content_index: CONTENT_INDEX,
525
- part: { type: 'output_text', text: '', annotations: [] },
526
- });
527
+ // content_part.added emitted only if we receive output_text
528
+ let contentPartAdded = false;
527
529
 
528
530
  let out = '';
529
531
  let reasoning = '';
530
532
 
531
533
  // Tool call tracking (only if allowTools)
532
534
  const toolCallsMap = new Map(); // index -> { callId, name, outputIndex, arguments, partialArgs }
535
+ const toolCallsById = new Map(); // callId -> index
533
536
  const TOOL_BASE_INDEX = 1; // After message item
537
+ let nextToolIndex = 0;
534
538
 
535
539
  while (true) {
536
540
  const { done, value } = await reader.read();
@@ -563,12 +567,22 @@ async function streamChatToResponses(upstreamBody, res, responsesRequest, ids, a
563
567
  // Handle tool_calls (only if allowTools)
564
568
  if (allowTools && delta.tool_calls && Array.isArray(delta.tool_calls)) {
565
569
  for (const tc of delta.tool_calls) {
566
- const index = tc.index;
567
- if (index == null) continue;
570
+ let index = tc.index;
571
+ const tcId = tc.id;
572
+
573
+ if (index == null) {
574
+ if (tcId && toolCallsById.has(tcId)) {
575
+ index = toolCallsById.get(tcId);
576
+ } else {
577
+ index = nextToolIndex++;
578
+ }
579
+ } else if (index >= nextToolIndex) {
580
+ nextToolIndex = index + 1;
581
+ }
568
582
 
569
583
  if (!toolCallsMap.has(index)) {
570
584
  // New tool call - send output_item.added
571
- const callId = tc.id || `call_${randomUUID().replace(/-/g, '')}`;
585
+ const callId = tcId || `call_${randomUUID().replace(/-/g, '')}`;
572
586
  const name = tc.function?.name || '';
573
587
  const outputIndex = TOOL_BASE_INDEX + index;
574
588
 
@@ -579,6 +593,7 @@ async function streamChatToResponses(upstreamBody, res, responsesRequest, ids, a
579
593
  arguments: '',
580
594
  partialArgs: ''
581
595
  });
596
+ if (callId) toolCallsById.set(callId, index);
582
597
 
583
598
  const fnItemInProgress = {
584
599
  id: callId,
@@ -675,6 +690,16 @@ async function streamChatToResponses(upstreamBody, res, responsesRequest, ids, a
675
690
  }
676
691
 
677
692
  if (typeof delta.content === 'string' && delta.content.length) {
693
+ if (!contentPartAdded) {
694
+ sse({
695
+ type: 'response.content_part.added',
696
+ item_id: msgId,
697
+ output_index: OUTPUT_INDEX,
698
+ content_index: CONTENT_INDEX,
699
+ part: { type: 'output_text', text: '', annotations: [] },
700
+ });
701
+ contentPartAdded = true;
702
+ }
678
703
  out += delta.content;
679
704
  sse({
680
705
  type: 'response.output_text.delta',
@@ -699,28 +724,40 @@ async function streamChatToResponses(upstreamBody, res, responsesRequest, ids, a
699
724
  });
700
725
  }
701
726
 
702
- sse({
703
- type: 'response.output_text.done',
704
- item_id: msgId,
705
- output_index: OUTPUT_INDEX,
706
- content_index: CONTENT_INDEX,
707
- text: out,
708
- });
727
+ if (out.length) {
728
+ sse({
729
+ type: 'response.output_text.done',
730
+ item_id: msgId,
731
+ output_index: OUTPUT_INDEX,
732
+ content_index: CONTENT_INDEX,
733
+ text: out,
734
+ });
709
735
 
710
- sse({
711
- type: 'response.content_part.done',
712
- item_id: msgId,
713
- output_index: OUTPUT_INDEX,
714
- content_index: CONTENT_INDEX,
715
- part: { type: 'output_text', text: out, annotations: [] },
716
- });
736
+ if (contentPartAdded) {
737
+ sse({
738
+ type: 'response.content_part.done',
739
+ item_id: msgId,
740
+ output_index: OUTPUT_INDEX,
741
+ content_index: CONTENT_INDEX,
742
+ part: { type: 'output_text', text: out, annotations: [] },
743
+ });
744
+ }
745
+ }
746
+
747
+ const msgContent = [];
748
+ if (reasoning.length) {
749
+ msgContent.push({ type: 'reasoning_text', text: reasoning, annotations: [] });
750
+ }
751
+ if (out.length) {
752
+ msgContent.push({ type: 'output_text', text: out, annotations: [] });
753
+ }
717
754
 
718
755
  const msgItemDone = {
719
756
  id: msgId,
720
757
  type: 'message',
721
758
  status: 'completed',
722
759
  role: 'assistant',
723
- content: [{ type: 'output_text', text: out, annotations: [] }],
760
+ content: msgContent,
724
761
  };
725
762
 
726
763
  sse({
@@ -729,8 +766,11 @@ async function streamChatToResponses(upstreamBody, res, responsesRequest, ids, a
729
766
  item: msgItemDone,
730
767
  });
731
768
 
732
- // Build final output array: message item + any function_call items
733
- const finalOutput = [msgItemDone];
769
+ // Build final output array: message item (if any) + any function_call items
770
+ const finalOutput = [];
771
+ if (msgContent.length > 0 || toolCallsMap.size === 0) {
772
+ finalOutput.push(msgItemDone);
773
+ }
734
774
  if (allowTools && toolCallsMap.size > 0) {
735
775
  const ordered = Array.from(toolCallsMap.entries()).sort((a, b) => a[0] - b[0]);
736
776
  for (const [, tcData] of ordered) {