@galaxy-yearn/codex-deepseek-gateway 0.1.7 → 0.2.1

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,8 +1,8 @@
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
- DEFAULT_MODEL_ALIASES,
4
5
  deepseekReasoningPayload,
5
- isDeprecatedModel,
6
6
  resolveModelAlias,
7
7
  } from './model-map.js';
8
8
 
@@ -33,23 +33,50 @@ const TOOL_OUTPUT_TYPES = new Set([
33
33
  'image_generation_call_output',
34
34
  ]);
35
35
  const CHAT_HISTORY_IGNORED_TOOL_ITEM_TYPES = new Set([
36
- 'web_search_call',
37
36
  'web_search_call_output',
38
- 'tool_search_call',
39
- 'tool_search_output',
40
37
  ]);
41
38
  const EMULATED_HOSTED_TOOL_TYPES = new Set(['web_search', 'web_search_preview']);
39
+ const BRIDGED_RESPONSES_TOOL_TYPES = new Set(['tool_search']);
40
+ const UNSUPPORTED_HOSTED_TOOL_TYPES = new Set([
41
+ 'file_search',
42
+ 'code_interpreter',
43
+ 'image_generation',
44
+ 'computer',
45
+ 'computer_use',
46
+ 'mcp',
47
+ 'local_shell',
48
+ ]);
42
49
  const DEEPSEEK_TOOL_INSTRUCTIONS_MARKER = 'The tools in this request are real callable functions available now.';
43
50
  const DEEPSEEK_TOOL_INSTRUCTIONS = [
44
51
  DEEPSEEK_TOOL_INSTRUCTIONS_MARKER,
45
- 'When a tool is needed, respond with Chat Completions tool_calls and JSON arguments matching the schema.',
46
- 'Do not write pseudo XML, DSML, or JSON tool calls in assistant text.',
47
- 'Do not claim a listed function is unavailable because its name is unfamiliar.',
52
+ 'When needed, use Chat Completions tool_calls with JSON arguments that match the schema.',
53
+ 'Never write tool calls in assistant text, including as XML, DSML, or JSON, or claim a listed function is unavailable merely because its name is unfamiliar.',
48
54
  ].join(' ');
49
- const DEEPSEEK_TOOL_DESCRIPTION_CHARS = 220;
50
- const DEEPSEEK_SCHEMA_DESCRIPTION_CHARS = 160;
51
- const CODEX_REASONING_EFFORTS = ['low', 'medium', 'high', 'xhigh'];
52
- const CODEX_SPAWN_AGENT_TOOL_NAME = 'multi_agent_v1__spawn_agent';
55
+ export const INTERNAL_COMMENTARY_TOOL = 'commentary';
56
+ const DEEPSEEK_COMMENTARY_INSTRUCTIONS = 'The user cannot see your thinking; commentary is the progress they see during tool work. In every tool-calling reply, include one commentary call first in the tool_calls array with a one- or two-sentence update. Never call it alone or use it for the final answer.';
57
+ const INTERNAL_COMMENTARY_TOOL_DEFINITION = {
58
+ type: 'function',
59
+ function: {
60
+ name: INTERNAL_COMMENTARY_TOOL,
61
+ description: 'Post the user-visible progress update. Include this function first in every tool_calls array with other tool calls. One or two sentences; never call it alone or use it for the final answer.',
62
+ parameters: {
63
+ type: 'object',
64
+ properties: {
65
+ text: {
66
+ type: 'string',
67
+ description: 'One or two short sentences on what you are doing or just found.',
68
+ },
69
+ },
70
+ required: ['text'],
71
+ additionalProperties: false,
72
+ },
73
+ },
74
+ };
75
+ const CHAT_FUNCTION_NAME_MAX_CHARS = 64;
76
+ const TOOL_NAME_HASH_CHARS = 8;
77
+ const DEEPSEEK_TOOL_DESCRIPTION_SOFT_CHARS = 900;
78
+ const DEEPSEEK_SCHEMA_DESCRIPTION_SOFT_CHARS = 480;
79
+ const DEEPSEEK_MAX_FUNCTION_TOOLS = 128;
53
80
  const CODEX_TOOL_SEARCH_TOOL_NAME = 'tool_search';
54
81
 
55
82
  function jsonString(value) {
@@ -57,23 +84,27 @@ function jsonString(value) {
57
84
  return JSON.stringify(value ?? {});
58
85
  }
59
86
 
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)
87
+ function sanitizeFunctionName(name, fallback = 'tool_call', maxChars = CHAT_FUNCTION_NAME_MAX_CHARS) {
88
+ const fallbackName = String(fallback || 'tool_call')
89
+ .replace(/[^a-zA-Z0-9_-]/g, '_')
90
+ .replace(/^_+/, '')
91
+ .slice(0, maxChars) || 'tool_call';
92
+ const candidate = String(name || fallbackName)
73
93
  .replace(/[^a-zA-Z0-9_-]/g, '_')
74
94
  .replace(/^_+/, '')
75
- .slice(0, 64);
76
- return candidate || fallback;
95
+ .slice(0, maxChars);
96
+ return candidate || fallbackName;
97
+ }
98
+
99
+ function stableToolNameHash(parts) {
100
+ const hashInput = parts.map((part) => String(part ?? '')).join('\0');
101
+ return createHash('sha256').update(hashInput).digest('hex').slice(0, TOOL_NAME_HASH_CHARS);
102
+ }
103
+
104
+ function appendToolNameHash(name, hash, fallback = 'tool_call') {
105
+ const suffix = `__${hash}`;
106
+ const headChars = Math.max(1, CHAT_FUNCTION_NAME_MAX_CHARS - suffix.length);
107
+ return `${sanitizeFunctionName(name, fallback, headChars)}${suffix}`;
77
108
  }
78
109
 
79
110
  function toolNamespace(tool) {
@@ -88,22 +119,42 @@ function toolBaseName(tool, fallback = 'tool_call') {
88
119
  }
89
120
 
90
121
  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);
122
+ const rawNamespace = String(namespace || '').trim();
123
+ const rawName = String(name || fallback);
124
+ const baseName = sanitizeFunctionName(rawName, fallback);
125
+ const encodedNamespace = rawNamespace ? sanitizeFunctionName(rawNamespace, 'namespace') : '';
126
+ const candidate = encodedNamespace ? `${encodedNamespace}__${baseName}` : baseName;
127
+ const encoded = sanitizeFunctionName(candidate, fallback);
128
+ const rawCandidate = rawNamespace ? `${rawNamespace}__${rawName}` : rawName;
129
+ if (encoded === rawCandidate && rawCandidate.length <= CHAT_FUNCTION_NAME_MAX_CHARS) {
130
+ return encoded;
131
+ }
132
+ return appendToolNameHash(candidate, stableToolNameHash([rawNamespace, rawName, fallback]), fallback);
95
133
  }
96
134
 
97
135
  function decodedToolName(name, toolNames) {
98
136
  const value = String(name || '');
99
137
  const known = toolNames?.get(value);
100
- return known ? { ...known } : { name: value };
138
+ if (!known) return { name: value };
139
+ return omitUndefined({
140
+ namespace: known.namespace,
141
+ name: known.original_name || known.name,
142
+ });
101
143
  }
102
144
 
103
145
  function isCodexToolSearchTool(name) {
104
146
  return String(name || '') === CODEX_TOOL_SEARCH_TOOL_NAME;
105
147
  }
106
148
 
149
+ function toolChoiceDisablesTools(toolChoice) {
150
+ if (String(toolChoice || '').toLowerCase() === 'none') return true;
151
+ return isObject(toolChoice) && String(toolChoice.type || '').toLowerCase() === 'none';
152
+ }
153
+
154
+ function hasChatToolCalls(message) {
155
+ return Array.isArray(message?.tool_calls) && message.tool_calls.length > 0;
156
+ }
157
+
107
158
  function omitUndefined(value) {
108
159
  if (!isObject(value)) return value;
109
160
  const result = {};
@@ -117,13 +168,31 @@ function compactText(value) {
117
168
  return String(value ?? '').replace(/\s+/g, ' ').trim();
118
169
  }
119
170
 
171
+ function compactStructuredText(value, softChars) {
172
+ const lines = String(value ?? '').replace(/\r\n?/g, '\n').split('\n');
173
+ while (lines.length && !lines[0].trim()) lines.shift();
174
+ while (lines.length && !lines.at(-1).trim()) lines.pop();
175
+ const structured = lines.join('\n').replace(/\n(?:[ \t]*\n){2,}/g, '\n\n');
176
+ if (!structured) return '';
177
+ const compact = compactText(structured);
178
+ return compact.length <= softChars ? compact : structured;
179
+ }
180
+
120
181
  function shortenText(value, maxChars) {
121
182
  const text = compactText(value);
122
183
  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()}...`;
184
+ if (maxChars <= 16) return `${text.slice(0, Math.max(0, maxChars - 3)).trimEnd()}...`;
185
+ const marker = ' ... ';
186
+ const contentChars = maxChars - marker.length;
187
+ const headChars = Math.max(24, Math.floor(contentChars * 0.72));
188
+ const tailChars = Math.max(12, contentChars - headChars);
189
+ const headSlice = text.slice(0, headChars);
190
+ const headBreak = Math.max(headSlice.lastIndexOf('. '), headSlice.lastIndexOf('; '), headSlice.lastIndexOf(', '), headSlice.lastIndexOf(' '));
191
+ const head = headSlice.slice(0, headBreak >= Math.floor(headChars * 0.55) ? headBreak : headSlice.length).trimEnd();
192
+ const tailSlice = text.slice(-tailChars);
193
+ const tailBreak = Math.max(tailSlice.indexOf('. '), tailSlice.indexOf('; '), tailSlice.indexOf(', '), tailSlice.indexOf(' '));
194
+ const tail = tailSlice.slice(tailBreak >= 0 && tailBreak <= Math.floor(tailChars * 0.45) ? tailBreak + 1 : 0).trimStart();
195
+ return `${head}${marker}${tail}`.slice(0, maxChars);
127
196
  }
128
197
 
129
198
  function textPart(text) {
@@ -246,9 +315,29 @@ function chatToolCallFromResponseItem(item) {
246
315
  };
247
316
  }
248
317
 
318
+ const GATEWAY_ENCRYPTED_REASONING_PREFIX = 'dsgw1:';
319
+
320
+ function encodeGatewayReasoning(text) {
321
+ const value = String(text ?? '');
322
+ if (!value) return null;
323
+ return `${GATEWAY_ENCRYPTED_REASONING_PREFIX}${Buffer.from(value, 'utf8').toString('base64')}`;
324
+ }
325
+
326
+ function decodeGatewayReasoning(encryptedContent) {
327
+ if (typeof encryptedContent !== 'string') return '';
328
+ if (!encryptedContent.startsWith(GATEWAY_ENCRYPTED_REASONING_PREFIX)) return '';
329
+ try {
330
+ return Buffer.from(encryptedContent.slice(GATEWAY_ENCRYPTED_REASONING_PREFIX.length), 'base64').toString('utf8');
331
+ } catch {
332
+ return '';
333
+ }
334
+ }
335
+
249
336
  function reasoningTextFromItem(item) {
250
337
  if (!isObject(item)) return '';
251
338
  if (typeof item.reasoning_content === 'string') return normalizeReasoningContent(item.reasoning_content);
339
+ const decoded = decodeGatewayReasoning(item.encrypted_content);
340
+ if (decoded) return normalizeReasoningContent(decoded);
252
341
  const rawParts = [];
253
342
  if (typeof item.text === 'string') rawParts.push(item.text);
254
343
  if (typeof item.content === 'string') rawParts.push(item.content);
@@ -281,7 +370,7 @@ function normalizeReasoningDisplayText(text) {
281
370
  .replace(/^[ \t]{0,3}(?:`{3,}|~{3,}).*$/, '')
282
371
  .replace(/^[ \t]{0,3}#{1,6}[ \t]+/, '')
283
372
  .replace(/^[ \t]{0,3}>[ \t]?/, '')
284
- .replace(/^[ \t]*[-*+][ \t]+(?:\[[ xX]\][ \t]+)?/, '\u2022 ')
373
+ .replace(/^[ \t]*[-*+][ \t]+(?:\[[ xX]\][ \t]+)?/, ' ')
285
374
  .replace(/^[ \t]*(\d+)\.[ \t]+/, '$1) ')
286
375
  .replace(/!\[([^\]]*)\]\([^)]+\)/g, '$1')
287
376
  .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
@@ -299,7 +388,36 @@ function reasoningDisplayText(text) {
299
388
  return normalizeReasoningDisplayText(text);
300
389
  }
301
390
 
