@mmmbuto/zai-codex-bridge 0.4.2 → 0.4.3

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,20 @@ 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.3] - 2026-01-16
9
+
10
+ ### Added
11
+ - Auto-enable tool bridging when tool-related fields are present in the request
12
+ - Extra logging to surface `allowTools` and `toolsPresent` per request
13
+ - Debug tool summary logging (types and sample names)
14
+
15
+ ### Fixed
16
+ - Correct output_index mapping for streaming tool call events
17
+ - Filter non-function tools to avoid upstream schema errors
18
+
19
+ ### Changed
20
+ - README guidance for MCP/tools troubleshooting and proxy startup
21
+
8
22
  ## [0.4.2] - 2026-01-16
9
23
 
10
24
  ### Changed
package/README.md CHANGED
@@ -99,7 +99,8 @@ Codex tool-calling / MCP memory requires an additional compatibility layer:
99
99
  - Codex uses **Responses API tool events** (function call items + arguments delta/done, plus function_call_output inputs)
100
100
  - Some upstream models/providers may not emit tool calls (or may emit them in a different shape)
101
101
 
102
- This proxy can **attempt** to bridge tools when enabled:
102
+ This proxy can **attempt** to bridge tools automatically when the request carries tool definitions
103
+ (`tools`, `tool_choice`, or tool outputs). You can also force it on:
103
104
 
104
105
  ```bash
105
106
  export ALLOW_TOOLS=1
@@ -111,6 +112,7 @@ Important:
111
112
  - Responses `tools` + `tool_choice` → Chat `tools` + `tool_choice`
112
113
  - Chat `tool_calls` (stream/non-stream) → Responses function-call events
113
114
  - Responses `function_call_output` → Chat `role=tool` messages
115
+ - Non-function tool types are dropped for Z.AI compatibility.
114
116
 
115
117
  (See repo changelog and docs for the exact implemented behavior.)
116
118
 
@@ -144,7 +146,8 @@ export ZAI_BASE_URL=https://api.z.ai/api/coding/paas/v4
144
146
  export LOG_LEVEL=info
145
147
 
146
148
  # Optional
147
- export ALLOW_TOOLS=1
149
+ export ALLOW_TOOLS=1 # force tool bridging (otherwise auto-enabled when tools are present)
150
+ export ALLOW_SYSTEM=1 # only if your provider supports system role
148
151
  ```
149
152
 
150
153
  ---
