@mmmbuto/zai-codex-bridge 0.4.0 → 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 +53 -0
- package/README.md +120 -82
- package/RELEASING.md +80 -0
- package/package.json +4 -2
- package/scripts/release-patch.js +60 -0
- package/scripts/test-curl.js +164 -0
- package/src/server.js +292 -31
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
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
|
+
|
|
22
|
+
## [0.4.2] - 2026-01-16
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- Replaced the README with expanded setup, usage, and troubleshooting guidance
|
|
26
|
+
- Clarified Codex provider configuration and proxy endpoint usage
|
|
27
|
+
|
|
28
|
+
## [0.4.1] - 2026-01-16
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
- Tool calling support (MCP/function calls) when `ALLOW_TOOLS=1`
|
|
32
|
+
- Bridging for `function_call_output` items to Chat `role: tool` messages
|
|
33
|
+
- Streaming support for `delta.tool_calls` with proper Responses API events
|
|
34
|
+
- Non-streaming support for `msg.tool_calls` in final response
|
|
35
|
+
- Tool call events: `response.output_item.added` (function_call), `response.function_call_arguments.delta`, `response.function_call_arguments.done`
|
|
36
|
+
- Automated tool call test in test suite
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
- `translateResponsesToChat()` now handles `type: function_call_output` items
|
|
40
|
+
- `streamChatToResponses()` now detects and emits tool call events
|
|
41
|
+
- `translateChatToResponses()` now includes `function_call` items in output array
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
- Tool responses (from MCP/function calls) are now correctly forwarded to upstream as `role: tool` messages
|
|
45
|
+
- Function call items are now properly included in `response.completed` output array
|
|
46
|
+
|
|
47
|
+
## [0.4.0] - Previous
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
- Initial release with Responses API to Chat Completions translation
|
|
51
|
+
- Streaming support with SSE
|
|
52
|
+
- Health check endpoint
|
|
53
|
+
- Zero-dependency implementation
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ZAI Codex Bridge
|
|
2
2
|
|
|
3
|
-
> Local proxy that translates OpenAI Responses API
|
|
3
|
+
> Local proxy that translates OpenAI **Responses API** ↔ Z.AI **Chat Completions** for Codex CLI
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.org/package/@mmmbuto/zai-codex-bridge)
|
|
6
6
|
[](https://github.com/DioNanos/zai-codex-bridge)
|
|
@@ -10,36 +10,39 @@
|
|
|
10
10
|
|
|
11
11
|
## What It Solves
|
|
12
12
|
|
|
13
|
-
Codex
|
|
13
|
+
Newer **Codex CLI** versions speak the OpenAI **Responses API** (e.g. `/v1/responses`, with `instructions` + `input` + event-stream semantics).
|
|
14
|
+
Some gateways/providers (including Z.AI endpoints) only expose legacy **Chat Completions** (`messages[]`).
|
|
14
15
|
|
|
15
16
|
This proxy:
|
|
16
|
-
1. Accepts Codex requests in **Responses format
|
|
17
|
-
2. Translates them to **Chat
|
|
17
|
+
1. Accepts Codex requests in **Responses** format
|
|
18
|
+
2. Translates them to **Chat Completions**
|
|
18
19
|
3. Forwards to Z.AI
|
|
19
|
-
4. Translates
|
|
20
|
+
4. Translates back to **Responses** format (stream + non-stream)
|
|
20
21
|
5. Returns to Codex
|
|
21
22
|
|
|
22
|
-
**Without this proxy**, Codex
|
|
23
|
+
**Without this proxy**, Codex may fail (example from upstream error payloads):
|
|
23
24
|
```json
|
|
24
25
|
{"error":{"code":"1214","message":"Incorrect role information"}}
|
|
25
26
|
```
|
|
26
27
|
|
|
28
|
+
> If you’re using **codex-termux** and a gateway that doesn’t fully match the Responses API, this proxy is the recommended compatibility layer.
|
|
29
|
+
|
|
27
30
|
---
|
|
28
31
|
|
|
29
32
|
## Features
|
|
30
33
|
|
|
31
|
-
-
|
|
34
|
+
- Responses API ↔ Chat Completions translation (request + response)
|
|
32
35
|
- Streaming support with SSE (Server-Sent Events)
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
+
- Health check endpoint (`/health`)
|
|
37
|
+
- Works on Linux/macOS/Windows (WSL) + Termux (ARM64)
|
|
38
|
+
- **Optional tool/MCP bridging** (see “Tools / MCP” below)
|
|
39
|
+
- Zero/low dependencies (Node built-ins only, unless noted by package.json)
|
|
36
40
|
|
|
37
41
|
---
|
|
38
42
|
|
|
39
43
|
## Requirements
|
|
40
44
|
|
|
41
|
-
- **Node.js**: 18
|
|
42
|
-
- **Platform**: Linux, macOS, Windows (WSL), Termux (ARM64)
|
|
45
|
+
- **Node.js**: 18+ (native `fetch`)
|
|
43
46
|
- **Port**: 31415 (default, configurable)
|
|
44
47
|
|
|
45
48
|
---
|
|
@@ -54,28 +57,34 @@ npm install -g @mmmbuto/zai-codex-bridge
|
|
|
54
57
|
|
|
55
58
|
## Quick Start
|
|
56
59
|
|
|
57
|
-
### 1
|
|
60
|
+
### 1) Start the Proxy
|
|
58
61
|
|
|
59
62
|
```bash
|
|
60
63
|
zai-codex-bridge
|
|
61
64
|
```
|
|
62
65
|
|
|
63
|
-
|
|
66
|
+
Default listen address:
|
|
67
|
+
|
|
68
|
+
- `http://127.0.0.1:31415`
|
|
64
69
|
|
|
65
|
-
### 2
|
|
70
|
+
### 2) Configure Codex
|
|
66
71
|
|
|
67
|
-
Add to `~/.codex/config.toml`:
|
|
72
|
+
Add this provider to `~/.codex/config.toml`:
|
|
68
73
|
|
|
69
74
|
```toml
|
|
70
75
|
[model_providers.zai_proxy]
|
|
71
76
|
name = "ZAI via local proxy"
|
|
72
|
-
base_url = "http://127.0.0.1:31415
|
|
77
|
+
base_url = "http://127.0.0.1:31415"
|
|
73
78
|
env_key = "OPENAI_API_KEY"
|
|
74
79
|
wire_api = "responses"
|
|
75
80
|
stream_idle_timeout_ms = 3000000
|
|
76
81
|
```
|
|
77
82
|
|
|
78
|
-
|
|
83
|
+
> Notes:
|
|
84
|
+
> - `base_url` is the server root. Codex will call `/v1/responses`; this proxy supports that path.
|
|
85
|
+
> - We keep `env_key = "OPENAI_API_KEY"` because Codex expects that key name. You can store your Z.AI key there.
|
|
86
|
+
|
|
87
|
+
### 3) Run Codex via the Proxy
|
|
79
88
|
|
|
80
89
|
```bash
|
|
81
90
|
export OPENAI_API_KEY="your-zai-api-key"
|
|
@@ -84,6 +93,31 @@ codex -m "GLM-4.7" -c model_provider="zai_proxy"
|
|
|
84
93
|
|
|
85
94
|
---
|
|
86
95
|
|
|
96
|
+
## Tools / MCP (optional)
|
|
97
|
+
|
|
98
|
+
Codex tool-calling / MCP memory requires an additional compatibility layer:
|
|
99
|
+
- Codex uses **Responses API tool events** (function call items + arguments delta/done, plus function_call_output inputs)
|
|
100
|
+
- Some upstream models/providers may not emit tool calls (or may emit them in a different shape)
|
|
101
|
+
|
|
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:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
export ALLOW_TOOLS=1
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Important:
|
|
110
|
+
- Tool support is **provider/model dependent**. If upstream never emits tool calls, the proxy can’t invent them.
|
|
111
|
+
- If tools are enabled, the proxy must translate:
|
|
112
|
+
- Responses `tools` + `tool_choice` → Chat `tools` + `tool_choice`
|
|
113
|
+
- Chat `tool_calls` (stream/non-stream) → Responses function-call events
|
|
114
|
+
- Responses `function_call_output` → Chat `role=tool` messages
|
|
115
|
+
- Non-function tool types are dropped for Z.AI compatibility.
|
|
116
|
+
|
|
117
|
+
(See repo changelog and docs for the exact implemented behavior.)
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
87
121
|
## CLI Usage
|
|
88
122
|
|
|
89
123
|
```bash
|
|
@@ -97,7 +131,7 @@ zai-codex-bridge --port 8080
|
|
|
97
131
|
zai-codex-bridge --log-level debug
|
|
98
132
|
|
|
99
133
|
# Custom Z.AI endpoint
|
|
100
|
-
zai-codex-bridge --zai-base-url https://
|
|
134
|
+
zai-codex-bridge --zai-base-url https://api.z.ai/api/coding/paas/v4
|
|
101
135
|
|
|
102
136
|
# Show help
|
|
103
137
|
zai-codex-bridge --help
|
|
@@ -106,17 +140,21 @@ zai-codex-bridge --help
|
|
|
106
140
|
### Environment Variables
|
|
107
141
|
|
|
108
142
|
```bash
|
|
109
|
-
export PORT=31415
|
|
110
143
|
export HOST=127.0.0.1
|
|
144
|
+
export PORT=31415
|
|
111
145
|
export ZAI_BASE_URL=https://api.z.ai/api/coding/paas/v4
|
|
112
146
|
export LOG_LEVEL=info
|
|
147
|
+
|
|
148
|
+
# Optional
|
|
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
|
|
113
151
|
```
|
|
114
152
|
|
|
115
153
|
---
|
|
116
154
|
|
|
117
|
-
## Auto-
|
|
155
|
+
## Auto-start the Proxy with Codex (recommended)
|
|
118
156
|
|
|
119
|
-
|
|
157
|
+
Use a shell function that starts the proxy only if needed:
|
|
120
158
|
|
|
121
159
|
```bash
|
|
122
160
|
codex-with-zai() {
|
|
@@ -125,114 +163,114 @@ codex-with-zai() {
|
|
|
125
163
|
local HEALTH="http://${HOST}:${PORT}/health"
|
|
126
164
|
local PROXY_PID=""
|
|
127
165
|
|
|
128
|
-
# Start proxy only if not responding
|
|
129
166
|
if ! curl -fsS "$HEALTH" >/dev/null 2>&1; then
|
|
130
|
-
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 &
|
|
131
168
|
PROXY_PID=$!
|
|
132
169
|
trap 'kill $PROXY_PID 2>/dev/null' EXIT INT TERM
|
|
133
|
-
sleep
|
|
170
|
+
sleep 1
|
|
134
171
|
fi
|
|
135
172
|
|
|
136
|
-
|
|
137
|
-
codex -m "GLM-4.7" -c model_provider="zai_proxy" "$@"
|
|
173
|
+
codex -c model_provider="zai_proxy" "$@"
|
|
138
174
|
}
|
|
139
175
|
```
|
|
140
176
|
|
|
141
177
|
Usage:
|
|
178
|
+
|
|
142
179
|
```bash
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
# Ctrl+D exits both
|
|
180
|
+
export OPENAI_API_KEY="your-zai-api-key"
|
|
181
|
+
codex-with-zai -m "GLM-4.7"
|
|
146
182
|
```
|
|
147
183
|
|
|
148
184
|
---
|
|
149
185
|
|
|
150
186
|
## API Endpoints
|
|
151
187
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
### `POST /v1/responses`
|
|
156
|
-
Same as `/responses` (for compatibility with Codex's path structure).
|
|
157
|
-
|
|
158
|
-
### `GET /health`
|
|
159
|
-
Health check endpoint.
|
|
188
|
+
- `POST /responses` — accepts Responses API requests
|
|
189
|
+
- `POST /v1/responses` — same as above (Codex default path)
|
|
190
|
+
- `GET /health` — health check
|
|
160
191
|
|
|
161
192
|
---
|
|
162
193
|
|
|
163
|
-
## Translation
|
|
194
|
+
## Translation Overview
|
|
164
195
|
|
|
165
196
|
### Request: Responses → Chat
|
|
166
197
|
|
|
167
|
-
```
|
|
168
|
-
// Input (Responses
|
|
198
|
+
```js
|
|
199
|
+
// Input (Responses)
|
|
169
200
|
{
|
|
170
|
-
model: "GLM-4.7",
|
|
171
|
-
instructions: "Be helpful",
|
|
172
|
-
input: [
|
|
173
|
-
|
|
174
|
-
],
|
|
175
|
-
max_output_tokens: 1000
|
|
201
|
+
"model": "GLM-4.7",
|
|
202
|
+
"instructions": "Be helpful",
|
|
203
|
+
"input": [{ "role": "user", "content": "Hello" }],
|
|
204
|
+
"max_output_tokens": 1000
|
|
176
205
|
}
|
|
177
206
|
|
|
178
|
-
// Output (Chat
|
|
207
|
+
// Output (Chat)
|
|
179
208
|
{
|
|
180
|
-
model: "GLM-4.7",
|
|
181
|
-
messages: [
|
|
182
|
-
{ role: "system", content: "Be helpful" },
|
|
183
|
-
{ role: "user", content: "Hello" }
|
|
209
|
+
"model": "GLM-4.7",
|
|
210
|
+
"messages": [
|
|
211
|
+
{ "role": "system", "content": "Be helpful" },
|
|
212
|
+
{ "role": "user", "content": "Hello" }
|
|
184
213
|
],
|
|
185
|
-
max_tokens: 1000
|
|
214
|
+
"max_tokens": 1000
|
|
186
215
|
}
|
|
187
216
|
```
|
|
188
217
|
|
|
189
|
-
### Response: Chat → Responses
|
|
218
|
+
### Response: Chat → Responses (simplified)
|
|
190
219
|
|
|
191
|
-
```
|
|
192
|
-
// Input (Chat
|
|
220
|
+
```js
|
|
221
|
+
// Input (Chat)
|
|
193
222
|
{
|
|
194
|
-
choices: [{
|
|
195
|
-
|
|
196
|
-
}],
|
|
197
|
-
usage: {
|
|
198
|
-
prompt_tokens: 10,
|
|
199
|
-
completion_tokens: 5
|
|
200
|
-
}
|
|
223
|
+
"choices": [{ "message": { "content": "Hi there!" } }],
|
|
224
|
+
"usage": { "prompt_tokens": 10, "completion_tokens": 5 }
|
|
201
225
|
}
|
|
202
226
|
|
|
203
|
-
// Output (Responses
|
|
227
|
+
// Output (Responses - simplified)
|
|
204
228
|
{
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
usage: {
|
|
208
|
-
input_tokens: 10,
|
|
209
|
-
output_tokens: 5
|
|
210
|
-
}
|
|
229
|
+
"status": "completed",
|
|
230
|
+
"output": [{ "type": "message", "content": [{ "type": "output_text", "text": "Hi there!" }] }],
|
|
231
|
+
"usage": { "input_tokens": 10, "output_tokens": 5 }
|
|
211
232
|
}
|
|
212
233
|
```
|
|
213
234
|
|
|
214
235
|
---
|
|
215
236
|
|
|
216
|
-
##
|
|
237
|
+
## Troubleshooting
|
|
217
238
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
239
|
+
### 401 / “token expired or incorrect”
|
|
240
|
+
- Verify the key is exported as `OPENAI_API_KEY` (or matches `env_key` in config.toml).
|
|
241
|
+
- Make sure the proxy is not overwriting Authorization headers.
|
|
221
242
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
243
|
+
### 404 on `/v1/responses`
|
|
244
|
+
- Ensure `base_url` points to the proxy root (example: `http://127.0.0.1:31415`).
|
|
245
|
+
- Confirm the proxy is running and `/health` returns `ok`.
|
|
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
|
+
|
|
254
|
+
### 502 Bad Gateway
|
|
255
|
+
- Proxy reached upstream but upstream failed. Enable debug:
|
|
256
|
+
```bash
|
|
257
|
+
LOG_LEVEL=debug zai-codex-bridge
|
|
258
|
+
```
|
|
225
259
|
|
|
226
260
|
---
|
|
227
261
|
|
|
228
|
-
##
|
|
262
|
+
## Versioning Policy
|
|
229
263
|
|
|
230
|
-
|
|
264
|
+
This repo follows **small, safe patch increments** while stabilizing provider compatibility:
|
|
265
|
+
|
|
266
|
+
- Keep patch bumps only: `0.4.0 → 0.4.1 → 0.4.2 → ...`
|
|
267
|
+
- No big jumps unless strictly necessary.
|
|
268
|
+
|
|
269
|
+
(See `CHANGELOG.md` for details once present.)
|
|
231
270
|
|
|
232
271
|
---
|
|
233
272
|
|
|
234
273
|
## License
|
|
235
274
|
|
|
236
|
-
MIT License
|
|
237
|
-
|
|
275
|
+
MIT License — Copyright (c) 2026 Davide A. Guglielmi
|
|
238
276
|
See [LICENSE](LICENSE) for details.
|
package/RELEASING.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Releasing
|
|
2
|
+
|
|
3
|
+
This document describes the release process for zai-codex-bridge.
|
|
4
|
+
|
|
5
|
+
## Version Policy
|
|
6
|
+
|
|
7
|
+
- **Patch releases only** (0.4.0 → 0.4.1 → 0.4.2, etc.)
|
|
8
|
+
- No minor or major bumps without explicit discussion
|
|
9
|
+
- Always increment by +0.0.1 from current version
|
|
10
|
+
|
|
11
|
+
## Release Steps
|
|
12
|
+
|
|
13
|
+
### 1. Run Tests
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Set your API key
|
|
17
|
+
export ZAI_API_KEY="sk-your-key"
|
|
18
|
+
|
|
19
|
+
# Run test suite
|
|
20
|
+
npm run test:curl
|
|
21
|
+
# or
|
|
22
|
+
npm test
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### 2. Bump Version
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Use the release script (recommended)
|
|
29
|
+
npm run release:patch
|
|
30
|
+
|
|
31
|
+
# Or manually edit package.json and change:
|
|
32
|
+
# "version": "0.4.0" -> "version": "0.4.1"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 3. Update CHANGELOG.md
|
|
36
|
+
|
|
37
|
+
Add an entry for the new version following [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) format.
|
|
38
|
+
|
|
39
|
+
### 4. Commit
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
git add package.json CHANGELOG.md
|
|
43
|
+
git commit -m "chore: release v0.4.1"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 5. Tag
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
git tag v0.4.1
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 6. Push (Optional)
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
git push
|
|
56
|
+
git push --tags
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 7. Publish to npm
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npm publish
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## release:patch Script
|
|
66
|
+
|
|
67
|
+
The `npm run release:patch` script:
|
|
68
|
+
|
|
69
|
+
1. Verifies current version is 0.4.x
|
|
70
|
+
2. Bumps patch version by +0.0.1
|
|
71
|
+
3. Refuses to bump minor/major versions
|
|
72
|
+
4. Updates package.json in-place
|
|
73
|
+
|
|
74
|
+
Example:
|
|
75
|
+
```bash
|
|
76
|
+
$ npm run release:patch
|
|
77
|
+
Current version: 0.4.0
|
|
78
|
+
Bumping to: 0.4.1
|
|
79
|
+
Updated package.json
|
|
80
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mmmbuto/zai-codex-bridge",
|
|
3
|
-
"version": "0.4.
|
|
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": {
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node src/server.js",
|
|
11
|
-
"test
|
|
11
|
+
"test": "node scripts/test-curl.js",
|
|
12
|
+
"test:curl": "node scripts/test-curl.js",
|
|
13
|
+
"release:patch": "node scripts/release-patch.js"
|
|
12
14
|
},
|
|
13
15
|
"keywords": [
|
|
14
16
|
"codex",
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Safe patch version bumper
|
|
5
|
+
* Only allows patch releases (0.4.0 -> 0.4.1)
|
|
6
|
+
* Refuses minor/major bumps
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
const PACKAGE_PATH = path.join(__dirname, '..', 'package.json');
|
|
13
|
+
|
|
14
|
+
function bumpPatch(version) {
|
|
15
|
+
const parts = version.split('.').map(Number);
|
|
16
|
+
|
|
17
|
+
if (parts.length !== 3) {
|
|
18
|
+
throw new Error(`Invalid version format: ${version}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const [major, minor, patch] = parts;
|
|
22
|
+
|
|
23
|
+
// Only allow 0.4.x versions
|
|
24
|
+
if (major !== 0 || minor !== 4) {
|
|
25
|
+
console.error(`ERROR: Current version is ${version}`);
|
|
26
|
+
console.error('This script only supports patch releases for 0.4.x versions.');
|
|
27
|
+
console.error('For other version changes, edit package.json manually.');
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const newVersion = `0.4.${patch + 1}`;
|
|
32
|
+
return newVersion;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function main() {
|
|
36
|
+
// Read package.json
|
|
37
|
+
const pkg = JSON.parse(fs.readFileSync(PACKAGE_PATH, 'utf8'));
|
|
38
|
+
const currentVersion = pkg.version;
|
|
39
|
+
|
|
40
|
+
console.log(`Current version: ${currentVersion}`);
|
|
41
|
+
|
|
42
|
+
// Bump patch
|
|
43
|
+
const newVersion = bumpPatch(currentVersion);
|
|
44
|
+
console.log(`Bumping to: ${newVersion}`);
|
|
45
|
+
|
|
46
|
+
// Update package.json
|
|
47
|
+
pkg.version = newVersion;
|
|
48
|
+
|
|
49
|
+
// Write back
|
|
50
|
+
fs.writeFileSync(PACKAGE_PATH, JSON.stringify(pkg, null, 2) + '\n');
|
|
51
|
+
|
|
52
|
+
console.log('Updated package.json');
|
|
53
|
+
console.log('\nNext steps:');
|
|
54
|
+
console.log(' 1. Update CHANGELOG.md');
|
|
55
|
+
console.log(' 2. Commit: git add package.json CHANGELOG.md && git commit -m "chore: release v' + newVersion + '"');
|
|
56
|
+
console.log(' 3. Tag: git tag v' + newVersion);
|
|
57
|
+
console.log(' 4. Publish: npm publish');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
main();
|
package/scripts/test-curl.js
CHANGED
|
@@ -135,6 +135,155 @@ async function testStreamingFormat() {
|
|
|
135
135
|
});
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
async function testToolCall() {
|
|
139
|
+
console.log('\n=== Testing POST /v1/responses (Tool Call) ===\n');
|
|
140
|
+
console.log('Note: This test requires ALLOW_TOOLS=1 and upstream model support for tools.\n');
|
|
141
|
+
|
|
142
|
+
const payload = {
|
|
143
|
+
model: 'GLM-4.7',
|
|
144
|
+
instructions: 'You are a helpful assistant.',
|
|
145
|
+
input: [
|
|
146
|
+
{
|
|
147
|
+
role: 'user',
|
|
148
|
+
content: 'What is the weather in Tokyo? Use the get_weather tool.'
|
|
149
|
+
}
|
|
150
|
+
],
|
|
151
|
+
tools: [
|
|
152
|
+
{
|
|
153
|
+
type: 'function',
|
|
154
|
+
function: {
|
|
155
|
+
name: 'get_weather',
|
|
156
|
+
description: 'Get the current weather for a location',
|
|
157
|
+
parameters: {
|
|
158
|
+
type: 'object',
|
|
159
|
+
properties: {
|
|
160
|
+
location: {
|
|
161
|
+
type: 'string',
|
|
162
|
+
description: 'The city and state, e.g. San Francisco, CA'
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
required: ['location']
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
],
|
|
170
|
+
tool_choice: 'auto',
|
|
171
|
+
stream: true
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
return new Promise((resolve, reject) => {
|
|
175
|
+
const options = {
|
|
176
|
+
hostname: PROXY_HOST,
|
|
177
|
+
port: PROXY_PORT,
|
|
178
|
+
path: '/v1/responses',
|
|
179
|
+
method: 'POST',
|
|
180
|
+
headers: {
|
|
181
|
+
'Content-Type': 'application/json',
|
|
182
|
+
'Authorization': `Bearer ${ZAI_API_KEY}`
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const req = http.request(options, (res) => {
|
|
187
|
+
console.log('Status:', res.statusCode);
|
|
188
|
+
|
|
189
|
+
if (res.statusCode !== 200) {
|
|
190
|
+
let body = '';
|
|
191
|
+
res.on('data', (chunk) => body += chunk);
|
|
192
|
+
res.on('end', () => {
|
|
193
|
+
console.log('Error response:', body);
|
|
194
|
+
resolve({ status: 'error', message: body });
|
|
195
|
+
});
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
console.log('\nStreaming response:');
|
|
200
|
+
let buffer = '';
|
|
201
|
+
let foundToolCall = false;
|
|
202
|
+
let foundOutputItemAdded = false;
|
|
203
|
+
let foundFunctionCallDelta = false;
|
|
204
|
+
let foundOutputItemDone = false;
|
|
205
|
+
let foundResponseCompleted = false;
|
|
206
|
+
|
|
207
|
+
res.on('data', (chunk) => {
|
|
208
|
+
buffer += chunk.toString();
|
|
209
|
+
const events = buffer.split('\n\n');
|
|
210
|
+
buffer = events.pop() || '';
|
|
211
|
+
|
|
212
|
+
for (const evt of events) {
|
|
213
|
+
const lines = evt.split('\n');
|
|
214
|
+
for (const line of lines) {
|
|
215
|
+
if (!line.startsWith('data:')) continue;
|
|
216
|
+
const payload = line.slice(5).trim();
|
|
217
|
+
if (!payload || payload === '[DONE]') continue;
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
const data = JSON.parse(payload);
|
|
221
|
+
const type = data.type;
|
|
222
|
+
|
|
223
|
+
// Look for tool call events
|
|
224
|
+
if (type === 'response.output_item.added') {
|
|
225
|
+
if (data.item?.type === 'function_call') {
|
|
226
|
+
foundOutputItemAdded = true;
|
|
227
|
+
console.log('[EVENT] output_item.added (function_call):', data.item?.name);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (type === 'response.function_call_arguments.delta') {
|
|
232
|
+
foundFunctionCallDelta = true;
|
|
233
|
+
process.stdout.write('.');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (type === 'response.output_item.done') {
|
|
237
|
+
if (data.item?.type === 'function_call') {
|
|
238
|
+
foundOutputItemDone = true;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (type === 'response.completed') {
|
|
243
|
+
foundResponseCompleted = true;
|
|
244
|
+
}
|
|
245
|
+
} catch (e) {
|
|
246
|
+
// Skip parse errors
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
res.on('end', () => {
|
|
253
|
+
console.log();
|
|
254
|
+
console.log('\n=== Tool Call Test Results ===');
|
|
255
|
+
|
|
256
|
+
if (!foundToolCall && !foundOutputItemAdded) {
|
|
257
|
+
console.log('SKIP: upstream did not return tool_calls');
|
|
258
|
+
console.log('This may mean:');
|
|
259
|
+
console.log(' - ALLOW_TOOLS is not enabled on the proxy');
|
|
260
|
+
console.log(' - The model does not support tool calls');
|
|
261
|
+
console.log(' - The prompt did not trigger a tool call');
|
|
262
|
+
resolve({ status: 'skipped', reason: 'no tool calls from upstream' });
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const passed = foundOutputItemAdded && foundFunctionCallDelta && foundOutputItemDone && foundResponseCompleted;
|
|
267
|
+
console.log('output_item.added (function_call):', foundOutputItemAdded ? 'PASS' : 'FAIL');
|
|
268
|
+
console.log('function_call_arguments.delta:', foundFunctionCallDelta ? 'PASS' : 'FAIL');
|
|
269
|
+
console.log('output_item.done (function_call):', foundOutputItemDone ? 'PASS' : 'FAIL');
|
|
270
|
+
console.log('response.completed:', foundResponseCompleted ? 'PASS' : 'FAIL');
|
|
271
|
+
console.log('\nOverall:', passed ? 'PASS' : 'FAIL');
|
|
272
|
+
|
|
273
|
+
resolve({ status: passed ? 'pass' : 'fail', results: { foundOutputItemAdded, foundFunctionCallDelta, foundOutputItemDone, foundResponseCompleted } });
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
req.on('error', (err) => {
|
|
278
|
+
console.error('Request error:', err.message);
|
|
279
|
+
reject(err);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
req.write(JSON.stringify(payload, null, 2));
|
|
283
|
+
req.end();
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
138
287
|
async function main() {
|
|
139
288
|
console.log('zai-codex-bridge Manual Test');
|
|
140
289
|
console.log('================================');
|
|
@@ -151,7 +300,22 @@ async function main() {
|
|
|
151
300
|
await testResponsesFormat();
|
|
152
301
|
await testStreamingFormat();
|
|
153
302
|
|
|
303
|
+
// Tool call test (optional - depends on upstream support)
|
|
304
|
+
console.log('\n\n=== Tool Support Tests ===');
|
|
305
|
+
const toolResult = await testToolCall();
|
|
306
|
+
|
|
154
307
|
console.log('\n=== All Tests Complete ===\n');
|
|
308
|
+
console.log('Summary:');
|
|
309
|
+
console.log(' Health: PASS');
|
|
310
|
+
console.log(' Non-streaming: PASS');
|
|
311
|
+
console.log(' Streaming: PASS');
|
|
312
|
+
if (toolResult.status === 'pass') {
|
|
313
|
+
console.log(' Tool calls: PASS');
|
|
314
|
+
} else if (toolResult.status === 'skipped') {
|
|
315
|
+
console.log(' Tool calls: SKIPPED (upstream does not support or did not return tool_calls)');
|
|
316
|
+
} else {
|
|
317
|
+
console.log(' Tool calls: FAIL or ERROR');
|
|
318
|
+
}
|
|
155
319
|
} catch (error) {
|
|
156
320
|
console.error('\nError:', error.message);
|
|
157
321
|
process.exit(1);
|
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
|
|
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)
|
|
@@ -143,18 +204,41 @@ function translateResponsesToChat(request) {
|
|
|
143
204
|
content: request.input
|
|
144
205
|
});
|
|
145
206
|
} else if (Array.isArray(request.input)) {
|
|
146
|
-
// Array of ResponseItem objects
|
|
207
|
+
// Array of ResponseItem objects
|
|
147
208
|
for (const item of request.input) {
|
|
209
|
+
// Handle function_call_output items (tool responses) - only if allowTools
|
|
210
|
+
if (allowTools && item.type === 'function_call_output') {
|
|
211
|
+
const toolMsg = {
|
|
212
|
+
role: 'tool',
|
|
213
|
+
tool_call_id: item.call_id || item.tool_call_id || '',
|
|
214
|
+
content: ''
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// Extract content from output or content field
|
|
218
|
+
if (item.output !== undefined) {
|
|
219
|
+
toolMsg.content = typeof item.output === 'string'
|
|
220
|
+
? item.output
|
|
221
|
+
: JSON.stringify(item.output);
|
|
222
|
+
} else if (item.content !== undefined) {
|
|
223
|
+
toolMsg.content = typeof item.content === 'string'
|
|
224
|
+
? item.content
|
|
225
|
+
: JSON.stringify(item.content);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
messages.push(toolMsg);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
148
232
|
// Only process items with a 'role' field (Message items)
|
|
149
233
|
// Skip Reasoning, FunctionCall, LocalShellCall, etc.
|
|
150
234
|
if (!item.role) continue;
|
|
151
235
|
|
|
152
236
|
// Map non-standard roles to Z.AI-compatible roles
|
|
153
|
-
// Z.AI accepts: system, user, assistant
|
|
237
|
+
// Z.AI accepts: system, user, assistant, tool
|
|
154
238
|
let role = item.role;
|
|
155
239
|
if (role === 'developer') {
|
|
156
240
|
role = 'user'; // Map developer to user
|
|
157
|
-
} else if (role !== 'system' && role !== 'user' && role !== 'assistant') {
|
|
241
|
+
} else if (role !== 'system' && role !== 'user' && role !== 'assistant' && role !== 'tool') {
|
|
158
242
|
// Skip any other non-standard roles
|
|
159
243
|
continue;
|
|
160
244
|
}
|
|
@@ -164,13 +248,13 @@ function translateResponsesToChat(request) {
|
|
|
164
248
|
content: flattenContent(item.content)
|
|
165
249
|
};
|
|
166
250
|
|
|
167
|
-
// Handle tool calls if present (only if
|
|
168
|
-
if (
|
|
251
|
+
// Handle tool calls if present (only if allowTools)
|
|
252
|
+
if (allowTools && item.tool_calls && Array.isArray(item.tool_calls)) {
|
|
169
253
|
msg.tool_calls = item.tool_calls;
|
|
170
254
|
}
|
|
171
255
|
|
|
172
|
-
// Handle tool call ID for tool responses (only if
|
|
173
|
-
if (
|
|
256
|
+
// Handle tool call ID for tool responses (only if allowTools)
|
|
257
|
+
if (allowTools && item.tool_call_id) {
|
|
174
258
|
msg.tool_call_id = item.tool_call_id;
|
|
175
259
|
}
|
|
176
260
|
|
|
@@ -203,27 +287,49 @@ function translateResponsesToChat(request) {
|
|
|
203
287
|
chatRequest.top_p = request.top_p;
|
|
204
288
|
}
|
|
205
289
|
|
|
206
|
-
// Tools handling (only if
|
|
207
|
-
if (
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
+
|
|
219
322
|
// Only add tools array if there are valid tools
|
|
220
323
|
if (chatRequest.tools.length === 0) {
|
|
221
324
|
delete chatRequest.tools;
|
|
222
325
|
}
|
|
223
326
|
}
|
|
224
327
|
|
|
225
|
-
if (
|
|
328
|
+
if (allowTools && request.tool_choice) {
|
|
226
329
|
chatRequest.tool_choice = request.tool_choice;
|
|
330
|
+
if (!chatRequest.tools || chatRequest.tools.length === 0) {
|
|
331
|
+
delete chatRequest.tool_choice;
|
|
332
|
+
}
|
|
227
333
|
}
|
|
228
334
|
|
|
229
335
|
log('debug', 'Translated Responses->Chat:', {
|
|
@@ -238,8 +344,9 @@ function translateResponsesToChat(request) {
|
|
|
238
344
|
/**
|
|
239
345
|
* Translate Chat Completions response to Responses format
|
|
240
346
|
* Handles both output_text and reasoning_text content
|
|
347
|
+
* Handles tool_calls if present (only if allowTools)
|
|
241
348
|
*/
|
|
242
|
-
function translateChatToResponses(chatResponse, responsesRequest, ids) {
|
|
349
|
+
function translateChatToResponses(chatResponse, responsesRequest, ids, allowTools) {
|
|
243
350
|
const msg = chatResponse.choices?.[0]?.message ?? {};
|
|
244
351
|
const outputText = msg.content ?? '';
|
|
245
352
|
const reasoningText = msg.reasoning_content ?? '';
|
|
@@ -262,6 +369,27 @@ function translateChatToResponses(chatResponse, responsesRequest, ids) {
|
|
|
262
369
|
content,
|
|
263
370
|
};
|
|
264
371
|
|
|
372
|
+
// Build output array: message item + any function_call items
|
|
373
|
+
const finalOutput = [msgItem];
|
|
374
|
+
|
|
375
|
+
// Handle tool_calls (only if allowTools)
|
|
376
|
+
if (allowTools && msg.tool_calls && Array.isArray(msg.tool_calls)) {
|
|
377
|
+
for (const tc of msg.tool_calls) {
|
|
378
|
+
const callId = tc.id || `call_${randomUUID().replace(/-/g, '')}`;
|
|
379
|
+
const name = tc.function?.name || '';
|
|
380
|
+
const args = tc.function?.arguments || '';
|
|
381
|
+
|
|
382
|
+
finalOutput.push({
|
|
383
|
+
id: callId,
|
|
384
|
+
type: 'function_call',
|
|
385
|
+
status: 'completed',
|
|
386
|
+
call_id: callId,
|
|
387
|
+
name: name,
|
|
388
|
+
arguments: typeof args === 'string' ? args : JSON.stringify(args),
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
265
393
|
return buildResponseObject({
|
|
266
394
|
id: responseId,
|
|
267
395
|
model: responsesRequest?.model || chatResponse.model || DEFAULT_MODEL,
|
|
@@ -269,7 +397,7 @@ function translateChatToResponses(chatResponse, responsesRequest, ids) {
|
|
|
269
397
|
created_at: createdAt,
|
|
270
398
|
completed_at: nowSec(),
|
|
271
399
|
input: responsesRequest?.input || [],
|
|
272
|
-
output:
|
|
400
|
+
output: finalOutput,
|
|
273
401
|
tools: responsesRequest?.tools || [],
|
|
274
402
|
});
|
|
275
403
|
}
|
|
@@ -341,7 +469,7 @@ async function makeUpstreamRequest(path, body, headers) {
|
|
|
341
469
|
* Handle streaming response from Z.AI with proper Responses API event format
|
|
342
470
|
* Separates reasoning_content, content, and tool_calls into distinct events
|
|
343
471
|
*/
|
|
344
|
-
async function streamChatToResponses(upstreamBody, res, responsesRequest, ids) {
|
|
472
|
+
async function streamChatToResponses(upstreamBody, res, responsesRequest, ids, allowTools) {
|
|
345
473
|
const decoder = new TextDecoder();
|
|
346
474
|
const reader = upstreamBody.getReader();
|
|
347
475
|
let buffer = '';
|
|
@@ -400,6 +528,10 @@ async function streamChatToResponses(upstreamBody, res, responsesRequest, ids) {
|
|
|
400
528
|
let out = '';
|
|
401
529
|
let reasoning = '';
|
|
402
530
|
|
|
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
|
|
534
|
+
|
|
403
535
|
while (true) {
|
|
404
536
|
const { done, value } = await reader.read();
|
|
405
537
|
if (done) break;
|
|
@@ -428,6 +560,108 @@ async function streamChatToResponses(upstreamBody, res, responsesRequest, ids) {
|
|
|
428
560
|
|
|
429
561
|
const delta = chunk.choices?.[0]?.delta || {};
|
|
430
562
|
|
|
563
|
+
// Handle tool_calls (only if allowTools)
|
|
564
|
+
if (allowTools && delta.tool_calls && Array.isArray(delta.tool_calls)) {
|
|
565
|
+
for (const tc of delta.tool_calls) {
|
|
566
|
+
const index = tc.index;
|
|
567
|
+
if (index == null) continue;
|
|
568
|
+
|
|
569
|
+
if (!toolCallsMap.has(index)) {
|
|
570
|
+
// New tool call - send output_item.added
|
|
571
|
+
const callId = tc.id || `call_${randomUUID().replace(/-/g, '')}`;
|
|
572
|
+
const name = tc.function?.name || '';
|
|
573
|
+
const outputIndex = TOOL_BASE_INDEX + index;
|
|
574
|
+
|
|
575
|
+
toolCallsMap.set(index, {
|
|
576
|
+
callId,
|
|
577
|
+
name,
|
|
578
|
+
outputIndex,
|
|
579
|
+
arguments: '',
|
|
580
|
+
partialArgs: ''
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
const fnItemInProgress = {
|
|
584
|
+
id: callId,
|
|
585
|
+
type: 'function_call',
|
|
586
|
+
status: 'in_progress',
|
|
587
|
+
call_id: callId,
|
|
588
|
+
name: name,
|
|
589
|
+
arguments: '',
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
sse({
|
|
593
|
+
type: 'response.output_item.added',
|
|
594
|
+
output_index: outputIndex,
|
|
595
|
+
item: fnItemInProgress,
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
if (name) {
|
|
599
|
+
sse({
|
|
600
|
+
type: 'response.function_call_name.done',
|
|
601
|
+
item_id: callId,
|
|
602
|
+
output_index: outputIndex,
|
|
603
|
+
name: name,
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const tcData = toolCallsMap.get(index);
|
|
609
|
+
|
|
610
|
+
// Handle name update if it comes later
|
|
611
|
+
if (tc.function?.name && !tcData.name) {
|
|
612
|
+
tcData.name = tc.function.name;
|
|
613
|
+
sse({
|
|
614
|
+
type: 'response.function_call_name.done',
|
|
615
|
+
item_id: tcData.callId,
|
|
616
|
+
output_index: tcData.outputIndex,
|
|
617
|
+
name: tcData.name,
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Handle arguments delta
|
|
622
|
+
if (tc.function?.arguments && typeof tc.function.arguments === 'string') {
|
|
623
|
+
tcData.partialArgs += tc.function.arguments;
|
|
624
|
+
|
|
625
|
+
sse({
|
|
626
|
+
type: 'response.function_call_arguments.delta',
|
|
627
|
+
item_id: tcData.callId,
|
|
628
|
+
output_index: tcData.outputIndex,
|
|
629
|
+
delta: tc.function.arguments,
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Check if this tool call is done (finish_reason comes later in the choice)
|
|
634
|
+
const finishReason = chunk.choices?.[0]?.finish_reason;
|
|
635
|
+
if (finishReason === 'tool_calls' || (tc.function?.arguments && tc.function.arguments.length > 0 && chunk.choices?.[0]?.delta !== null)) {
|
|
636
|
+
tcData.arguments = tcData.partialArgs;
|
|
637
|
+
|
|
638
|
+
sse({
|
|
639
|
+
type: 'response.function_call_arguments.done',
|
|
640
|
+
item_id: tcData.callId,
|
|
641
|
+
output_index: tcData.outputIndex,
|
|
642
|
+
arguments: tcData.arguments,
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
const fnItemDone = {
|
|
646
|
+
id: tcData.callId,
|
|
647
|
+
type: 'function_call',
|
|
648
|
+
status: 'completed',
|
|
649
|
+
call_id: tcData.callId,
|
|
650
|
+
name: tcData.name,
|
|
651
|
+
arguments: tcData.arguments,
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
sse({
|
|
655
|
+
type: 'response.output_item.done',
|
|
656
|
+
output_index: tcData.outputIndex,
|
|
657
|
+
item: fnItemDone,
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
// Skip to next iteration after handling tool_calls
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
|
|
431
665
|
// NON mescolare reasoning in output_text
|
|
432
666
|
if (typeof delta.reasoning_content === 'string' && delta.reasoning_content.length) {
|
|
433
667
|
reasoning += delta.reasoning_content;
|
|
@@ -495,6 +729,22 @@ async function streamChatToResponses(upstreamBody, res, responsesRequest, ids) {
|
|
|
495
729
|
item: msgItemDone,
|
|
496
730
|
});
|
|
497
731
|
|
|
732
|
+
// Build final output array: message item + any function_call items
|
|
733
|
+
const finalOutput = [msgItemDone];
|
|
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) {
|
|
737
|
+
finalOutput.push({
|
|
738
|
+
id: tcData.callId,
|
|
739
|
+
type: 'function_call',
|
|
740
|
+
status: 'completed',
|
|
741
|
+
call_id: tcData.callId,
|
|
742
|
+
name: tcData.name,
|
|
743
|
+
arguments: tcData.arguments,
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
498
748
|
const completed = buildResponseObject({
|
|
499
749
|
id: responseId,
|
|
500
750
|
model: responsesRequest?.model || DEFAULT_MODEL,
|
|
@@ -502,14 +752,14 @@ async function streamChatToResponses(upstreamBody, res, responsesRequest, ids) {
|
|
|
502
752
|
created_at: createdAt,
|
|
503
753
|
completed_at: nowSec(),
|
|
504
754
|
input: responsesRequest?.input || [],
|
|
505
|
-
output:
|
|
755
|
+
output: finalOutput,
|
|
506
756
|
tools: responsesRequest?.tools || [],
|
|
507
757
|
});
|
|
508
758
|
|
|
509
759
|
sse({ type: 'response.completed', response: completed });
|
|
510
760
|
res.end();
|
|
511
761
|
|
|
512
|
-
log('info', `Stream completed - ${out.length} output, ${reasoning.length} reasoning`);
|
|
762
|
+
log('info', `Stream completed - ${out.length} output, ${reasoning.length} reasoning, ${toolCallsMap.size} tool_calls`);
|
|
513
763
|
}
|
|
514
764
|
|
|
515
765
|
/**
|
|
@@ -543,19 +793,30 @@ async function handlePostRequest(req, res) {
|
|
|
543
793
|
return;
|
|
544
794
|
}
|
|
545
795
|
|
|
796
|
+
const hasTools = requestHasTools(request);
|
|
797
|
+
const allowTools = ALLOW_TOOLS_ENV || hasTools;
|
|
798
|
+
|
|
546
799
|
log('info', 'Incoming request:', {
|
|
547
800
|
path,
|
|
548
801
|
format: detectFormat(request),
|
|
549
802
|
model: request.model,
|
|
803
|
+
allowTools,
|
|
804
|
+
toolsPresent: hasTools,
|
|
550
805
|
authHeader: req.headers['authorization'] || req.headers['Authorization'] || 'none'
|
|
551
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
|
+
}
|
|
552
813
|
|
|
553
814
|
let upstreamBody;
|
|
554
815
|
const format = detectFormat(request);
|
|
555
816
|
|
|
556
817
|
if (format === 'responses') {
|
|
557
818
|
// Translate Responses to Chat
|
|
558
|
-
upstreamBody = translateResponsesToChat(request);
|
|
819
|
+
upstreamBody = translateResponsesToChat(request, allowTools);
|
|
559
820
|
} else if (format === 'chat') {
|
|
560
821
|
// Pass through Chat format
|
|
561
822
|
upstreamBody = request;
|
|
@@ -605,7 +866,7 @@ async function handlePostRequest(req, res) {
|
|
|
605
866
|
});
|
|
606
867
|
|
|
607
868
|
try {
|
|
608
|
-
await streamChatToResponses(upstreamResponse.body, res, request, ids);
|
|
869
|
+
await streamChatToResponses(upstreamResponse.body, res, request, ids, allowTools);
|
|
609
870
|
log('info', 'Streaming completed');
|
|
610
871
|
} catch (e) {
|
|
611
872
|
log('error', 'Streaming error:', e);
|
|
@@ -620,7 +881,7 @@ async function handlePostRequest(req, res) {
|
|
|
620
881
|
msgId: `msg_${randomUUID().replace(/-/g, '')}`,
|
|
621
882
|
};
|
|
622
883
|
|
|
623
|
-
const response = translateChatToResponses(chatResponse, request, ids);
|
|
884
|
+
const response = translateChatToResponses(chatResponse, request, ids, allowTools);
|
|
624
885
|
|
|
625
886
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
626
887
|
res.end(JSON.stringify(response));
|