@galaxy-yearn/codex-deepseek-gateway 0.1.6 → 0.2.0

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/protocol.js CHANGED
@@ -1,4 +1,6 @@
1
- import { generateId, isObject, normalizeRole, toText } from './common.js';
1
+ import { createHash } from 'node:crypto';
2
+
3
+ import { generateId, isObject, normalizeRole, parseJsonObject, safeJsonParse, toText } from './common.js';
2
4
  import {
3
5
  DEFAULT_MODEL_ALIASES,
4
6
  deepseekReasoningPayload,
@@ -33,12 +35,19 @@ const TOOL_OUTPUT_TYPES = new Set([
33
35
  'image_generation_call_output',
34
36
  ]);
35
37
  const CHAT_HISTORY_IGNORED_TOOL_ITEM_TYPES = new Set([
36
- 'web_search_call',
37
38
  'web_search_call_output',
38
- 'tool_search_call',
39
- 'tool_search_output',
40
39
  ]);
41
40
  const EMULATED_HOSTED_TOOL_TYPES = new Set(['web_search', 'web_search_preview']);
41
+ const BRIDGED_RESPONSES_TOOL_TYPES = new Set(['tool_search']);
42
+ const UNSUPPORTED_HOSTED_TOOL_TYPES = new Set([
43
+ 'file_search',
44
+ 'code_interpreter',
45
+ 'image_generation',
46
+ 'computer',
47
+ 'computer_use',
48
+ 'mcp',
49
+ 'local_shell',
50
+ ]);
42
51
  const DEEPSEEK_TOOL_INSTRUCTIONS_MARKER = 'The tools in this request are real callable functions available now.';
43
52
  const DEEPSEEK_TOOL_INSTRUCTIONS = [
44
53
  DEEPSEEK_TOOL_INSTRUCTIONS_MARKER,
@@ -46,8 +55,30 @@ const DEEPSEEK_TOOL_INSTRUCTIONS = [
46
55
  'Do not write pseudo XML, DSML, or JSON tool calls in assistant text.',
47
56
  'Do not claim a listed function is unavailable because its name is unfamiliar.',
48
57
  ].join(' ');
49
- const DEEPSEEK_TOOL_DESCRIPTION_CHARS = 220;
50
- const DEEPSEEK_SCHEMA_DESCRIPTION_CHARS = 160;
58
+ export const INTERNAL_COMMENTARY_TOOL = 'commentary';
59
+ const DEEPSEEK_COMMENTARY_INSTRUCTIONS = 'The user cannot see your thinking; commentary is the only progress they see between tool batches. Include one commentary call, first, in every tool_calls batch, with one or two sentences on what you are doing. Never call it alone; never put the final answer in it — deliver the final answer as plain assistant text with no tool calls.';
60
+ const INTERNAL_COMMENTARY_TOOL_DEFINITION = {
61
+ type: 'function',
62
+ function: {
63
+ name: INTERNAL_COMMENTARY_TOOL,
64
+ description: "Post a short progress update the user sees immediately. The user cannot see your thinking; this is the only progress they see between tool batches. Include one commentary call, first, in every tool_calls batch, e.g. commentary(text: 'Config loader read; now checking how sessions persist.'). One or two sentences. Never call it alone; never put the final answer in it.",
65
+ parameters: {
66
+ type: 'object',
67
+ properties: {
68
+ text: {
69
+ type: 'string',
70
+ description: 'One or two short sentences on what you are doing or just found.',
71
+ },
72
+ },
73
+ required: ['text'],
74
+ additionalProperties: false,
75
+ },
76
+ },
77
+ };
78
+ const CHAT_FUNCTION_NAME_MAX_CHARS = 64;
79
+ const TOOL_NAME_HASH_CHARS = 8;
80
+ const DEEPSEEK_TOOL_DESCRIPTION_CHARS = 900;
81
+ const DEEPSEEK_SCHEMA_DESCRIPTION_CHARS = 480;
51
82
  const CODEX_REASONING_EFFORTS = ['low', 'medium', 'high', 'xhigh'];
52
83
  const CODEX_SPAWN_AGENT_TOOL_NAME = 'multi_agent_v1__spawn_agent';
53
84
  const CODEX_TOOL_SEARCH_TOOL_NAME = 'tool_search';
@@ -57,23 +88,27 @@ function jsonString(value) {
57
88
  return JSON.stringify(value ?? {});
58
89
  }
59
90
 
60
- function parseJsonObject(value) {
61
- if (isObject(value)) return value;
62
- if (typeof value !== 'string') return {};
63
- try {
64
- const parsed = JSON.parse(value);
65
- return isObject(parsed) ? parsed : {};
66
- } catch {
67
- return {};
68
- }
69
- }
70
-
71
- function sanitizeFunctionName(name, fallback = 'tool_call') {
72
- const candidate = String(name || fallback)
91
+ function sanitizeFunctionName(name, fallback = 'tool_call', maxChars = CHAT_FUNCTION_NAME_MAX_CHARS) {
92
+ const fallbackName = String(fallback || 'tool_call')
93
+ .replace(/[^a-zA-Z0-9_-]/g, '_')
94
+ .replace(/^_+/, '')
95
+ .slice(0, maxChars) || 'tool_call';
96
+ const candidate = String(name || fallbackName)
73
97
  .replace(/[^a-zA-Z0-9_-]/g, '_')
74
98
  .replace(/^_+/, '')
75
- .slice(0, 64);
76
- return candidate || fallback;
99
+ .slice(0, maxChars);
100
+ return candidate || fallbackName;
101
+ }
102
+
103
+ function stableToolNameHash(parts) {
104
+ const hashInput = parts.map((part) => String(part ?? '')).join('\0');
105
+ return createHash('sha256').update(hashInput).digest('hex').slice(0, TOOL_NAME_HASH_CHARS);
106
+ }
107
+
108
+ function appendToolNameHash(name, hash, fallback = 'tool_call') {
109
+ const suffix = `__${hash}`;
110
+ const headChars = Math.max(1, CHAT_FUNCTION_NAME_MAX_CHARS - suffix.length);
111
+ return `${sanitizeFunctionName(name, fallback, headChars)}${suffix}`;
77
112
  }
78
113
 
79
114
  function toolNamespace(tool) {
@@ -88,22 +123,42 @@ function toolBaseName(tool, fallback = 'tool_call') {
88
123
  }
89
124
 
90
125
  function encodeToolName(namespace, name, fallback = 'tool_call') {
91
- const baseName = sanitizeFunctionName(name, fallback);
92
- if (!namespace) return baseName;
93
- const encodedNamespace = sanitizeFunctionName(namespace, 'namespace');
94
- return sanitizeFunctionName(`${encodedNamespace}__${baseName}`, fallback);
126
+ const rawNamespace = String(namespace || '').trim();
127
+ const rawName = String(name || fallback);
128
+ const baseName = sanitizeFunctionName(rawName, fallback);
129
+ const encodedNamespace = rawNamespace ? sanitizeFunctionName(rawNamespace, 'namespace') : '';
130
+ const candidate = encodedNamespace ? `${encodedNamespace}__${baseName}` : baseName;
131
+ const encoded = sanitizeFunctionName(candidate, fallback);
132
+ const rawCandidate = rawNamespace ? `${rawNamespace}__${rawName}` : rawName;
133
+ if (encoded === rawCandidate && rawCandidate.length <= CHAT_FUNCTION_NAME_MAX_CHARS) {
134
+ return encoded;
135
+ }
136
+ return appendToolNameHash(candidate, stableToolNameHash([rawNamespace, rawName, fallback]), fallback);
95
137
  }
96
138
 
97
139
  function decodedToolName(name, toolNames) {
98
140
  const value = String(name || '');
99
141
  const known = toolNames?.get(value);
100
- return known ? { ...known } : { name: value };
142
+ if (!known) return { name: value };
143
+ return omitUndefined({
144
+ namespace: known.namespace,
145
+ name: known.original_name || known.name,
146
+ });
101
147
  }
102
148
 
103
149
  function isCodexToolSearchTool(name) {
104
150
  return String(name || '') === CODEX_TOOL_SEARCH_TOOL_NAME;
105
151
  }
106
152
 
153
+ function toolChoiceDisablesTools(toolChoice) {
154
+ if (String(toolChoice || '').toLowerCase() === 'none') return true;
155
+ return isObject(toolChoice) && String(toolChoice.type || '').toLowerCase() === 'none';
156
+ }
157
+
158
+ function hasChatToolCalls(message) {
159
+ return Array.isArray(message?.tool_calls) && message.tool_calls.length > 0;
160
+ }
161
+
107
162
  function omitUndefined(value) {
108
163
  if (!isObject(value)) return value;
109
164
  const result = {};
@@ -120,10 +175,18 @@ function compactText(value) {
120
175
  function shortenText(value, maxChars) {
121
176
  const text = compactText(value);
122
177
  if (!text || text.length <= maxChars) return text;
123
- const softLimit = Math.max(24, Math.floor(maxChars * 0.65));
124
- const slice = text.slice(0, maxChars - 3);
125
- const breakAt = Math.max(slice.lastIndexOf('. '), slice.lastIndexOf('; '), slice.lastIndexOf(', '), slice.lastIndexOf(' '));
126
- return `${slice.slice(0, breakAt >= softLimit ? breakAt : slice.length).trimEnd()}...`;
178
+ if (maxChars <= 16) return `${text.slice(0, Math.max(0, maxChars - 3)).trimEnd()}...`;
179
+ const marker = ' ... ';
180
+ const contentChars = maxChars - marker.length;
181
+ const headChars = Math.max(24, Math.floor(contentChars * 0.72));
182
+ const tailChars = Math.max(12, contentChars - headChars);
183
+ const headSlice = text.slice(0, headChars);
184
+ const headBreak = Math.max(headSlice.lastIndexOf('. '), headSlice.lastIndexOf('; '), headSlice.lastIndexOf(', '), headSlice.lastIndexOf(' '));
185
+ const head = headSlice.slice(0, headBreak >= Math.floor(headChars * 0.55) ? headBreak : headSlice.length).trimEnd();
186
+ const tailSlice = text.slice(-tailChars);
187
+ const tailBreak = Math.max(tailSlice.indexOf('. '), tailSlice.indexOf('; '), tailSlice.indexOf(', '), tailSlice.indexOf(' '));
188
+ const tail = tailSlice.slice(tailBreak >= 0 && tailBreak <= Math.floor(tailChars * 0.45) ? tailBreak + 1 : 0).trimStart();
189
+ return `${head}${marker}${tail}`.slice(0, maxChars);
127
190
  }
128
191
 
129
192
  function textPart(text) {
@@ -246,9 +309,29 @@ function chatToolCallFromResponseItem(item) {
246
309
  };
247
310
  }
248
311
 
312
+ const GATEWAY_ENCRYPTED_REASONING_PREFIX = 'dsgw1:';
313
+
314
+ function encodeGatewayReasoning(text) {
315
+ const value = String(text ?? '');
316
+ if (!value) return null;
317
+ return `${GATEWAY_ENCRYPTED_REASONING_PREFIX}${Buffer.from(value, 'utf8').toString('base64')}`;
318
+ }
319
+
320
+ function decodeGatewayReasoning(encryptedContent) {
321
+ if (typeof encryptedContent !== 'string') return '';
322
+ if (!encryptedContent.startsWith(GATEWAY_ENCRYPTED_REASONING_PREFIX)) return '';
323
+ try {
324
+ return Buffer.from(encryptedContent.slice(GATEWAY_ENCRYPTED_REASONING_PREFIX.length), 'base64').toString('utf8');
325
+ } catch {
326
+ return '';
327
+ }
328
+ }
329
+
249
330
  function reasoningTextFromItem(item) {
250
331
  if (!isObject(item)) return '';
251
332
  if (typeof item.reasoning_content === 'string') return normalizeReasoningContent(item.reasoning_content);
333
+ const decoded = decodeGatewayReasoning(item.encrypted_content);
334
+ if (decoded) return normalizeReasoningContent(decoded);
252
335
  const rawParts = [];
253
336
  if (typeof item.text === 'string') rawParts.push(item.text);
254
337
  if (typeof item.content === 'string') rawParts.push(item.content);
@@ -281,7 +364,7 @@ function normalizeReasoningDisplayText(text) {
281
364
  .replace(/^[ \t]{0,3}(?:`{3,}|~{3,}).*$/, '')
282
365
  .replace(/^[ \t]{0,3}#{1,6}[ \t]+/, '')
283
366
  .replace(/^[ \t]{0,3}>[ \t]?/, '')
284
- .replace(/^[ \t]*[-*+][ \t]+(?:\[[ xX]\][ \t]+)?/, '\u2022 ')
367
+ .replace(/^[ \t]*[-*+][ \t]+(?:\[[ xX]\][ \t]+)?/, ' ')
285
368
  .replace(/^[ \t]*(\d+)\.[ \t]+/, '$1) ')
286
369
  .replace(/!\[([^\]]*)\]\([^)]+\)/g, '$1')
287
370
  .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
@@ -299,7 +382,36 @@ function reasoningDisplayText(text) {
299
382
  return normalizeReasoningDisplayText(text);
300
383
  }
301
384
 
385
+ function compactToolSearchOutputTool(tool) {
386
+ if (!isObject(tool)) return null;
387
+ const namespace = toolNamespace(tool);
388
+ const name = toolBaseName(tool, '');
389
+ if (!name) return null;
390
+ const schema = normalizeJsonSchemaObject(tool.function?.parameters ?? tool.parameters ?? tool.input_schema);
391
+ const properties = Object.keys(isObject(schema.properties) ? schema.properties : {});
392
+ const required = Array.isArray(schema.required) ? schema.required.map(String) : [];
393
+ const description = tool.function?.description ?? tool.description ?? '';
394
+ return omitUndefined({
395
+ name: encodeToolName(namespace, name),
396
+ description: description ? shortenText(description, 220) : undefined,
397
+ required: required.length ? required : undefined,
398
+ properties: properties.length ? properties : undefined,
399
+ });
400
+ }
401
+
302
402
  function toolOutputContent(item) {
403
+ if (item?.type === 'tool_search_output' && Array.isArray(item.tools)) {
404
+ const tools = expandTools(item.tools)
405
+ .map(compactToolSearchOutputTool)
406
+ .filter(Boolean);
407
+ return JSON.stringify(omitUndefined({
408
+ status: item.status || 'completed',
409
+ note: tools.length
410
+ ? 'Discovered tools are loaded into the current tool list and can be called directly by these names.'
411
+ : undefined,
412
+ discovered_tools: tools,
413
+ }));
414
+ }
303
415
  const output = item.output ?? item.content ?? item.result ?? item.error ?? '';
304
416
  if (typeof output === 'string') return output;
305
417
  if (Array.isArray(output)) return contentToChatContent(output);
@@ -330,46 +442,127 @@ function normalizeMessage(message) {
330
442
  return normalized;
331
443
  }
332
444
 
445
+ function assistantMessageIsEmpty(message) {
446
+ if (hasChatToolCalls(message)) return false;
447
+ const content = message?.content;
448
+ if (typeof content === 'string') return content.length === 0;
449
+ if (Array.isArray(content)) return content.length === 0;
450
+ return content == null;
451
+ }
452
+
453
+ function webSearchCallNote(item) {
454
+ const action = isObject(item?.action) ? item.action : {};
455
+ const actionType = action.type || 'search';
456
+ if (actionType === 'open_page') {
457
+ return action.url ? `Opened page ${action.url}.` : 'Opened a web page.';
458
+ }
459
+ if (actionType === 'find_in_page') {
460
+ const target = action.url || 'a web page';
461
+ return action.query ? `Searched within ${target} for "${action.query}".` : `Searched within ${target}.`;
462
+ }
463
+ const query = action.query ? `"${action.query}"` : 'the web';
464
+ const sources = (Array.isArray(action.sources) ? action.sources : [])
465
+ .filter(isObject)
466
+ .slice(0, 3)
467
+ .map((source) => (source.title && source.url ? `${source.title} <${source.url}>` : source.url || source.title))
468
+ .filter(Boolean)
469
+ .join('; ');
470
+ return `Searched the web for ${query}${sources ? ` (sources: ${sources})` : ''}.`;
471
+ }
472
+
333
473
  function extractMessagesFromResponsesInput(input) {
334
474
  if (Array.isArray(input)) {
335
475
  const messages = [];
336
476
  let pendingUserContent = [];
337
477
  let pendingReasoningContent = '';
338
478
  let pendingAssistantToolMessage = null;
479
+ let pendingAssistantMessage = null;
480
+ let pendingWebSearchNotes = [];
339
481
  const flushPendingUserContent = () => {
340
482
  if (!pendingUserContent.length) return;
341
483
  messages.push({ role: 'user', content: contentToChatContent(pendingUserContent) });
342
484
  pendingUserContent = [];
343
485
  };
486
+ const flushPendingWebSearchNotes = () => {
487
+ if (!pendingWebSearchNotes.length) return;
488
+ messages.push({
489
+ role: 'assistant',
490
+ content: `[Earlier web activity] ${pendingWebSearchNotes.join(' ')}`,
491
+ });
492
+ pendingWebSearchNotes = [];
493
+ };
344
494
  const flushPendingAssistantToolMessage = () => {
345
495
  if (!pendingAssistantToolMessage) return;
346
496
  messages.push(pendingAssistantToolMessage);
347
497
  pendingAssistantToolMessage = null;
348
498
  pendingReasoningContent = '';
349
499
  };
500
+ const flushPendingAssistantMessage = () => {
501
+ if (!pendingAssistantMessage) return;
502
+ const message = pendingAssistantMessage;
503
+ pendingAssistantMessage = null;
504
+ if (assistantMessageIsEmpty(message)) {
505
+ if (message.reasoning_content && !pendingReasoningContent) {
506
+ pendingReasoningContent = message.reasoning_content;
507
+ }
508
+ return;
509
+ }
510
+ messages.push(message);
511
+ };
512
+ const flushAssistantState = () => {
513
+ flushPendingAssistantToolMessage();
514
+ flushPendingAssistantMessage();
515
+ };
350
516
  const ensurePendingAssistantToolMessage = () => {
351
517
  if (!pendingAssistantToolMessage) {
352
- pendingAssistantToolMessage = {
353
- role: 'assistant',
354
- content: '',
355
- tool_calls: [],
356
- };
357
- if (pendingReasoningContent) {
518
+ if (pendingAssistantMessage) {
519
+ pendingAssistantToolMessage = pendingAssistantMessage;
520
+ pendingAssistantMessage = null;
521
+ if (!Array.isArray(pendingAssistantToolMessage.tool_calls)) {
522
+ pendingAssistantToolMessage.tool_calls = [];
523
+ }
524
+ } else {
525
+ pendingAssistantToolMessage = {
526
+ role: 'assistant',
527
+ content: '',
528
+ tool_calls: [],
529
+ };
530
+ }
531
+ if (!pendingAssistantToolMessage.reasoning_content && pendingReasoningContent) {
358
532
  pendingAssistantToolMessage.reasoning_content = pendingReasoningContent;
359
533
  }
360
534
  }
361
535
  return pendingAssistantToolMessage;
362
536
  };
537
+ const pushRoleMessage = (item) => {
538
+ flushPendingUserContent();
539
+ flushPendingAssistantToolMessage();
540
+ flushPendingWebSearchNotes();
541
+ flushPendingAssistantMessage();
542
+ const normalized = normalizeMessage(item);
543
+ if (!normalized) return;
544
+ if (normalized.role === 'assistant' && !normalized.reasoning_content && pendingReasoningContent) {
545
+ normalized.reasoning_content = pendingReasoningContent;
546
+ pendingReasoningContent = '';
547
+ }
548
+ if (normalized.role === 'assistant' && !hasChatToolCalls(normalized)) {
549
+ pendingAssistantMessage = normalized;
550
+ return;
551
+ }
552
+ messages.push(normalized);
553
+ };
363
554
 
364
555
  for (const item of input) {
365
556
  if (typeof item === 'string') {
366
- flushPendingAssistantToolMessage();
557
+ flushAssistantState();
558
+ flushPendingWebSearchNotes();
367
559
  pendingUserContent.push({ type: 'input_text', text: item });
368
560
  continue;
369
561
  }
370
562
  if (!isObject(item)) continue;
371
563
  if (isInputContentPart(item)) {
372
- flushPendingAssistantToolMessage();
564
+ flushAssistantState();
565
+ flushPendingWebSearchNotes();
373
566
  pendingUserContent.push(item);
374
567
  continue;
375
568
  }
@@ -378,45 +571,40 @@ function extractMessagesFromResponsesInput(input) {
378
571
  continue;
379
572
  }
380
573
  if (item.type === 'message' && item.role) {
574
+ pushRoleMessage(item);
575
+ continue;
576
+ }
577
+ if (item.type === 'web_search_call') {
381
578
  flushPendingUserContent();
382
- flushPendingAssistantToolMessage();
383
- const normalized = normalizeMessage(item);
384
- if (normalized?.role === 'assistant' && !normalized.reasoning_content && pendingReasoningContent) {
385
- normalized.reasoning_content = pendingReasoningContent;
386
- pendingReasoningContent = '';
387
- }
388
- if (normalized) messages.push(normalized);
579
+ flushAssistantState();
580
+ pendingWebSearchNotes.push(webSearchCallNote(item));
389
581
  continue;
390
582
  }
391
583
  if (CHAT_HISTORY_IGNORED_TOOL_ITEM_TYPES.has(item.type)) {
392
584
  flushPendingUserContent();
393
- flushPendingAssistantToolMessage();
585
+ flushAssistantState();
394
586
  continue;
395
587
  }
396
588
  if (TOOL_OUTPUT_TYPES.has(item.type) || String(item.type || '').endsWith('_call_output')) {
397
589
  flushPendingUserContent();
398
- flushPendingAssistantToolMessage();
590
+ flushPendingWebSearchNotes();
591
+ flushAssistantState();
399
592
  messages.push(responseToolOutputToMessage(item));
400
593
  continue;
401
594
  }
402
595
  if (TOOL_CALL_TYPES.has(item.type) || String(item.type || '').endsWith('_call')) {
403
596
  flushPendingUserContent();
597
+ flushPendingWebSearchNotes();
404
598
  ensurePendingAssistantToolMessage().tool_calls.push(chatToolCallFromResponseItem(item));
405
599
  continue;
406
600
  }
407
601
  if (item.role) {
408
- flushPendingUserContent();
409
- flushPendingAssistantToolMessage();
410
- const normalized = normalizeMessage(item);
411
- if (normalized?.role === 'assistant' && !normalized.reasoning_content && pendingReasoningContent) {
412
- normalized.reasoning_content = pendingReasoningContent;
413
- pendingReasoningContent = '';
414
- }
415
- if (normalized) messages.push(normalized);
602
+ pushRoleMessage(item);
416
603
  }
417
604
  }
418
605
  flushPendingUserContent();
419
- flushPendingAssistantToolMessage();
606
+ flushAssistantState();
607
+ flushPendingWebSearchNotes();
420
608
  return messages;
421
609
  }
422
610
 
@@ -442,14 +630,147 @@ function extractMessagesFromResponsesInput(input) {
442
630
  return [];
443
631
  }
444
632
 
633
+ const CUSTOM_TOOL_INPUT_HINT = 'Pass the complete raw tool input as the "input" string argument. Do not wrap it in extra JSON, quotes, or markdown fences.';
634
+ const CUSTOM_TOOL_GRAMMAR_CHARS = 1600;
635
+ const CUSTOM_TOOL_DESCRIPTION_CHARS = 3600;
636
+
637
+ function truncateRawText(value, maxChars) {
638
+ const text = String(value ?? '');
639
+ if (text.length <= maxChars) return text;
640
+ return `${text.slice(0, Math.max(0, maxChars - 4))}\n...`;
641
+ }
642
+
643
+ function customToolShimParameters() {
644
+ return {
645
+ type: 'object',
646
+ properties: {
647
+ input: {
648
+ type: 'string',
649
+ description: 'Complete raw input text for this tool.',
650
+ },
651
+ },
652
+ required: ['input'],
653
+ additionalProperties: false,
654
+ };
655
+ }
656
+
657
+ function customToolShim(tool) {
658
+ const format = isObject(tool.format) ? tool.format : {};
659
+ const baseName = toolBaseName(tool, 'custom_tool');
660
+ const chunks = [
661
+ compactText(tool.description) || `Freeform tool ${baseName}.`,
662
+ CUSTOM_TOOL_INPUT_HINT,
663
+ ];
664
+ if (format.syntax) chunks.push(`Input syntax: ${format.syntax}.`);
665
+ if (typeof format.definition === 'string' && format.definition.trim()) {
666
+ chunks.push(`Input grammar:\n${truncateRawText(format.definition.trim(), CUSTOM_TOOL_GRAMMAR_CHARS)}`);
667
+ }
668
+ const parameters = customToolShimParameters();
669
+ const sourceSchema = isObject(tool.input_schema) ? tool.input_schema : isObject(tool.parameters) ? tool.parameters : null;
670
+ const sourceInputDescription = sourceSchema?.properties?.input?.description;
671
+ if (typeof sourceInputDescription === 'string' && sourceInputDescription) {
672
+ parameters.properties.input.description = sourceInputDescription;
673
+ }
674
+ return {
675
+ type: 'function',
676
+ gateway_custom_tool: true,
677
+ function: {
678
+ name: encodeToolName(toolNamespace(tool), baseName),
679
+ description: truncateRawText(chunks.join('\n'), CUSTOM_TOOL_DESCRIPTION_CHARS),
680
+ parameters,
681
+ },
682
+ };
683
+ }
684
+
685
+ function buildCustomToolNames(tools) {
686
+ const names = new Set();
687
+ for (const tool of expandTools(tools)) {
688
+ if (!isObject(tool) || tool.type !== 'custom') continue;
689
+ const normalizedTool = normalizeTool(tool);
690
+ const fn = normalizedTool?.type === 'function' ? normalizedTool.function : null;
691
+ if (fn?.name) names.add(fn.name);
692
+ }
693
+ return names;
694
+ }
695
+
696
+ function customToolInputFromArguments(argumentsText) {
697
+ if (typeof argumentsText !== 'string' || !argumentsText) return '';
698
+ const parsed = safeJsonParse(argumentsText);
699
+ if (parsed.ok && typeof parsed.value === 'string') return parsed.value;
700
+ if (parsed.ok && isObject(parsed.value)) {
701
+ const input = parsed.value.input;
702
+ if (typeof input === 'string') return input;
703
+ if (input !== undefined) return jsonString(input);
704
+ return argumentsText;
705
+ }
706
+ return argumentsText;
707
+ }
708
+
709
+ function stripGatewayToolMarkers(tools) {
710
+ if (!Array.isArray(tools)) return tools;
711
+ return tools.map((tool) => {
712
+ if (!isObject(tool) || tool.gateway_custom_tool === undefined) return tool;
713
+ const { gateway_custom_tool, ...rest } = tool;
714
+ return rest;
715
+ });
716
+ }
717
+
718
+ const UNAVAILABLE_TOOL_GUIDANCE = 'Do not call this tool; if the task depends on it, tell the user it is unavailable.';
719
+
720
+ function unavailableHostedToolShim(tool, reason) {
721
+ const capability = String(tool.type || 'tool');
722
+ const detail = reason || `Unavailable capability: the client requested the hosted ${capability} tool, but this gateway cannot execute it.`;
723
+ return {
724
+ type: 'function',
725
+ function: {
726
+ name: encodeToolName(toolNamespace(tool), toolBaseName(tool, capability)),
727
+ description: `${detail} ${UNAVAILABLE_TOOL_GUIDANCE}`,
728
+ parameters: { type: 'object', properties: {}, additionalProperties: false },
729
+ },
730
+ };
731
+ }
732
+
733
+ export function unavailableWebSearchToolShims(tools) {
734
+ const shims = [];
735
+ const seen = new Set();
736
+ for (const tool of expandTools(tools)) {
737
+ if (!isObject(tool) || typeof tool.type !== 'string' || !EMULATED_HOSTED_TOOL_TYPES.has(tool.type)) continue;
738
+ const shim = unavailableHostedToolShim(
739
+ tool,
740
+ 'Unavailable capability: web search was requested, but the gateway has no search provider configured.',
741
+ );
742
+ if (seen.has(shim.function.name)) continue;
743
+ seen.add(shim.function.name);
744
+ shims.push(shim);
745
+ }
746
+ return shims;
747
+ }
748
+
445
749
  function normalizeTool(tool) {
446
750
  if (!isObject(tool)) return tool;
447
751
  if (tool.type === 'namespace') return null;
448
752
  if (typeof tool.type === 'string' && EMULATED_HOSTED_TOOL_TYPES.has(tool.type)) {
449
753
  return null;
450
754
  }
755
+ if (typeof tool.type === 'string' && UNSUPPORTED_HOSTED_TOOL_TYPES.has(tool.type)) {
756
+ return unavailableHostedToolShim(tool);
757
+ }
758
+ if (tool.type === 'custom') {
759
+ return customToolShim(tool);
760
+ }
761
+ if (typeof tool.type === 'string' && BRIDGED_RESPONSES_TOOL_TYPES.has(tool.type)) {
762
+ return {
763
+ type: 'function',
764
+ function: omitUndefined({
765
+ name: encodeToolName(toolNamespace(tool), tool.type),
766
+ description: tool.description,
767
+ parameters: normalizeJsonSchemaObject(tool.parameters || tool.input_schema),
768
+ strict: tool.strict,
769
+ }),
770
+ };
771
+ }
451
772
  if (tool.type === 'function' && isObject(tool.function)) {
452
- const { namespace, ...fn } = tool.function;
773
+ const { namespace, defer_loading, ...fn } = tool.function;
453
774
  return {
454
775
  type: 'function',
455
776
  function: omitUndefined({
@@ -461,7 +782,7 @@ function normalizeTool(tool) {
461
782
  }),
462
783
  };
463
784
  }
464
- if (tool.type === 'function' || tool.type === 'custom' || tool.name || tool.parameters || tool.input_schema) {
785
+ if (tool.type === 'function') {
465
786
  return {
466
787
  type: 'function',
467
788
  function: omitUndefined({
@@ -472,6 +793,17 @@ function normalizeTool(tool) {
472
793
  }),
473
794
  };
474
795
  }
796
+ if (!tool.type && (tool.name || tool.parameters || tool.input_schema)) {
797
+ return {
798
+ type: 'function',
799
+ function: omitUndefined({
800
+ name: encodeToolName(toolNamespace(tool), toolBaseName(tool, 'tool_call')),
801
+ description: tool.description,
802
+ parameters: normalizeJsonSchemaObject(tool.parameters || tool.input_schema),
803
+ strict: tool.strict,
804
+ }),
805
+ };
806
+ }
475
807
  return null;
476
808
  }
477
809
 
@@ -518,8 +850,19 @@ function normalizeTools(tools) {
518
850
  return normalized.length ? normalized : undefined;
519
851
  }
520
852
 
853
+ function hasRunnableChatTools(normalized) {
854
+ return Array.isArray(applyAllowedTools(normalizeTools(normalized?.tools), normalized?.tool_choice));
855
+ }
856
+
857
+ function toolSearchDiscoveryKey(tool) {
858
+ if (tool.type === 'namespace') {
859
+ return `namespace:${tool.name || tool.namespace || tool.server_label || ''}`;
860
+ }
861
+ return `${tool.type || 'function'}:${toolNamespace(tool)}:${toolBaseName(tool, '')}`;
862
+ }
863
+
521
864
  function collectToolSearchOutputTools(input) {
522
- const discovered = [];
865
+ const discovered = new Map();
523
866
  const scan = (value) => {
524
867
  if (Array.isArray(value)) {
525
868
  for (const item of value) scan(item);
@@ -527,14 +870,16 @@ function collectToolSearchOutputTools(input) {
527
870
  }
528
871
  if (!isObject(value)) return;
529
872
  if (value.type === 'tool_search_output' && Array.isArray(value.tools)) {
530
- discovered.push(...value.tools.filter(isObject));
873
+ for (const tool of value.tools) {
874
+ if (isObject(tool)) discovered.set(toolSearchDiscoveryKey(tool), tool);
875
+ }
531
876
  return;
532
877
  }
533
878
  if (Array.isArray(value.input)) scan(value.input);
534
879
  if (Array.isArray(value.content)) scan(value.content);
535
880
  };
536
881
  scan(input);
537
- return discovered;
882
+ return [...discovered.values()];
538
883
  }
539
884
 
540
885
  function mergeToolsWithToolSearchOutput(requestTools, input) {
@@ -572,6 +917,7 @@ function normalizeJsonSchemaObject(schema) {
572
917
  }
573
918
 
574
919
  function normalizeToolChoice(toolChoice) {
920
+ if (toolChoiceDisablesTools(toolChoice)) return 'none';
575
921
  if (!isObject(toolChoice)) return toolChoice;
576
922
  if (toolChoice.type === 'function' && toolChoice.name) {
577
923
  return {
@@ -639,6 +985,7 @@ function allowedToolNames(toolChoice) {
639
985
  }
640
986
 
641
987
  function applyAllowedTools(tools, toolChoice) {
988
+ if (toolChoiceDisablesTools(toolChoice)) return undefined;
642
989
  const allowed = allowedToolNames(toolChoice);
643
990
  if (!allowed || !Array.isArray(tools)) return tools;
644
991
  const filtered = tools.filter((tool) => {
@@ -710,7 +1057,7 @@ function convertItemToToolSearchCall(item) {
710
1057
  return item;
711
1058
  }
712
1059
 
713
- function responseItemFromChatToolCall(toolCall, toolNames) {
1060
+ function responseItemFromChatToolCall(toolCall, toolNames, customToolNames) {
714
1061
  const callId = toolCall.id || generateId('call');
715
1062
  if (isCodexToolSearchTool(toolCall.function?.name)) {
716
1063
  return {
@@ -723,6 +1070,16 @@ function responseItemFromChatToolCall(toolCall, toolNames) {
723
1070
  };
724
1071
  }
725
1072
  const decoded = decodedToolName(toolCall.function?.name, toolNames);
1073
+ if (customToolNames?.has(toolCall.function?.name)) {
1074
+ return omitUndefined({
1075
+ type: 'custom_tool_call',
1076
+ id: callId,
1077
+ call_id: callId,
1078
+ name: decoded.name,
1079
+ input: customToolInputFromArguments(toolCall.function?.arguments || ''),
1080
+ status: 'completed',
1081
+ });
1082
+ }
726
1083
  return omitUndefined({
727
1084
  type: 'function_call',
728
1085
  id: callId,
@@ -749,14 +1106,17 @@ function buildToolNames(tools) {
749
1106
  const names = new Map();
750
1107
  for (const tool of expandTools(tools)) {
751
1108
  const namespace = toolNamespace(tool);
752
- if (!namespace) continue;
753
1109
  const normalizedTool = normalizeTool(tool);
754
1110
  const fn = normalizedTool?.type === 'function' ? normalizedTool.function : null;
755
1111
  if (!fn?.name) continue;
756
- names.set(fn.name, {
757
- namespace: sanitizeFunctionName(namespace, 'namespace'),
758
- name: sanitizeFunctionName(toolBaseName(tool, tool.type)),
759
- });
1112
+ const baseName = String(toolBaseName(tool, tool.type));
1113
+ const sanitizedBaseName = sanitizeFunctionName(baseName);
1114
+ if (!namespace && fn.name === baseName) continue;
1115
+ names.set(fn.name, omitUndefined({
1116
+ namespace: namespace || undefined,
1117
+ name: sanitizedBaseName,
1118
+ original_name: baseName,
1119
+ }));
760
1120
  }
761
1121
  return names;
762
1122
  }
@@ -850,11 +1210,12 @@ function deepSeekSpawnAgentDescription() {
850
1210
  }
851
1211
 
852
1212
  function isCodexSpawnAgentTool(name) {
853
- return name === CODEX_SPAWN_AGENT_TOOL_NAME || name === 'spawn_agent';
1213
+ return name === CODEX_SPAWN_AGENT_TOOL_NAME;
854
1214
  }
855
1215
 
856
1216
  function simplifyToolForDeepSeek(tool, config = {}) {
857
1217
  if (tool?.type !== 'function' || !isObject(tool.function)) return tool;
1218
+ if (tool.gateway_custom_tool) return tool;
858
1219
  const fn = tool.function;
859
1220
  let parameters = simplifyJsonSchemaDescriptions(fn.parameters);
860
1221
  if (isCodexSpawnAgentTool(fn.name)) {
@@ -882,21 +1243,36 @@ function hasDeepSeekToolInstructions(messages) {
882
1243
 
883
1244
  function addDeepSeekToolInstructions(messages, tools) {
884
1245
  if (!Array.isArray(tools) || !tools.length || hasDeepSeekToolInstructions(messages)) return messages;
1246
+ const instructions = requestToolsIncludeCommentary(tools)
1247
+ ? `${DEEPSEEK_TOOL_INSTRUCTIONS} ${DEEPSEEK_COMMENTARY_INSTRUCTIONS}`
1248
+ : DEEPSEEK_TOOL_INSTRUCTIONS;
885
1249
  if (!messages.length || messages[0]?.role !== 'system') {
886
- return [{ role: 'system', content: DEEPSEEK_TOOL_INSTRUCTIONS }, ...messages];
1250
+ return [{ role: 'system', content: instructions }, ...messages];
887
1251
  }
888
1252
  return [
889
1253
  {
890
1254
  ...messages[0],
891
- content: `${toText(messages[0].content)}\n\n${DEEPSEEK_TOOL_INSTRUCTIONS}`,
1255
+ content: `${toText(messages[0].content)}\n\n${instructions}`,
892
1256
  },
893
1257
  ...messages.slice(1),
894
1258
  ];
895
1259
  }
896
1260
 
1261
+ function isDeepSeekBetaBaseUrl(baseUrl) {
1262
+ return /\/beta\/?$/.test(String(baseUrl || ''));
1263
+ }
1264
+
897
1265
  function adaptToolsForProvider(tools, provider, config = {}) {
898
1266
  if (provider !== 'deepseek' || !Array.isArray(tools)) return tools;
899
- return tools.map((tool) => simplifyToolForDeepSeek(tool, config));
1267
+ const keepStrict = isDeepSeekBetaBaseUrl(config.upstreamBaseUrl);
1268
+ return tools.map((tool) => {
1269
+ const simplified = simplifyToolForDeepSeek(tool, config);
1270
+ if (!keepStrict && simplified?.type === 'function' && isObject(simplified.function) && simplified.function.strict !== undefined) {
1271
+ const { strict, ...fn } = simplified.function;
1272
+ return { ...simplified, function: fn };
1273
+ }
1274
+ return simplified;
1275
+ });
900
1276
  }
901
1277
 
902
1278
  function normalizeToolCallArguments(name, argumentsText, toolSchema) {
@@ -965,6 +1341,16 @@ function normalizeFunctionCallItemArguments(item, toolSchemas, config = {}) {
965
1341
  item.arguments = normalizeCodexSpawnAgentArguments(encodedName, item.arguments, config);
966
1342
  }
967
1343
 
1344
+ function functionCallItemNeedsArgumentNormalization(item, toolSchemas, config = {}) {
1345
+ if (!item || item.type !== 'function_call') return false;
1346
+ const encodedName = encodeToolName(item.namespace, item.name);
1347
+ return Boolean(toolSchemas?.has(encodedName) || isCodexSpawnAgentTool(encodedName));
1348
+ }
1349
+
1350
+ function hasToolCallFunctionName(toolCall) {
1351
+ return typeof toolCall?.function?.name === 'string' && toolCall.function.name.length > 0;
1352
+ }
1353
+
968
1354
  function sanitizeToolCallsForChatCompletion(toolCalls) {
969
1355
  return toolCalls.map((toolCall) => {
970
1356
  if (!isObject(toolCall)) return toolCall;
@@ -983,11 +1369,51 @@ function sanitizeToolCallsForChatCompletion(toolCalls) {
983
1369
  });
984
1370
  }
985
1371
 
1372
+ function multimodalPlaceholder(kind, hint) {
1373
+ const cleanHint = compactText(hint);
1374
+ const shortHint = cleanHint && !cleanHint.startsWith('data:') ? shortenText(cleanHint, 120) : '';
1375
+ return `[${kind} omitted: DeepSeek accepts text input only${shortHint ? `; source: ${shortHint}` : ''}]`;
1376
+ }
1377
+
1378
+ function deepseekTextOnlyContent(content) {
1379
+ if (content == null) return '';
1380
+ if (typeof content === 'string') return content;
1381
+ const parts = Array.isArray(content) ? content : [content];
1382
+ const chunks = [];
1383
+ for (const part of parts) {
1384
+ if (typeof part === 'string') {
1385
+ chunks.push(part);
1386
+ continue;
1387
+ }
1388
+ if (!isObject(part)) continue;
1389
+ if (part.type === 'text') {
1390
+ chunks.push(String(part.text ?? ''));
1391
+ continue;
1392
+ }
1393
+ if (part.type === 'image_url') {
1394
+ chunks.push(multimodalPlaceholder('image', part.image_url?.url || part.image_url?.file_id));
1395
+ continue;
1396
+ }
1397
+ if (part.type === 'file') {
1398
+ chunks.push(multimodalPlaceholder('file', part.file?.filename || part.file?.file_url || part.file?.file_id));
1399
+ continue;
1400
+ }
1401
+ if (part.type === 'input_audio') {
1402
+ chunks.push(multimodalPlaceholder('audio', part.input_audio?.file_id));
1403
+ continue;
1404
+ }
1405
+ const text = toText(part);
1406
+ if (text) chunks.push(text);
1407
+ }
1408
+ return chunks.filter((chunk) => chunk !== '').join('\n');
1409
+ }
1410
+
986
1411
  function sanitizeMessageForChatCompletion(message, provider = 'generic') {
987
1412
  if (!isObject(message)) return message;
1413
+ const chatContent = contentToChatContent(message.content);
988
1414
  const sanitized = {
989
1415
  role: normalizeRole(message.role, provider),
990
- content: contentToChatContent(message.content),
1416
+ content: provider === 'deepseek' ? deepseekTextOnlyContent(chatContent) : chatContent,
991
1417
  };
992
1418
  if (message.name !== undefined) sanitized.name = message.name;
993
1419
  if (Array.isArray(message.tool_calls)) {
@@ -1000,11 +1426,93 @@ function sanitizeMessageForChatCompletion(message, provider = 'generic') {
1000
1426
  return sanitized;
1001
1427
  }
1002
1428
 
1429
+ function deepSeekInstructionBlock(message, includePriorityLabels) {
1430
+ const content = toText(message?.content).trim();
1431
+ if (!content) return '';
1432
+ if (!includePriorityLabels) return content;
1433
+ const label = message.role === 'developer'
1434
+ ? 'Developer instructions (priority below system):'
1435
+ : 'System instructions (highest priority):';
1436
+ return `${label}\n${content}`;
1437
+ }
1438
+
1439
+ function sanitizeMessagesForChatCompletion(messages, provider = 'generic') {
1440
+ const source = Array.isArray(messages) ? messages : [];
1441
+ if (provider !== 'deepseek') {
1442
+ return source.map((message) => sanitizeMessageForChatCompletion(message, provider));
1443
+ }
1444
+
1445
+ const result = [];
1446
+ let instructionBlock = [];
1447
+ const flushInstructionBlock = () => {
1448
+ if (!instructionBlock.length) return;
1449
+ const hasDeveloper = instructionBlock.some((message) => message?.role === 'developer');
1450
+ const content = instructionBlock
1451
+ .map((message) => deepSeekInstructionBlock(message, hasDeveloper))
1452
+ .filter(Boolean)
1453
+ .join('\n\n');
1454
+ if (content) result.push({ role: 'system', content });
1455
+ instructionBlock = [];
1456
+ };
1457
+
1458
+ for (const message of source) {
1459
+ if (message?.role === 'system' || message?.role === 'developer') {
1460
+ instructionBlock.push(message);
1461
+ continue;
1462
+ }
1463
+ flushInstructionBlock();
1464
+ result.push(sanitizeMessageForChatCompletion(message, provider));
1465
+ }
1466
+ flushInstructionBlock();
1467
+ return result;
1468
+ }
1469
+
1470
+ const DEEPSEEK_DEFAULT_MAX_TOKENS = 65536;
1471
+
1472
+ function deepseekDefaultMaxTokens(config = {}) {
1473
+ const value = Number(config.upstreamMaxTokens);
1474
+ if (Number.isFinite(value) && value > 0) return Math.floor(value);
1475
+ return DEEPSEEK_DEFAULT_MAX_TOKENS;
1476
+ }
1477
+
1478
+ const DEEPSEEK_JSON_SCHEMA_INSTRUCTIONS_MARKER = 'Respond with a single valid json object';
1479
+
1480
+ function jsonSchemaFromResponseFormat(responseFormat) {
1481
+ if (!isObject(responseFormat)) return null;
1482
+ const container = isObject(responseFormat.json_schema) ? responseFormat.json_schema : responseFormat;
1483
+ return isObject(container.schema) ? container.schema : null;
1484
+ }
1485
+
1486
+ function addDeepSeekJsonSchemaInstructions(messages, responseFormat) {
1487
+ if (messages.some((message) => message?.role === 'system' && toText(message.content).includes(DEEPSEEK_JSON_SCHEMA_INSTRUCTIONS_MARKER))) {
1488
+ return messages;
1489
+ }
1490
+ const schema = jsonSchemaFromResponseFormat(responseFormat);
1491
+ const schemaText = schema ? truncateRawText(JSON.stringify(schema), 6000) : '';
1492
+ const instructions = [
1493
+ `${DEEPSEEK_JSON_SCHEMA_INSTRUCTIONS_MARKER} and nothing else: no prose, no markdown fences.`,
1494
+ schemaText
1495
+ ? `The json object must conform to this JSON Schema:\n${schemaText}`
1496
+ : 'Use the json object shape requested in the conversation.',
1497
+ ].join('\n');
1498
+ if (!messages.length || messages[0]?.role !== 'system') {
1499
+ return [{ role: 'system', content: instructions }, ...messages];
1500
+ }
1501
+ return [
1502
+ {
1503
+ ...messages[0],
1504
+ content: `${toText(messages[0].content)}\n\n${instructions}`,
1505
+ },
1506
+ ...messages.slice(1),
1507
+ ];
1508
+ }
1509
+
1003
1510
  function patchDeepSeekThinkingHistory(messages, request) {
1004
1511
  if (request.thinking?.type !== 'enabled' && !request.reasoning_effort) return messages;
1005
1512
  return messages.map((message) => {
1006
1513
  if (message?.role !== 'assistant') return message;
1007
1514
  if (typeof message.reasoning_content === 'string') return message;
1515
+ if (!hasChatToolCalls(message)) return message;
1008
1516
  return {
1009
1517
  ...message,
1010
1518
  reasoning_content: '',
@@ -1020,8 +1528,12 @@ function normalizeOutputTextPart(text, annotations = []) {
1020
1528
  };
1021
1529
  }
1022
1530
 
1531
+ const REASONING_SUMMARY_HEADER = '**Reasoning**\n\n';
1532
+
1023
1533
  function reasoningSummaryText(text) {
1024
- return reasoningDisplayText(text);
1534
+ const display = reasoningDisplayText(text);
1535
+ if (!display) return '';
1536
+ return `${REASONING_SUMMARY_HEADER}${display}`;
1025
1537
  }
1026
1538
 
1027
1539
  function normalizeSummaryTextPart(text) {
@@ -1074,6 +1586,16 @@ function snapshotResponseItem(item) {
1074
1586
  arguments: isObject(item.arguments) ? { ...item.arguments } : toolSearchArgumentsFromText(item.arguments),
1075
1587
  });
1076
1588
  }
1589
+ if (item.type === 'custom_tool_call') {
1590
+ return omitUndefined({
1591
+ type: item.type,
1592
+ id: item.id,
1593
+ call_id: item.call_id,
1594
+ status: item.status,
1595
+ name: item.name,
1596
+ input: typeof item.input === 'string' ? item.input : '',
1597
+ });
1598
+ }
1077
1599
  return {
1078
1600
  ...item,
1079
1601
  content: Array.isArray(item.content) ? item.content.map(snapshotResponsePart) : item.content,
@@ -1204,7 +1726,9 @@ export function toChatCompletionsRequest(normalized, overrides = {}) {
1204
1726
 
1205
1727
  export function assistantMessageFromResponseOutput(output) {
1206
1728
  const messageItems = Array.isArray(output) ? output.filter((item) => item.type === 'message') : [];
1207
- const functionItems = Array.isArray(output) ? output.filter((item) => item.type === 'function_call') : [];
1729
+ const functionItems = Array.isArray(output)
1730
+ ? output.filter((item) => item.type === 'function_call' || item.type === 'custom_tool_call' || item.type === 'tool_search_call')
1731
+ : [];
1208
1732
  const reasoningItems = Array.isArray(output) ? output.filter((item) => item.type === 'reasoning') : [];
1209
1733
  const contentParts = messageItems.map((item) => item.content || []).flat();
1210
1734
  const chatParts = contentParts.map(contentPartToChatPart).filter(Boolean);
@@ -1217,14 +1741,7 @@ export function assistantMessageFromResponseOutput(output) {
1217
1741
  role: 'assistant',
1218
1742
  content,
1219
1743
  };
1220
- const toolCalls = functionItems.map((item) => ({
1221
- id: item.call_id || item.id,
1222
- type: 'function',
1223
- function: {
1224
- name: encodeToolName(item.namespace, item.name),
1225
- arguments: item.arguments || '',
1226
- },
1227
- }));
1744
+ const toolCalls = functionItems.map(chatToolCallFromResponseItem);
1228
1745
  if (toolCalls.length) assistant.tool_calls = toolCalls;
1229
1746
  const reasoningContent = reasoningItems
1230
1747
  .map((item) => reasoningTextFromItem(item))
@@ -1245,15 +1762,64 @@ export function extractToolCallIdsFromMessages(messages) {
1245
1762
  return ids;
1246
1763
  }
1247
1764
 
1765
+ export function requestToolsIncludeCommentary(tools) {
1766
+ for (const tool of Array.isArray(tools) ? tools : []) {
1767
+ if (!isObject(tool)) continue;
1768
+ const name = isObject(tool.function) ? tool.function.name : tool.name;
1769
+ if (String(name || '') === INTERNAL_COMMENTARY_TOOL) return true;
1770
+ }
1771
+ return false;
1772
+ }
1773
+
1774
+ export function bridgedCommentaryToolCallsFromMessage(message, tools) {
1775
+ if (requestToolsIncludeCommentary(tools)) return [];
1776
+ const toolCalls = Array.isArray(message?.tool_calls) ? message.tool_calls : [];
1777
+ return toolCalls.filter(
1778
+ (toolCall) => toolCall?.type === 'function' && String(toolCall?.function?.name || '') === INTERNAL_COMMENTARY_TOOL,
1779
+ );
1780
+ }
1781
+
1782
+ export function stripBridgedCommentaryFromCompletion(completion, tools) {
1783
+ if (requestToolsIncludeCommentary(tools)) return completion;
1784
+ const choice = Array.isArray(completion?.choices) ? completion.choices[0] : null;
1785
+ const message = choice?.message;
1786
+ if (!Array.isArray(message?.tool_calls)) return completion;
1787
+ const kept = message.tool_calls.filter(
1788
+ (toolCall) => !(toolCall?.type === 'function' && String(toolCall?.function?.name || '') === INTERNAL_COMMENTARY_TOOL),
1789
+ );
1790
+ if (kept.length === message.tool_calls.length) return completion;
1791
+ const nextMessage = { ...message };
1792
+ if (kept.length) nextMessage.tool_calls = kept;
1793
+ else delete nextMessage.tool_calls;
1794
+ return {
1795
+ ...completion,
1796
+ choices: [{ ...choice, message: nextMessage }, ...completion.choices.slice(1)],
1797
+ };
1798
+ }
1799
+
1800
+ function commentaryTextFromArguments(argumentsText) {
1801
+ const args = parseJsonObject(argumentsText);
1802
+ const text = String(args.text ?? args.message ?? args.update ?? args.content ?? '').trim();
1803
+ if (text) return text;
1804
+ const raw = String(argumentsText ?? '').trim();
1805
+ if (!raw || raw.startsWith('{') || raw === 'null') return '';
1806
+ return raw;
1807
+ }
1808
+
1809
+ function addInternalCommentaryTool(tools, toolChoice) {
1810
+ if (!Array.isArray(tools) || !tools.length) return tools;
1811
+ if (toolChoiceDisablesTools(toolChoice)) return tools;
1812
+ if (requestToolsIncludeCommentary(tools)) return tools;
1813
+ return [...tools, JSON.parse(JSON.stringify(INTERNAL_COMMENTARY_TOOL_DEFINITION))];
1814
+ }
1815
+
1248
1816
  export function toProviderChatCompletionsRequest(chatRequest, config = {}) {
1249
1817
  const provider = config.upstreamProvider || 'generic';
1250
1818
  const modelAlias = resolveModelAlias(chatRequest.model, config);
1251
1819
  const fallbackReasoning = !chatRequest.reasoning && config.codexReasoningEffort
1252
1820
  ? { effort: config.codexReasoningEffort }
1253
1821
  : chatRequest.reasoning;
1254
- const messages = Array.isArray(chatRequest.messages)
1255
- ? chatRequest.messages.map((message) => sanitizeMessageForChatCompletion(message, provider))
1256
- : [];
1822
+ const messages = sanitizeMessagesForChatCompletion(chatRequest.messages, provider);
1257
1823
  const request = {
1258
1824
  model: modelAlias.upstreamModel || config.upstreamModel || chatRequest.model,
1259
1825
  messages,
@@ -1282,7 +1848,7 @@ export function toProviderChatCompletionsRequest(chatRequest, config = {}) {
1282
1848
  if (provider === 'deepseek') {
1283
1849
  delete request.reasoning_effort;
1284
1850
  delete request.parallel_tool_calls;
1285
- request.tools = adaptToolsForProvider(request.tools, provider, config);
1851
+ request.tools = addInternalCommentaryTool(adaptToolsForProvider(request.tools, provider, config), chatRequest.tool_choice);
1286
1852
  request.messages = addDeepSeekToolInstructions(request.messages, request.tools);
1287
1853
  if (request.user !== undefined) {
1288
1854
  request.user_id = request.user;
@@ -1293,15 +1859,23 @@ export function toProviderChatCompletionsRequest(chatRequest, config = {}) {
1293
1859
  } else {
1294
1860
  delete request.stream_options;
1295
1861
  }
1862
+ if (request.max_tokens == null) {
1863
+ request.max_tokens = deepseekDefaultMaxTokens(config);
1864
+ }
1296
1865
  if (request.response_format?.type === 'json_schema') {
1866
+ request.messages = addDeepSeekJsonSchemaInstructions(request.messages, request.response_format);
1297
1867
  request.response_format = { type: 'json_object' };
1298
1868
  }
1299
1869
  Object.assign(request, deepseekReasoningPayload({ alias: modelAlias, reasoning: fallbackReasoning }));
1300
- request.messages = patchDeepSeekThinkingHistory(request.messages, request);
1301
1870
  }
1302
1871
 
1303
1872
  Object.assign(request, modelAlias.extraBody);
1304
1873
 
1874
+ if (provider === 'deepseek') {
1875
+ request.messages = patchDeepSeekThinkingHistory(request.messages, request);
1876
+ }
1877
+ request.tools = stripGatewayToolMarkers(request.tools);
1878
+
1305
1879
  for (const key of Object.keys(request)) {
1306
1880
  if (request[key] === undefined) delete request[key];
1307
1881
  }
@@ -1363,7 +1937,7 @@ function normalizeResponsesUsage(usage) {
1363
1937
  return {
1364
1938
  input_tokens: inputTokens,
1365
1939
  input_tokens_details: usage.input_tokens_details ?? {
1366
- cached_tokens: usage.prompt_tokens_details?.cached_tokens ?? 0,
1940
+ cached_tokens: usage.prompt_tokens_details?.cached_tokens ?? usage.prompt_cache_hit_tokens ?? 0,
1367
1941
  },
1368
1942
  output_tokens: outputTokens,
1369
1943
  output_tokens_details: usage.output_tokens_details ?? {
@@ -1378,6 +1952,7 @@ function outputTextFromOutput(output) {
1378
1952
  const chunks = [];
1379
1953
  for (const item of output) {
1380
1954
  if (item?.type !== 'message' || !Array.isArray(item.content)) continue;
1955
+ if (item.phase === 'commentary') continue;
1381
1956
  for (const part of item.content) {
1382
1957
  if (part?.type === 'output_text' && typeof part.text === 'string') {
1383
1958
  chunks.push(part.text);
@@ -1413,29 +1988,174 @@ export function createResponseEnvelope({
1413
1988
  });
1414
1989
  }
1415
1990
 
1416
- export function convertChatCompletionToResponses({ completion, model, previousResponseId, normalized, responseId = generateId('resp'), config = {} }) {
1417
- const createdAt = completion.created || Date.now() / 1000;
1418
- const choice = Array.isArray(completion.choices) ? completion.choices[0] : null;
1419
- const message = choice?.message || {};
1420
- const content = chatContentToResponseOutputParts(message.content);
1421
- const toolSchemas = buildToolSchemas(normalized?.tools);
1422
- const toolNames = buildToolNames(normalized?.tools);
1423
- const output = [];
1991
+ const PARALLEL_TOOL_WRAPPER_NAMES = new Set(['multi_tool_use.parallel', 'multi_tool_use_parallel']);
1992
+ const EMITTED_TOOL_NAME_PREFIX = 'functions.';
1424
1993
 
1425
- if (content.length || message.tool_calls?.length) {
1426
- output.push({
1427
- type: 'message',
1428
- id: generateId('msg'),
1429
- role: 'assistant',
1430
- content,
1431
- status: 'completed',
1432
- phase: message.phase || 'final_answer',
1433
- });
1434
- }
1994
+ export function isParallelToolWrapperName(name) {
1995
+ const raw = String(name || '').toLowerCase();
1996
+ const stripped = raw.startsWith(EMITTED_TOOL_NAME_PREFIX) ? raw.slice(EMITTED_TOOL_NAME_PREFIX.length) : raw;
1997
+ return PARALLEL_TOOL_WRAPPER_NAMES.has(stripped);
1998
+ }
1435
1999
 
1436
- if (Array.isArray(message.tool_calls)) {
1437
- for (const toolCall of message.tool_calls) {
1438
- const item = responseItemFromChatToolCall(toolCall, toolNames);
2000
+ function toolNameMatchKey(name) {
2001
+ return String(name || '').toLowerCase().replace(/\./g, '__');
2002
+ }
2003
+
2004
+ export function resolveEmittedToolName(name, knownNames) {
2005
+ const raw = String(name || '');
2006
+ if (!raw || !knownNames) return name;
2007
+ const known = knownNames instanceof Set ? knownNames : new Set(knownNames);
2008
+ if (!known.size || known.has(raw)) return name;
2009
+ const candidates = [raw];
2010
+ if (raw.startsWith(EMITTED_TOOL_NAME_PREFIX)) candidates.push(raw.slice(EMITTED_TOOL_NAME_PREFIX.length));
2011
+ for (const candidate of candidates) {
2012
+ if (known.has(candidate)) return candidate;
2013
+ }
2014
+ const knownByKey = new Map();
2015
+ for (const knownName of known) {
2016
+ const key = toolNameMatchKey(knownName);
2017
+ if (!knownByKey.has(key)) knownByKey.set(key, knownName);
2018
+ }
2019
+ for (const candidate of candidates) {
2020
+ const match = knownByKey.get(toolNameMatchKey(candidate));
2021
+ if (match) return match;
2022
+ }
2023
+ return name;
2024
+ }
2025
+
2026
+ export function chatToolNamesFromTools(tools) {
2027
+ const names = [];
2028
+ for (const tool of Array.isArray(tools) ? tools : []) {
2029
+ if (!isObject(tool)) continue;
2030
+ const name = isObject(tool.function) ? tool.function.name : tool.name;
2031
+ if (name) names.push(String(name));
2032
+ }
2033
+ return names;
2034
+ }
2035
+
2036
+ export function resolveEmittedToolCallNamesInCompletion(completion, knownNames) {
2037
+ const choice = Array.isArray(completion?.choices) ? completion.choices[0] : null;
2038
+ const toolCalls = choice?.message?.tool_calls;
2039
+ if (!Array.isArray(toolCalls) || !toolCalls.length) return completion;
2040
+ let changed = false;
2041
+ const resolved = toolCalls.map((toolCall) => {
2042
+ const name = toolCall?.function?.name;
2043
+ if (!name) return toolCall;
2044
+ const resolvedName = resolveEmittedToolName(name, knownNames);
2045
+ if (resolvedName === name) return toolCall;
2046
+ changed = true;
2047
+ return { ...toolCall, function: { ...toolCall.function, name: resolvedName } };
2048
+ });
2049
+ if (!changed) return completion;
2050
+ return {
2051
+ ...completion,
2052
+ choices: completion.choices.map((entry, index) => (index === 0
2053
+ ? { ...entry, message: { ...entry.message, tool_calls: resolved } }
2054
+ : entry)),
2055
+ };
2056
+ }
2057
+
2058
+ function parallelToolUsesFromArguments(argumentsText) {
2059
+ const parsed = safeJsonParse(String(argumentsText || ''));
2060
+ if (!parsed.ok || !isObject(parsed.value)) return null;
2061
+ const uses = Array.isArray(parsed.value.tool_uses) ? parsed.value.tool_uses : null;
2062
+ if (!uses || !uses.length) return null;
2063
+ const calls = [];
2064
+ for (const use of uses) {
2065
+ if (!isObject(use)) return null;
2066
+ const name = String(use.recipient_name || use.name || '').replace(/^functions\./, '');
2067
+ if (!name) return null;
2068
+ const parameters = use.parameters ?? use.arguments ?? {};
2069
+ calls.push({
2070
+ name,
2071
+ arguments: typeof parameters === 'string' ? parameters : JSON.stringify(parameters),
2072
+ });
2073
+ }
2074
+ return calls;
2075
+ }
2076
+
2077
+ export function expandParallelToolCalls(toolCalls) {
2078
+ if (!Array.isArray(toolCalls)) return toolCalls;
2079
+ if (!toolCalls.some((toolCall) => isParallelToolWrapperName(toolCall?.function?.name))) return toolCalls;
2080
+ const expanded = [];
2081
+ for (const toolCall of toolCalls) {
2082
+ const uses = isParallelToolWrapperName(toolCall?.function?.name)
2083
+ ? parallelToolUsesFromArguments(toolCall.function?.arguments)
2084
+ : null;
2085
+ if (!uses) {
2086
+ expanded.push(toolCall);
2087
+ continue;
2088
+ }
2089
+ for (const use of uses) {
2090
+ expanded.push({
2091
+ id: generateId('call'),
2092
+ type: 'function',
2093
+ function: { name: use.name, arguments: use.arguments },
2094
+ });
2095
+ }
2096
+ }
2097
+ return expanded;
2098
+ }
2099
+
2100
+ export function expandParallelToolCallsInCompletion(completion) {
2101
+ const choice = Array.isArray(completion?.choices) ? completion.choices[0] : null;
2102
+ const toolCalls = choice?.message?.tool_calls;
2103
+ if (!Array.isArray(toolCalls)) return completion;
2104
+ const expanded = expandParallelToolCalls(toolCalls);
2105
+ if (expanded === toolCalls) return completion;
2106
+ return {
2107
+ ...completion,
2108
+ choices: completion.choices.map((entry, index) => (index === 0
2109
+ ? { ...entry, message: { ...entry.message, tool_calls: expanded } }
2110
+ : entry)),
2111
+ };
2112
+ }
2113
+
2114
+ export function convertChatCompletionToResponses({ completion: rawCompletion, model, previousResponseId, normalized, responseId = generateId('resp'), config = {} }) {
2115
+ const toolSchemas = buildToolSchemas(normalized?.tools);
2116
+ const knownToolNames = new Set([...toolSchemas.keys(), INTERNAL_COMMENTARY_TOOL]);
2117
+ const completion = resolveEmittedToolCallNamesInCompletion(
2118
+ expandParallelToolCallsInCompletion(rawCompletion),
2119
+ knownToolNames,
2120
+ );
2121
+ const createdAt = completion.created || Date.now() / 1000;
2122
+ const choice = Array.isArray(completion.choices) ? completion.choices[0] : null;
2123
+ const message = choice?.message || {};
2124
+ const content = chatContentToResponseOutputParts(message.content);
2125
+ const messageHasToolCalls = hasChatToolCalls(message);
2126
+ const toolNames = buildToolNames(normalized?.tools);
2127
+ const customToolNames = buildCustomToolNames(normalized?.tools);
2128
+ const output = [];
2129
+
2130
+ if (content.length) {
2131
+ output.push({
2132
+ type: 'message',
2133
+ id: generateId('msg'),
2134
+ role: 'assistant',
2135
+ content,
2136
+ status: 'completed',
2137
+ phase: message.phase || (messageHasToolCalls ? 'commentary' : 'final_answer'),
2138
+ });
2139
+ }
2140
+
2141
+ if (messageHasToolCalls) {
2142
+ const bridgedCommentary = !requestToolsIncludeCommentary(normalized?.tools);
2143
+ for (const toolCall of message.tool_calls) {
2144
+ if (bridgedCommentary && toolCall?.type === 'function' && String(toolCall?.function?.name || '') === INTERNAL_COMMENTARY_TOOL) {
2145
+ const text = commentaryTextFromArguments(toolCall.function?.arguments);
2146
+ if (text) {
2147
+ output.push({
2148
+ type: 'message',
2149
+ id: generateId('msg'),
2150
+ role: 'assistant',
2151
+ content: [normalizeOutputTextPart(text)],
2152
+ status: 'completed',
2153
+ phase: 'commentary',
2154
+ });
2155
+ }
2156
+ continue;
2157
+ }
2158
+ const item = responseItemFromChatToolCall(toolCall, toolNames, customToolNames);
1439
2159
  normalizeFunctionCallItemArguments(item, toolSchemas, config);
1440
2160
  output.push(item);
1441
2161
  }
@@ -1449,7 +2169,7 @@ export function convertChatCompletionToResponses({ completion, model, previousRe
1449
2169
  summary: [normalizeSummaryTextPart(reasoningSummaryText(reasoningText))],
1450
2170
  content: [],
1451
2171
  reasoning_content: reasoningText,
1452
- encrypted_content: null,
2172
+ encrypted_content: encodeGatewayReasoning(reasoningText),
1453
2173
  status: 'completed',
1454
2174
  });
1455
2175
  }
@@ -1475,9 +2195,10 @@ export class ResponsesStreamMapper {
1475
2195
  createdAt = Date.now() / 1000,
1476
2196
  previousResponseId = null,
1477
2197
  normalized,
1478
- bufferOutputUntilDone = false,
1479
2198
  emitReasoningSummary = true,
1480
2199
  emitReasoningText = false,
2200
+ holdToolItemEvents = false,
2201
+ knownToolNames,
1481
2202
  config = {},
1482
2203
  } = {}) {
1483
2204
  this.responseId = responseId;
@@ -1485,9 +2206,10 @@ export class ResponsesStreamMapper {
1485
2206
  this.createdAt = createdAt;
1486
2207
  this.previousResponseId = previousResponseId;
1487
2208
  this.normalized = normalized;
1488
- this.bufferOutputUntilDone = Boolean(bufferOutputUntilDone);
1489
2209
  this.emitReasoningSummary = Boolean(emitReasoningSummary);
1490
2210
  this.emitReasoningText = Boolean(emitReasoningText);
2211
+ this.holdToolItemEvents = Boolean(holdToolItemEvents);
2212
+ this.roundStartIndex = 0;
1491
2213
  this.sequenceNumber = 0;
1492
2214
  this.output = [];
1493
2215
  this.messageItem = null;
@@ -1495,19 +2217,33 @@ export class ResponsesStreamMapper {
1495
2217
  this.toolItems = new Map();
1496
2218
  this.text = '';
1497
2219
  this.reasoningText = '';
1498
- this.streamReasoningLive = true;
1499
2220
  this.reasoningItemDone = false;
1500
2221
  this.finishReason = null;
1501
2222
  this.usage = null;
2223
+ this.pendingFinishReason = null;
2224
+ this.pendingUsage = null;
1502
2225
  this.completedAt = null;
1503
2226
  this.finalized = false;
2227
+ this.messageItemAdded = false;
1504
2228
  this.messageContentAdded = false;
2229
+ this.messageItemClosed = false;
2230
+ this.holdVisibleText = false;
1505
2231
  this.reasoningContentAdded = false;
1506
2232
  this.reasoningSummaryAdded = false;
1507
2233
  this.reasoningItemAdded = false;
1508
2234
  this.streamedReasoningSummaryText = '';
1509
2235
  this.toolSchemas = buildToolSchemas(normalized?.tools);
2236
+ this.knownToolNames = new Set([
2237
+ ...(Array.isArray(knownToolNames) || knownToolNames instanceof Set ? knownToolNames : []),
2238
+ ...this.toolSchemas.keys(),
2239
+ INTERNAL_COMMENTARY_TOOL,
2240
+ ]);
1510
2241
  this.toolNames = buildToolNames(normalized?.tools);
2242
+ this.customToolNames = buildCustomToolNames(normalized?.tools);
2243
+ this.bridgedCommentaryTool = !requestToolsIncludeCommentary(normalized?.tools);
2244
+ this.toolItemsAdded = new Set();
2245
+ this.toolItemsBufferedArguments = new Set();
2246
+ this.toolItemsStreamedArgumentLengths = new Map();
1511
2247
  this.config = config;
1512
2248
  }
1513
2249
 
@@ -1548,27 +2284,92 @@ export class ResponsesStreamMapper {
1548
2284
  }
1549
2285
 
1550
2286
  ensureMessageItem() {
2287
+ this.ensureMessageItemState();
2288
+ if (!this.messageItemAdded) {
2289
+ this.messageItemAdded = true;
2290
+ return {
2291
+ type: 'response.output_item.added',
2292
+ sequence_number: this.nextSequence(),
2293
+ output_index: this.output.length - 1,
2294
+ item: snapshotResponseItem(this.messageItem),
2295
+ };
2296
+ }
2297
+ return null;
2298
+ }
2299
+
2300
+ ensureMessageItemState() {
1551
2301
  if (!this.messageItem) {
1552
2302
  this.messageItem = {
1553
2303
  id: generateId('msg'),
1554
2304
  type: 'message',
1555
2305
  status: 'in_progress',
1556
2306
  role: 'assistant',
1557
- phase: 'final_answer',
2307
+ phase: this.messageStartsAsCommentary() ? 'commentary' : 'final_answer',
1558
2308
  content: [],
1559
2309
  };
1560
2310
  this.output.push(this.messageItem);
2311
+ }
2312
+ }
2313
+
2314
+ messageStartsAsCommentary() {
2315
+ return this.toolItems.size > 0 || (this.config.upstreamProvider === 'deepseek' && hasRunnableChatTools(this.normalized));
2316
+ }
2317
+
2318
+ finalizeMessagePhase() {
2319
+ if (!this.messageItem || this.toolItems.size > 0) return;
2320
+ this.messageItem.phase = 'final_answer';
2321
+ }
2322
+
2323
+ closeMessageItemAsCommentary(status = 'completed') {
2324
+ if (!this.messageItem || this.messageItemClosed) return [];
2325
+ this.messageItem.phase = 'commentary';
2326
+ if (!this.messageItemAdded) return [];
2327
+ this.messageItemClosed = true;
2328
+ this.messageItem.status = status;
2329
+ this.messageItem.content[0].text = this.text;
2330
+ const outputIndex = this.output.indexOf(this.messageItem);
2331
+ return [
2332
+ {
2333
+ type: 'response.output_text.done',
2334
+ sequence_number: this.nextSequence(),
2335
+ output_index: outputIndex,
2336
+ content_index: 0,
2337
+ item_id: this.messageItem.id,
2338
+ text: this.text,
2339
+ logprobs: [],
2340
+ },
2341
+ {
2342
+ type: 'response.content_part.done',
2343
+ sequence_number: this.nextSequence(),
2344
+ output_index: outputIndex,
2345
+ content_index: 0,
2346
+ item_id: this.messageItem.id,
2347
+ part: snapshotResponsePart(this.messageItem.content[0]),
2348
+ },
2349
+ {
2350
+ type: 'response.output_item.done',
2351
+ sequence_number: this.nextSequence(),
2352
+ output_index: outputIndex,
2353
+ item: snapshotResponseItem(this.messageItem),
2354
+ },
2355
+ ];
2356
+ }
2357
+
2358
+ ensureReasoningItem() {
2359
+ this.ensureReasoningItemState();
2360
+ if (!this.reasoningItemAdded) {
2361
+ this.reasoningItemAdded = true;
1561
2362
  return {
1562
2363
  type: 'response.output_item.added',
1563
2364
  sequence_number: this.nextSequence(),
1564
- output_index: this.output.length - 1,
1565
- item: snapshotResponseItem(this.messageItem),
2365
+ output_index: this.output.indexOf(this.reasoningItem),
2366
+ item: snapshotResponseItem(this.reasoningItem),
1566
2367
  };
1567
2368
  }
1568
2369
  return null;
1569
2370
  }
1570
2371
 
1571
- ensureReasoningItem() {
2372
+ ensureReasoningItemState() {
1572
2373
  if (!this.reasoningItem) {
1573
2374
  this.reasoningItem = {
1574
2375
  id: generateId('rs'),
@@ -1580,16 +2381,6 @@ export class ResponsesStreamMapper {
1580
2381
  };
1581
2382
  this.output.push(this.reasoningItem);
1582
2383
  }
1583
- if (!this.reasoningItemAdded) {
1584
- this.reasoningItemAdded = true;
1585
- return {
1586
- type: 'response.output_item.added',
1587
- sequence_number: this.nextSequence(),
1588
- output_index: this.output.indexOf(this.reasoningItem),
1589
- item: snapshotResponseItem(this.reasoningItem),
1590
- };
1591
- }
1592
- return null;
1593
2384
  }
1594
2385
 
1595
2386
  ensureReasoningContentPart(events) {
@@ -1619,7 +2410,13 @@ export class ResponsesStreamMapper {
1619
2410
  else delete this.reasoningItem.reasoning_content;
1620
2411
  }
1621
2412
 
1622
- appendReasoningSummaryDelta(events) {
2413
+ reasoningSummarySourceText({ final = false } = {}) {
2414
+ if (final) return this.reasoningText;
2415
+ const boundary = this.reasoningText.lastIndexOf('\n');
2416
+ return boundary < 0 ? '' : this.reasoningText.slice(0, boundary + 1);
2417
+ }
2418
+
2419
+ appendReasoningSummaryDelta(events, { final = false } = {}) {
1623
2420
  if (!this.emitReasoningSummary || !this.reasoningItem) return;
1624
2421
  if (!this.reasoningSummaryAdded) {
1625
2422
  this.reasoningSummaryAdded = true;
@@ -1633,14 +2430,17 @@ export class ResponsesStreamMapper {
1633
2430
  part: snapshotResponsePart(this.reasoningItem.summary[0]),
1634
2431
  });
1635
2432
  }
1636
- const nextSummaryText = reasoningSummaryText(this.reasoningText);
1637
- this.reasoningItem.summary[0].text = nextSummaryText;
2433
+ const nextSummaryText = reasoningSummaryText(this.reasoningSummarySourceText({ final }));
2434
+ if (final) this.reasoningItem.summary[0].text = nextSummaryText;
2435
+ if (nextSummaryText === this.streamedReasoningSummaryText) return;
1638
2436
  if (!nextSummaryText.startsWith(this.streamedReasoningSummaryText)) {
2437
+ if (!final) return;
1639
2438
  this.streamedReasoningSummaryText = nextSummaryText;
1640
2439
  return;
1641
2440
  }
1642
2441
  const deltaText = nextSummaryText.slice(this.streamedReasoningSummaryText.length);
1643
2442
  this.streamedReasoningSummaryText = nextSummaryText;
2443
+ this.reasoningItem.summary[0].text = nextSummaryText;
1644
2444
  for (const delta of splitBufferedReasoningDeltas(deltaText)) {
1645
2445
  events.push({
1646
2446
  type: 'response.reasoning_summary_text.delta',
@@ -1653,28 +2453,20 @@ export class ResponsesStreamMapper {
1653
2453
  }
1654
2454
  }
1655
2455
 
2456
+ holdVisibleTextUntilDone() {
2457
+ this.holdVisibleText = true;
2458
+ }
2459
+
1656
2460
  textDelta(delta) {
1657
- if (this.bufferOutputUntilDone) {
1658
- if (!this.messageItem) {
1659
- this.messageItem = {
1660
- id: generateId('msg'),
1661
- type: 'message',
1662
- status: 'in_progress',
1663
- role: 'assistant',
1664
- phase: 'final_answer',
1665
- content: [normalizeOutputTextPart('')],
1666
- };
1667
- this.output.push(this.messageItem);
1668
- }
2461
+ const events = [...this.closeReasoningItem('completed')];
2462
+ if (this.messageItemClosed) return events;
2463
+ if (!this.messageItemAdded && (this.holdVisibleText || this.toolItems.size > 0)) {
2464
+ this.ensureMessageItemState();
2465
+ if (!this.messageItem.content.length) this.messageItem.content.push(normalizeOutputTextPart(''));
1669
2466
  this.text += delta;
1670
2467
  this.messageItem.content[0].text = this.text;
1671
- return [];
1672
- }
1673
- const events = [];
1674
- if (this.streamReasoningLive) {
1675
- events.push(...this.closeReasoningItem('completed'));
2468
+ return events;
1676
2469
  }
1677
- this.streamReasoningLive = false;
1678
2470
  const added = this.ensureMessageItem();
1679
2471
  if (added) events.push(added);
1680
2472
  if (!this.messageContentAdded) {
@@ -1705,15 +2497,11 @@ export class ResponsesStreamMapper {
1705
2497
 
1706
2498
  reasoningDelta(delta) {
1707
2499
  this.reasoningText += delta;
1708
- if (this.bufferOutputUntilDone) {
1709
- const events = [];
1710
- const added = this.ensureReasoningItem();
1711
- if (added) events.push(added);
2500
+ if (this.reasoningItemDone) {
1712
2501
  this.syncReasoningItemContent();
1713
- this.appendReasoningSummaryDelta(events);
1714
- return events;
2502
+ return [];
1715
2503
  }
1716
- if (!this.streamReasoningLive) {
2504
+ if (this.messageItem || this.toolItems.size > 0) {
1717
2505
  if (!this.reasoningItem) {
1718
2506
  this.reasoningItem = {
1719
2507
  id: generateId('rs'),
@@ -1748,89 +2536,222 @@ export class ResponsesStreamMapper {
1748
2536
  return events;
1749
2537
  }
1750
2538
 
1751
- functionDelta(toolCall) {
1752
- if (this.bufferOutputUntilDone) {
1753
- const index = toolCall.index ?? 0;
1754
- let item = this.toolItems.get(index);
1755
- if (!item) {
1756
- const callId = toolCall.id || generateId('call');
1757
- item = isCodexToolSearchTool(toolCall.function?.name)
1758
- ? {
1759
- id: generateId('tsc'),
1760
- type: 'tool_search_call',
1761
- status: 'in_progress',
1762
- call_id: callId,
1763
- execution: 'client',
1764
- arguments: '',
1765
- }
1766
- : {
1767
- id: callId,
1768
- type: 'function_call',
1769
- status: 'in_progress',
1770
- call_id: callId,
1771
- ...decodedToolName(toolCall.function?.name, this.toolNames),
1772
- arguments: '',
1773
- };
1774
- this.toolItems.set(index, item);
1775
- this.output.push(item);
1776
- }
1777
- if (isCodexToolSearchTool(toolCall.function?.name)) {
1778
- item = convertItemToToolSearchCall(item);
1779
- } else if (toolCall.function?.name && item.type !== 'tool_search_call') {
1780
- Object.assign(item, decodedToolName(toolCall.function.name, this.toolNames));
1781
- }
1782
- item.arguments += toolCall.function?.arguments || '';
1783
- return [];
2539
+ createToolItem(toolCall) {
2540
+ const callId = toolCall.id || generateId('call');
2541
+ const name = toolCall.function?.name;
2542
+ if (isCodexToolSearchTool(name)) {
2543
+ return {
2544
+ id: generateId('tsc'),
2545
+ type: 'tool_search_call',
2546
+ status: 'in_progress',
2547
+ call_id: callId,
2548
+ execution: 'client',
2549
+ arguments: '',
2550
+ };
1784
2551
  }
1785
- const events = [];
1786
- if (this.streamReasoningLive) {
1787
- events.push(...this.closeReasoningItem('completed'));
2552
+ if (name && this.customToolNames.has(name)) {
2553
+ return {
2554
+ id: callId,
2555
+ type: 'custom_tool_call',
2556
+ status: 'in_progress',
2557
+ call_id: callId,
2558
+ ...decodedToolName(name, this.toolNames),
2559
+ input: '',
2560
+ arguments: '',
2561
+ };
2562
+ }
2563
+ return {
2564
+ id: callId,
2565
+ type: 'function_call',
2566
+ status: 'in_progress',
2567
+ call_id: callId,
2568
+ ...decodedToolName(name, this.toolNames),
2569
+ arguments: '',
2570
+ };
2571
+ }
2572
+
2573
+ applyToolCallName(item, name) {
2574
+ if (!name || item.type === 'tool_search_call') return;
2575
+ Object.assign(item, decodedToolName(name, this.toolNames));
2576
+ if (item.type === 'function_call' && this.customToolNames.has(name)) {
2577
+ item.type = 'custom_tool_call';
2578
+ if (typeof item.input !== 'string') item.input = '';
2579
+ }
2580
+ }
2581
+
2582
+ functionDelta(rawToolCall) {
2583
+ let toolCall = rawToolCall;
2584
+ const emittedName = toolCall.function?.name;
2585
+ if (emittedName) {
2586
+ const resolvedName = resolveEmittedToolName(emittedName, this.knownToolNames);
2587
+ if (resolvedName !== emittedName) {
2588
+ toolCall = { ...toolCall, function: { ...toolCall.function, name: resolvedName } };
2589
+ }
1788
2590
  }
1789
- this.streamReasoningLive = false;
2591
+ const events = [...this.closeReasoningItem('completed')];
2592
+ events.push(...this.closeMessageItemAsCommentary('completed'));
1790
2593
  const index = toolCall.index ?? 0;
1791
2594
  let item = this.toolItems.get(index);
1792
2595
  if (!item) {
1793
- const callId = toolCall.id || generateId('call');
1794
- item = isCodexToolSearchTool(toolCall.function?.name)
1795
- ? {
1796
- id: generateId('tsc'),
1797
- type: 'tool_search_call',
1798
- status: 'in_progress',
1799
- call_id: callId,
1800
- execution: 'client',
1801
- arguments: '',
1802
- }
1803
- : {
1804
- id: callId,
1805
- type: 'function_call',
1806
- status: 'in_progress',
1807
- call_id: callId,
1808
- ...decodedToolName(toolCall.function?.name, this.toolNames),
1809
- arguments: '',
1810
- };
2596
+ item = this.createToolItem(toolCall);
1811
2597
  this.toolItems.set(index, item);
1812
2598
  this.output.push(item);
1813
- events.push({
1814
- type: 'response.output_item.added',
1815
- sequence_number: this.nextSequence(),
1816
- output_index: this.output.length - 1,
1817
- item: snapshotResponseItem(item),
1818
- });
2599
+ if (this.holdToolItemEvents || isParallelToolWrapperName(toolCall.function?.name)) {
2600
+ this.toolItemsBufferedArguments.add(index);
2601
+ }
2602
+ if (!this.holdToolItemEvents && hasToolCallFunctionName(toolCall) && !isParallelToolWrapperName(toolCall.function?.name) && !this.isBridgedCommentaryToolItem(item)) {
2603
+ events.push(...this.addToolItemEvents(index));
2604
+ }
1819
2605
  }
1820
2606
  if (isCodexToolSearchTool(toolCall.function?.name)) {
2607
+ const wasAdded = this.toolItemsAdded.has(index);
1821
2608
  item = convertItemToToolSearchCall(item);
1822
- } else if (toolCall.function?.name && item.type !== 'tool_search_call') {
1823
- Object.assign(item, decodedToolName(toolCall.function.name, this.toolNames));
2609
+ this.toolItems.set(index, item);
2610
+ if (!wasAdded && !this.holdToolItemEvents && hasToolCallFunctionName(toolCall)) {
2611
+ events.push(...this.addToolItemEvents(index));
2612
+ }
2613
+ } else if (toolCall.function?.name) {
2614
+ this.applyToolCallName(item, toolCall.function.name);
2615
+ if (!this.holdToolItemEvents && !this.toolItemsAdded.has(index) && !isParallelToolWrapperName(item.name) && !this.isBridgedCommentaryToolItem(item)) {
2616
+ events.push(...this.addToolItemEvents(index));
2617
+ }
1824
2618
  }
1825
- const delta = toolCall.function?.arguments || '';
1826
- item.arguments += delta;
1827
- if (delta && item.type === 'function_call') {
2619
+ item.arguments += toolCall.function?.arguments || '';
2620
+ if (functionCallItemNeedsArgumentNormalization(item, this.toolSchemas, this.config)) {
2621
+ this.toolItemsBufferedArguments.add(index);
2622
+ }
2623
+ const streamedLength = this.toolItemsStreamedArgumentLengths.get(index) || 0;
2624
+ const argumentDelta = item.arguments.slice(streamedLength);
2625
+ if (
2626
+ argumentDelta &&
2627
+ item.type === 'function_call' &&
2628
+ this.toolItemsAdded.has(index) &&
2629
+ !this.toolItemsBufferedArguments.has(index)
2630
+ ) {
2631
+ this.toolItemsStreamedArgumentLengths.set(index, item.arguments.length);
1828
2632
  events.push({
1829
2633
  type: 'response.function_call_arguments.delta',
1830
2634
  sequence_number: this.nextSequence(),
1831
2635
  output_index: this.output.indexOf(item),
1832
2636
  item_id: item.id,
1833
- delta,
2637
+ delta: argumentDelta,
2638
+ });
2639
+ }
2640
+ return events;
2641
+ }
2642
+
2643
+ addToolItemEvents(index) {
2644
+ if (this.toolItemsAdded.has(index)) return [];
2645
+ const item = this.toolItems.get(index);
2646
+ if (!item) return [];
2647
+ this.toolItemsAdded.add(index);
2648
+ return [{
2649
+ type: 'response.output_item.added',
2650
+ sequence_number: this.nextSequence(),
2651
+ output_index: this.output.indexOf(item),
2652
+ item: snapshotResponseItem(item),
2653
+ }];
2654
+ }
2655
+
2656
+ expandParallelToolItems() {
2657
+ for (const [index, item] of [...this.toolItems.entries()]) {
2658
+ if (item.type !== 'function_call' || !isParallelToolWrapperName(item.name)) continue;
2659
+ if (this.toolItemsAdded.has(index)) continue;
2660
+ const uses = parallelToolUsesFromArguments(item.arguments);
2661
+ if (!uses) continue;
2662
+ const at = this.output.indexOf(item);
2663
+ this.toolItems.delete(index);
2664
+ this.toolItemsBufferedArguments.delete(index);
2665
+ this.toolItemsStreamedArgumentLengths.delete(index);
2666
+ const expandedItems = uses.map((use, position) => {
2667
+ const key = `${index}:${position}`;
2668
+ const expanded = this.createToolItem({
2669
+ id: generateId('call'),
2670
+ function: { name: resolveEmittedToolName(use.name, this.knownToolNames) },
2671
+ });
2672
+ expanded.arguments = use.arguments;
2673
+ this.toolItems.set(key, expanded);
2674
+ this.toolItemsBufferedArguments.add(key);
2675
+ return expanded;
2676
+ });
2677
+ if (at >= 0) this.output.splice(at, 1, ...expandedItems);
2678
+ else this.output.push(...expandedItems);
2679
+ }
2680
+ }
2681
+
2682
+ isBridgedCommentaryToolItem(item) {
2683
+ return Boolean(this.bridgedCommentaryTool) && item?.type === 'function_call' && item?.name === INTERNAL_COMMENTARY_TOOL;
2684
+ }
2685
+
2686
+ convertBridgedCommentaryItems() {
2687
+ const events = [];
2688
+ for (const [index, item] of [...this.toolItems.entries()]) {
2689
+ if (!this.isBridgedCommentaryToolItem(item) || this.toolItemsAdded.has(index)) continue;
2690
+ this.toolItems.delete(index);
2691
+ this.toolItemsBufferedArguments.delete(index);
2692
+ this.toolItemsStreamedArgumentLengths.delete(index);
2693
+ const at = this.output.indexOf(item);
2694
+ const text = commentaryTextFromArguments(item.arguments);
2695
+ if (!text) {
2696
+ if (at >= 0) this.output.splice(at, 1);
2697
+ continue;
2698
+ }
2699
+ const messageItem = {
2700
+ type: 'message',
2701
+ id: generateId('msg'),
2702
+ role: 'assistant',
2703
+ status: 'completed',
2704
+ phase: 'commentary',
2705
+ content: [normalizeOutputTextPart(text)],
2706
+ };
2707
+ if (at >= 0) this.output.splice(at, 1, messageItem);
2708
+ else this.output.push(messageItem);
2709
+ const outputIndex = this.output.indexOf(messageItem);
2710
+ events.push({
2711
+ type: 'response.output_item.added',
2712
+ sequence_number: this.nextSequence(),
2713
+ output_index: outputIndex,
2714
+ item: snapshotResponseItem({ ...messageItem, status: 'in_progress', content: [] }),
2715
+ });
2716
+ events.push({
2717
+ type: 'response.content_part.added',
2718
+ sequence_number: this.nextSequence(),
2719
+ output_index: outputIndex,
2720
+ content_index: 0,
2721
+ item_id: messageItem.id,
2722
+ part: snapshotResponsePart(normalizeOutputTextPart('')),
2723
+ });
2724
+ events.push({
2725
+ type: 'response.output_text.delta',
2726
+ sequence_number: this.nextSequence(),
2727
+ output_index: outputIndex,
2728
+ content_index: 0,
2729
+ item_id: messageItem.id,
2730
+ delta: text,
2731
+ logprobs: [],
2732
+ });
2733
+ events.push({
2734
+ type: 'response.output_text.done',
2735
+ sequence_number: this.nextSequence(),
2736
+ output_index: outputIndex,
2737
+ content_index: 0,
2738
+ item_id: messageItem.id,
2739
+ text,
2740
+ logprobs: [],
2741
+ });
2742
+ events.push({
2743
+ type: 'response.content_part.done',
2744
+ sequence_number: this.nextSequence(),
2745
+ output_index: outputIndex,
2746
+ content_index: 0,
2747
+ item_id: messageItem.id,
2748
+ part: snapshotResponsePart(messageItem.content[0]),
2749
+ });
2750
+ events.push({
2751
+ type: 'response.output_item.done',
2752
+ sequence_number: this.nextSequence(),
2753
+ output_index: outputIndex,
2754
+ item: snapshotResponseItem(messageItem),
1834
2755
  });
1835
2756
  }
1836
2757
  return events;
@@ -1839,33 +2760,24 @@ export class ResponsesStreamMapper {
1839
2760
  finalize(finishReason = 'stop', usage = null) {
1840
2761
  if (this.finalized) return [];
1841
2762
  this.finalized = true;
2763
+ this.expandParallelToolItems();
1842
2764
  const status = finishReason === 'length' ? 'incomplete' : 'completed';
1843
-
1844
- if (this.bufferOutputUntilDone) {
1845
- this.finishReason = finishReason;
1846
- this.usage = normalizeResponsesUsage(usage);
1847
- this.completedAt = Date.now() / 1000;
1848
-
1849
- if (this.reasoningItem) {
1850
- this.reasoningItem.status = status;
1851
- this.syncReasoningItemContent();
1852
- }
1853
- if (this.messageItem) {
1854
- this.messageItem.status = status;
1855
- if (!this.messageItem.content.length) {
1856
- this.messageItem.content.push(normalizeOutputTextPart(''));
1857
- }
2765
+ this.finishReason = finishReason;
2766
+ this.usage = normalizeResponsesUsage(usage);
2767
+ this.completedAt = Date.now() / 1000;
2768
+ const events = [];
2769
+ if (this.messageItem) {
2770
+ this.messageItem.status = status;
2771
+ const outputIndex = this.output.indexOf(this.messageItem);
2772
+ if (!this.messageItemClosed) {
2773
+ this.finalizeMessagePhase();
2774
+ if (!this.messageItem.content.length) this.messageItem.content.push(normalizeOutputTextPart(''));
1858
2775
  this.messageItem.content[0].text = this.text;
1859
2776
  }
1860
-
1861
- const events = [];
1862
- if (this.reasoningItem && this.bufferOutputUntilDone) {
1863
- events.push(...this.flushBufferedReasoning(status));
1864
- }
1865
- events.push(...this.closeReasoningItem(status));
1866
-
1867
- if (this.messageItem) {
1868
- const outputIndex = this.output.indexOf(this.messageItem);
2777
+ if (!this.messageItemAdded) {
2778
+ this.messageItemAdded = true;
2779
+ this.messageContentAdded = true;
2780
+ this.messageItemClosed = true;
1869
2781
  events.push({
1870
2782
  type: 'response.output_item.added',
1871
2783
  sequence_number: this.nextSequence(),
@@ -1895,36 +2807,22 @@ export class ResponsesStreamMapper {
1895
2807
  logprobs: [],
1896
2808
  });
1897
2809
  }
1898
- events.push({
1899
- type: 'response.output_text.done',
1900
- sequence_number: this.nextSequence(),
1901
- output_index: outputIndex,
1902
- content_index: 0,
1903
- item_id: this.messageItem.id,
1904
- text: this.text,
1905
- logprobs: [],
1906
- });
1907
- events.push({
1908
- type: 'response.content_part.done',
1909
- sequence_number: this.nextSequence(),
1910
- output_index: outputIndex,
1911
- content_index: 0,
1912
- item_id: this.messageItem.id,
1913
- part: snapshotResponsePart(this.messageItem.content[0]),
1914
- });
1915
- events.push({
1916
- type: 'response.output_item.done',
1917
- sequence_number: this.nextSequence(),
1918
- output_index: outputIndex,
1919
- item: snapshotResponseItem(this.messageItem),
1920
- });
2810
+ events.push(...this.messageDoneEvents(outputIndex));
2811
+ } else if (!this.messageItemClosed) {
2812
+ this.messageItemClosed = true;
2813
+ events.push(...this.messageDoneEvents(outputIndex));
1921
2814
  }
1922
-
1923
- for (const item of this.toolItems.values()) {
1924
- item.status = status;
1925
- if (item.type === 'tool_search_call') finalizeToolSearchCallItemArguments(item);
1926
- else normalizeFunctionCallItemArguments(item, this.toolSchemas, this.config);
1927
- const outputIndex = this.output.indexOf(item);
2815
+ }
2816
+ events.push(...this.closeReasoningItem(status));
2817
+ events.push(...this.convertBridgedCommentaryItems());
2818
+ for (const [index, item] of this.toolItems.entries()) {
2819
+ item.status = status;
2820
+ if (item.type === 'tool_search_call') finalizeToolSearchCallItemArguments(item);
2821
+ else if (item.type === 'custom_tool_call') item.input = customToolInputFromArguments(item.arguments);
2822
+ else normalizeFunctionCallItemArguments(item, this.toolSchemas, this.config);
2823
+ const outputIndex = this.output.indexOf(item);
2824
+ if (!this.toolItemsAdded.has(index)) {
2825
+ this.toolItemsAdded.add(index);
1928
2826
  events.push({
1929
2827
  type: 'response.output_item.added',
1930
2828
  sequence_number: this.nextSequence(),
@@ -1932,121 +2830,31 @@ export class ResponsesStreamMapper {
1932
2830
  item: snapshotResponseItem(item.type === 'tool_search_call' ? {
1933
2831
  ...item,
1934
2832
  status: 'in_progress',
2833
+ } : item.type === 'custom_tool_call' ? {
2834
+ ...item,
2835
+ status: 'in_progress',
2836
+ input: '',
1935
2837
  } : {
1936
2838
  ...item,
1937
2839
  status: 'in_progress',
1938
2840
  arguments: '',
1939
2841
  }),
1940
2842
  });
1941
- if (item.type === 'function_call' && item.arguments) {
1942
- events.push({
1943
- type: 'response.function_call_arguments.delta',
1944
- sequence_number: this.nextSequence(),
1945
- output_index: outputIndex,
1946
- item_id: item.id,
1947
- delta: item.arguments,
1948
- });
1949
- }
1950
- if (item.type === 'function_call') {
1951
- events.push({
1952
- type: 'response.function_call_arguments.done',
1953
- sequence_number: this.nextSequence(),
1954
- output_index: outputIndex,
1955
- item_id: item.id,
1956
- arguments: item.arguments,
1957
- });
1958
- }
1959
- events.push({
1960
- type: 'response.output_item.done',
1961
- sequence_number: this.nextSequence(),
1962
- output_index: outputIndex,
1963
- item: snapshotResponseItem(item),
1964
- });
1965
- }
1966
-
1967
- const response = createBaseResponse({
1968
- id: this.responseId,
1969
- model: this.model,
1970
- createdAt: this.createdAt,
1971
- status,
1972
- output: this.output,
1973
- previousResponseId: this.previousResponseId,
1974
- usage: normalizeResponsesUsage(usage),
1975
- normalized: this.normalized,
1976
- completedAt: Date.now() / 1000,
1977
- incompleteReason: finishReason === 'length' ? 'max_output_tokens' : null,
1978
- });
1979
- events.push({
1980
- type: status === 'incomplete' ? 'response.incomplete' : 'response.completed',
1981
- sequence_number: this.nextSequence(),
1982
- response,
1983
- });
1984
- return events;
1985
- }
1986
-
1987
- const events = [];
1988
- this.finishReason = finishReason;
1989
- this.usage = normalizeResponsesUsage(usage);
1990
- this.completedAt = Date.now() / 1000;
1991
- if (this.reasoningItem) {
1992
- this.reasoningItem.status = finishReason === 'length' ? 'incomplete' : 'completed';
1993
- if (this.emitReasoningSummary && this.reasoningText && !this.reasoningSummaryAdded) {
1994
- this.reasoningSummaryAdded = true;
1995
- this.reasoningItem.summary.push(normalizeSummaryTextPart(reasoningSummaryText(this.reasoningText)));
1996
2843
  }
1997
- this.syncReasoningItemContent();
1998
- if (this.reasoningItem.summary[0]) {
1999
- this.reasoningItem.summary[0].text = reasoningSummaryText(this.reasoningText);
2000
- }
2001
- }
2002
- if (this.messageItem) {
2003
- this.messageItem.status = finishReason === 'length' ? 'incomplete' : 'completed';
2004
- this.messageItem.content[0].text = this.text;
2005
- if (!this.messageContentAdded) {
2006
- this.messageContentAdded = true;
2844
+ if (item.type === 'function_call' && this.toolItemsBufferedArguments.has(index) && item.arguments) {
2007
2845
  events.push({
2008
- type: 'response.content_part.added',
2846
+ type: 'response.function_call_arguments.delta',
2009
2847
  sequence_number: this.nextSequence(),
2010
- output_index: this.output.indexOf(this.messageItem),
2011
- content_index: 0,
2012
- item_id: this.messageItem.id,
2013
- part: snapshotResponsePart(this.messageItem.content[0]),
2848
+ output_index: outputIndex,
2849
+ item_id: item.id,
2850
+ delta: item.arguments,
2014
2851
  });
2015
2852
  }
2016
- events.push({
2017
- type: 'response.output_text.done',
2018
- sequence_number: this.nextSequence(),
2019
- output_index: this.output.indexOf(this.messageItem),
2020
- content_index: 0,
2021
- item_id: this.messageItem.id,
2022
- text: this.text,
2023
- logprobs: [],
2024
- });
2025
- events.push({
2026
- type: 'response.content_part.done',
2027
- sequence_number: this.nextSequence(),
2028
- output_index: this.output.indexOf(this.messageItem),
2029
- content_index: 0,
2030
- item_id: this.messageItem.id,
2031
- part: snapshotResponsePart(this.messageItem.content[0]),
2032
- });
2033
- events.push({
2034
- type: 'response.output_item.done',
2035
- sequence_number: this.nextSequence(),
2036
- output_index: this.output.indexOf(this.messageItem),
2037
- item: snapshotResponseItem(this.messageItem),
2038
- });
2039
- }
2040
- events.push(...this.closeReasoningItem(finishReason === 'length' ? 'incomplete' : 'completed'));
2041
- for (const item of this.toolItems.values()) {
2042
- item.status = finishReason === 'length' ? 'incomplete' : 'completed';
2043
- if (item.type === 'tool_search_call') finalizeToolSearchCallItemArguments(item);
2044
- else normalizeFunctionCallItemArguments(item, this.toolSchemas, this.config);
2045
2853
  if (item.type === 'function_call') {
2046
2854
  events.push({
2047
2855
  type: 'response.function_call_arguments.done',
2048
2856
  sequence_number: this.nextSequence(),
2049
- output_index: this.output.indexOf(item),
2857
+ output_index: outputIndex,
2050
2858
  item_id: item.id,
2051
2859
  arguments: item.arguments,
2052
2860
  });
@@ -2054,7 +2862,7 @@ export class ResponsesStreamMapper {
2054
2862
  events.push({
2055
2863
  type: 'response.output_item.done',
2056
2864
  sequence_number: this.nextSequence(),
2057
- output_index: this.output.indexOf(item),
2865
+ output_index: outputIndex,
2058
2866
  item: snapshotResponseItem(item),
2059
2867
  });
2060
2868
  }
@@ -2078,93 +2886,130 @@ export class ResponsesStreamMapper {
2078
2886
  return events;
2079
2887
  }
2080
2888
 
2081
- assistantMessage() {
2082
- return assistantMessageFromResponseOutput(this.output);
2083
- }
2084
-
2085
- flushBufferedReasoning(status = 'completed') {
2086
- if (!this.reasoningItem || this.reasoningItemDone) return [];
2087
- const events = [];
2088
- const outputIndex = this.output.indexOf(this.reasoningItem);
2089
- const summaryText = reasoningSummaryText(this.reasoningText);
2090
- if (!this.reasoningItemAdded) {
2091
- this.reasoningItemAdded = true;
2092
- events.push({
2093
- type: 'response.output_item.added',
2889
+ messageDoneEvents(outputIndex) {
2890
+ return [
2891
+ {
2892
+ type: 'response.output_text.done',
2094
2893
  sequence_number: this.nextSequence(),
2095
2894
  output_index: outputIndex,
2096
- item: snapshotResponseItem({
2097
- ...this.reasoningItem,
2098
- status: 'in_progress',
2099
- summary: [],
2100
- content: this.emitReasoningText ? [normalizeReasoningTextPart('')] : [],
2101
- }),
2102
- });
2103
- }
2104
- if (this.emitReasoningText) {
2105
- this.reasoningContentAdded = true;
2106
- events.push({
2107
- type: 'response.content_part.added',
2895
+ content_index: 0,
2896
+ item_id: this.messageItem.id,
2897
+ text: this.messageItem.content[0].text,
2898
+ logprobs: [],
2899
+ },
2900
+ {
2901
+ type: 'response.content_part.done',
2108
2902
  sequence_number: this.nextSequence(),
2109
2903
  output_index: outputIndex,
2110
2904
  content_index: 0,
2111
- item_id: this.reasoningItem.id,
2112
- part: snapshotResponsePart(normalizeReasoningTextPart('')),
2113
- });
2114
- if (this.reasoningText) {
2115
- for (const delta of splitBufferedReasoningDeltas(this.reasoningText)) {
2116
- events.push({
2117
- type: 'response.reasoning_text.delta',
2118
- sequence_number: this.nextSequence(),
2119
- output_index: outputIndex,
2120
- item_id: this.reasoningItem.id,
2121
- content_index: 0,
2122
- delta,
2123
- });
2124
- }
2125
- }
2126
- }
2127
- if (this.emitReasoningSummary && !this.reasoningSummaryAdded) {
2128
- this.reasoningSummaryAdded = true;
2129
- this.reasoningItem.summary = [normalizeSummaryTextPart(summaryText)];
2130
- events.push({
2131
- type: 'response.reasoning_summary_part.added',
2905
+ item_id: this.messageItem.id,
2906
+ part: snapshotResponsePart(this.messageItem.content[0]),
2907
+ },
2908
+ {
2909
+ type: 'response.output_item.done',
2132
2910
  sequence_number: this.nextSequence(),
2133
2911
  output_index: outputIndex,
2134
- item_id: this.reasoningItem.id,
2135
- summary_index: 0,
2136
- part: snapshotResponsePart(normalizeSummaryTextPart('')),
2137
- });
2138
- for (const delta of splitBufferedReasoningDeltas(summaryText)) {
2139
- events.push({
2140
- type: 'response.reasoning_summary_text.delta',
2141
- sequence_number: this.nextSequence(),
2142
- output_index: outputIndex,
2143
- item_id: this.reasoningItem.id,
2144
- summary_index: 0,
2145
- delta,
2146
- });
2147
- }
2148
- this.streamedReasoningSummaryText = summaryText;
2149
- } else if (this.emitReasoningSummary && this.reasoningItem.summary[0]) {
2150
- this.reasoningItem.summary[0].text = summaryText;
2912
+ item: snapshotResponseItem(this.messageItem),
2913
+ },
2914
+ ];
2915
+ }
2916
+
2917
+ assistantMessage() {
2918
+ return assistantMessageFromResponseOutput(this.output);
2919
+ }
2920
+
2921
+ markRoundStart() {
2922
+ this.roundStartIndex = this.output.length;
2923
+ }
2924
+
2925
+ roundAssistantMessage() {
2926
+ return assistantMessageFromResponseOutput(this.output.slice(this.roundStartIndex));
2927
+ }
2928
+
2929
+ removeToolItems(predicate = () => true) {
2930
+ for (const [index, item] of [...this.toolItems.entries()]) {
2931
+ if (!predicate(item)) continue;
2932
+ if (this.toolItemsAdded.has(index)) continue;
2933
+ this.toolItems.delete(index);
2934
+ this.toolItemsBufferedArguments.delete(index);
2935
+ this.toolItemsStreamedArgumentLengths.delete(index);
2936
+ const at = this.output.indexOf(item);
2937
+ if (at >= 0) this.output.splice(at, 1);
2151
2938
  }
2152
- this.reasoningItem.status = status;
2153
- this.syncReasoningItemContent();
2939
+ }
2940
+
2941
+ beginNextRound() {
2942
+ const events = [...this.closeReasoningItem('completed'), ...this.closeMessageItemAsCommentary('completed')];
2943
+ if (this.reasoningItem && !this.reasoningItemAdded) {
2944
+ const at = this.output.indexOf(this.reasoningItem);
2945
+ if (at >= 0) this.output.splice(at, 1);
2946
+ }
2947
+ if (this.messageItem && !this.messageItemAdded) {
2948
+ const at = this.output.indexOf(this.messageItem);
2949
+ if (at >= 0) this.output.splice(at, 1);
2950
+ }
2951
+ for (const [index, item] of this.toolItems.entries()) {
2952
+ if (this.toolItemsAdded.has(index)) continue;
2953
+ const at = this.output.indexOf(item);
2954
+ if (at >= 0) this.output.splice(at, 1);
2955
+ }
2956
+ this.messageItem = null;
2957
+ this.reasoningItem = null;
2958
+ this.toolItems = new Map();
2959
+ this.toolItemsAdded = new Set();
2960
+ this.toolItemsBufferedArguments = new Set();
2961
+ this.toolItemsStreamedArgumentLengths = new Map();
2962
+ this.text = '';
2963
+ this.reasoningText = '';
2964
+ this.reasoningItemDone = false;
2965
+ this.messageItemAdded = false;
2966
+ this.messageContentAdded = false;
2967
+ this.messageItemClosed = false;
2968
+ this.holdVisibleText = false;
2969
+ this.reasoningContentAdded = false;
2970
+ this.reasoningSummaryAdded = false;
2971
+ this.reasoningItemAdded = false;
2972
+ this.streamedReasoningSummaryText = '';
2973
+ this.pendingFinishReason = null;
2974
+ this.pendingUsage = null;
2154
2975
  return events;
2155
2976
  }
2156
2977
 
2978
+ replaceBufferedAssistantText(text) {
2979
+ if (this.messageItemAdded || this.messageContentAdded) return null;
2980
+ if (!this.messageItem) return this.textDelta(String(text ?? ''));
2981
+ this.text = String(text ?? '');
2982
+ if (this.messageItem.content.length) this.messageItem.content[0].text = this.text;
2983
+ return [];
2984
+ }
2985
+
2157
2986
  closeReasoningItem(status = 'completed') {
2158
2987
  if (!this.reasoningItem || this.reasoningItemDone) return [];
2159
2988
  this.reasoningItemDone = true;
2160
2989
  this.reasoningItem.status = status;
2161
2990
  this.syncReasoningItemContent();
2162
- const summaryText = reasoningSummaryText(this.reasoningText);
2163
- if (this.emitReasoningSummary && this.reasoningText && !this.reasoningSummaryAdded) {
2164
- this.reasoningSummaryAdded = true;
2165
- this.reasoningItem.summary.push(normalizeSummaryTextPart(summaryText));
2166
- }
2991
+ this.reasoningItem.encrypted_content = encodeGatewayReasoning(normalizeReasoningContent(this.reasoningText));
2167
2992
  const events = [];
2993
+ if (!this.reasoningItemAdded) {
2994
+ this.reasoningItemAdded = true;
2995
+ const addedItem = {
2996
+ ...this.reasoningItem,
2997
+ status: 'in_progress',
2998
+ summary: [],
2999
+ content: this.emitReasoningText ? [normalizeReasoningTextPart('')] : [],
3000
+ encrypted_content: null,
3001
+ };
3002
+ delete addedItem.reasoning_content;
3003
+ events.push({
3004
+ type: 'response.output_item.added',
3005
+ sequence_number: this.nextSequence(),
3006
+ output_index: this.output.indexOf(this.reasoningItem),
3007
+ item: snapshotResponseItem(addedItem),
3008
+ });
3009
+ }
3010
+ if (this.emitReasoningSummary && this.reasoningText) {
3011
+ this.appendReasoningSummaryDelta(events, { final: true });
3012
+ }
2168
3013
  if (this.reasoningSummaryAdded) {
2169
3014
  for (const [summaryIndex, part] of this.reasoningItem.summary.entries()) {
2170
3015
  events.push({
@@ -2212,11 +3057,35 @@ export class ResponsesStreamMapper {
2212
3057
  return events;
2213
3058
  }
2214
3059
 
3060
+ streamFailed(message) {
3061
+ if (this.finalized) return [];
3062
+ this.finalized = true;
3063
+ this.completedAt = Date.now() / 1000;
3064
+ const response = this.response('failed');
3065
+ response.error = { code: 'upstream_error', message: String(message || 'upstream stream failed') };
3066
+ return [{
3067
+ type: 'response.failed',
3068
+ sequence_number: this.nextSequence(),
3069
+ response,
3070
+ }];
3071
+ }
3072
+
2215
3073
  mapChatEvent(event) {
2216
3074
  if (!event) return [];
2217
- if (event.done) return this.finalize(this.finishReason || 'stop', this.usage);
2218
- const payload = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
3075
+ if (event.done) {
3076
+ if (this.pendingFinishReason || !event.eof) {
3077
+ return this.finalize(this.pendingFinishReason || 'stop', this.pendingUsage);
3078
+ }
3079
+ return this.streamFailed('upstream stream ended before completion');
3080
+ }
3081
+ let payload = event.data;
3082
+ if (typeof payload === 'string') {
3083
+ const parsed = safeJsonParse(payload);
3084
+ if (!parsed.ok) return [];
3085
+ payload = parsed.value;
3086
+ }
2219
3087
  if (!isObject(payload)) return [];
3088
+ if (isObject(payload.usage)) this.pendingUsage = payload.usage;
2220
3089
  const choice = Array.isArray(payload.choices) ? payload.choices[0] : null;
2221
3090
  if (!choice) return [];
2222
3091
  const delta = choice.delta || choice.message || {};
@@ -2236,7 +3105,7 @@ export class ResponsesStreamMapper {
2236
3105
  }
2237
3106
  }
2238
3107
  if (choice.finish_reason) {
2239
- events.push(...this.finalize(choice.finish_reason, payload.usage ?? null));
3108
+ this.pendingFinishReason = choice.finish_reason;
2240
3109
  }
2241
3110
  return events;
2242
3111
  }