391
+ function compactToolSearchOutputTool(tool) {
392
+ if (!isObject(tool)) return null;
393
+ const namespace = toolNamespace(tool);
394
+ const name = toolBaseName(tool, '');
395
+ if (!name) return null;
396
+ const schema = normalizeJsonSchemaObject(tool.function?.parameters ?? tool.parameters ?? tool.input_schema);
397
+ const properties = Object.keys(isObject(schema.properties) ? schema.properties : {});
398
+ const required = Array.isArray(schema.required) ? schema.required.map(String) : [];
399
+ const description = tool.function?.description ?? tool.description ?? '';
400
+ return omitUndefined({
401
+ name: encodeToolName(namespace, name),
402
+ description: description ? shortenText(description, 220) : undefined,
403
+ required: required.length ? required : undefined,
404
+ properties: properties.length ? properties : undefined,
405
+ });
406
+ }
407
+
302
408
  function toolOutputContent(item) {
409
+ if (item?.type === 'tool_search_output' && Array.isArray(item.tools)) {
410
+ const tools = expandTools(item.tools)
411
+ .map(compactToolSearchOutputTool)
412
+ .filter(Boolean);
413
+ return JSON.stringify(omitUndefined({
414
+ status: item.status || 'completed',
415
+ note: tools.length
416
+ ? 'Discovered tools are loaded into the current tool list and can be called directly by these names.'
417
+ : undefined,
418
+ discovered_tools: tools,
419
+ }));
420
+ }
303
421
  const output = item.output ?? item.content ?? item.result ?? item.error ?? '';
304
422
  if (typeof output === 'string') return output;
305
423
  if (Array.isArray(output)) return contentToChatContent(output);
@@ -330,46 +448,127 @@ function normalizeMessage(message) {
330
448
  return normalized;
331
449
  }
332
450
 
451
+ function assistantMessageIsEmpty(message) {
452
+ if (hasChatToolCalls(message)) return false;
453
+ const content = message?.content;
454
+ if (typeof content === 'string') return content.length === 0;
455
+ if (Array.isArray(content)) return content.length === 0;
456
+ return content == null;
457
+ }
458
+
459
+ function webSearchCallNote(item) {
460
+ const action = isObject(item?.action) ? item.action : {};
461
+ const actionType = action.type || 'search';
462
+ if (actionType === 'open_page') {
463
+ return action.url ? `Opened page ${action.url}.` : 'Opened a web page.';
464
+ }
465
+ if (actionType === 'find_in_page') {
466
+ const target = action.url || 'a web page';
467
+ return action.query ? `Searched within ${target} for "${action.query}".` : `Searched within ${target}.`;
468
+ }
469
+ const query = action.query ? `"${action.query}"` : 'the web';
470
+ const sources = (Array.isArray(action.sources) ? action.sources : [])
471
+ .filter(isObject)
472
+ .slice(0, 3)
473
+ .map((source) => (source.title && source.url ? `${source.title} <${source.url}>` : source.url || source.title))
474
+ .filter(Boolean)
475
+ .join('; ');
476
+ return `Searched the web for ${query}${sources ? ` (sources: ${sources})` : ''}.`;
477
+ }
478
+
333
479
  function extractMessagesFromResponsesInput(input) {
334
480
  if (Array.isArray(input)) {
335
481
  const messages = [];
336
482
  let pendingUserContent = [];
337
483
  let pendingReasoningContent = '';
338
484
  let pendingAssistantToolMessage = null;
485
+ let pendingAssistantMessage = null;
486
+ let pendingWebSearchNotes = [];
339
487
  const flushPendingUserContent = () => {
340
488
  if (!pendingUserContent.length) return;
341
489
  messages.push({ role: 'user', content: contentToChatContent(pendingUserContent) });
342
490
  pendingUserContent = [];
343
491
  };
492
+ const flushPendingWebSearchNotes = () => {
493
+ if (!pendingWebSearchNotes.length) return;
494
+ messages.push({
495
+ role: 'assistant',
496
+ content: `[Earlier web activity] ${pendingWebSearchNotes.join(' ')}`,
497
+ });
498
+ pendingWebSearchNotes = [];
499
+ };
344
500
  const flushPendingAssistantToolMessage = () => {
345
501
  if (!pendingAssistantToolMessage) return;
346
502
  messages.push(pendingAssistantToolMessage);
347
503
  pendingAssistantToolMessage = null;
348
504
  pendingReasoningContent = '';
349
505
  };
506
+ const flushPendingAssistantMessage = () => {
507
+ if (!pendingAssistantMessage) return;
508
+ const message = pendingAssistantMessage;
509
+ pendingAssistantMessage = null;
510
+ if (assistantMessageIsEmpty(message)) {
511
+ if (message.reasoning_content && !pendingReasoningContent) {
512
+ pendingReasoningContent = message.reasoning_content;
513
+ }
514
+ return;
515
+ }
516
+ messages.push(message);
517
+ };
518
+ const flushAssistantState = () => {
519
+ flushPendingAssistantToolMessage();
520
+ flushPendingAssistantMessage();
521
+ };
350
522
  const ensurePendingAssistantToolMessage = () => {
351
523
  if (!pendingAssistantToolMessage) {
352
- pendingAssistantToolMessage = {
353
- role: 'assistant',
354
- content: '',
355
- tool_calls: [],
356
- };
357
- if (pendingReasoningContent) {
524
+ if (pendingAssistantMessage) {
525
+ pendingAssistantToolMessage = pendingAssistantMessage;
526
+ pendingAssistantMessage = null;
527
+ if (!Array.isArray(pendingAssistantToolMessage.tool_calls)) {
528
+ pendingAssistantToolMessage.tool_calls = [];
529
+ }
530
+ } else {
531
+ pendingAssistantToolMessage = {
532
+ role: 'assistant',
533
+ content: '',
534
+ tool_calls: [],
535
+ };
536
+ }
537
+ if (!pendingAssistantToolMessage.reasoning_content && pendingReasoningContent) {
358
538
  pendingAssistantToolMessage.reasoning_content = pendingReasoningContent;
359
539
  }
360
540
  }
361
541
  return pendingAssistantToolMessage;
362
542
  };
543
+ const pushRoleMessage = (item) => {
544
+ flushPendingUserContent();
545
+ flushPendingAssistantToolMessage();
546
+ flushPendingWebSearchNotes();
547
+ flushPendingAssistantMessage();
548
+ const normalized = normalizeMessage(item);
549
+ if (!normalized) return;
550
+ if (normalized.role === 'assistant' && !normalized.reasoning_content && pendingReasoningContent) {
551
+ normalized.reasoning_content = pendingReasoningContent;
552
+ pendingReasoningContent = '';
553
+ }
554
+ if (normalized.role === 'assistant' && !hasChatToolCalls(normalized)) {
555
+ pendingAssistantMessage = normalized;
556
+ return;
557
+ }
558
+ messages.push(normalized);
559
+ };
363
560
 
364
561
  for (const item of input) {
365
562
  if (typeof item === 'string') {
366
- flushPendingAssistantToolMessage();
563
+ flushAssistantState();
564
+ flushPendingWebSearchNotes();
367
565
  pendingUserContent.push({ type: 'input_text', text: item });
368
566
  continue;
369
567
  }
370
568
  if (!isObject(item)) continue;
371
569
  if (isInputContentPart(item)) {
372
- flushPendingAssistantToolMessage();
570
+ flushAssistantState();
571
+ flushPendingWebSearchNotes();
373
572
  pendingUserContent.push(item);
374
573
  continue;
375
574
  }
@@ -378,45 +577,40 @@ function extractMessagesFromResponsesInput(input) {
378
577
  continue;
379
578
  }
380
579
  if (item.type === 'message' && item.role) {
580
+ pushRoleMessage(item);
581
+ continue;
582
+ }
583
+ if (item.type === 'web_search_call') {
381
584
  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);
585
+ flushAssistantState();
586
+ pendingWebSearchNotes.push(webSearchCallNote(item));
389
587
  continue;
390
588
  }
391
589
  if (CHAT_HISTORY_IGNORED_TOOL_ITEM_TYPES.has(item.type)) {
392
590
  flushPendingUserContent();
393
- flushPendingAssistantToolMessage();
591
+ flushAssistantState();
394
592
  continue;
395
593
  }
396
594
  if (TOOL_OUTPUT_TYPES.has(item.type) || String(item.type || '').endsWith('_call_output')) {
397
595
  flushPendingUserContent();
398
- flushPendingAssistantToolMessage();
596
+ flushPendingWebSearchNotes();
597
+ flushAssistantState();
399
598
  messages.push(responseToolOutputToMessage(item));
400
599
  continue;
401
600
  }
402
601
  if (TOOL_CALL_TYPES.has(item.type) || String(item.type || '').endsWith('_call')) {
403
602
  flushPendingUserContent();
603
+ flushPendingWebSearchNotes();
404
604
  ensurePendingAssistantToolMessage().tool_calls.push(chatToolCallFromResponseItem(item));
405
605
  continue;
406
606
  }
407
607
  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);
608
+ pushRoleMessage(item);
416
609
  }
417
610
  }
418
611
  flushPendingUserContent();
419
- flushPendingAssistantToolMessage();
612
+ flushAssistantState();
613
+ flushPendingWebSearchNotes();
420
614
  return messages;
421
615
  }
422
616
 
@@ -442,14 +636,145 @@ function extractMessagesFromResponsesInput(input) {
442
636
  return [];
443
637
  }
444
638
 
