@mmmbuto/zai-codex-bridge 0.3.2 → 0.4.2
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 +39 -0
- package/README.md +109 -81
- 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 +373 -279
package/src/server.js
CHANGED
|
@@ -11,20 +11,62 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
const http = require('http');
|
|
14
|
-
const
|
|
15
|
-
const { createGunzip } = require('zlib');
|
|
16
|
-
const { pipeline } = require('stream');
|
|
14
|
+
const { randomUUID } = require('crypto');
|
|
17
15
|
|
|
18
16
|
// Configuration from environment
|
|
19
17
|
const PORT = parseInt(process.env.PORT || '31415', 10);
|
|
20
18
|
const HOST = process.env.HOST || '127.0.0.1';
|
|
21
19
|
const ZAI_BASE_URL = process.env.ZAI_BASE_URL || 'https://api.z.ai/api/coding/paas/v4';
|
|
22
20
|
const LOG_LEVEL = process.env.LOG_LEVEL || 'info';
|
|
21
|
+
const DEFAULT_MODEL = process.env.DEFAULT_MODEL || 'glm-4.7';
|
|
23
22
|
|
|
24
23
|
// Env toggles for compatibility
|
|
25
24
|
const ALLOW_SYSTEM = process.env.ALLOW_SYSTEM === '1';
|
|
26
25
|
const ALLOW_TOOLS = process.env.ALLOW_TOOLS === '1';
|
|
27
26
|
|
|
27
|
+
function nowSec() {
|
|
28
|
+
return Math.floor(Date.now() / 1000);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function buildResponseObject({
|
|
32
|
+
id,
|
|
33
|
+
model,
|
|
34
|
+
status,
|
|
35
|
+
created_at,
|
|
36
|
+
completed_at = null,
|
|
37
|
+
input = [],
|
|
38
|
+
output = [],
|
|
39
|
+
tools = [],
|
|
40
|
+
}) {
|
|
41
|
+
// Struttura compatibile con Responses API per Codex CLI
|
|
42
|
+
return {
|
|
43
|
+
id,
|
|
44
|
+
object: 'response',
|
|
45
|
+
created_at,
|
|
46
|
+
status,
|
|
47
|
+
completed_at,
|
|
48
|
+
error: null,
|
|
49
|
+
incomplete_details: null,
|
|
50
|
+
input,
|
|
51
|
+
instructions: null,
|
|
52
|
+
max_output_tokens: null,
|
|
53
|
+
model,
|
|
54
|
+
output,
|
|
55
|
+
previous_response_id: null,
|
|
56
|
+
reasoning_effort: null,
|
|
57
|
+
store: false,
|
|
58
|
+
temperature: 1,
|
|
59
|
+
text: { format: { type: 'text' } },
|
|
60
|
+
tool_choice: 'auto',
|
|
61
|
+
tools,
|
|
62
|
+
top_p: 1,
|
|
63
|
+
truncation: 'disabled',
|
|
64
|
+
usage: null,
|
|
65
|
+
user: null,
|
|
66
|
+
metadata: {},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
28
70
|
/**
|
|
29
71
|
* Logger
|
|
30
72
|
*/
|
|
@@ -101,18 +143,41 @@ function translateResponsesToChat(request) {
|
|
|
101
143
|
content: request.input
|
|
102
144
|
});
|
|
103
145
|
} else if (Array.isArray(request.input)) {
|
|
104
|
-
// Array of ResponseItem objects
|
|
146
|
+
// Array of ResponseItem objects
|
|
105
147
|
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') {
|
|
150
|
+
const toolMsg = {
|
|
151
|
+
role: 'tool',
|
|
152
|
+
tool_call_id: item.call_id || item.tool_call_id || '',
|
|
153
|
+
content: ''
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// Extract content from output or content field
|
|
157
|
+
if (item.output !== undefined) {
|
|
158
|
+
toolMsg.content = typeof item.output === 'string'
|
|
159
|
+
? item.output
|
|
160
|
+
: JSON.stringify(item.output);
|
|
161
|
+
} else if (item.content !== undefined) {
|
|
162
|
+
toolMsg.content = typeof item.content === 'string'
|
|
163
|
+
? item.content
|
|
164
|
+
: JSON.stringify(item.content);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
messages.push(toolMsg);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
106
171
|
// Only process items with a 'role' field (Message items)
|
|
107
172
|
// Skip Reasoning, FunctionCall, LocalShellCall, etc.
|
|
108
173
|
if (!item.role) continue;
|
|
109
174
|
|
|
110
175
|
// Map non-standard roles to Z.AI-compatible roles
|
|
111
|
-
// Z.AI accepts: system, user, assistant
|
|
176
|
+
// Z.AI accepts: system, user, assistant, tool
|
|
112
177
|
let role = item.role;
|
|
113
178
|
if (role === 'developer') {
|
|
114
179
|
role = 'user'; // Map developer to user
|
|
115
|
-
} else if (role !== 'system' && role !== 'user' && role !== 'assistant') {
|
|
180
|
+
} else if (role !== 'system' && role !== 'user' && role !== 'assistant' && role !== 'tool') {
|
|
116
181
|
// Skip any other non-standard roles
|
|
117
182
|
continue;
|
|
118
183
|
}
|
|
@@ -196,42 +261,62 @@ function translateResponsesToChat(request) {
|
|
|
196
261
|
/**
|
|
197
262
|
* Translate Chat Completions response to Responses format
|
|
198
263
|
* Handles both output_text and reasoning_text content
|
|
264
|
+
* Handles tool_calls if present (only if ALLOW_TOOLS)
|
|
199
265
|
*/
|
|
200
|
-
function translateChatToResponses(
|
|
201
|
-
const
|
|
202
|
-
const
|
|
203
|
-
const
|
|
266
|
+
function translateChatToResponses(chatResponse, responsesRequest, ids) {
|
|
267
|
+
const msg = chatResponse.choices?.[0]?.message ?? {};
|
|
268
|
+
const outputText = msg.content ?? '';
|
|
269
|
+
const reasoningText = msg.reasoning_content ?? '';
|
|
270
|
+
|
|
271
|
+
const createdAt = ids?.createdAt ?? nowSec();
|
|
272
|
+
const responseId = ids?.responseId ?? `resp_${randomUUID().replace(/-/g, '')}`;
|
|
273
|
+
const msgId = ids?.msgId ?? `msg_${randomUUID().replace(/-/g, '')}`;
|
|
204
274
|
|
|
205
275
|
const content = [];
|
|
206
276
|
if (reasoningText) {
|
|
207
|
-
content.push({ type: 'reasoning_text', text: reasoningText });
|
|
277
|
+
content.push({ type: 'reasoning_text', text: reasoningText, annotations: [] });
|
|
208
278
|
}
|
|
209
|
-
content.push({ type: 'output_text', text: outputText });
|
|
279
|
+
content.push({ type: 'output_text', text: outputText, annotations: [] });
|
|
210
280
|
|
|
211
|
-
const
|
|
212
|
-
id:
|
|
213
|
-
|
|
214
|
-
created_at: createdAt,
|
|
215
|
-
model,
|
|
281
|
+
const msgItem = {
|
|
282
|
+
id: msgId,
|
|
283
|
+
type: 'message',
|
|
216
284
|
status: 'completed',
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
type: 'message',
|
|
220
|
-
id: mid,
|
|
221
|
-
role: 'assistant',
|
|
222
|
-
content
|
|
223
|
-
}
|
|
224
|
-
]
|
|
285
|
+
role: 'assistant',
|
|
286
|
+
content,
|
|
225
287
|
};
|
|
226
288
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
outputLength: outputText.length,
|
|
230
|
-
reasoningLength: reasoningText.length,
|
|
231
|
-
status: response.status
|
|
232
|
-
});
|
|
289
|
+
// Build output array: message item + any function_call items
|
|
290
|
+
const finalOutput = [msgItem];
|
|
233
291
|
|
|
234
|
-
|
|
292
|
+
// Handle tool_calls (only if ALLOW_TOOLS)
|
|
293
|
+
if (ALLOW_TOOLS && msg.tool_calls && Array.isArray(msg.tool_calls)) {
|
|
294
|
+
for (const tc of msg.tool_calls) {
|
|
295
|
+
const callId = tc.id || `call_${randomUUID().replace(/-/g, '')}`;
|
|
296
|
+
const name = tc.function?.name || '';
|
|
297
|
+
const args = tc.function?.arguments || '';
|
|
298
|
+
|
|
299
|
+
finalOutput.push({
|
|
300
|
+
id: callId,
|
|
301
|
+
type: 'function_call',
|
|
302
|
+
status: 'completed',
|
|
303
|
+
call_id: callId,
|
|
304
|
+
name: name,
|
|
305
|
+
arguments: typeof args === 'string' ? args : JSON.stringify(args),
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return buildResponseObject({
|
|
311
|
+
id: responseId,
|
|
312
|
+
model: responsesRequest?.model || chatResponse.model || DEFAULT_MODEL,
|
|
313
|
+
status: 'completed',
|
|
314
|
+
created_at: createdAt,
|
|
315
|
+
completed_at: nowSec(),
|
|
316
|
+
input: responsesRequest?.input || [],
|
|
317
|
+
output: finalOutput,
|
|
318
|
+
tools: responsesRequest?.tools || [],
|
|
319
|
+
});
|
|
235
320
|
}
|
|
236
321
|
|
|
237
322
|
/**
|
|
@@ -301,291 +386,294 @@ async function makeUpstreamRequest(path, body, headers) {
|
|
|
301
386
|
* Handle streaming response from Z.AI with proper Responses API event format
|
|
302
387
|
* Separates reasoning_content, content, and tool_calls into distinct events
|
|
303
388
|
*/
|
|
304
|
-
async function streamChatToResponses(
|
|
389
|
+
async function streamChatToResponses(upstreamBody, res, responsesRequest, ids) {
|
|
390
|
+
const decoder = new TextDecoder();
|
|
391
|
+
const reader = upstreamBody.getReader();
|
|
305
392
|
let buffer = '';
|
|
306
|
-
let seq = 0;
|
|
307
393
|
|
|
308
|
-
|
|
309
|
-
|
|
394
|
+
const createdAt = ids.createdAt;
|
|
395
|
+
const responseId = ids.responseId;
|
|
396
|
+
const msgId = ids.msgId;
|
|
310
397
|
|
|
311
|
-
|
|
312
|
-
const
|
|
313
|
-
|
|
398
|
+
let seq = 1;
|
|
399
|
+
const OUTPUT_INDEX = 0;
|
|
400
|
+
const CONTENT_INDEX = 0;
|
|
314
401
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
// Responses streaming: only "data: {json}\n\n"
|
|
319
|
-
res.write(`data: ${JSON.stringify(ev)}\n\n`);
|
|
402
|
+
function sse(obj) {
|
|
403
|
+
if (obj.sequence_number == null) obj.sequence_number = seq++;
|
|
404
|
+
res.write(`data: ${JSON.stringify(obj)}\n\n`);
|
|
320
405
|
}
|
|
321
406
|
|
|
322
|
-
//
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
},
|
|
407
|
+
// response.created / response.in_progress
|
|
408
|
+
const baseResp = buildResponseObject({
|
|
409
|
+
id: responseId,
|
|
410
|
+
model: responsesRequest?.model || DEFAULT_MODEL,
|
|
411
|
+
status: 'in_progress',
|
|
412
|
+
created_at: createdAt,
|
|
413
|
+
completed_at: null,
|
|
414
|
+
input: responsesRequest?.input || [],
|
|
415
|
+
output: [],
|
|
416
|
+
tools: responsesRequest?.tools || [],
|
|
333
417
|
});
|
|
334
418
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
});
|
|
419
|
+
sse({ type: 'response.created', response: baseResp });
|
|
420
|
+
sse({ type: 'response.in_progress', response: baseResp });
|
|
421
|
+
|
|
422
|
+
// output_item.added + content_part.added (output_text)
|
|
423
|
+
const msgItemInProgress = {
|
|
424
|
+
id: msgId,
|
|
425
|
+
type: 'message',
|
|
426
|
+
status: 'in_progress',
|
|
427
|
+
role: 'assistant',
|
|
428
|
+
content: [],
|
|
429
|
+
};
|
|
347
430
|
|
|
348
|
-
|
|
349
|
-
send({
|
|
431
|
+
sse({
|
|
350
432
|
type: 'response.output_item.added',
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
item: {
|
|
354
|
-
type: 'message',
|
|
355
|
-
id: messageItemId,
|
|
356
|
-
role: 'assistant',
|
|
357
|
-
content: [],
|
|
358
|
-
},
|
|
433
|
+
output_index: OUTPUT_INDEX,
|
|
434
|
+
item: msgItemInProgress,
|
|
359
435
|
});
|
|
360
436
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
output_index: 0,
|
|
369
|
-
content_index: 0,
|
|
370
|
-
text: reasoningText,
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
send({
|
|
375
|
-
type: 'response.output_text.done',
|
|
376
|
-
sequence_number: seq++,
|
|
377
|
-
item_id: messageItemId,
|
|
378
|
-
output_index: 0,
|
|
379
|
-
content_index: reasoningText ? 1 : 0,
|
|
380
|
-
text: outputText,
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
// close any tool call items
|
|
384
|
-
for (const [callId, st] of toolCalls.entries()) {
|
|
385
|
-
send({
|
|
386
|
-
type: 'response.function_call_arguments.done',
|
|
387
|
-
sequence_number: seq++,
|
|
388
|
-
item_id: st.itemId,
|
|
389
|
-
output_index: st.outputIndex,
|
|
390
|
-
arguments: st.args,
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
send({
|
|
394
|
-
type: 'response.output_item.done',
|
|
395
|
-
sequence_number: seq++,
|
|
396
|
-
output_index: st.outputIndex,
|
|
397
|
-
item: {
|
|
398
|
-
type: 'function_call',
|
|
399
|
-
id: st.itemId,
|
|
400
|
-
call_id: callId,
|
|
401
|
-
name: st.name,
|
|
402
|
-
arguments: st.args,
|
|
403
|
-
},
|
|
404
|
-
});
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// output_item.done for message
|
|
408
|
-
const messageContent = [];
|
|
409
|
-
if (reasoningText) messageContent.push({ type: 'reasoning_text', text: reasoningText });
|
|
410
|
-
messageContent.push({ type: 'output_text', text: outputText });
|
|
411
|
-
|
|
412
|
-
send({
|
|
413
|
-
type: 'response.output_item.done',
|
|
414
|
-
sequence_number: seq++,
|
|
415
|
-
output_index: 0,
|
|
416
|
-
item: {
|
|
417
|
-
type: 'message',
|
|
418
|
-
id: messageItemId,
|
|
419
|
-
role: 'assistant',
|
|
420
|
-
content: messageContent,
|
|
421
|
-
},
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
// response.completed
|
|
425
|
-
const outputItems = [
|
|
426
|
-
{
|
|
427
|
-
type: 'message',
|
|
428
|
-
id: messageItemId,
|
|
429
|
-
role: 'assistant',
|
|
430
|
-
content: messageContent,
|
|
431
|
-
},
|
|
432
|
-
...Array.from(toolCalls.entries()).map(([callId, st]) => ({
|
|
433
|
-
type: 'function_call',
|
|
434
|
-
id: st.itemId,
|
|
435
|
-
call_id: callId,
|
|
436
|
-
name: st.name,
|
|
437
|
-
arguments: st.args,
|
|
438
|
-
})),
|
|
439
|
-
];
|
|
440
|
-
|
|
441
|
-
send({
|
|
442
|
-
type: 'response.completed',
|
|
443
|
-
sequence_number: seq++,
|
|
444
|
-
response: {
|
|
445
|
-
id: responseId,
|
|
446
|
-
object: 'response',
|
|
447
|
-
created_at: createdAt,
|
|
448
|
-
status: 'completed',
|
|
449
|
-
output: outputItems,
|
|
450
|
-
},
|
|
451
|
-
});
|
|
437
|
+
sse({
|
|
438
|
+
type: 'response.content_part.added',
|
|
439
|
+
item_id: msgId,
|
|
440
|
+
output_index: OUTPUT_INDEX,
|
|
441
|
+
content_index: CONTENT_INDEX,
|
|
442
|
+
part: { type: 'output_text', text: '', annotations: [] },
|
|
443
|
+
});
|
|
452
444
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
res.end();
|
|
445
|
+
let out = '';
|
|
446
|
+
let reasoning = '';
|
|
456
447
|
|
|
457
|
-
|
|
458
|
-
}
|
|
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
|
|
459
451
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
buffer += chunkStr;
|
|
452
|
+
while (true) {
|
|
453
|
+
const { done, value } = await reader.read();
|
|
454
|
+
if (done) break;
|
|
464
455
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
// Keep the last line if it's incomplete (doesn't end with data pattern)
|
|
469
|
-
buffer = lines.pop() || '';
|
|
456
|
+
buffer += decoder.decode(value, { stream: true });
|
|
457
|
+
const events = buffer.split('\n\n');
|
|
458
|
+
buffer = events.pop() || '';
|
|
470
459
|
|
|
460
|
+
for (const evt of events) {
|
|
461
|
+
const lines = evt.split('\n');
|
|
471
462
|
for (const line of lines) {
|
|
472
|
-
if (!line.
|
|
473
|
-
// Skip empty lines and comments (starting with :)
|
|
474
|
-
if (line.trim() && !line.startsWith(':')) {
|
|
475
|
-
log('debug', 'Non-data line:', line.substring(0, 50));
|
|
476
|
-
}
|
|
477
|
-
continue;
|
|
478
|
-
}
|
|
479
|
-
|
|
463
|
+
if (!line.startsWith('data:')) continue;
|
|
480
464
|
const payload = line.slice(5).trim();
|
|
465
|
+
if (!payload) continue;
|
|
481
466
|
if (payload === '[DONE]') {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
return;
|
|
467
|
+
// termina upstream
|
|
468
|
+
continue;
|
|
485
469
|
}
|
|
486
470
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
let json;
|
|
471
|
+
let chunk;
|
|
490
472
|
try {
|
|
491
|
-
|
|
492
|
-
} catch
|
|
493
|
-
log('warn', 'Failed to parse SSE payload:', e.message, 'payload:', payload.substring(0, 100));
|
|
473
|
+
chunk = JSON.parse(payload);
|
|
474
|
+
} catch {
|
|
494
475
|
continue;
|
|
495
476
|
}
|
|
496
477
|
|
|
497
|
-
const
|
|
498
|
-
const delta = choice?.delta ?? {};
|
|
478
|
+
const delta = chunk.choices?.[0]?.delta || {};
|
|
499
479
|
|
|
500
|
-
//
|
|
501
|
-
if (
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
sequence_number: seq++,
|
|
506
|
-
item_id: messageItemId,
|
|
507
|
-
output_index: 0,
|
|
508
|
-
content_index: 0,
|
|
509
|
-
delta: delta.reasoning_content,
|
|
510
|
-
});
|
|
511
|
-
log('debug', `Reasoning delta: ${delta.reasoning_content.substring(0, 30)}...`);
|
|
512
|
-
}
|
|
480
|
+
// Handle tool_calls (only if ALLOW_TOOLS)
|
|
481
|
+
if (ALLOW_TOOLS && delta.tool_calls && Array.isArray(delta.tool_calls)) {
|
|
482
|
+
for (const tc of delta.tool_calls) {
|
|
483
|
+
const index = tc.index;
|
|
484
|
+
if (index == null) continue;
|
|
513
485
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
type: 'response.output_text.delta',
|
|
519
|
-
sequence_number: seq++,
|
|
520
|
-
item_id: messageItemId,
|
|
521
|
-
output_index: 0,
|
|
522
|
-
content_index: reasoningText ? 1 : 0,
|
|
523
|
-
delta: delta.content,
|
|
524
|
-
});
|
|
525
|
-
log('debug', `Output delta: ${delta.content.substring(0, 30)}...`);
|
|
526
|
-
}
|
|
486
|
+
if (!toolCallsMap.has(index)) {
|
|
487
|
+
// New tool call - send output_item.added
|
|
488
|
+
const callId = tc.id || `call_${randomUUID().replace(/-/g, '')}`;
|
|
489
|
+
const name = tc.function?.name || '';
|
|
527
490
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
for (const tc of delta.tool_calls) {
|
|
531
|
-
// tc: {id, type:"function", function:{name, arguments}}
|
|
532
|
-
const callId = tc.id || `call_${tc.index ?? 0}`;
|
|
533
|
-
const name = tc.function?.name || 'unknown';
|
|
534
|
-
const argsDelta = tc.function?.arguments || '';
|
|
535
|
-
|
|
536
|
-
let st = toolCalls.get(callId);
|
|
537
|
-
if (!st) {
|
|
538
|
-
st = {
|
|
539
|
-
itemId: `fc_${crypto.randomUUID().replace(/-/g, '')}`,
|
|
540
|
-
outputIndex: nextOutputIndex++,
|
|
491
|
+
toolCallsMap.set(index, {
|
|
492
|
+
callId,
|
|
541
493
|
name,
|
|
542
|
-
|
|
494
|
+
arguments: '',
|
|
495
|
+
partialArgs: ''
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
const fnItemInProgress = {
|
|
499
|
+
id: callId,
|
|
500
|
+
type: 'function_call',
|
|
501
|
+
status: 'in_progress',
|
|
502
|
+
call_id: callId,
|
|
503
|
+
name: name,
|
|
504
|
+
arguments: '',
|
|
543
505
|
};
|
|
544
|
-
toolCalls.set(callId, st);
|
|
545
506
|
|
|
546
|
-
|
|
507
|
+
sse({
|
|
547
508
|
type: 'response.output_item.added',
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
509
|
+
output_index: nextOutputIndex,
|
|
510
|
+
item: fnItemInProgress,
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
if (name) {
|
|
514
|
+
sse({
|
|
515
|
+
type: 'response.function_call_name.done',
|
|
516
|
+
item_id: callId,
|
|
517
|
+
output_index: nextOutputIndex,
|
|
518
|
+
name: name,
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const tcData = toolCallsMap.get(index);
|
|
524
|
+
|
|
525
|
+
// Handle name update if it comes later
|
|
526
|
+
if (tc.function?.name && !tcData.name) {
|
|
527
|
+
tcData.name = tc.function.name;
|
|
528
|
+
sse({
|
|
529
|
+
type: 'response.function_call_name.done',
|
|
530
|
+
item_id: tcData.callId,
|
|
531
|
+
output_index: OUTPUT_INDEX + index,
|
|
532
|
+
name: tcData.name,
|
|
557
533
|
});
|
|
558
|
-
log('debug', `Tool call added: ${name} (${callId})`);
|
|
559
534
|
}
|
|
560
535
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
536
|
+
// Handle arguments delta
|
|
537
|
+
if (tc.function?.arguments && typeof tc.function.arguments === 'string') {
|
|
538
|
+
tcData.partialArgs += tc.function.arguments;
|
|
539
|
+
|
|
540
|
+
sse({
|
|
564
541
|
type: 'response.function_call_arguments.delta',
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
542
|
+
item_id: tcData.callId,
|
|
543
|
+
output_index: OUTPUT_INDEX + index,
|
|
544
|
+
delta: tc.function.arguments,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Check if this tool call is done (finish_reason comes later in the choice)
|
|
549
|
+
const finishReason = chunk.choices?.[0]?.finish_reason;
|
|
550
|
+
if (finishReason === 'tool_calls' || (tc.function?.arguments && tc.function.arguments.length > 0 && chunk.choices?.[0]?.delta !== null)) {
|
|
551
|
+
tcData.arguments = tcData.partialArgs;
|
|
552
|
+
|
|
553
|
+
sse({
|
|
554
|
+
type: 'response.function_call_arguments.done',
|
|
555
|
+
item_id: tcData.callId,
|
|
556
|
+
output_index: OUTPUT_INDEX + index,
|
|
557
|
+
arguments: tcData.arguments,
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
const fnItemDone = {
|
|
561
|
+
id: tcData.callId,
|
|
562
|
+
type: 'function_call',
|
|
563
|
+
status: 'completed',
|
|
564
|
+
call_id: tcData.callId,
|
|
565
|
+
name: tcData.name,
|
|
566
|
+
arguments: tcData.arguments,
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
sse({
|
|
570
|
+
type: 'response.output_item.done',
|
|
571
|
+
output_index: OUTPUT_INDEX + index,
|
|
572
|
+
item: fnItemDone,
|
|
569
573
|
});
|
|
570
574
|
}
|
|
571
575
|
}
|
|
576
|
+
// Skip to next iteration after handling tool_calls
|
|
577
|
+
continue;
|
|
572
578
|
}
|
|
573
579
|
|
|
574
|
-
//
|
|
575
|
-
if (
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
580
|
+
// NON mescolare reasoning in output_text
|
|
581
|
+
if (typeof delta.reasoning_content === 'string' && delta.reasoning_content.length) {
|
|
582
|
+
reasoning += delta.reasoning_content;
|
|
583
|
+
sse({
|
|
584
|
+
type: 'response.reasoning_text.delta',
|
|
585
|
+
item_id: msgId,
|
|
586
|
+
output_index: OUTPUT_INDEX,
|
|
587
|
+
content_index: CONTENT_INDEX,
|
|
588
|
+
delta: delta.reasoning_content,
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (typeof delta.content === 'string' && delta.content.length) {
|
|
593
|
+
out += delta.content;
|
|
594
|
+
sse({
|
|
595
|
+
type: 'response.output_text.delta',
|
|
596
|
+
item_id: msgId,
|
|
597
|
+
output_index: OUTPUT_INDEX,
|
|
598
|
+
content_index: CONTENT_INDEX,
|
|
599
|
+
delta: delta.content,
|
|
600
|
+
});
|
|
579
601
|
}
|
|
580
602
|
}
|
|
581
603
|
}
|
|
582
|
-
} catch (e) {
|
|
583
|
-
log('error', 'Stream processing error:', e);
|
|
584
604
|
}
|
|
585
605
|
|
|
586
|
-
//
|
|
587
|
-
|
|
588
|
-
|
|
606
|
+
// done events
|
|
607
|
+
if (reasoning.length) {
|
|
608
|
+
sse({
|
|
609
|
+
type: 'response.reasoning_text.done',
|
|
610
|
+
item_id: msgId,
|
|
611
|
+
output_index: OUTPUT_INDEX,
|
|
612
|
+
content_index: CONTENT_INDEX,
|
|
613
|
+
text: reasoning,
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
sse({
|
|
618
|
+
type: 'response.output_text.done',
|
|
619
|
+
item_id: msgId,
|
|
620
|
+
output_index: OUTPUT_INDEX,
|
|
621
|
+
content_index: CONTENT_INDEX,
|
|
622
|
+
text: out,
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
sse({
|
|
626
|
+
type: 'response.content_part.done',
|
|
627
|
+
item_id: msgId,
|
|
628
|
+
output_index: OUTPUT_INDEX,
|
|
629
|
+
content_index: CONTENT_INDEX,
|
|
630
|
+
part: { type: 'output_text', text: out, annotations: [] },
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
const msgItemDone = {
|
|
634
|
+
id: msgId,
|
|
635
|
+
type: 'message',
|
|
636
|
+
status: 'completed',
|
|
637
|
+
role: 'assistant',
|
|
638
|
+
content: [{ type: 'output_text', text: out, annotations: [] }],
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
sse({
|
|
642
|
+
type: 'response.output_item.done',
|
|
643
|
+
output_index: OUTPUT_INDEX,
|
|
644
|
+
item: msgItemDone,
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
// Build final output array: message item + any function_call items
|
|
648
|
+
const finalOutput = [msgItemDone];
|
|
649
|
+
if (ALLOW_TOOLS && toolCallsMap.size > 0) {
|
|
650
|
+
for (const [index, tcData] of toolCallsMap.entries()) {
|
|
651
|
+
finalOutput.push({
|
|
652
|
+
id: tcData.callId,
|
|
653
|
+
type: 'function_call',
|
|
654
|
+
status: 'completed',
|
|
655
|
+
call_id: tcData.callId,
|
|
656
|
+
name: tcData.name,
|
|
657
|
+
arguments: tcData.arguments,
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const completed = buildResponseObject({
|
|
663
|
+
id: responseId,
|
|
664
|
+
model: responsesRequest?.model || DEFAULT_MODEL,
|
|
665
|
+
status: 'completed',
|
|
666
|
+
created_at: createdAt,
|
|
667
|
+
completed_at: nowSec(),
|
|
668
|
+
input: responsesRequest?.input || [],
|
|
669
|
+
output: finalOutput,
|
|
670
|
+
tools: responsesRequest?.tools || [],
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
sse({ type: 'response.completed', response: completed });
|
|
674
|
+
res.end();
|
|
675
|
+
|
|
676
|
+
log('info', `Stream completed - ${out.length} output, ${reasoning.length} reasoning, ${toolCallsMap.size} tool_calls`);
|
|
589
677
|
}
|
|
590
678
|
|
|
591
679
|
/**
|
|
@@ -667,17 +755,21 @@ async function handlePostRequest(req, res) {
|
|
|
667
755
|
|
|
668
756
|
// Handle streaming response
|
|
669
757
|
if (upstreamBody.stream) {
|
|
670
|
-
const
|
|
671
|
-
|
|
758
|
+
const ids = {
|
|
759
|
+
createdAt: nowSec(),
|
|
760
|
+
responseId: `resp_${randomUUID().replace(/-/g, '')}`,
|
|
761
|
+
msgId: `msg_${randomUUID().replace(/-/g, '')}`,
|
|
762
|
+
};
|
|
672
763
|
log('info', 'Starting streaming response');
|
|
673
764
|
res.writeHead(200, {
|
|
674
765
|
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
675
766
|
'Cache-Control': 'no-cache',
|
|
676
|
-
'Connection': 'keep-alive'
|
|
767
|
+
'Connection': 'keep-alive',
|
|
768
|
+
'X-Accel-Buffering': 'no',
|
|
677
769
|
});
|
|
678
770
|
|
|
679
771
|
try {
|
|
680
|
-
await streamChatToResponses(upstreamResponse.body, res,
|
|
772
|
+
await streamChatToResponses(upstreamResponse.body, res, request, ids);
|
|
681
773
|
log('info', 'Streaming completed');
|
|
682
774
|
} catch (e) {
|
|
683
775
|
log('error', 'Streaming error:', e);
|
|
@@ -685,12 +777,14 @@ async function handlePostRequest(req, res) {
|
|
|
685
777
|
} else {
|
|
686
778
|
// Non-streaming response
|
|
687
779
|
const chatResponse = await upstreamResponse.json();
|
|
688
|
-
const msg = chatResponse?.choices?.[0]?.message ?? {};
|
|
689
|
-
const outputText = msg.content ?? '';
|
|
690
|
-
const reasoningText = msg.reasoning_content ?? '';
|
|
691
|
-
const model = chatResponse?.model ?? upstreamBody.model ?? 'GLM';
|
|
692
780
|
|
|
693
|
-
const
|
|
781
|
+
const ids = {
|
|
782
|
+
createdAt: nowSec(),
|
|
783
|
+
responseId: `resp_${randomUUID().replace(/-/g, '')}`,
|
|
784
|
+
msgId: `msg_${randomUUID().replace(/-/g, '')}`,
|
|
785
|
+
};
|
|
786
|
+
|
|
787
|
+
const response = translateChatToResponses(chatResponse, request, ids);
|
|
694
788
|
|
|
695
789
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
696
790
|
res.end(JSON.stringify(response));
|