@@ -161,7 +164,7 @@ codex-with-zai() {
161
164
  local PROXY_PID=""
162
165
 
163
166
  if ! curl -fsS "$HEALTH" >/dev/null 2>&1; then
164
- zai-codex-bridge --host "$HOST" --port "$PORT" >/dev/null 2>&1 &
167
+ ALLOW_TOOLS=1 zai-codex-bridge --host "$HOST" --port "$PORT" >/dev/null 2>&1 &
165
168
  PROXY_PID=$!
166
169
  trap 'kill $PROXY_PID 2>/dev/null' EXIT INT TERM
167
170
  sleep 1
@@ -241,6 +244,13 @@ codex-with-zai -m "GLM-4.7"
241
244
  - Ensure `base_url` points to the proxy root (example: `http://127.0.0.1:31415`).
242
245
  - Confirm the proxy is running and `/health` returns `ok`.
243
246
 
247
+ ### MCP/tools not being called
248
+ - Check proxy logs for `allowTools: true` and `toolsPresent: true`.
249
+ - If `toolsPresent: false`, Codex did not send tool definitions (verify your provider config).
250
+ - If tools are present but the model prints literal `<function=...>` markup or never emits tool calls,
251
+ your upstream model likely doesn’t support tool calling.
252
+ - If your provider supports `system` role, try `ALLOW_SYSTEM=1` to improve tool adherence.
253
+
244
254
  ### 502 Bad Gateway
245
255
  - Proxy reached upstream but upstream failed. Enable debug:
246
256
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmmbuto/zai-codex-bridge",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
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
@@ -22,7 +22,7 @@ const DEFAULT_MODEL = process.env.DEFAULT_MODEL || 'glm-4.7';
22
22
 
23
23
  // Env toggles for compatibility
24
24
  const ALLOW_SYSTEM = process.env.ALLOW_SYSTEM === '1';
25
- const ALLOW_TOOLS = process.env.ALLOW_TOOLS === '1';
25
+ const ALLOW_TOOLS_ENV = process.env.ALLOW_TOOLS === '1';
26
26
 
27
27
  function nowSec() {
28
28
  return Math.floor(Date.now() / 1000);
@@ -90,6 +90,67 @@ function detectFormat(body) {
90
90
  return 'unknown';
91
91
  }
92
92
 
93
+ /**
94
+ * Detect if request carries tool-related data
95
+ */
96
+ function requestHasTools(request) {
97
+ if (!request || typeof request !== 'object') return false;
98
+
99
+ if (Array.isArray(request.tools) && request.tools.length > 0) return true;
100
+ if (request.tool_choice) return true;
101
+
102
+ if (Array.isArray(request.input)) {
103
+ for (const item of request.input) {
104
+ if (!item) continue;
105
+ if (item.type === 'function_call_output') return true;
106
+ if (Array.isArray(item.tool_calls) && item.tool_calls.length > 0) return true;
107
+ if (item.tool_call_id) return true;
108
+ }
109
+ }
110
+
111
+ if (Array.isArray(request.messages)) {
112
+ for (const msg of request.messages) {
113
+ if (!msg) continue;
114
+ if (msg.role === 'tool') return true;
115
+ if (Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) return true;
116
+ if (msg.tool_call_id) return true;
117
+ }
118
+ }
119
+
120
+ return false;
121
+ }
122
+
123
+ function summarizeTools(tools, limit = 8) {
124
+ if (!Array.isArray(tools)) return null;
125
+ const types = {};
126
+ const names = [];
127
+
128
+ for (const tool of tools) {
129
+ const type = tool?.type || 'unknown';
130
+ types[type] = (types[type] || 0) + 1;
131
+ if (names.length < limit) {
132
+ if (type === 'function') {
133
+ names.push(tool?.function?.name || '(missing_name)');
134
+ } else {
135
+ names.push(type);
136
+ }
137
+ }
138
+ }
139
+
140
+ return { count: tools.length, types, sample_names: names };
141
+ }
142
+
143
+ function summarizeToolShape(tool) {
144
+ if (!tool || typeof tool !== 'object') return null;
145
+ return {
146
+ keys: Object.keys(tool),
147
+ type: tool.type,
148
+ name: tool.name,
149
+ functionKeys: tool.function && typeof tool.function === 'object' ? Object.keys(tool.function) : null,
150
+ functionName: tool.function?.name
151
+ };
152
+ }
153
+
93
154
  /**
94
155
  * Flatten content parts to string - supports text, input_text, output_text
95
156
  */
@@ -113,7 +174,7 @@ function flattenContent(content) {
113
174
  /**
114
175
  * Translate Responses format to Chat Completions format
115
176
  */
116
- function translateResponsesToChat(request) {
177
+ function translateResponsesToChat(request, allowTools) {
117
178
  const messages = [];
118
179
 
119
180
  // Add system message from instructions (with ALLOW_SYSTEM toggle)
@@ -145,8 +206,8 @@ function translateResponsesToChat(request) {
145
206
  } else if (Array.isArray(request.input)) {
146
207
  // Array of ResponseItem objects
147
208
  for (const item of request.input) {
148
- // Handle function_call_output items (tool responses) - only if ALLOW_TOOLS
149
- if (ALLOW_TOOLS && item.type === 'function_call_output') {
209
+ // Handle function_call_output items (tool responses) - only if allowTools
210
+ if (allowTools && item.type === 'function_call_output') {
150
211
  const toolMsg = {
151
212
  role: 'tool',
152
213
  tool_call_id: item.call_id || item.tool_call_id || '',
@@ -187,13 +248,13 @@ function translateResponsesToChat(request) {
187
248
  content: flattenContent(item.content)
188
249
  };
189
250
 
190
- // Handle tool calls if present (only if ALLOW_TOOLS)
191
- if (ALLOW_TOOLS && item.tool_calls && Array.isArray(item.tool_calls)) {
251
+ // Handle tool calls if present (only if allowTools)
252
+ if (allowTools && item.tool_calls && Array.isArray(item.tool_calls)) {
192
253
  msg.tool_calls = item.tool_calls;
193
254
  }
194
255
 
195
- // Handle tool call ID for tool responses (only if ALLOW_TOOLS)
196
- if (ALLOW_TOOLS && item.tool_call_id) {
256
+ // Handle tool call ID for tool responses (only if allowTools)
257
+ if (allowTools && item.tool_call_id) {
197
258
  msg.tool_call_id = item.tool_call_id;
198
259
  }
199
260
 
@@ -226,27 +287,49 @@ function translateResponsesToChat(request) {
226
287
  chatRequest.top_p = request.top_p;
227
288
  }
228
289
 
229
- // Tools handling (only if ALLOW_TOOLS)
230
- if (ALLOW_TOOLS && request.tools && Array.isArray(request.tools)) {
231
- // Filter out tools with null or empty function
232
- chatRequest.tools = request.tools.filter(tool => {
233
- if (tool.type === 'function') {
234
- // Check if function has required fields
235
- return tool.function && typeof tool.function === 'object' &&
236
- tool.function.name && tool.function.name.length > 0 &&
237
- tool.function.parameters !== undefined && tool.function.parameters !== null;
238
- }
239
- // Keep non-function tools (if any)
240
- return true;
241
- });
290
+ // Tools handling (only if allowTools)
291
+ if (allowTools && request.tools && Array.isArray(request.tools)) {
292
+ const originalCount = request.tools.length;
293
+ const normalized = [];
294
+
295
+ for (const tool of request.tools) {
296
+ if (!tool || tool.type !== 'function') continue;
297
+ const fn = tool.function && typeof tool.function === 'object' ? tool.function : null;
298
+ const name = (fn?.name || tool.name || '').trim();
299
+ if (!name) continue;
300
+
301
+ // Prefer nested function fields, fall back to top-level ones if present
302
+ const description = fn?.description ?? tool.description;
303
+ const parameters = fn?.parameters ?? tool.parameters ?? { type: 'object', properties: {} };
304
+
305
+ const functionObj = { name, parameters };
306
+ if (description) functionObj.description = description;
307
+
308
+ // Send minimal tool schema for upstream compatibility
309
+ normalized.push({
310
+ type: 'function',
311
+ function: functionObj
312
+ });
313
+ }
314
+
315
+ chatRequest.tools = normalized;
316
+
317
+ const dropped = originalCount - chatRequest.tools.length;
318
+ if (dropped > 0) {
319
+ log('warn', `Dropped ${dropped} non-function or invalid tools for upstream compatibility`);
320
+ }
321
+
242
322
  // Only add tools array if there are valid tools
243
323
  if (chatRequest.tools.length === 0) {
244
324
  delete chatRequest.tools;
245
325
  }
246
326
  }
247
327
 
248
- if (ALLOW_TOOLS && request.tool_choice) {
328
+ if (allowTools && request.tool_choice) {
249
329
  chatRequest.tool_choice = request.tool_choice;
330
+ if (!chatRequest.tools || chatRequest.tools.length === 0) {
331
+ delete chatRequest.tool_choice;
332
+ }
250
333
  }
251
334
 
252
335
  log('debug', 'Translated Responses->Chat:', {
@@ -261,9 +344,9 @@ function translateResponsesToChat(request) {
261
344
  /**
262
345
  * Translate Chat Completions response to Responses format
263
346
  * Handles both output_text and reasoning_text content
264
- * Handles tool_calls if present (only if ALLOW_TOOLS)
347
+ * Handles tool_calls if present (only if allowTools)
265
348
  */
266
- function translateChatToResponses(chatResponse, responsesRequest, ids) {
349
+ function translateChatToResponses(chatResponse, responsesRequest, ids, allowTools) {
267
350
  const msg = chatResponse.choices?.[0]?.message ?? {};
268
351
  const outputText = msg.content ?? '';
269
352
  const reasoningText = msg.reasoning_content ?? '';
@@ -289,8 +372,8 @@ function translateChatToResponses(chatResponse, responsesRequest, ids) {
289
372
  // Build output array: message item + any function_call items
290
373
  const finalOutput = [msgItem];
291
374
 
292
- // Handle tool_calls (only if ALLOW_TOOLS)
293
- if (ALLOW_TOOLS && msg.tool_calls && Array.isArray(msg.tool_calls)) {
375
+ // Handle tool_calls (only if allowTools)
376
+ if (allowTools && msg.tool_calls && Array.isArray(msg.tool_calls)) {
294
377
  for (const tc of msg.tool_calls) {
295
378
  const callId = tc.id || `call_${randomUUID().replace(/-/g, '')}`;
296
379
  const name = tc.function?.name || '';
@@ -386,7 +469,7 @@ async function makeUpstreamRequest(path, body, headers) {
386
469
  * Handle streaming response from Z.AI with proper Responses API event format
387
470
  * Separates reasoning_content, content, and tool_calls into distinct events
388
471
  */
389
- async function streamChatToResponses(upstreamBody, res, responsesRequest, ids) {
472
+ async function streamChatToResponses(upstreamBody, res, responsesRequest, ids, allowTools) {
390
473
  const decoder = new TextDecoder();
391
474
  const reader = upstreamBody.getReader();
392
475
  let buffer = '';
@@ -445,9 +528,9 @@ async function streamChatToResponses(upstreamBody, res, responsesRequest, ids) {
445
528
  let out = '';
446
529
  let reasoning = '';
447
530
 
448
- // Tool call tracking (only if ALLOW_TOOLS)
449
- const toolCallsMap = new Map(); // index -> { callId, name, arguments, partialArgs }
450
- let nextOutputIndex = 1; // After message item
531
+ // Tool call tracking (only if allowTools)
532
+ const toolCallsMap = new Map(); // index -> { callId, name, outputIndex, arguments, partialArgs }
533
+ const TOOL_BASE_INDEX = 1; // After message item
451
534
 
452
535
  while (true) {
453
536
  const { done, value } = await reader.read();
@@ -477,8 +560,8 @@ async function streamChatToResponses(upstreamBody, res, responsesRequest, ids) {
477
560
 
478
561
  const delta = chunk.choices?.[0]?.delta || {};
479
562
 
480
- // Handle tool_calls (only if ALLOW_TOOLS)
481
- if (ALLOW_TOOLS && delta.tool_calls && Array.isArray(delta.tool_calls)) {
563
+ // Handle tool_calls (only if allowTools)
564
+ if (allowTools && delta.tool_calls && Array.isArray(delta.tool_calls)) {
482
565
  for (const tc of delta.tool_calls) {
483
566
  const index = tc.index;
484
567
  if (index == null) continue;
@@ -487,10 +570,12 @@ async function streamChatToResponses(upstreamBody, res, responsesRequest, ids) {
487
570
  // New tool call - send output_item.added
488
571
  const callId = tc.id || `call_${randomUUID().replace(/-/g, '')}`;
489
572
  const name = tc.function?.name || '';
573
+ const outputIndex = TOOL_BASE_INDEX + index;
490
574
 
491
575
  toolCallsMap.set(index, {
492
576
  callId,
493
577
  name,
578
+ outputIndex,
494
579
  arguments: '',
495
580
  partialArgs: ''
496
581
  });
@@ -506,7 +591,7 @@ async function streamChatToResponses(upstreamBody, res, responsesRequest, ids) {
506
591
 
507
592
  sse({
508
593
  type: 'response.output_item.added',
509
- output_index: nextOutputIndex,
594
+ output_index: outputIndex,
510
595
  item: fnItemInProgress,
511
596
  });
512
597
 
@@ -514,7 +599,7 @@ async function streamChatToResponses(upstreamBody, res, responsesRequest, ids) {
514
599
  sse({
515
600
  type: 'response.function_call_name.done',
516
601
  item_id: callId,
517
- output_index: nextOutputIndex,
602
+ output_index: outputIndex,
518
603
  name: name,
519
604
  });
520
605
  }
@@ -528,7 +613,7 @@ async function streamChatToResponses(upstreamBody, res, responsesRequest, ids) {
528
613
  sse({
529
614
  type: 'response.function_call_name.done',
530
615
  item_id: tcData.callId,
531
- output_index: OUTPUT_INDEX + index,
616
+ output_index: tcData.outputIndex,
532
617
  name: tcData.name,
533
618
  });
534
619
  }
@@ -540,7 +625,7 @@ async function streamChatToResponses(upstreamBody, res, responsesRequest, ids) {
540
625
  sse({
541
626
  type: 'response.function_call_arguments.delta',
542
627
  item_id: tcData.callId,
543
- output_index: OUTPUT_INDEX + index,
628
+ output_index: tcData.outputIndex,
544
629
  delta: tc.function.arguments,
545
630
  });
546
631
  }
@@ -553,7 +638,7 @@ async function streamChatToResponses(upstreamBody, res, responsesRequest, ids) {
553
638
  sse({
554
639
  type: 'response.function_call_arguments.done',
555
640
  item_id: tcData.callId,
556
- output_index: OUTPUT_INDEX + index,
641
+ output_index: tcData.outputIndex,
557
642
  arguments: tcData.arguments,
558
643
  });
559
644
 
@@ -568,7 +653,7 @@ async function streamChatToResponses(upstreamBody, res, responsesRequest, ids) {
568
653
 
569
654
  sse({
570
655
  type: 'response.output_item.done',
571
- output_index: OUTPUT_INDEX + index,
656
+ output_index: tcData.outputIndex,
572
657
  item: fnItemDone,
573
658
  });
574
659
  }
@@ -646,8 +731,9 @@ async function streamChatToResponses(upstreamBody, res, responsesRequest, ids) {
646
731
 
647
732
  // Build final output array: message item + any function_call items
648
733
  const finalOutput = [msgItemDone];
649
- if (ALLOW_TOOLS && toolCallsMap.size > 0) {
650
- for (const [index, tcData] of toolCallsMap.entries()) {
734
+ if (allowTools && toolCallsMap.size > 0) {
735
+ const ordered = Array.from(toolCallsMap.entries()).sort((a, b) => a[0] - b[0]);
736
+ for (const [, tcData] of ordered) {
651
737
  finalOutput.push({
652
738
  id: tcData.callId,
653
739
  type: 'function_call',
@@ -707,19 +793,30 @@ async function handlePostRequest(req, res) {
707
793
  return;
708
794
  }
709
795
 
796
+ const hasTools = requestHasTools(request);
797
+ const allowTools = ALLOW_TOOLS_ENV || hasTools;
798
+
710
799
  log('info', 'Incoming request:', {
711
800
  path,
712
801
  format: detectFormat(request),
713
802
  model: request.model,
803
+ allowTools,
804
+ toolsPresent: hasTools,
714
805
  authHeader: req.headers['authorization'] || req.headers['Authorization'] || 'none'
715
806
  });
807
+ if (hasTools) {
808
+ log('debug', 'Tools summary:', summarizeTools(request.tools));
809
+ if (request.tools && request.tools[0]) {
810
+ log('debug', 'Tool[0] shape:', summarizeToolShape(request.tools[0]));
811
+ }
812
+ }
716
813
 
717
814
  let upstreamBody;
718
815
  const format = detectFormat(request);
719
816
 
720
817
  if (format === 'responses') {
721
818
  // Translate Responses to Chat
722
- upstreamBody = translateResponsesToChat(request);
819
+ upstreamBody = translateResponsesToChat(request, allowTools);
723
820
  } else if (format === 'chat') {
724
821
  // Pass through Chat format
725
822
  upstreamBody = request;
@@ -769,7 +866,7 @@ async function handlePostRequest(req, res) {
769
866
  });
770
867
 
771
868
  try {
772
- await streamChatToResponses(upstreamResponse.body, res, request, ids);
869
+ await streamChatToResponses(upstreamResponse.body, res, request, ids, allowTools);
773
870
  log('info', 'Streaming completed');
774
871
  } catch (e) {
775
872
  log('error', 'Streaming error:', e);
@@ -784,7 +881,7 @@ async function handlePostRequest(req, res) {
784
881
  msgId: `msg_${randomUUID().replace(/-/g, '')}`,
785
882
  };
786
883
 
787
- const response = translateChatToResponses(chatResponse, request, ids);
884
+ const response = translateChatToResponses(chatResponse, request, ids, allowTools);
788
885
 
789
886
  res.writeHead(200, { 'Content-Type': 'application/json' });
790
887
  res.end(JSON.stringify(response));