639
+ 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.';
640
+
641
+ function truncateRawText(value, maxChars) {
642
+ const text = String(value ?? '');
643
+ if (text.length <= maxChars) return text;
644
+ return `${text.slice(0, Math.max(0, maxChars - 4))}\n...`;
645
+ }
646
+
647
+ function customToolShimParameters() {
648
+ return {
649
+ type: 'object',
650
+ properties: {
651
+ input: {
652
+ type: 'string',
653
+ description: 'Complete raw input text for this tool.',
654
+ },
655
+ },
656
+ required: ['input'],
657
+ additionalProperties: false,
658
+ };
659
+ }
660
+
661
+ function customToolShim(tool) {
662
+ const format = isObject(tool.format) ? tool.format : {};
663
+ const baseName = toolBaseName(tool, 'custom_tool');
664
+ const chunks = [
665
+ compactStructuredText(tool.description, DEEPSEEK_TOOL_DESCRIPTION_SOFT_CHARS) || `Freeform tool ${baseName}.`,
666
+ CUSTOM_TOOL_INPUT_HINT,
667
+ ];
668
+ if (format.syntax) chunks.push(`Input syntax: ${format.syntax}.`);
669
+ if (typeof format.definition === 'string' && format.definition.trim()) {
670
+ chunks.push(`Input grammar:\n${format.definition.trim()}`);
671
+ }
672
+ const parameters = customToolShimParameters();
673
+ const sourceSchema = isObject(tool.input_schema) ? tool.input_schema : isObject(tool.parameters) ? tool.parameters : null;
674
+ const sourceInputDescription = sourceSchema?.properties?.input?.description;
675
+ if (typeof sourceInputDescription === 'string' && sourceInputDescription) {
676
+ parameters.properties.input.description = compactStructuredText(sourceInputDescription, DEEPSEEK_SCHEMA_DESCRIPTION_SOFT_CHARS);
677
+ }
678
+ return {
679
+ type: 'function',
680
+ gateway_custom_tool: true,
681
+ function: {
682
+ name: encodeToolName(toolNamespace(tool), baseName),
683
+ description: chunks.join('\n'),
684
+ parameters,
685
+ },
686
+ };
687
+ }
688
+
689
+ function buildCustomToolNames(tools) {
690
+ const names = new Set();
691
+ for (const tool of expandTools(tools)) {
692
+ if (!isObject(tool) || tool.type !== 'custom') continue;
693
+ const normalizedTool = normalizeTool(tool);
694
+ const fn = normalizedTool?.type === 'function' ? normalizedTool.function : null;
695
+ if (fn?.name) names.add(fn.name);
696
+ }
697
+ return names;
698
+ }
699
+
700
+ function customToolInputFromArguments(argumentsText) {
701
+ if (typeof argumentsText !== 'string' || !argumentsText) return '';
702
+ const parsed = safeJsonParse(argumentsText);
703
+ if (parsed.ok && typeof parsed.value === 'string') return parsed.value;
704
+ if (parsed.ok && isObject(parsed.value)) {
705
+ const input = parsed.value.input;
706
+ if (typeof input === 'string') return input;
707
+ if (input !== undefined) return jsonString(input);
708
+ return argumentsText;
709
+ }
710
+ return argumentsText;
711
+ }
712
+
713
+ function stripGatewayToolMarkers(tools) {
714
+ if (!Array.isArray(tools)) return tools;
715
+ return tools.map((tool) => {
716
+ if (!isObject(tool) || tool.gateway_custom_tool === undefined) return tool;
717
+ const { gateway_custom_tool, ...rest } = tool;
718
+ return rest;
719
+ });
720
+ }
721
+
722
+ const UNAVAILABLE_TOOL_GUIDANCE = 'Do not call this tool; if the task depends on it, tell the user it is unavailable.';
723
+
724
+ function unavailableHostedToolShim(tool, reason) {
725
+ const capability = String(tool.type || 'tool');
726
+ const detail = reason || `Unavailable capability: the client requested the hosted ${capability} tool, but this gateway cannot execute it.`;
727
+ return {
728
+ type: 'function',
729
+ function: {
730
+ name: encodeToolName(toolNamespace(tool), toolBaseName(tool, capability)),
731
+ description: `${detail} ${UNAVAILABLE_TOOL_GUIDANCE}`,
732
+ parameters: { type: 'object', properties: {}, additionalProperties: false },
733
+ },
734
+ };
735
+ }
736
+
737
+ export function unavailableWebSearchToolShims(tools) {
738
+ const shims = [];
739
+ const seen = new Set();
740
+ for (const tool of expandTools(tools)) {
741
+ if (!isObject(tool) || typeof tool.type !== 'string' || !EMULATED_HOSTED_TOOL_TYPES.has(tool.type)) continue;
742
+ const shim = unavailableHostedToolShim(
743
+ tool,
744
+ 'Unavailable capability: web search was requested, but the gateway has no search provider configured.',
745
+ );
746
+ if (seen.has(shim.function.name)) continue;
747
+ seen.add(shim.function.name);
748
+ shims.push(shim);
749
+ }
750
+ return shims;
751
+ }
752
+
445
753
  function normalizeTool(tool) {
446
754
  if (!isObject(tool)) return tool;
447
755
  if (tool.type === 'namespace') return null;
448
756
  if (typeof tool.type === 'string' && EMULATED_HOSTED_TOOL_TYPES.has(tool.type)) {
449
757
  return null;
450
758
  }
759
+ if (typeof tool.type === 'string' && UNSUPPORTED_HOSTED_TOOL_TYPES.has(tool.type)) {
760
+ return unavailableHostedToolShim(tool);
761
+ }
762
+ if (tool.type === 'custom') {
763
+ return customToolShim(tool);
764
+ }
765
+ if (typeof tool.type === 'string' && BRIDGED_RESPONSES_TOOL_TYPES.has(tool.type)) {
766
+ return {
767
+ type: 'function',
768
+ function: omitUndefined({
769
+ name: encodeToolName(toolNamespace(tool), tool.type),
770
+ description: tool.description,
771
+ parameters: normalizeJsonSchemaObject(tool.parameters || tool.input_schema),
772
+ strict: tool.strict,
773
+ }),
774
+ };
775
+ }
451
776
  if (tool.type === 'function' && isObject(tool.function)) {
452
- const { namespace, ...fn } = tool.function;
777
+ const { namespace, defer_loading, ...fn } = tool.function;
453
778
  return {
454
779
  type: 'function',
455
780
  function: omitUndefined({
@@ -461,7 +786,7 @@ function normalizeTool(tool) {
461
786
  }),
462
787
  };
463
788
  }
464
- if (tool.type === 'function' || tool.type === 'custom' || tool.name || tool.parameters || tool.input_schema) {
789
+ if (tool.type === 'function') {
465
790
  return {
466
791
  type: 'function',
467
792
  function: omitUndefined({
@@ -472,6 +797,17 @@ function normalizeTool(tool) {
472
797
  }),
473
798
  };
474
799
  }
800
+ if (!tool.type && (tool.name || tool.parameters || tool.input_schema)) {
801
+ return {
802
+ type: 'function',
803
+ function: omitUndefined({
804
+ name: encodeToolName(toolNamespace(tool), toolBaseName(tool, 'tool_call')),
805
+ description: tool.description,
806
+ parameters: normalizeJsonSchemaObject(tool.parameters || tool.input_schema),
807
+ strict: tool.strict,
808
+ }),
809
+ };
810
+ }
475
811
  return null;
476
812
  }
477
813
 
@@ -518,8 +854,19 @@ function normalizeTools(tools) {
518
854
  return normalized.length ? normalized : undefined;
519
855
  }
520
856
 
857
+ function hasRunnableChatTools(normalized) {
858
+ return Array.isArray(applyAllowedTools(normalizeTools(normalized?.tools), normalized?.tool_choice));
859
+ }
860
+
861
+ function toolSearchDiscoveryKey(tool) {
862
+ if (tool.type === 'namespace') {
863
+ return `namespace:${tool.name || tool.namespace || tool.server_label || ''}`;
864
+ }
865
+ return `${tool.type || 'function'}:${toolNamespace(tool)}:${toolBaseName(tool, '')}`;
866
+ }
867
+
521
868
  function collectToolSearchOutputTools(input) {
522
- const discovered = [];
869
+ const discovered = new Map();
523
870
  const scan = (value) => {
524
871
  if (Array.isArray(value)) {
525
872
  for (const item of value) scan(item);
@@ -527,14 +874,16 @@ function collectToolSearchOutputTools(input) {
527
874
  }
528
875
  if (!isObject(value)) return;
529
876
  if (value.type === 'tool_search_output' && Array.isArray(value.tools)) {
530
- discovered.push(...value.tools.filter(isObject));
877
+ for (const tool of value.tools) {
878
+ if (isObject(tool)) discovered.set(toolSearchDiscoveryKey(tool), tool);
879
+ }
531
880
  return;
532
881
  }
533
882
  if (Array.isArray(value.input)) scan(value.input);
534
883
  if (Array.isArray(value.content)) scan(value.content);
535
884
  };
536
885
  scan(input);
537
- return discovered;
886
+ return [...discovered.values()];
538
887
  }
539
888
 
540
889
  function mergeToolsWithToolSearchOutput(requestTools, input) {
@@ -572,6 +921,7 @@ function normalizeJsonSchemaObject(schema) {
572
921
  }
573
922
 
574
923
  function normalizeToolChoice(toolChoice) {
924
+ if (toolChoiceDisablesTools(toolChoice)) return 'none';
575
925
  if (!isObject(toolChoice)) return toolChoice;
576
926
  if (toolChoice.type === 'function' && toolChoice.name) {
577
927
  return {
@@ -639,6 +989,7 @@ function allowedToolNames(toolChoice) {
639
989
  }
640
990
 
641
991
  function applyAllowedTools(tools, toolChoice) {
992
+ if (toolChoiceDisablesTools(toolChoice)) return undefined;
642
993
  const allowed = allowedToolNames(toolChoice);
643
994
  if (!allowed || !Array.isArray(tools)) return tools;
644
995
  const filtered = tools.filter((tool) => {
@@ -710,7 +1061,7 @@ function convertItemToToolSearchCall(item) {
710
1061
  return item;
711
1062
  }
712
1063
 
713
- function responseItemFromChatToolCall(toolCall, toolNames) {
1064
+ function responseItemFromChatToolCall(toolCall, toolNames, customToolNames) {
714
1065
  const callId = toolCall.id || generateId('call');
715
1066
  if (isCodexToolSearchTool(toolCall.function?.name)) {
716
1067
  return {
@@ -723,6 +1074,16 @@ function responseItemFromChatToolCall(toolCall, toolNames) {
723
1074
  };
724
1075
  }
725
1076
  const decoded = decodedToolName(toolCall.function?.name, toolNames);
1077
+ if (customToolNames?.has(toolCall.function?.name)) {
1078
+ return omitUndefined({
1079
+ type: 'custom_tool_call',
1080
+ id: callId,
1081
+ call_id: callId,
1082
+ name: decoded.name,
1083
+ input: customToolInputFromArguments(toolCall.function?.arguments || ''),
1084
+ status: 'completed',
1085
+ });
1086
+ }
726
1087
  return omitUndefined({
727
1088
  type: 'function_call',
728
1089
  id: callId,
@@ -749,14 +1110,17 @@ function buildToolNames(tools) {
749
1110
  const names = new Map();
750
1111
  for (const tool of expandTools(tools)) {
751
1112
  const namespace = toolNamespace(tool);
752
- if (!namespace) continue;
753
1113
  const normalizedTool = normalizeTool(tool);
754
1114
  const fn = normalizedTool?.type === 'function' ? normalizedTool.function : null;
755
1115
  if (!fn?.name) continue;
756
- names.set(fn.name, {
757
- namespace: sanitizeFunctionName(namespace, 'namespace'),
758
- name: sanitizeFunctionName(toolBaseName(tool, tool.type)),
759
- });
1116
+ const baseName = String(toolBaseName(tool, tool.type));
1117
+ const sanitizedBaseName = sanitizeFunctionName(baseName);
1118
+ if (!namespace && fn.name === baseName) continue;
1119
+ names.set(fn.name, omitUndefined({
1120
+ namespace: namespace || undefined,
1121
+ name: sanitizedBaseName,
1122
+ original_name: baseName,
1123
+ }));
760
1124
  }
761
1125
  return names;
762
1126
  }
@@ -767,7 +1131,7 @@ function simplifyJsonSchemaDescriptions(schema) {
767
1131
  const next = {};
768
1132
  for (const [key, value] of Object.entries(schema)) {
769
1133
  if (key === 'description' && typeof value === 'string') {
770
- const description = shortenText(value, DEEPSEEK_SCHEMA_DESCRIPTION_CHARS);
1134
+ const description = compactStructuredText(value, DEEPSEEK_SCHEMA_DESCRIPTION_SOFT_CHARS);
771
1135
  if (description) next.description = description;
772
1136
  continue;
773
1137
  }
@@ -790,82 +1154,15 @@ function defaultDeepSeekToolDescription(name) {
790
1154
  return compactText(`Call ${readable || name || 'this function'} when needed.`);
791
1155
  }
792
1156
 
793
- function deepSeekGatewayModelAliases(config = {}) {
794
- const aliases = isObject(config.modelAliases) && Object.keys(config.modelAliases).length
795
- ? config.modelAliases
796
- : DEFAULT_MODEL_ALIASES;
797
- const spawnConfig = { ...config, upstreamProvider: 'deepseek', modelAliases: aliases };
798
- const names = [];
799
- for (const name of Object.keys(aliases)) {
800
- if (!name || isDeprecatedModel(name)) continue;
801
- const alias = resolveModelAlias(name, spawnConfig);
802
- if (isDeprecatedModel(alias.upstreamModel)) continue;
803
- names.push(name);
804
- }
805
- return [...new Set(names)];
806
- }
807
-
808
- function enumStringProperty(property, values, description) {
809
- const source = isObject(property) ? property : {};
810
- const next = { ...source };
811
- delete next.const;
812
- delete next.oneOf;
813
- delete next.anyOf;
814
- delete next.allOf;
815
- delete next.examples;
816
- if (next.default !== undefined && !values.includes(next.default)) delete next.default;
817
- next.type = 'string';
818
- next.enum = values;
819
- next.description = description;
820
- return next;
821
- }
822
-
823
- function deepSeekSpawnAgentParameters(parameters, config = {}) {
824
- const next = normalizeJsonSchemaObject(parameters);
825
- const properties = isObject(next.properties) ? { ...next.properties } : {};
826
- const modelAliases = deepSeekGatewayModelAliases(config);
827
- properties.model = enumStringProperty(
828
- properties.model,
829
- modelAliases,
830
- 'Model for the sub-agent. Omit to inherit.',
831
- );
832
- properties.reasoning_effort = enumStringProperty(
833
- properties.reasoning_effort,
834
- CODEX_REASONING_EFFORTS,
835
- 'Codex reasoning effort for the sub-agent. Omit to inherit.',
836
- );
837
- return {
838
- ...next,
839
- properties,
840
- };
841
- }
842
-
843
- function deepSeekSpawnAgentDescription() {
844
- const description = [
845
- 'Spawn a Codex-native sub-agent.',
846
- 'Omit model/reasoning_effort to inherit.',
847
- 'Valid values are enforced by the schema.',
848
- ].filter(Boolean).join(' ');
849
- return shortenText(description, DEEPSEEK_TOOL_DESCRIPTION_CHARS);
850
- }
851
-
852
- function isCodexSpawnAgentTool(name) {
853
- return name === CODEX_SPAWN_AGENT_TOOL_NAME || name === 'spawn_agent';
854
- }
855
-
856
- function simplifyToolForDeepSeek(tool, config = {}) {
1157
+ function simplifyToolForDeepSeek(tool) {
857
1158
  if (tool?.type !== 'function' || !isObject(tool.function)) return tool;
1159
+ if (tool.gateway_custom_tool) return tool;
858
1160
  const fn = tool.function;
859
- let parameters = simplifyJsonSchemaDescriptions(fn.parameters);
860
- if (isCodexSpawnAgentTool(fn.name)) {
861
- parameters = deepSeekSpawnAgentParameters(parameters, config);
862
- }
1161
+ const parameters = simplifyJsonSchemaDescriptions(fn.parameters);
863
1162
  const requiredText = requiredParametersText(parameters);
864
- const descriptionBudget = Math.max(32, DEEPSEEK_TOOL_DESCRIPTION_CHARS - requiredText.length);
865
- const baseDescription = isCodexSpawnAgentTool(fn.name)
866
- ? deepSeekSpawnAgentDescription()
867
- : shortenText(fn.description, descriptionBudget) || defaultDeepSeekToolDescription(fn.name);
868
- const description = shortenText(`${baseDescription}${requiredText}`, DEEPSEEK_TOOL_DESCRIPTION_CHARS);
1163
+ const baseDescription = compactStructuredText(fn.description, DEEPSEEK_TOOL_DESCRIPTION_SOFT_CHARS)
1164
+ || defaultDeepSeekToolDescription(fn.name);
1165
+ const description = `${baseDescription}${requiredText}`;
869
1166
  return {
870
1167
  ...tool,
871
1168
  function: omitUndefined({
@@ -882,21 +1179,47 @@ function hasDeepSeekToolInstructions(messages) {
882
1179
 
883
1180
  function addDeepSeekToolInstructions(messages, tools) {
884
1181
  if (!Array.isArray(tools) || !tools.length || hasDeepSeekToolInstructions(messages)) return messages;
1182
+ const instructions = requestToolsIncludeCommentary(tools)
1183
+ ? `${DEEPSEEK_TOOL_INSTRUCTIONS} ${DEEPSEEK_COMMENTARY_INSTRUCTIONS}`
1184
+ : DEEPSEEK_TOOL_INSTRUCTIONS;
885
1185
  if (!messages.length || messages[0]?.role !== 'system') {
886
- return [{ role: 'system', content: DEEPSEEK_TOOL_INSTRUCTIONS }, ...messages];
1186
+ return [{ role: 'system', content: instructions }, ...messages];
887
1187
  }
888
1188
  return [
889
1189
  {
890
1190
  ...messages[0],
891
- content: `${toText(messages[0].content)}\n\n${DEEPSEEK_TOOL_INSTRUCTIONS}`,
1191
+ content: `${toText(messages[0].content)}\n\n${instructions}`,
892
1192
  },
893
1193
  ...messages.slice(1),
894
1194
  ];
895
1195
  }
896
1196
 
1197
+ function isDeepSeekBetaBaseUrl(baseUrl) {
1198
+ return /\/beta\/?$/.test(String(baseUrl || ''));
1199
+ }
1200
+
897
1201
  function adaptToolsForProvider(tools, provider, config = {}) {
898
1202
  if (provider !== 'deepseek' || !Array.isArray(tools)) return tools;
899
- return tools.map((tool) => simplifyToolForDeepSeek(tool, config));
1203
+ const keepStrict = isDeepSeekBetaBaseUrl(config.upstreamBaseUrl);
1204
+ return tools.map((tool) => {
1205
+ const simplified = simplifyToolForDeepSeek(tool);
1206
+ if (!keepStrict && simplified?.type === 'function' && isObject(simplified.function) && simplified.function.strict !== undefined) {
1207
+ const { strict, ...fn } = simplified.function;
1208
+ return { ...simplified, function: fn };
1209
+ }
1210
+ return simplified;
1211
+ });
1212
+ }
1213
+
1214
+ function assertDeepSeekToolCapacity(tools) {
1215
+ const count = Array.isArray(tools)
1216
+ ? tools.filter((tool) => tool?.type === 'function' && isObject(tool.function)).length
1217
+ : 0;
1218
+ if (count <= DEEPSEEK_MAX_FUNCTION_TOOLS) return;
1219
+ const error = new RangeError(`DeepSeek supports at most ${DEEPSEEK_MAX_FUNCTION_TOOLS} function tools; received ${count} after gateway adaptation.`);
1220
+ error.statusCode = 400;
1221
+ error.code = 'too_many_tools';
1222
+ throw error;
900
1223
  }
901
1224
 
902
1225
  function normalizeToolCallArguments(name, argumentsText, toolSchema) {
@@ -921,48 +1244,20 @@ function normalizeToolCallArguments(name, argumentsText, toolSchema) {
921
1244
  }
922
1245
  }
923
1246
 
924
- function normalizeReasoningEffortName(value) {
925
- return value == null ? '' : String(value).toLowerCase().replaceAll('_', '-');
1247
+ function normalizeFunctionCallItemArguments(item, toolSchemas) {
1248
+ if (!item || item.type !== 'function_call') return;
1249
+ const encodedName = encodeToolName(item.namespace, item.name);
1250
+ item.arguments = normalizeToolCallArguments(encodedName, item.arguments, toolSchemas?.get(encodedName));
926
1251
  }
927
1252
 
928
- function normalizeCodexSpawnAgentArguments(encodedName, argumentsText, config = {}) {
929
- if (!isCodexSpawnAgentTool(encodedName) || !argumentsText) return argumentsText;
930
- let parsed;
931
- try {
932
- parsed = JSON.parse(argumentsText);
933
- } catch {
934
- return argumentsText;
935
- }
936
- if (!isObject(parsed)) return argumentsText;
937
- const next = { ...parsed };
938
- let changed = false;
939
- const allowedModels = deepSeekGatewayModelAliases(config);
940
-
941
- if (typeof next.model === 'string') {
942
- const requestedModel = next.model;
943
- if (allowedModels.length && !allowedModels.includes(requestedModel)) {
944
- delete next.model;
945
- changed = true;
946
- }
947
- }
948
-
949
- if (typeof next.reasoning_effort === 'string') {
950
- const requestedEffort = normalizeReasoningEffortName(next.reasoning_effort);
951
- if (!CODEX_REASONING_EFFORTS.includes(requestedEffort)) {
952
- delete next.reasoning_effort;
953
- changed = true;
954
- return JSON.stringify(next);
955
- }
956
- }
957
-
958
- return changed ? JSON.stringify(next) : argumentsText;
1253
+ function functionCallItemNeedsArgumentNormalization(item, toolSchemas) {
1254
+ if (!item || item.type !== 'function_call') return false;
1255
+ const encodedName = encodeToolName(item.namespace, item.name);
1256
+ return Boolean(toolSchemas?.has(encodedName));
959
1257
  }
960
1258
 
961
- function normalizeFunctionCallItemArguments(item, toolSchemas, config = {}) {
962
- if (!item || item.type !== 'function_call') return;
963
- const encodedName = encodeToolName(item.namespace, item.name);
964
- item.arguments = normalizeToolCallArguments(encodedName, item.arguments, toolSchemas?.get(encodedName));
965
- item.arguments = normalizeCodexSpawnAgentArguments(encodedName, item.arguments, config);
1259
+ function hasToolCallFunctionName(toolCall) {
1260
+ return typeof toolCall?.function?.name === 'string' && toolCall.function.name.length > 0;
966
1261
  }
967
1262
 
968
1263
  function sanitizeToolCallsForChatCompletion(toolCalls) {
@@ -983,11 +1278,51 @@ function sanitizeToolCallsForChatCompletion(toolCalls) {
983
1278
  });
984
1279
  }
985
1280
 
1281
+ function multimodalPlaceholder(kind, hint) {
1282
+ const cleanHint = compactText(hint);
1283
+ const shortHint = cleanHint && !cleanHint.startsWith('data:') ? shortenText(cleanHint, 120) : '';
1284
+ return `[${kind} omitted: DeepSeek accepts text input only${shortHint ? `; source: ${shortHint}` : ''}]`;
1285
+ }
1286
+
1287
+ function deepseekTextOnlyContent(content) {
1288
+ if (content == null) return '';
1289
+ if (typeof content === 'string') return content;
1290
+ const parts = Array.isArray(content) ? content : [content];
1291
+ const chunks = [];
1292
+ for (const part of parts) {
1293
+ if (typeof part === 'string') {
1294
+ chunks.push(part);
1295
+ continue;
1296
+ }
1297
+ if (!isObject(part)) continue;
1298
+ if (part.type === 'text') {
1299
+ chunks.push(String(part.text ?? ''));
1300
+ continue;
1301
+ }
1302
+ if (part.type === 'image_url') {
1303
+ chunks.push(multimodalPlaceholder('image', part.image_url?.url || part.image_url?.file_id));
1304
+ continue;
1305
+ }
1306
+ if (part.type === 'file') {
1307
+ chunks.push(multimodalPlaceholder('file', part.file?.filename || part.file?.file_url || part.file?.file_id));
1308
+ continue;
1309
+ }
1310
+ if (part.type === 'input_audio') {
1311
+ chunks.push(multimodalPlaceholder('audio', part.input_audio?.file_id));
1312
+ continue;
1313
+ }
1314
+ const text = toText(part);
1315
+ if (text) chunks.push(text);
1316
+ }
1317
+ return chunks.filter((chunk) => chunk !== '').join('\n');
1318
+ }
1319
+
986
1320
  function sanitizeMessageForChatCompletion(message, provider = 'generic') {
987
1321
  if (!isObject(message)) return message;
1322
+ const chatContent = contentToChatContent(message.content);
988
1323
  const sanitized = {
989
1324
  role: normalizeRole(message.role, provider),
990
- content: contentToChatContent(message.content),
1325
+ content: provider === 'deepseek' ? deepseekTextOnlyContent(chatContent) : chatContent,
991
1326
  };
992
1327
  if (message.name !== undefined) sanitized.name = message.name;
993
1328
  if (Array.isArray(message.tool_calls)) {
@@ -1000,11 +1335,93 @@ function sanitizeMessageForChatCompletion(message, provider = 'generic') {
1000
1335
  return sanitized;
1001
1336
  }
1002
1337
 
1338
+ function deepSeekInstructionBlock(message, includePriorityLabels) {
1339
+ const content = toText(message?.content).trim();
1340
+ if (!content) return '';
1341
+ if (!includePriorityLabels) return content;
1342
+ const label = message.role === 'developer'
1343
+ ? 'Developer instructions (priority below system):'
1344
+ : 'System instructions (highest priority):';
1345
+ return `${label}\n${content}`;
1346
+ }
1347
+
1348
+ function sanitizeMessagesForChatCompletion(messages, provider = 'generic') {
1349
+ const source = Array.isArray(messages) ? messages : [];
1350
+ if (provider !== 'deepseek') {
1351
+ return source.map((message) => sanitizeMessageForChatCompletion(message, provider));
1352
+ }
1353
+
1354
+ const result = [];
1355
+ let instructionBlock = [];
1356
+ const flushInstructionBlock = () => {
1357
+ if (!instructionBlock.length) return;
1358
+ const hasDeveloper = instructionBlock.some((message) => message?.role === 'developer');
1359
+ const content = instructionBlock
1360
+ .map((message) => deepSeekInstructionBlock(message, hasDeveloper))
1361
+ .filter(Boolean)
1362
+ .join('\n\n');
1363
+ if (content) result.push({ role: 'system', content });
1364
+ instructionBlock = [];
1365
+ };
1366
+
1367
+ for (const message of source) {
1368
+ if (message?.role === 'system' || message?.role === 'developer') {
1369
+ instructionBlock.push(message);
1370
+ continue;
1371
+ }
1372
+ flushInstructionBlock();
1373
+ result.push(sanitizeMessageForChatCompletion(message, provider));
1374
+ }
1375
+ flushInstructionBlock();
1376
+ return result;
1377
+ }
1378
+
1379
+ const DEEPSEEK_DEFAULT_MAX_TOKENS = 65536;
1380
+
1381
+ function deepseekDefaultMaxTokens(config = {}) {
1382
+ const value = Number(config.upstreamMaxTokens);
1383
+ if (Number.isFinite(value) && value > 0) return Math.floor(value);
1384
+ return DEEPSEEK_DEFAULT_MAX_TOKENS;
1385
+ }
1386
+
1387
+ const DEEPSEEK_JSON_SCHEMA_INSTRUCTIONS_MARKER = 'Return only one valid JSON object';
1388
+
1389
+ function jsonSchemaFromResponseFormat(responseFormat) {
1390
+ if (!isObject(responseFormat)) return null;
1391
+ const container = isObject(responseFormat.json_schema) ? responseFormat.json_schema : responseFormat;
1392
+ return isObject(container.schema) ? container.schema : null;
1393
+ }
1394
+
1395
+ function addDeepSeekJsonSchemaInstructions(messages, responseFormat) {
1396
+ if (messages.some((message) => message?.role === 'system' && toText(message.content).includes(DEEPSEEK_JSON_SCHEMA_INSTRUCTIONS_MARKER))) {
1397
+ return messages;
1398
+ }
1399
+ const schema = jsonSchemaFromResponseFormat(responseFormat);
1400
+ const schemaText = schema ? truncateRawText(JSON.stringify(schema), 6000) : '';
1401
+ const instructions = [
1402
+ `${DEEPSEEK_JSON_SCHEMA_INSTRUCTIONS_MARKER}, with no prose or Markdown.`,
1403
+ schemaText
1404
+ ? `It must match this JSON Schema:\n${schemaText}`
1405
+ : 'Use the object shape requested in the conversation.',
1406
+ ].join('\n');
1407
+ if (!messages.length || messages[0]?.role !== 'system') {
1408
+ return [{ role: 'system', content: instructions }, ...messages];
1409
+ }
1410
+ return [
1411
+ {
1412
+ ...messages[0],
1413
+ content: `${toText(messages[0].content)}\n\n${instructions}`,
1414
+ },
1415
+ ...messages.slice(1),
1416
+ ];
1417
+ }
1418
+
1003
1419
  function patchDeepSeekThinkingHistory(messages, request) {
1004
1420
  if (request.thinking?.type !== 'enabled' && !request.reasoning_effort) return messages;
1005
1421
  return messages.map((message) => {
1006
1422
  if (message?.role !== 'assistant') return message;
1007
1423
  if (typeof message.reasoning_content === 'string') return message;
1424
+ if (!hasChatToolCalls(message)) return message;
1008
1425
  return {
1009
1426
  ...message,
1010
1427
  reasoning_content: '',
@@ -1020,8 +1437,12 @@ function normalizeOutputTextPart(text, annotations = []) {
1020
1437
  };
1021
1438
  }
1022
1439
 
1440
+ const REASONING_SUMMARY_HEADER = '**Reasoning**\n\n';
1441
+
1023
1442
  function reasoningSummaryText(text) {
1024
- return reasoningDisplayText(text);
1443
+ const display = reasoningDisplayText(text);
1444
+ if (!display) return '';
1445
+ return `${REASONING_SUMMARY_HEADER}${display}`;
1025
1446
  }
1026
1447
 
1027
1448
  function normalizeSummaryTextPart(text) {
@@ -1074,6 +1495,16 @@ function snapshotResponseItem(item) {
1074
1495
  arguments: isObject(item.arguments) ? { ...item.arguments } : toolSearchArgumentsFromText(item.arguments),
1075
1496
  });
1076
1497
  }
1498
+ if (item.type === 'custom_tool_call') {
1499
+ return omitUndefined({
1500
+ type: item.type,
1501
+ id: item.id,
1502
+ call_id: item.call_id,
1503
+ status: item.status,
1504
+ name: item.name,
1505
+ input: typeof item.input === 'string' ? item.input : '',
1506
+ });
1507
+ }
1077
1508
  return {
1078
1509
  ...item,
1079
1510
  content: Array.isArray(item.content) ? item.content.map(snapshotResponsePart) : item.content,
@@ -1134,12 +1565,14 @@ function normalizeInstructions(instructions) {
1134
1565
  return toText(instructions);
1135
1566
  }
1136
1567
 
1137
- export function normalizeResponsesRequest(request) {
1568
+ export function normalizeResponsesRequest(request, { restoreDiscoveredTools = true } = {}) {
1138
1569
  const messages = extractMessagesFromResponsesInput(request.input ?? request);
1139
1570
  const model = request.model || request.model_id || request.upstream_model || '';
1140
1571
  const instructions = normalizeInstructions(request.instructions);
1141
1572
  const responseFormat = request.response_format || request.text?.format;
1142
- const tools = mergeToolsWithToolSearchOutput(request.tools, request.input ?? request);
1573
+ const tools = restoreDiscoveredTools
1574
+ ? mergeToolsWithToolSearchOutput(request.tools, request.input ?? request)
1575
+ : request.tools;
1143
1576
  return {
1144
1577
  model,
1145
1578
  messages,
@@ -1161,10 +1594,8 @@ export function normalizeResponsesRequest(request) {
1161
1594
  text: request.text,
1162
1595
  truncation: request.truncation,
1163
1596
  user: request.user,
1164
- previous_response_id: request.previous_response_id,
1165
1597
  store: request.store,
1166
1598
  include: request.include,
1167
- conversation: request.conversation,
1168
1599
  };
1169
1600
  }
1170
1601
 
@@ -1204,7 +1635,9 @@ export function toChatCompletionsRequest(normalized, overrides = {}) {
1204
1635
 
1205
1636
  export function assistantMessageFromResponseOutput(output) {
1206
1637
  const messageItems = Array.isArray(output) ? output.filter((item) => item.type === 'message') : [];
1207
- const functionItems = Array.isArray(output) ? output.filter((item) => item.type === 'function_call') : [];
1638
+ const functionItems = Array.isArray(output)
1639
+ ? output.filter((item) => item.type === 'function_call' || item.type === 'custom_tool_call' || item.type === 'tool_search_call')
1640
+ : [];
1208
1641
  const reasoningItems = Array.isArray(output) ? output.filter((item) => item.type === 'reasoning') : [];
1209
1642
  const contentParts = messageItems.map((item) => item.content || []).flat();
1210
1643
  const chatParts = contentParts.map(contentPartToChatPart).filter(Boolean);
@@ -1217,14 +1650,7 @@ export function assistantMessageFromResponseOutput(output) {
1217
1650
  role: 'assistant',
1218
1651
  content,
1219
1652
  };
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
- }));
1653
+ const toolCalls = functionItems.map(chatToolCallFromResponseItem);
1228
1654
  if (toolCalls.length) assistant.tool_calls = toolCalls;
1229
1655
  const reasoningContent = reasoningItems
1230
1656
  .map((item) => reasoningTextFromItem(item))
@@ -1245,15 +1671,64 @@ export function extractToolCallIdsFromMessages(messages) {
1245
1671
  return ids;
1246
1672
  }
1247
1673
 
1248
- export function toProviderChatCompletionsRequest(chatRequest, config = {}) {
1249
- const provider = config.upstreamProvider || 'generic';
1250
- const modelAlias = resolveModelAlias(chatRequest.model, config);
1251
- const fallbackReasoning = !chatRequest.reasoning && config.codexReasoningEffort
1252
- ? { effort: config.codexReasoningEffort }
1253
- : chatRequest.reasoning;
1254
- const messages = Array.isArray(chatRequest.messages)
1255
- ? chatRequest.messages.map((message) => sanitizeMessageForChatCompletion(message, provider))
1256
- : [];
1674
+ export function requestToolsIncludeCommentary(tools) {
1675
+ for (const tool of Array.isArray(tools) ? tools : []) {
1676
+ if (!isObject(tool)) continue;
1677
+ const name = isObject(tool.function) ? tool.function.name : tool.name;
1678
+ if (String(name || '') === INTERNAL_COMMENTARY_TOOL) return true;
1679
+ }
1680
+ return false;
1681
+ }
1682
+
1683
+ export function bridgedCommentaryToolCallsFromMessage(message, tools) {
1684
+ if (requestToolsIncludeCommentary(tools)) return [];
1685
+ const toolCalls = Array.isArray(message?.tool_calls) ? message.tool_calls : [];
1686
+ return toolCalls.filter(
1687
+ (toolCall) => toolCall?.type === 'function' && String(toolCall?.function?.name || '') === INTERNAL_COMMENTARY_TOOL,
1688
+ );
1689
+ }
1690
+
1691
+ export function stripBridgedCommentaryFromCompletion(completion, tools) {
1692
+ if (requestToolsIncludeCommentary(tools)) return completion;
1693
+ const choice = Array.isArray(completion?.choices) ? completion.choices[0] : null;
1694
+ const message = choice?.message;
1695
+ if (!Array.isArray(message?.tool_calls)) return completion;
1696
+ const kept = message.tool_calls.filter(
1697
+ (toolCall) => !(toolCall?.type === 'function' && String(toolCall?.function?.name || '') === INTERNAL_COMMENTARY_TOOL),
1698
+ );
1699
+ if (kept.length === message.tool_calls.length) return completion;
1700
+ const nextMessage = { ...message };
1701
+ if (kept.length) nextMessage.tool_calls = kept;
1702
+ else delete nextMessage.tool_calls;
1703
+ return {
1704
+ ...completion,
1705
+ choices: [{ ...choice, message: nextMessage }, ...completion.choices.slice(1)],
1706
+ };
1707
+ }
1708
+
1709
+ function commentaryTextFromArguments(argumentsText) {
1710
+ const args = parseJsonObject(argumentsText);
1711
+ const text = String(args.text ?? args.message ?? args.update ?? args.content ?? '').trim();
1712
+ if (text) return text;
1713
+ const raw = String(argumentsText ?? '').trim();
1714
+ if (!raw || raw.startsWith('{') || raw === 'null') return '';
1715
+ return raw;
1716
+ }
1717
+
1718
+ function addInternalCommentaryTool(tools, toolChoice) {
1719
+ if (!Array.isArray(tools) || !tools.length) return tools;
1720
+ if (toolChoiceDisablesTools(toolChoice)) return tools;
1721
+ if (requestToolsIncludeCommentary(tools)) return tools;
1722
+ return [...tools, JSON.parse(JSON.stringify(INTERNAL_COMMENTARY_TOOL_DEFINITION))];
1723
+ }
1724
+
1725
+ export function toProviderChatCompletionsRequest(chatRequest, config = {}) {
1726
+ const provider = config.upstreamProvider || 'generic';
1727
+ const modelAlias = resolveModelAlias(chatRequest.model, config);
1728
+ const fallbackReasoning = !chatRequest.reasoning && config.codexReasoningEffort
1729
+ ? { effort: config.codexReasoningEffort }
1730
+ : chatRequest.reasoning;
1731
+ const messages = sanitizeMessagesForChatCompletion(chatRequest.messages, provider);
1257
1732
  const request = {
1258
1733
  model: modelAlias.upstreamModel || config.upstreamModel || chatRequest.model,
1259
1734
  messages,
@@ -1282,7 +1757,10 @@ export function toProviderChatCompletionsRequest(chatRequest, config = {}) {
1282
1757
  if (provider === 'deepseek') {
1283
1758
  delete request.reasoning_effort;
1284
1759
  delete request.parallel_tool_calls;
1285
- request.tools = adaptToolsForProvider(request.tools, provider, config);
1760
+ delete request.frequency_penalty;
1761
+ delete request.presence_penalty;
1762
+ request.tools = addInternalCommentaryTool(adaptToolsForProvider(request.tools, provider, config), chatRequest.tool_choice);
1763
+ assertDeepSeekToolCapacity(request.tools);
1286
1764
  request.messages = addDeepSeekToolInstructions(request.messages, request.tools);
1287
1765
  if (request.user !== undefined) {
1288
1766
  request.user_id = request.user;
@@ -1293,15 +1771,23 @@ export function toProviderChatCompletionsRequest(chatRequest, config = {}) {
1293
1771
  } else {
1294
1772
  delete request.stream_options;
1295
1773
  }
1774
+ if (request.max_tokens == null) {
1775
+ request.max_tokens = deepseekDefaultMaxTokens(config);
1776
+ }
1296
1777
  if (request.response_format?.type === 'json_schema') {
1778
+ request.messages = addDeepSeekJsonSchemaInstructions(request.messages, request.response_format);
1297
1779
  request.response_format = { type: 'json_object' };
1298
1780
  }
1299
1781
  Object.assign(request, deepseekReasoningPayload({ alias: modelAlias, reasoning: fallbackReasoning }));
1300
- request.messages = patchDeepSeekThinkingHistory(request.messages, request);
1301
1782
  }
1302
1783
 
1303
1784
  Object.assign(request, modelAlias.extraBody);
1304
1785
 
1786
+ if (provider === 'deepseek') {
1787
+ request.messages = patchDeepSeekThinkingHistory(request.messages, request);
1788
+ }
1789
+ request.tools = stripGatewayToolMarkers(request.tools);
1790
+
1305
1791
  for (const key of Object.keys(request)) {
1306
1792
  if (request[key] === undefined) delete request[key];
1307
1793
  }
@@ -1325,6 +1811,7 @@ function createBaseResponse({
1325
1811
  normalized,
1326
1812
  completedAt = null,
1327
1813
  incompleteReason = null,
1814
+ error = null,
1328
1815
  }) {
1329
1816
  return {
1330
1817
  id,
@@ -1333,7 +1820,7 @@ function createBaseResponse({
1333
1820
  completed_at: completedAt == null ? null : Math.floor(completedAt),
1334
1821
  status,
1335
1822
  background: false,
1336
- error: null,
1823
+ error,
1337
1824
  incomplete_details: incompleteReason ? { reason: incompleteReason } : null,
1338
1825
  instructions: normalized?.instructions || null,
1339
1826
  max_output_tokens: normalized?.max_tokens ?? null,
@@ -1356,6 +1843,37 @@ function createBaseResponse({
1356
1843
  };
1357
1844
  }
1358
1845
 
1846
+ function deepSeekFinishReasonOutcome(finishReason) {
1847
+ if (finishReason === 'stop' || finishReason === 'tool_calls') {
1848
+ return { status: 'completed', incompleteReason: null, error: null };
1849
+ }
1850
+ if (finishReason === 'length') {
1851
+ return { status: 'incomplete', incompleteReason: 'max_output_tokens', error: null };
1852
+ }
1853
+ if (finishReason === 'content_filter') {
1854
+ return { status: 'incomplete', incompleteReason: 'content_filter', error: null };
1855
+ }
1856
+ if (finishReason === 'insufficient_system_resource') {
1857
+ return {
1858
+ status: 'failed',
1859
+ incompleteReason: null,
1860
+ error: {
1861
+ code: 'server_is_overloaded',
1862
+ message: 'DeepSeek ended the response because upstream resources were insufficient.',
1863
+ },
1864
+ };
1865
+ }
1866
+ const label = finishReason == null || finishReason === '' ? 'missing' : String(finishReason);
1867
+ return {
1868
+ status: 'failed',
1869
+ incompleteReason: null,
1870
+ error: {
1871
+ code: 'upstream_error',
1872
+ message: `DeepSeek returned an unsupported finish_reason: ${label}.`,
1873
+ },
1874
+ };
1875
+ }
1876
+
1359
1877
  function normalizeResponsesUsage(usage) {
1360
1878
  if (!isObject(usage)) return null;
1361
1879
  const inputTokens = usage.input_tokens ?? usage.prompt_tokens ?? 0;
@@ -1363,7 +1881,7 @@ function normalizeResponsesUsage(usage) {
1363
1881
  return {
1364
1882
  input_tokens: inputTokens,
1365
1883
  input_tokens_details: usage.input_tokens_details ?? {
1366
- cached_tokens: usage.prompt_tokens_details?.cached_tokens ?? 0,
1884
+ cached_tokens: usage.prompt_tokens_details?.cached_tokens ?? usage.prompt_cache_hit_tokens ?? 0,
1367
1885
  },
1368
1886
  output_tokens: outputTokens,
1369
1887
  output_tokens_details: usage.output_tokens_details ?? {
@@ -1378,6 +1896,7 @@ function outputTextFromOutput(output) {
1378
1896
  const chunks = [];
1379
1897
  for (const item of output) {
1380
1898
  if (item?.type !== 'message' || !Array.isArray(item.content)) continue;
1899
+ if (item.phase === 'commentary') continue;
1381
1900
  for (const part of item.content) {
1382
1901
  if (part?.type === 'output_text' && typeof part.text === 'string') {
1383
1902
  chunks.push(part.text);
@@ -1413,29 +1932,176 @@ export function createResponseEnvelope({
1413
1932
  });
1414
1933
  }
1415
1934
 
1416
- export function convertChatCompletionToResponses({ completion, model, previousResponseId, normalized, responseId = generateId('resp'), config = {} }) {
1935
+ const PARALLEL_TOOL_WRAPPER_NAMES = new Set(['multi_tool_use.parallel', 'multi_tool_use_parallel']);
1936
+ const EMITTED_TOOL_NAME_PREFIX = 'functions.';
1937
+
1938
+ function isParallelToolWrapperName(name) {
1939
+ const raw = String(name || '').toLowerCase();
1940
+ const stripped = raw.startsWith(EMITTED_TOOL_NAME_PREFIX) ? raw.slice(EMITTED_TOOL_NAME_PREFIX.length) : raw;
1941
+ return PARALLEL_TOOL_WRAPPER_NAMES.has(stripped);
1942
+ }
1943
+
1944
+ function toolNameMatchKey(name) {
1945
+ return String(name || '').toLowerCase().replace(/\./g, '__');
1946
+ }
1947
+
1948
+ function resolveEmittedToolName(name, knownNames) {
1949
+ const raw = String(name || '');
1950
+ if (!raw || !knownNames) return name;
1951
+ const known = knownNames instanceof Set ? knownNames : new Set(knownNames);
1952
+ if (!known.size || known.has(raw)) return name;
1953
+ const candidates = [raw];
1954
+ if (raw.startsWith(EMITTED_TOOL_NAME_PREFIX)) candidates.push(raw.slice(EMITTED_TOOL_NAME_PREFIX.length));
1955
+ for (const candidate of candidates) {
1956
+ if (known.has(candidate)) return candidate;
1957
+ }
1958
+ const knownByKey = new Map();
1959
+ for (const knownName of known) {
1960
+ const key = toolNameMatchKey(knownName);
1961
+ if (!knownByKey.has(key)) knownByKey.set(key, knownName);
1962
+ }
1963
+ for (const candidate of candidates) {
1964
+ const match = knownByKey.get(toolNameMatchKey(candidate));
1965
+ if (match) return match;
1966
+ }
1967
+ return name;
1968
+ }
1969
+
1970
+ export function chatToolNamesFromTools(tools) {
1971
+ const names = [];
1972
+ for (const tool of Array.isArray(tools) ? tools : []) {
1973
+ if (!isObject(tool)) continue;
1974
+ const name = isObject(tool.function) ? tool.function.name : tool.name;
1975
+ if (name) names.push(String(name));
1976
+ }
1977
+ return names;
1978
+ }
1979
+
1980
+ export function resolveEmittedToolCallNamesInCompletion(completion, knownNames) {
1981
+ const choice = Array.isArray(completion?.choices) ? completion.choices[0] : null;
1982
+ const toolCalls = choice?.message?.tool_calls;
1983
+ if (!Array.isArray(toolCalls) || !toolCalls.length) return completion;
1984
+ let changed = false;
1985
+ const resolved = toolCalls.map((toolCall) => {
1986
+ const name = toolCall?.function?.name;
1987
+ if (!name) return toolCall;
1988
+ const resolvedName = resolveEmittedToolName(name, knownNames);
1989
+ if (resolvedName === name) return toolCall;
1990
+ changed = true;
1991
+ return { ...toolCall, function: { ...toolCall.function, name: resolvedName } };
1992
+ });
1993
+ if (!changed) return completion;
1994
+ return {
1995
+ ...completion,
1996
+ choices: completion.choices.map((entry, index) => (index === 0
1997
+ ? { ...entry, message: { ...entry.message, tool_calls: resolved } }
1998
+ : entry)),
1999
+ };
2000
+ }
2001
+
2002
+ function parallelToolUsesFromArguments(argumentsText) {
2003
+ const parsed = safeJsonParse(String(argumentsText || ''));
2004
+ if (!parsed.ok || !isObject(parsed.value)) return null;
2005
+ const uses = Array.isArray(parsed.value.tool_uses) ? parsed.value.tool_uses : null;
2006
+ if (!uses || !uses.length) return null;
2007
+ const calls = [];
2008
+ for (const use of uses) {
2009
+ if (!isObject(use)) return null;
2010
+ const name = String(use.recipient_name || use.name || '').replace(/^functions\./, '');
2011
+ if (!name) return null;
2012
+ const parameters = use.parameters ?? use.arguments ?? {};
2013
+ calls.push({
2014
+ name,
2015
+ arguments: typeof parameters === 'string' ? parameters : JSON.stringify(parameters),
2016
+ });
2017
+ }
2018
+ return calls;
2019
+ }
2020
+
2021
+ function expandParallelToolCalls(toolCalls) {
2022
+ if (!Array.isArray(toolCalls)) return toolCalls;
2023
+ if (!toolCalls.some((toolCall) => isParallelToolWrapperName(toolCall?.function?.name))) return toolCalls;
2024
+ const expanded = [];
2025
+ for (const toolCall of toolCalls) {
2026
+ const uses = isParallelToolWrapperName(toolCall?.function?.name)
2027
+ ? parallelToolUsesFromArguments(toolCall.function?.arguments)
2028
+ : null;
2029
+ if (!uses) {
2030
+ expanded.push(toolCall);
2031
+ continue;
2032
+ }
2033
+ for (const use of uses) {
2034
+ expanded.push({
2035
+ id: generateId('call'),
2036
+ type: 'function',
2037
+ function: { name: use.name, arguments: use.arguments },
2038
+ });
2039
+ }
2040
+ }
2041
+ return expanded;
2042
+ }
2043
+
2044
+ export function expandParallelToolCallsInCompletion(completion) {
2045
+ const choice = Array.isArray(completion?.choices) ? completion.choices[0] : null;
2046
+ const toolCalls = choice?.message?.tool_calls;
2047
+ if (!Array.isArray(toolCalls)) return completion;
2048
+ const expanded = expandParallelToolCalls(toolCalls);
2049
+ if (expanded === toolCalls) return completion;
2050
+ return {
2051
+ ...completion,
2052
+ choices: completion.choices.map((entry, index) => (index === 0
2053
+ ? { ...entry, message: { ...entry.message, tool_calls: expanded } }
2054
+ : entry)),
2055
+ };
2056
+ }
2057
+
2058
+ export function convertChatCompletionToResponses({ completion: rawCompletion, model, previousResponseId, normalized, responseId = generateId('resp'), config = {} }) {
2059
+ const toolSchemas = buildToolSchemas(normalized?.tools);
2060
+ const knownToolNames = new Set([...toolSchemas.keys(), INTERNAL_COMMENTARY_TOOL]);
2061
+ const completion = resolveEmittedToolCallNamesInCompletion(
2062
+ expandParallelToolCallsInCompletion(rawCompletion),
2063
+ knownToolNames,
2064
+ );
1417
2065
  const createdAt = completion.created || Date.now() / 1000;
1418
2066
  const choice = Array.isArray(completion.choices) ? completion.choices[0] : null;
2067
+ const outcome = deepSeekFinishReasonOutcome(choice?.finish_reason);
1419
2068
  const message = choice?.message || {};
1420
2069
  const content = chatContentToResponseOutputParts(message.content);
1421
- const toolSchemas = buildToolSchemas(normalized?.tools);
2070
+ const messageHasToolCalls = hasChatToolCalls(message);
1422
2071
  const toolNames = buildToolNames(normalized?.tools);
2072
+ const customToolNames = buildCustomToolNames(normalized?.tools);
1423
2073
  const output = [];
1424
2074
 
1425
- if (content.length || message.tool_calls?.length) {
2075
+ if (content.length) {
1426
2076
  output.push({
1427
2077
  type: 'message',
1428
2078
  id: generateId('msg'),
1429
2079
  role: 'assistant',
1430
2080
  content,
1431
- status: 'completed',
1432
- phase: message.phase || 'final_answer',
2081
+ status: outcome.status,
2082
+ phase: message.phase || (messageHasToolCalls ? 'commentary' : 'final_answer'),
1433
2083
  });
1434
2084
  }
1435
2085
 
1436
- if (Array.isArray(message.tool_calls)) {
2086
+ if (messageHasToolCalls) {
2087
+ const bridgedCommentary = !requestToolsIncludeCommentary(normalized?.tools);
1437
2088
  for (const toolCall of message.tool_calls) {
1438
- const item = responseItemFromChatToolCall(toolCall, toolNames);
2089
+ if (bridgedCommentary && toolCall?.type === 'function' && String(toolCall?.function?.name || '') === INTERNAL_COMMENTARY_TOOL) {
2090
+ const text = commentaryTextFromArguments(toolCall.function?.arguments);
2091
+ if (text) {
2092
+ output.push({
2093
+ type: 'message',
2094
+ id: generateId('msg'),
2095
+ role: 'assistant',
2096
+ content: [normalizeOutputTextPart(text)],
2097
+ status: 'completed',
2098
+ phase: 'commentary',
2099
+ });
2100
+ }
2101
+ continue;
2102
+ }
2103
+ const item = responseItemFromChatToolCall(toolCall, toolNames, customToolNames);
2104
+ item.status = outcome.status;
1439
2105
  normalizeFunctionCallItemArguments(item, toolSchemas, config);
1440
2106
  output.push(item);
1441
2107
  }
@@ -1449,8 +2115,8 @@ export function convertChatCompletionToResponses({ completion, model, previousRe
1449
2115
  summary: [normalizeSummaryTextPart(reasoningSummaryText(reasoningText))],
1450
2116
  content: [],
1451
2117
  reasoning_content: reasoningText,
1452
- encrypted_content: null,
1453
- status: 'completed',
2118
+ encrypted_content: encodeGatewayReasoning(reasoningText),
2119
+ status: outcome.status,
1454
2120
  });
1455
2121
  }
1456
2122
 
@@ -1458,13 +2124,14 @@ export function convertChatCompletionToResponses({ completion, model, previousRe
1458
2124
  id: responseId,
1459
2125
  model,
1460
2126
  createdAt,
1461
- status: choice?.finish_reason === 'length' ? 'incomplete' : 'completed',
2127
+ status: outcome.status,
1462
2128
  output,
1463
2129
  previousResponseId: previousResponseId ?? null,
1464
2130
  usage: normalizeResponsesUsage(completion.usage),
1465
2131
  normalized,
1466
2132
  completedAt: Date.now() / 1000,
1467
- incompleteReason: choice?.finish_reason === 'length' ? 'max_output_tokens' : null,
2133
+ incompleteReason: outcome.incompleteReason,
2134
+ error: outcome.error,
1468
2135
  });
1469
2136
  }
1470
2137
 
@@ -1475,9 +2142,10 @@ export class ResponsesStreamMapper {
1475
2142
  createdAt = Date.now() / 1000,
1476
2143
  previousResponseId = null,
1477
2144
  normalized,
1478
- bufferOutputUntilDone = false,
1479
2145
  emitReasoningSummary = true,
1480
2146
  emitReasoningText = false,
2147
+ holdToolItemEvents = false,
2148
+ knownToolNames,
1481
2149
  config = {},
1482
2150
  } = {}) {
1483
2151
  this.responseId = responseId;
@@ -1485,9 +2153,10 @@ export class ResponsesStreamMapper {
1485
2153
  this.createdAt = createdAt;
1486
2154
  this.previousResponseId = previousResponseId;
1487
2155
  this.normalized = normalized;
1488
- this.bufferOutputUntilDone = Boolean(bufferOutputUntilDone);
1489
2156
  this.emitReasoningSummary = Boolean(emitReasoningSummary);
1490
2157
  this.emitReasoningText = Boolean(emitReasoningText);
2158
+ this.holdToolItemEvents = Boolean(holdToolItemEvents);
2159
+ this.roundStartIndex = 0;
1491
2160
  this.sequenceNumber = 0;
1492
2161
  this.output = [];
1493
2162
  this.messageItem = null;
@@ -1495,19 +2164,34 @@ export class ResponsesStreamMapper {
1495
2164
  this.toolItems = new Map();
1496
2165
  this.text = '';
1497
2166
  this.reasoningText = '';
1498
- this.streamReasoningLive = true;
1499
2167
  this.reasoningItemDone = false;
1500
2168
  this.finishReason = null;
1501
2169
  this.usage = null;
2170
+ this.pendingFinishReason = null;
2171
+ this.pendingUsage = null;
1502
2172
  this.completedAt = null;
1503
2173
  this.finalized = false;
2174
+ this.terminalStatus = null;
2175
+ this.messageItemAdded = false;
1504
2176
  this.messageContentAdded = false;
2177
+ this.messageItemClosed = false;
2178
+ this.holdVisibleText = false;
1505
2179
  this.reasoningContentAdded = false;
1506
2180
  this.reasoningSummaryAdded = false;
1507
2181
  this.reasoningItemAdded = false;
1508
2182
  this.streamedReasoningSummaryText = '';
1509
2183
  this.toolSchemas = buildToolSchemas(normalized?.tools);
2184
+ this.knownToolNames = new Set([
2185
+ ...(Array.isArray(knownToolNames) || knownToolNames instanceof Set ? knownToolNames : []),
2186
+ ...this.toolSchemas.keys(),
2187
+ INTERNAL_COMMENTARY_TOOL,
2188
+ ]);
1510
2189
  this.toolNames = buildToolNames(normalized?.tools);
2190
+ this.customToolNames = buildCustomToolNames(normalized?.tools);
2191
+ this.bridgedCommentaryTool = !requestToolsIncludeCommentary(normalized?.tools);
2192
+ this.toolItemsAdded = new Set();
2193
+ this.toolItemsBufferedArguments = new Set();
2194
+ this.toolItemsStreamedArgumentLengths = new Map();
1511
2195
  this.config = config;
1512
2196
  }
1513
2197
 
@@ -1517,6 +2201,7 @@ export class ResponsesStreamMapper {
1517
2201
  }
1518
2202
 
1519
2203
  response(status = 'in_progress') {
2204
+ const outcome = deepSeekFinishReasonOutcome(this.finishReason);
1520
2205
  return createBaseResponse({
1521
2206
  id: this.responseId,
1522
2207
  model: this.model,
@@ -1527,7 +2212,8 @@ export class ResponsesStreamMapper {
1527
2212
  usage: this.usage,
1528
2213
  normalized: this.normalized,
1529
2214
  completedAt: this.completedAt,
1530
- incompleteReason: this.finishReason === 'length' ? 'max_output_tokens' : null,
2215
+ incompleteReason: status === 'in_progress' ? null : outcome.incompleteReason,
2216
+ error: status === 'failed' ? outcome.error : null,
1531
2217
  });
1532
2218
  }
1533
2219
 
@@ -1548,27 +2234,92 @@ export class ResponsesStreamMapper {
1548
2234
  }
1549
2235
 
1550
2236
  ensureMessageItem() {
2237
+ this.ensureMessageItemState();
2238
+ if (!this.messageItemAdded) {
2239
+ this.messageItemAdded = true;
2240
+ return {
2241
+ type: 'response.output_item.added',
2242
+ sequence_number: this.nextSequence(),
2243
+ output_index: this.output.length - 1,
2244
+ item: snapshotResponseItem(this.messageItem),
2245
+ };
2246
+ }
2247
+ return null;
2248
+ }
2249
+
2250
+ ensureMessageItemState() {
1551
2251
  if (!this.messageItem) {
1552
2252
  this.messageItem = {
1553
2253
  id: generateId('msg'),
1554
2254
  type: 'message',
1555
2255
  status: 'in_progress',
1556
2256
  role: 'assistant',
1557
- phase: 'final_answer',
2257
+ phase: this.messageStartsAsCommentary() ? 'commentary' : 'final_answer',
1558
2258
  content: [],
1559
2259
  };
1560
2260
  this.output.push(this.messageItem);
2261
+ }
2262
+ }
2263
+
2264
+ messageStartsAsCommentary() {
2265
+ return this.toolItems.size > 0 || (this.config.upstreamProvider === 'deepseek' && hasRunnableChatTools(this.normalized));
2266
+ }
2267
+
2268
+ finalizeMessagePhase() {
2269
+ if (!this.messageItem || this.toolItems.size > 0) return;
2270
+ this.messageItem.phase = 'final_answer';
2271
+ }
2272
+
2273
+ closeMessageItemAsCommentary(status = 'completed') {
2274
+ if (!this.messageItem || this.messageItemClosed) return [];
2275
+ this.messageItem.phase = 'commentary';
2276
+ if (!this.messageItemAdded) return [];
2277
+ this.messageItemClosed = true;
2278
+ this.messageItem.status = status;
2279
+ this.messageItem.content[0].text = this.text;
2280
+ const outputIndex = this.output.indexOf(this.messageItem);
2281
+ return [
2282
+ {
2283
+ type: 'response.output_text.done',
2284
+ sequence_number: this.nextSequence(),
2285
+ output_index: outputIndex,
2286
+ content_index: 0,
2287
+ item_id: this.messageItem.id,
2288
+ text: this.text,
2289
+ logprobs: [],
2290
+ },
2291
+ {
2292
+ type: 'response.content_part.done',
2293
+ sequence_number: this.nextSequence(),
2294
+ output_index: outputIndex,
2295
+ content_index: 0,
2296
+ item_id: this.messageItem.id,
2297
+ part: snapshotResponsePart(this.messageItem.content[0]),
2298
+ },
2299
+ {
2300
+ type: 'response.output_item.done',
2301
+ sequence_number: this.nextSequence(),
2302
+ output_index: outputIndex,
2303
+ item: snapshotResponseItem(this.messageItem),
2304
+ },
2305
+ ];
2306
+ }
2307
+
2308
+ ensureReasoningItem() {
2309
+ this.ensureReasoningItemState();
2310
+ if (!this.reasoningItemAdded) {
2311
+ this.reasoningItemAdded = true;
1561
2312
  return {
1562
2313
  type: 'response.output_item.added',
1563
2314
  sequence_number: this.nextSequence(),
1564
- output_index: this.output.length - 1,
1565
- item: snapshotResponseItem(this.messageItem),
2315
+ output_index: this.output.indexOf(this.reasoningItem),
2316
+ item: snapshotResponseItem(this.reasoningItem),
1566
2317
  };
1567
2318
  }
1568
2319
  return null;
1569
2320
  }
1570
2321
 
1571
- ensureReasoningItem() {
2322
+ ensureReasoningItemState() {
1572
2323
  if (!this.reasoningItem) {
1573
2324
  this.reasoningItem = {
1574
2325
  id: generateId('rs'),
@@ -1580,16 +2331,6 @@ export class ResponsesStreamMapper {
1580
2331
  };
1581
2332
  this.output.push(this.reasoningItem);
1582
2333
  }
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
2334
  }
1594
2335
 
1595
2336
  ensureReasoningContentPart(events) {
@@ -1619,7 +2360,13 @@ export class ResponsesStreamMapper {
1619
2360
  else delete this.reasoningItem.reasoning_content;
1620
2361
  }
1621
2362
 
1622
- appendReasoningSummaryDelta(events) {
2363
+ reasoningSummarySourceText({ final = false } = {}) {
2364
+ if (final) return this.reasoningText;
2365
+ const boundary = this.reasoningText.lastIndexOf('\n');
2366
+ return boundary < 0 ? '' : this.reasoningText.slice(0, boundary + 1);
2367
+ }
2368
+
2369
+ appendReasoningSummaryDelta(events, { final = false } = {}) {
1623
2370
  if (!this.emitReasoningSummary || !this.reasoningItem) return;
1624
2371
  if (!this.reasoningSummaryAdded) {
1625
2372
  this.reasoningSummaryAdded = true;
@@ -1633,14 +2380,17 @@ export class ResponsesStreamMapper {
1633
2380
  part: snapshotResponsePart(this.reasoningItem.summary[0]),
1634
2381
  });
1635
2382
  }
1636
- const nextSummaryText = reasoningSummaryText(this.reasoningText);
1637
- this.reasoningItem.summary[0].text = nextSummaryText;
2383
+ const nextSummaryText = reasoningSummaryText(this.reasoningSummarySourceText({ final }));
2384
+ if (final) this.reasoningItem.summary[0].text = nextSummaryText;
2385
+ if (nextSummaryText === this.streamedReasoningSummaryText) return;
1638
2386
  if (!nextSummaryText.startsWith(this.streamedReasoningSummaryText)) {
2387
+ if (!final) return;
1639
2388
  this.streamedReasoningSummaryText = nextSummaryText;
1640
2389
  return;
1641
2390
  }
1642
2391
  const deltaText = nextSummaryText.slice(this.streamedReasoningSummaryText.length);
1643
2392
  this.streamedReasoningSummaryText = nextSummaryText;
2393
+ this.reasoningItem.summary[0].text = nextSummaryText;
1644
2394
  for (const delta of splitBufferedReasoningDeltas(deltaText)) {
1645
2395
  events.push({
1646
2396
  type: 'response.reasoning_summary_text.delta',
@@ -1653,28 +2403,20 @@ export class ResponsesStreamMapper {
1653
2403
  }
1654
2404
  }
1655
2405
 
2406
+ holdVisibleTextUntilDone() {
2407
+ this.holdVisibleText = true;
2408
+ }
2409
+
1656
2410
  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
- }
2411
+ const events = [...this.closeReasoningItem('completed')];
2412
+ if (this.messageItemClosed) return events;
2413
+ if (!this.messageItemAdded && (this.holdVisibleText || this.toolItems.size > 0)) {
2414
+ this.ensureMessageItemState();
2415
+ if (!this.messageItem.content.length) this.messageItem.content.push(normalizeOutputTextPart(''));
1669
2416
  this.text += delta;
1670
2417
  this.messageItem.content[0].text = this.text;
1671
- return [];
1672
- }
1673
- const events = [];
1674
- if (this.streamReasoningLive) {
1675
- events.push(...this.closeReasoningItem('completed'));
2418
+ return events;
1676
2419
  }
1677
- this.streamReasoningLive = false;
1678
2420
  const added = this.ensureMessageItem();
1679
2421
  if (added) events.push(added);
1680
2422
  if (!this.messageContentAdded) {
@@ -1705,15 +2447,11 @@ export class ResponsesStreamMapper {
1705
2447
 
1706
2448
  reasoningDelta(delta) {
1707
2449
  this.reasoningText += delta;
1708
- if (this.bufferOutputUntilDone) {
1709
- const events = [];
1710
- const added = this.ensureReasoningItem();
1711
- if (added) events.push(added);
2450
+ if (this.reasoningItemDone) {
1712
2451
  this.syncReasoningItemContent();
1713
- this.appendReasoningSummaryDelta(events);
1714
- return events;
2452
+ return [];
1715
2453
  }
1716
- if (!this.streamReasoningLive) {
2454
+ if (this.messageItem || this.toolItems.size > 0) {
1717
2455
  if (!this.reasoningItem) {
1718
2456
  this.reasoningItem = {
1719
2457
  id: generateId('rs'),
@@ -1748,124 +2486,250 @@ export class ResponsesStreamMapper {
1748
2486
  return events;
1749
2487
  }
1750
2488
 
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 [];
2489
+ createToolItem(toolCall) {
2490
+ const callId = toolCall.id || generateId('call');
2491
+ const name = toolCall.function?.name;
2492
+ if (isCodexToolSearchTool(name)) {
2493
+ return {
2494
+ id: generateId('tsc'),
2495
+ type: 'tool_search_call',
2496
+ status: 'in_progress',
2497
+ call_id: callId,
2498
+ execution: 'client',
2499
+ arguments: '',
2500
+ };
1784
2501
  }
1785
- const events = [];
1786
- if (this.streamReasoningLive) {
1787
- events.push(...this.closeReasoningItem('completed'));
2502
+ if (name && this.customToolNames.has(name)) {
2503
+ return {
2504
+ id: callId,
2505
+ type: 'custom_tool_call',
2506
+ status: 'in_progress',
2507
+ call_id: callId,
2508
+ ...decodedToolName(name, this.toolNames),
2509
+ input: '',
2510
+ arguments: '',
2511
+ };
2512
+ }
2513
+ return {
2514
+ id: callId,
2515
+ type: 'function_call',
2516
+ status: 'in_progress',
2517
+ call_id: callId,
2518
+ ...decodedToolName(name, this.toolNames),
2519
+ arguments: '',
2520
+ };
2521
+ }
2522
+
2523
+ applyToolCallName(item, name) {
2524
+ if (!name || item.type === 'tool_search_call') return;
2525
+ Object.assign(item, decodedToolName(name, this.toolNames));
2526
+ if (item.type === 'function_call' && this.customToolNames.has(name)) {
2527
+ item.type = 'custom_tool_call';
2528
+ if (typeof item.input !== 'string') item.input = '';
1788
2529
  }
1789
- this.streamReasoningLive = false;
2530
+ }
2531
+
2532
+ functionDelta(rawToolCall) {
2533
+ let toolCall = rawToolCall;
2534
+ const emittedName = toolCall.function?.name;
2535
+ if (emittedName) {
2536
+ const resolvedName = resolveEmittedToolName(emittedName, this.knownToolNames);
2537
+ if (resolvedName !== emittedName) {
2538
+ toolCall = { ...toolCall, function: { ...toolCall.function, name: resolvedName } };
2539
+ }
2540
+ }
2541
+ const events = [...this.closeReasoningItem('completed')];
2542
+ events.push(...this.closeMessageItemAsCommentary('completed'));
1790
2543
  const index = toolCall.index ?? 0;
1791
2544
  let item = this.toolItems.get(index);
1792
2545
  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
- };
2546
+ item = this.createToolItem(toolCall);
1811
2547
  this.toolItems.set(index, item);
1812
2548
  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
- });
2549
+ if (this.holdToolItemEvents || isParallelToolWrapperName(toolCall.function?.name)) {
2550
+ this.toolItemsBufferedArguments.add(index);
2551
+ }
2552
+ if (!this.holdToolItemEvents && hasToolCallFunctionName(toolCall) && !isParallelToolWrapperName(toolCall.function?.name) && !this.isBridgedCommentaryToolItem(item)) {
2553
+ events.push(...this.addToolItemEvents(index));
2554
+ }
1819
2555
  }
1820
2556
  if (isCodexToolSearchTool(toolCall.function?.name)) {
2557
+ const wasAdded = this.toolItemsAdded.has(index);
1821
2558
  item = convertItemToToolSearchCall(item);
1822
- } else if (toolCall.function?.name && item.type !== 'tool_search_call') {
1823
- Object.assign(item, decodedToolName(toolCall.function.name, this.toolNames));
2559
+ this.toolItems.set(index, item);
2560
+ if (!wasAdded && !this.holdToolItemEvents && hasToolCallFunctionName(toolCall)) {
2561
+ events.push(...this.addToolItemEvents(index));
2562
+ }
2563
+ } else if (toolCall.function?.name) {
2564
+ this.applyToolCallName(item, toolCall.function.name);
2565
+ if (!this.holdToolItemEvents && !this.toolItemsAdded.has(index) && !isParallelToolWrapperName(item.name) && !this.isBridgedCommentaryToolItem(item)) {
2566
+ events.push(...this.addToolItemEvents(index));
2567
+ }
2568
+ }
2569
+ item.arguments += toolCall.function?.arguments || '';
2570
+ if (functionCallItemNeedsArgumentNormalization(item, this.toolSchemas, this.config)) {
2571
+ this.toolItemsBufferedArguments.add(index);
1824
2572
  }
1825
- const delta = toolCall.function?.arguments || '';
1826
- item.arguments += delta;
1827
- if (delta && item.type === 'function_call') {
2573
+ const streamedLength = this.toolItemsStreamedArgumentLengths.get(index) || 0;
2574
+ const argumentDelta = item.arguments.slice(streamedLength);
2575
+ if (
2576
+ argumentDelta &&
2577
+ item.type === 'function_call' &&
2578
+ this.toolItemsAdded.has(index) &&
2579
+ !this.toolItemsBufferedArguments.has(index)
2580
+ ) {
2581
+ this.toolItemsStreamedArgumentLengths.set(index, item.arguments.length);
1828
2582
  events.push({
1829
2583
  type: 'response.function_call_arguments.delta',
1830
2584
  sequence_number: this.nextSequence(),
1831
2585
  output_index: this.output.indexOf(item),
1832
2586
  item_id: item.id,
1833
- delta,
2587
+ delta: argumentDelta,
1834
2588
  });
1835
2589
  }
1836
2590
  return events;
1837
2591
  }
1838
2592
 
1839
- finalize(finishReason = 'stop', usage = null) {
1840
- if (this.finalized) return [];
1841
- this.finalized = true;
1842
- const status = finishReason === 'length' ? 'incomplete' : 'completed';
2593
+ addToolItemEvents(index) {
2594
+ if (this.toolItemsAdded.has(index)) return [];
2595
+ const item = this.toolItems.get(index);
2596
+ if (!item) return [];
2597
+ this.toolItemsAdded.add(index);
2598
+ return [{
2599
+ type: 'response.output_item.added',
2600
+ sequence_number: this.nextSequence(),
2601
+ output_index: this.output.indexOf(item),
2602
+ item: snapshotResponseItem(item),
2603
+ }];
2604
+ }
2605
+
2606
+ expandParallelToolItems() {
2607
+ for (const [index, item] of [...this.toolItems.entries()]) {
2608
+ if (item.type !== 'function_call' || !isParallelToolWrapperName(item.name)) continue;
2609
+ if (this.toolItemsAdded.has(index)) continue;
2610
+ const uses = parallelToolUsesFromArguments(item.arguments);
2611
+ if (!uses) continue;
2612
+ const at = this.output.indexOf(item);
2613
+ this.toolItems.delete(index);
2614
+ this.toolItemsBufferedArguments.delete(index);
2615
+ this.toolItemsStreamedArgumentLengths.delete(index);
2616
+ const expandedItems = uses.map((use, position) => {
2617
+ const key = `${index}:${position}`;
2618
+ const expanded = this.createToolItem({
2619
+ id: generateId('call'),
2620
+ function: { name: resolveEmittedToolName(use.name, this.knownToolNames) },
2621
+ });
2622
+ expanded.arguments = use.arguments;
2623
+ this.toolItems.set(key, expanded);
2624
+ this.toolItemsBufferedArguments.add(key);
2625
+ return expanded;
2626
+ });
2627
+ if (at >= 0) this.output.splice(at, 1, ...expandedItems);
2628
+ else this.output.push(...expandedItems);
2629
+ }
2630
+ }
1843
2631
 
1844
- if (this.bufferOutputUntilDone) {
1845
- this.finishReason = finishReason;
1846
- this.usage = normalizeResponsesUsage(usage);
1847
- this.completedAt = Date.now() / 1000;
2632
+ isBridgedCommentaryToolItem(item) {
2633
+ return Boolean(this.bridgedCommentaryTool) && item?.type === 'function_call' && item?.name === INTERNAL_COMMENTARY_TOOL;
2634
+ }
1848
2635
 
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
- }
1858
- this.messageItem.content[0].text = this.text;
2636
+ convertBridgedCommentaryItems() {
2637
+ const events = [];
2638
+ for (const [index, item] of [...this.toolItems.entries()]) {
2639
+ if (!this.isBridgedCommentaryToolItem(item) || this.toolItemsAdded.has(index)) continue;
2640
+ this.toolItems.delete(index);
2641
+ this.toolItemsBufferedArguments.delete(index);
2642
+ this.toolItemsStreamedArgumentLengths.delete(index);
2643
+ const at = this.output.indexOf(item);
2644
+ const text = commentaryTextFromArguments(item.arguments);
2645
+ if (!text) {
2646
+ if (at >= 0) this.output.splice(at, 1);
2647
+ continue;
1859
2648
  }
2649
+ const messageItem = {
2650
+ type: 'message',
2651
+ id: generateId('msg'),
2652
+ role: 'assistant',
2653
+ status: 'completed',
2654
+ phase: 'commentary',
2655
+ content: [normalizeOutputTextPart(text)],
2656
+ };
2657
+ if (at >= 0) this.output.splice(at, 1, messageItem);
2658
+ else this.output.push(messageItem);
2659
+ const outputIndex = this.output.indexOf(messageItem);
2660
+ events.push({
2661
+ type: 'response.output_item.added',
2662
+ sequence_number: this.nextSequence(),
2663
+ output_index: outputIndex,
2664
+ item: snapshotResponseItem({ ...messageItem, status: 'in_progress', content: [] }),
2665
+ });
2666
+ events.push({
2667
+ type: 'response.content_part.added',
2668
+ sequence_number: this.nextSequence(),
2669
+ output_index: outputIndex,
2670
+ content_index: 0,
2671
+ item_id: messageItem.id,
2672
+ part: snapshotResponsePart(normalizeOutputTextPart('')),
2673
+ });
2674
+ events.push({
2675
+ type: 'response.output_text.delta',
2676
+ sequence_number: this.nextSequence(),
2677
+ output_index: outputIndex,
2678
+ content_index: 0,
2679
+ item_id: messageItem.id,
2680
+ delta: text,
2681
+ logprobs: [],
2682
+ });
2683
+ events.push({
2684
+ type: 'response.output_text.done',
2685
+ sequence_number: this.nextSequence(),
2686
+ output_index: outputIndex,
2687
+ content_index: 0,
2688
+ item_id: messageItem.id,
2689
+ text,
2690
+ logprobs: [],
2691
+ });
2692
+ events.push({
2693
+ type: 'response.content_part.done',
2694
+ sequence_number: this.nextSequence(),
2695
+ output_index: outputIndex,
2696
+ content_index: 0,
2697
+ item_id: messageItem.id,
2698
+ part: snapshotResponsePart(messageItem.content[0]),
2699
+ });
2700
+ events.push({
2701
+ type: 'response.output_item.done',
2702
+ sequence_number: this.nextSequence(),
2703
+ output_index: outputIndex,
2704
+ item: snapshotResponseItem(messageItem),
2705
+ });
2706
+ }
2707
+ return events;
2708
+ }
1860
2709
 
1861
- const events = [];
1862
- if (this.reasoningItem && this.bufferOutputUntilDone) {
1863
- events.push(...this.flushBufferedReasoning(status));
2710
+ finalize(finishReason = 'stop', usage = null) {
2711
+ if (this.finalized) return [];
2712
+ this.finalized = true;
2713
+ this.expandParallelToolItems();
2714
+ const outcome = deepSeekFinishReasonOutcome(finishReason);
2715
+ const status = outcome.status;
2716
+ this.terminalStatus = status;
2717
+ this.finishReason = finishReason;
2718
+ this.usage = normalizeResponsesUsage(usage);
2719
+ this.completedAt = Date.now() / 1000;
2720
+ const events = [];
2721
+ if (this.messageItem) {
2722
+ this.messageItem.status = status;
2723
+ const outputIndex = this.output.indexOf(this.messageItem);
2724
+ if (!this.messageItemClosed) {
2725
+ this.finalizeMessagePhase();
2726
+ if (!this.messageItem.content.length) this.messageItem.content.push(normalizeOutputTextPart(''));
2727
+ this.messageItem.content[0].text = this.text;
1864
2728
  }
1865
- events.push(...this.closeReasoningItem(status));
1866
-
1867
- if (this.messageItem) {
1868
- const outputIndex = this.output.indexOf(this.messageItem);
2729
+ if (!this.messageItemAdded) {
2730
+ this.messageItemAdded = true;
2731
+ this.messageContentAdded = true;
2732
+ this.messageItemClosed = true;
1869
2733
  events.push({
1870
2734
  type: 'response.output_item.added',
1871
2735
  sequence_number: this.nextSequence(),
@@ -1895,36 +2759,22 @@ export class ResponsesStreamMapper {
1895
2759
  logprobs: [],
1896
2760
  });
1897
2761
  }
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
- });
2762
+ events.push(...this.messageDoneEvents(outputIndex));
2763
+ } else if (!this.messageItemClosed) {
2764
+ this.messageItemClosed = true;
2765
+ events.push(...this.messageDoneEvents(outputIndex));
1921
2766
  }
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);
2767
+ }
2768
+ events.push(...this.closeReasoningItem(status));
2769
+ events.push(...this.convertBridgedCommentaryItems());
2770
+ for (const [index, item] of this.toolItems.entries()) {
2771
+ item.status = status;
2772
+ if (item.type === 'tool_search_call') finalizeToolSearchCallItemArguments(item);
2773
+ else if (item.type === 'custom_tool_call') item.input = customToolInputFromArguments(item.arguments);
2774
+ else normalizeFunctionCallItemArguments(item, this.toolSchemas, this.config);
2775
+ const outputIndex = this.output.indexOf(item);
2776
+ if (!this.toolItemsAdded.has(index)) {
2777
+ this.toolItemsAdded.add(index);
1928
2778
  events.push({
1929
2779
  type: 'response.output_item.added',
1930
2780
  sequence_number: this.nextSequence(),
@@ -1932,121 +2782,31 @@ export class ResponsesStreamMapper {
1932
2782
  item: snapshotResponseItem(item.type === 'tool_search_call' ? {
1933
2783
  ...item,
1934
2784
  status: 'in_progress',
2785
+ } : item.type === 'custom_tool_call' ? {
2786
+ ...item,
2787
+ status: 'in_progress',
2788
+ input: '',
1935
2789
  } : {
1936
2790
  ...item,
1937
2791
  status: 'in_progress',
1938
2792
  arguments: '',
1939
2793
  }),
1940
2794
  });
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
2795
  }
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
- }
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;
2796
+ if (item.type === 'function_call' && this.toolItemsBufferedArguments.has(index) && item.arguments) {
2007
2797
  events.push({
2008
- type: 'response.content_part.added',
2798
+ type: 'response.function_call_arguments.delta',
2009
2799
  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]),
2800
+ output_index: outputIndex,
2801
+ item_id: item.id,
2802
+ delta: item.arguments,
2014
2803
  });
2015
2804
  }
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
2805
  if (item.type === 'function_call') {
2046
2806
  events.push({
2047
2807
  type: 'response.function_call_arguments.done',
2048
2808
  sequence_number: this.nextSequence(),
2049
- output_index: this.output.indexOf(item),
2809
+ output_index: outputIndex,
2050
2810
  item_id: item.id,
2051
2811
  arguments: item.arguments,
2052
2812
  });
@@ -2054,7 +2814,7 @@ export class ResponsesStreamMapper {
2054
2814
  events.push({
2055
2815
  type: 'response.output_item.done',
2056
2816
  sequence_number: this.nextSequence(),
2057
- output_index: this.output.indexOf(item),
2817
+ output_index: outputIndex,
2058
2818
  item: snapshotResponseItem(item),
2059
2819
  });
2060
2820
  }
@@ -2068,103 +2828,141 @@ export class ResponsesStreamMapper {
2068
2828
  usage: normalizeResponsesUsage(usage),
2069
2829
  normalized: this.normalized,
2070
2830
  completedAt: Date.now() / 1000,
2071
- incompleteReason: finishReason === 'length' ? 'max_output_tokens' : null,
2831
+ incompleteReason: outcome.incompleteReason,
2832
+ error: outcome.error,
2072
2833
  });
2073
2834
  events.push({
2074
- type: status === 'incomplete' ? 'response.incomplete' : 'response.completed',
2835
+ type: status === 'completed' ? 'response.completed' : status === 'incomplete' ? 'response.incomplete' : 'response.failed',
2075
2836
  sequence_number: this.nextSequence(),
2076
2837
  response,
2077
2838
  });
2078
2839
  return events;
2079
2840
  }
2080
2841
 
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',
2842
+ messageDoneEvents(outputIndex) {
2843
+ return [
2844
+ {
2845
+ type: 'response.output_text.done',
2094
2846
  sequence_number: this.nextSequence(),
2095
2847
  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',
2848
+ content_index: 0,
2849
+ item_id: this.messageItem.id,
2850
+ text: this.messageItem.content[0].text,
2851
+ logprobs: [],
2852
+ },
2853
+ {
2854
+ type: 'response.content_part.done',
2108
2855
  sequence_number: this.nextSequence(),
2109
2856
  output_index: outputIndex,
2110
2857
  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',
2858
+ item_id: this.messageItem.id,
2859
+ part: snapshotResponsePart(this.messageItem.content[0]),
2860
+ },
2861
+ {
2862
+ type: 'response.output_item.done',
2132
2863
  sequence_number: this.nextSequence(),
2133
2864
  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;
2865
+ item: snapshotResponseItem(this.messageItem),
2866
+ },
2867
+ ];
2868
+ }
2869
+
2870
+ assistantMessage() {
2871
+ return assistantMessageFromResponseOutput(this.output);
2872
+ }
2873
+
2874
+ markRoundStart() {
2875
+ this.roundStartIndex = this.output.length;
2876
+ }
2877
+
2878
+ roundAssistantMessage() {
2879
+ return assistantMessageFromResponseOutput(this.output.slice(this.roundStartIndex));
2880
+ }
2881
+
2882
+ removeToolItems(predicate = () => true) {
2883
+ for (const [index, item] of [...this.toolItems.entries()]) {
2884
+ if (!predicate(item)) continue;
2885
+ if (this.toolItemsAdded.has(index)) continue;
2886
+ this.toolItems.delete(index);
2887
+ this.toolItemsBufferedArguments.delete(index);
2888
+ this.toolItemsStreamedArgumentLengths.delete(index);
2889
+ const at = this.output.indexOf(item);
2890
+ if (at >= 0) this.output.splice(at, 1);
2151
2891
  }
2152
- this.reasoningItem.status = status;
2153
- this.syncReasoningItemContent();
2892
+ }
2893
+
2894
+ beginNextRound() {
2895
+ const events = [...this.closeReasoningItem('completed'), ...this.closeMessageItemAsCommentary('completed')];
2896
+ if (this.reasoningItem && !this.reasoningItemAdded) {
2897
+ const at = this.output.indexOf(this.reasoningItem);
2898
+ if (at >= 0) this.output.splice(at, 1);
2899
+ }
2900
+ if (this.messageItem && !this.messageItemAdded) {
2901
+ const at = this.output.indexOf(this.messageItem);
2902
+ if (at >= 0) this.output.splice(at, 1);
2903
+ }
2904
+ for (const [index, item] of this.toolItems.entries()) {
2905
+ if (this.toolItemsAdded.has(index)) continue;
2906
+ const at = this.output.indexOf(item);
2907
+ if (at >= 0) this.output.splice(at, 1);
2908
+ }
2909
+ this.messageItem = null;
2910
+ this.reasoningItem = null;
2911
+ this.toolItems = new Map();
2912
+ this.toolItemsAdded = new Set();
2913
+ this.toolItemsBufferedArguments = new Set();
2914
+ this.toolItemsStreamedArgumentLengths = new Map();
2915
+ this.text = '';
2916
+ this.reasoningText = '';
2917
+ this.reasoningItemDone = false;
2918
+ this.messageItemAdded = false;
2919
+ this.messageContentAdded = false;
2920
+ this.messageItemClosed = false;
2921
+ this.holdVisibleText = false;
2922
+ this.reasoningContentAdded = false;
2923
+ this.reasoningSummaryAdded = false;
2924
+ this.reasoningItemAdded = false;
2925
+ this.streamedReasoningSummaryText = '';
2926
+ this.pendingFinishReason = null;
2927
+ this.pendingUsage = null;
2154
2928
  return events;
2155
2929
  }
2156
2930
 
2931
+ replaceBufferedAssistantText(text) {
2932
+ if (this.messageItemAdded || this.messageContentAdded) return null;
2933
+ if (!this.messageItem) return this.textDelta(String(text ?? ''));
2934
+ this.text = String(text ?? '');
2935
+ if (this.messageItem.content.length) this.messageItem.content[0].text = this.text;
2936
+ return [];
2937
+ }
2938
+
2157
2939
  closeReasoningItem(status = 'completed') {
2158
2940
  if (!this.reasoningItem || this.reasoningItemDone) return [];
2159
2941
  this.reasoningItemDone = true;
2160
2942
  this.reasoningItem.status = status;
2161
2943
  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
- }
2944
+ this.reasoningItem.encrypted_content = encodeGatewayReasoning(normalizeReasoningContent(this.reasoningText));
2167
2945
  const events = [];
2946
+ if (!this.reasoningItemAdded) {
2947
+ this.reasoningItemAdded = true;
2948
+ const addedItem = {
2949
+ ...this.reasoningItem,
2950
+ status: 'in_progress',
2951
+ summary: [],
2952
+ content: this.emitReasoningText ? [normalizeReasoningTextPart('')] : [],
2953
+ encrypted_content: null,
2954
+ };
2955
+ delete addedItem.reasoning_content;
2956
+ events.push({
2957
+ type: 'response.output_item.added',
2958
+ sequence_number: this.nextSequence(),
2959
+ output_index: this.output.indexOf(this.reasoningItem),
2960
+ item: snapshotResponseItem(addedItem),
2961
+ });
2962
+ }
2963
+ if (this.emitReasoningSummary && this.reasoningText) {
2964
+ this.appendReasoningSummaryDelta(events, { final: true });
2965
+ }
2168
2966
  if (this.reasoningSummaryAdded) {
2169
2967
  for (const [summaryIndex, part] of this.reasoningItem.summary.entries()) {
2170
2968
  events.push({
@@ -2212,11 +3010,36 @@ export class ResponsesStreamMapper {
2212
3010
  return events;
2213
3011
  }
2214
3012
 
3013
+ streamFailed(message) {
3014
+ if (this.finalized) return [];
3015
+ this.finalized = true;
3016
+ this.terminalStatus = 'failed';
3017
+ this.completedAt = Date.now() / 1000;
3018
+ const response = this.response('failed');
3019
+ response.error = { code: 'upstream_error', message: String(message || 'upstream stream failed') };
3020
+ return [{
3021
+ type: 'response.failed',
3022
+ sequence_number: this.nextSequence(),
3023
+ response,
3024
+ }];
3025
+ }
3026
+
2215
3027
  mapChatEvent(event) {
2216
3028
  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;
3029
+ if (event.done) {
3030
+ if (this.pendingFinishReason || !event.eof) {
3031
+ return this.finalize(this.pendingFinishReason || 'stop', this.pendingUsage);
3032
+ }
3033
+ return this.streamFailed('upstream stream ended before completion');
3034
+ }
3035
+ let payload = event.data;
3036
+ if (typeof payload === 'string') {
3037
+ const parsed = safeJsonParse(payload);
3038
+ if (!parsed.ok) return [];
3039
+ payload = parsed.value;
3040
+ }
2219
3041
  if (!isObject(payload)) return [];
3042
+ if (isObject(payload.usage)) this.pendingUsage = payload.usage;
2220
3043
  const choice = Array.isArray(payload.choices) ? payload.choices[0] : null;
2221
3044
  if (!choice) return [];
2222
3045
  const delta = choice.delta || choice.message || {};
@@ -2236,7 +3059,7 @@ export class ResponsesStreamMapper {
2236
3059
  }
2237
3060
  }
2238
3061
  if (choice.finish_reason) {
2239
- events.push(...this.finalize(choice.finish_reason, payload.usage ?? null));
3062
+ this.pendingFinishReason = choice.finish_reason;
2240
3063
  }
2241
3064
  return events;
2242
3065
  }