@mmmbuto/zai-codex-bridge 0.4.5 → 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,12 @@ 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
+
8
14
  ## [0.4.5] - 2026-01-16
9
15
 
10
16
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmmbuto/zai-codex-bridge",
3
- "version": "0.4.5",
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
@@ -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,13 +524,8 @@ 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 = '';
@@ -688,6 +690,16 @@ async function streamChatToResponses(upstreamBody, res, responsesRequest, ids, a
688
690
  }
689
691
 
690
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
+ }
691
703
  out += delta.content;
692
704
  sse({
693
705
  type: 'response.output_text.delta',
@@ -712,28 +724,40 @@ async function streamChatToResponses(upstreamBody, res, responsesRequest, ids, a
712
724
  });
713
725
  }
714
726
 
715
- sse({
716
- type: 'response.output_text.done',
717
- item_id: msgId,
718
- output_index: OUTPUT_INDEX,
719
- content_index: CONTENT_INDEX,
720
- text: out,
721
- });
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
+ });
722
735
 
723
- sse({
724
- type: 'response.content_part.done',
725
- item_id: msgId,
726
- output_index: OUTPUT_INDEX,
727
- content_index: CONTENT_INDEX,
728
- part: { type: 'output_text', text: out, annotations: [] },
729
- });
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
+ }
730
754
 
731
755
  const msgItemDone = {
732
756
  id: msgId,
733
757
  type: 'message',
734
758
  status: 'completed',
735
759
  role: 'assistant',
736
- content: [{ type: 'output_text', text: out, annotations: [] }],
760
+ content: msgContent,
737
761
  };
738
762
 
739
763
  sse({
@@ -742,8 +766,11 @@ async function streamChatToResponses(upstreamBody, res, responsesRequest, ids, a
742
766
  item: msgItemDone,
743
767
  });
744
768
 
745
- // Build final output array: message item + any function_call items
746
- 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
+ }
747
774
  if (allowTools && toolCallsMap.size > 0) {
748
775
  const ordered = Array.from(toolCallsMap.entries()).sort((a, b) => a[0] - b[0]);
749
776
  for (const [, tcData] of ordered) {