@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/src/server.js CHANGED
@@ -11,20 +11,62 @@
11
11
  */
12
12
 
13
13
  const http = require('http');
14
- const crypto = require('crypto');
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 - filter only Message items with role
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(outputText, reasoningText = '', responseId = null, messageItemId = null, model = 'unknown') {
201
- const rid = responseId || `resp_${crypto.randomUUID().replace(/-/g, '')}`;
202
- const mid = messageItemId || `msg_${crypto.randomUUID().replace(/-/g, '')}`;
203
- const createdAt = Math.floor(Date.now() / 1000);
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 response = {
212
- id: rid,
213
- object: 'response',
214
- created_at: createdAt,
215
- model,
281
+ const msgItem = {
282
+ id: msgId,
283
+ type: 'message',
216
284
  status: 'completed',
217
- output: [
218
- {
219
- type: 'message',
220
- id: mid,
221
- role: 'assistant',
222
- content
223
- }
224
- ]
285
+ role: 'assistant',
286
+ content,
225
287
  };
226
288
 
227
- log('debug', 'Translated Chat->Responses:', {
228
- id: response.id,
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
- return response;
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(stream, res, responseId, messageItemId) {
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
- let outputText = '';
309
- let reasoningText = '';
394
+ const createdAt = ids.createdAt;
395
+ const responseId = ids.responseId;
396
+ const msgId = ids.msgId;
310
397
 
311
- // tool call state: key = call_id (or id), value = { itemId, outputIndex, name, args }
312
- const toolCalls = new Map();
313
- let nextOutputIndex = 1; // 0 is the message item
398
+ let seq = 1;
399
+ const OUTPUT_INDEX = 0;
400
+ const CONTENT_INDEX = 0;
314
401
 
315
- const createdAt = Math.floor(Date.now() / 1000);
316
-
317
- function send(ev) {
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
- // 1) response.created
323
- send({
324
- type: 'response.created',
325
- sequence_number: seq++,
326
- response: {
327
- id: responseId,
328
- object: 'response',
329
- created_at: createdAt,
330
- status: 'in_progress',
331
- output: [],
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
- // 2) response.in_progress
336
- send({
337
- type: 'response.in_progress',
338
- sequence_number: seq++,
339
- response: {
340
- id: responseId,
341
- object: 'response',
342
- created_at: createdAt,
343
- status: 'in_progress',
344
- output: [],
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
- // 3) message item added (output_index=0)
349
- send({
431
+ sse({
350
432
  type: 'response.output_item.added',
351
- sequence_number: seq++,
352
- output_index: 0,
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
- async function finalizeAndClose() {
362
- // done events (if we received deltas)
363
- if (reasoningText) {
364
- send({
365
- type: 'response.reasoning_text.done',
366
- sequence_number: seq++,
367
- item_id: messageItemId,
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
- // SSE terminator
454
- res.write('data: [DONE]\n\n');
455
- res.end();
445
+ let out = '';
446
+ let reasoning = '';
456
447
 
457
- log('info', `Stream completed - ${outputText.length} output, ${reasoningText.length} reasoning, ${toolCalls.size} tools`);
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
- try {
461
- for await (const chunk of stream) {
462
- const chunkStr = Buffer.from(chunk).toString('utf8');
463
- buffer += chunkStr;
452
+ while (true) {
453
+ const { done, value } = await reader.read();
454
+ if (done) break;
464
455
 
465
- // Z.ai stream: SSE lines "data: {...}\n"
466
- // Split by newline and process each complete line
467
- const lines = buffer.split('\n');
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.trim() || !line.startsWith('data:')) {
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
- log('info', 'Stream received [DONE]');
483
- await finalizeAndClose();
484
- return;
467
+ // termina upstream
468
+ continue;
485
469
  }
486
470
 
487
- if (!payload) continue;
488
-
489
- let json;
471
+ let chunk;
490
472
  try {
491
- json = JSON.parse(payload);
492
- } catch (e) {
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 choice = json?.choices?.[0];
498
- const delta = choice?.delta ?? {};
478
+ const delta = chunk.choices?.[0]?.delta || {};
499
479
 
500
- // 1) reasoning
501
- if (typeof delta.reasoning_content === 'string' && delta.reasoning_content.length) {
502
- reasoningText += delta.reasoning_content;
503
- send({
504
- type: 'response.reasoning_text.delta',
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
- // 2) normal output
515
- if (typeof delta.content === 'string' && delta.content.length) {
516
- outputText += delta.content;
517
- send({
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
- // 3) tool calls (OpenAI-style in chat.completions delta.tool_calls)
529
- if (Array.isArray(delta.tool_calls)) {
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
- args: '',
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
- send({
507
+ sse({
547
508
  type: 'response.output_item.added',
548
- sequence_number: seq++,
549
- output_index: st.outputIndex,
550
- item: {
551
- type: 'function_call',
552
- id: st.itemId,
553
- call_id: callId,
554
- name: st.name,
555
- arguments: '',
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
- if (argsDelta) {
562
- st.args += argsDelta;
563
- send({
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
- sequence_number: seq++,
566
- item_id: st.itemId,
567
- output_index: st.outputIndex,
568
- delta: argsDelta,
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
- // 4) finish
575
- if (choice?.finish_reason) {
576
- log('info', `Stream finish_reason: ${choice.finish_reason}`);
577
- await finalizeAndClose();
578
- return;
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
- // fallback (stream finished without finish_reason)
587
- log('warn', 'Stream ended without finish_reason, finalizing anyway');
588
- await finalizeAndClose();
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 responseId = `resp_${crypto.randomUUID().replace(/-/g, '')}`;
671
- const messageItemId = `msg_${crypto.randomUUID().replace(/-/g, '')}`;
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, responseId, messageItemId);
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 response = translateChatToResponses(outputText, reasoningText, null, null, model);
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));