@galaxy-yearn/codex-deepseek-gateway 0.2.0 → 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
@@ -2,9 +2,7 @@ import { createHash } from 'node:crypto';
2
2
 
3
3
  import { generateId, isObject, normalizeRole, parseJsonObject, safeJsonParse, toText } from './common.js';
4
4
  import {
5
- DEFAULT_MODEL_ALIASES,
6
5
  deepseekReasoningPayload,
7
- isDeprecatedModel,
8
6
  resolveModelAlias,
9
7
  } from './model-map.js';
10
8
 
@@ -51,17 +49,16 @@ const UNSUPPORTED_HOSTED_TOOL_TYPES = new Set([
51
49
  const DEEPSEEK_TOOL_INSTRUCTIONS_MARKER = 'The tools in this request are real callable functions available now.';
52
50
  const DEEPSEEK_TOOL_INSTRUCTIONS = [
53
51
  DEEPSEEK_TOOL_INSTRUCTIONS_MARKER,
54
- 'When a tool is needed, respond with Chat Completions tool_calls and JSON arguments matching the schema.',
55
- 'Do not write pseudo XML, DSML, or JSON tool calls in assistant text.',
56
- '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.',
57
54
  ].join(' ');
58
55
  export const INTERNAL_COMMENTARY_TOOL = 'commentary';
59
- const DEEPSEEK_COMMENTARY_INSTRUCTIONS = 'The user cannot see your thinking; commentary is the only progress they see between tool batches. Include one commentary call, first, in every tool_calls batch, with one or two sentences on what you are doing. Never call it alone; never put the final answer in it deliver the final answer as plain assistant text with no tool calls.';
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.';
60
57
  const INTERNAL_COMMENTARY_TOOL_DEFINITION = {
61
58
  type: 'function',
62
59
  function: {
63
60
  name: INTERNAL_COMMENTARY_TOOL,
64
- description: "Post a short progress update the user sees immediately. The user cannot see your thinking; this is the only progress they see between tool batches. Include one commentary call, first, in every tool_calls batch, e.g. commentary(text: 'Config loader read; now checking how sessions persist.'). One or two sentences. Never call it alone; never put the final answer in it.",
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.',
65
62
  parameters: {
66
63
  type: 'object',
67
64
  properties: {
@@ -77,10 +74,9 @@ const INTERNAL_COMMENTARY_TOOL_DEFINITION = {
77
74
  };
78
75
  const CHAT_FUNCTION_NAME_MAX_CHARS = 64;
79
76
  const TOOL_NAME_HASH_CHARS = 8;
80
- const DEEPSEEK_TOOL_DESCRIPTION_CHARS = 900;
81
- const DEEPSEEK_SCHEMA_DESCRIPTION_CHARS = 480;
82
- const CODEX_REASONING_EFFORTS = ['low', 'medium', 'high', 'xhigh'];
83
- const CODEX_SPAWN_AGENT_TOOL_NAME = 'multi_agent_v1__spawn_agent';
77
+ const DEEPSEEK_TOOL_DESCRIPTION_SOFT_CHARS = 900;
78
+ const DEEPSEEK_SCHEMA_DESCRIPTION_SOFT_CHARS = 480;
79
+ const DEEPSEEK_MAX_FUNCTION_TOOLS = 128;
84
80
  const CODEX_TOOL_SEARCH_TOOL_NAME = 'tool_search';
85
81
 
86
82
  function jsonString(value) {
@@ -172,6 +168,16 @@ function compactText(value) {
172
168
  return String(value ?? '').replace(/\s+/g, ' ').trim();
173
169
  }
174
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
+
175
181
  function shortenText(value, maxChars) {
176
182
  const text = compactText(value);
177
183
  if (!text || text.length <= maxChars) return text;
@@ -631,8 +637,6 @@ function extractMessagesFromResponsesInput(input) {
631
637
  }
632
638
 
633
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.';
634
- const CUSTOM_TOOL_GRAMMAR_CHARS = 1600;
635
- const CUSTOM_TOOL_DESCRIPTION_CHARS = 3600;
636
640
 
637
641
  function truncateRawText(value, maxChars) {
638
642
  const text = String(value ?? '');
@@ -658,25 +662,25 @@ function customToolShim(tool) {
658
662
  const format = isObject(tool.format) ? tool.format : {};
659
663
  const baseName = toolBaseName(tool, 'custom_tool');
660
664
  const chunks = [
661
- compactText(tool.description) || `Freeform tool ${baseName}.`,
665
+ compactStructuredText(tool.description, DEEPSEEK_TOOL_DESCRIPTION_SOFT_CHARS) || `Freeform tool ${baseName}.`,
662
666
  CUSTOM_TOOL_INPUT_HINT,
663
667
  ];
664
668
  if (format.syntax) chunks.push(`Input syntax: ${format.syntax}.`);
665
669
  if (typeof format.definition === 'string' && format.definition.trim()) {
666
- chunks.push(`Input grammar:\n${truncateRawText(format.definition.trim(), CUSTOM_TOOL_GRAMMAR_CHARS)}`);
670
+ chunks.push(`Input grammar:\n${format.definition.trim()}`);
667
671
  }
668
672
  const parameters = customToolShimParameters();
669
673
  const sourceSchema = isObject(tool.input_schema) ? tool.input_schema : isObject(tool.parameters) ? tool.parameters : null;
670
674
  const sourceInputDescription = sourceSchema?.properties?.input?.description;
671
675
  if (typeof sourceInputDescription === 'string' && sourceInputDescription) {
672
- parameters.properties.input.description = sourceInputDescription;
676
+ parameters.properties.input.description = compactStructuredText(sourceInputDescription, DEEPSEEK_SCHEMA_DESCRIPTION_SOFT_CHARS);
673
677
  }
674
678
  return {
675
679
  type: 'function',
676
680
  gateway_custom_tool: true,
677
681
  function: {
678
682
  name: encodeToolName(toolNamespace(tool), baseName),
679
- description: truncateRawText(chunks.join('\n'), CUSTOM_TOOL_DESCRIPTION_CHARS),
683
+ description: chunks.join('\n'),
680
684
  parameters,
681
685
  },
682
686
  };
@@ -1127,7 +1131,7 @@ function simplifyJsonSchemaDescriptions(schema) {
1127
1131
  const next = {};
1128
1132
  for (const [key, value] of Object.entries(schema)) {
1129
1133
  if (key === 'description' && typeof value === 'string') {
1130
- const description = shortenText(value, DEEPSEEK_SCHEMA_DESCRIPTION_CHARS);
1134
+ const description = compactStructuredText(value, DEEPSEEK_SCHEMA_DESCRIPTION_SOFT_CHARS);
1131
1135
  if (description) next.description = description;
1132
1136
  continue;
1133
1137
  }
@@ -1150,83 +1154,15 @@ function defaultDeepSeekToolDescription(name) {
1150
1154
  return compactText(`Call ${readable || name || 'this function'} when needed.`);
1151
1155
  }
1152
1156
 
1153
- function deepSeekGatewayModelAliases(config = {}) {
1154
- const aliases = isObject(config.modelAliases) && Object.keys(config.modelAliases).length
1155
- ? config.modelAliases
1156
- : DEFAULT_MODEL_ALIASES;
1157
- const spawnConfig = { ...config, upstreamProvider: 'deepseek', modelAliases: aliases };
1158
- const names = [];
1159
- for (const name of Object.keys(aliases)) {
1160
- if (!name || isDeprecatedModel(name)) continue;
1161
- const alias = resolveModelAlias(name, spawnConfig);
1162
- if (isDeprecatedModel(alias.upstreamModel)) continue;
1163
- names.push(name);
1164
- }
1165
- return [...new Set(names)];
1166
- }
1167
-
1168
- function enumStringProperty(property, values, description) {
1169
- const source = isObject(property) ? property : {};
1170
- const next = { ...source };
1171
- delete next.const;
1172
- delete next.oneOf;
1173
- delete next.anyOf;
1174
- delete next.allOf;
1175
- delete next.examples;
1176
- if (next.default !== undefined && !values.includes(next.default)) delete next.default;
1177
- next.type = 'string';
1178
- next.enum = values;
1179
- next.description = description;
1180
- return next;
1181
- }
1182
-
1183
- function deepSeekSpawnAgentParameters(parameters, config = {}) {
1184
- const next = normalizeJsonSchemaObject(parameters);
1185
- const properties = isObject(next.properties) ? { ...next.properties } : {};
1186
- const modelAliases = deepSeekGatewayModelAliases(config);
1187
- properties.model = enumStringProperty(
1188
- properties.model,
1189
- modelAliases,
1190
- 'Model for the sub-agent. Omit to inherit.',
1191
- );
1192
- properties.reasoning_effort = enumStringProperty(
1193
- properties.reasoning_effort,
1194
- CODEX_REASONING_EFFORTS,
1195
- 'Codex reasoning effort for the sub-agent. Omit to inherit.',
1196
- );
1197
- return {
1198
- ...next,
1199
- properties,
1200
- };
1201
- }
1202
-
1203
- function deepSeekSpawnAgentDescription() {
1204
- const description = [
1205
- 'Spawn a Codex-native sub-agent.',
1206
- 'Omit model/reasoning_effort to inherit.',
1207
- 'Valid values are enforced by the schema.',
1208
- ].filter(Boolean).join(' ');
1209
- return shortenText(description, DEEPSEEK_TOOL_DESCRIPTION_CHARS);
1210
- }
1211
-
1212
- function isCodexSpawnAgentTool(name) {
1213
- return name === CODEX_SPAWN_AGENT_TOOL_NAME;
1214
- }
1215
-
1216
- function simplifyToolForDeepSeek(tool, config = {}) {
1157
+ function simplifyToolForDeepSeek(tool) {
1217
1158
  if (tool?.type !== 'function' || !isObject(tool.function)) return tool;
1218
1159
  if (tool.gateway_custom_tool) return tool;
1219
1160
  const fn = tool.function;
1220
- let parameters = simplifyJsonSchemaDescriptions(fn.parameters);
1221
- if (isCodexSpawnAgentTool(fn.name)) {
1222
- parameters = deepSeekSpawnAgentParameters(parameters, config);
1223
- }
1161
+ const parameters = simplifyJsonSchemaDescriptions(fn.parameters);
1224
1162
  const requiredText = requiredParametersText(parameters);
1225
- const descriptionBudget = Math.max(32, DEEPSEEK_TOOL_DESCRIPTION_CHARS - requiredText.length);
1226
- const baseDescription = isCodexSpawnAgentTool(fn.name)
1227
- ? deepSeekSpawnAgentDescription()
1228
- : shortenText(fn.description, descriptionBudget) || defaultDeepSeekToolDescription(fn.name);
1229
- 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}`;
1230
1166
  return {
1231
1167
  ...tool,
1232
1168
  function: omitUndefined({
@@ -1266,7 +1202,7 @@ function adaptToolsForProvider(tools, provider, config = {}) {
1266
1202
  if (provider !== 'deepseek' || !Array.isArray(tools)) return tools;
1267
1203
  const keepStrict = isDeepSeekBetaBaseUrl(config.upstreamBaseUrl);
1268
1204
  return tools.map((tool) => {
1269
- const simplified = simplifyToolForDeepSeek(tool, config);
1205
+ const simplified = simplifyToolForDeepSeek(tool);
1270
1206
  if (!keepStrict && simplified?.type === 'function' && isObject(simplified.function) && simplified.function.strict !== undefined) {
1271
1207
  const { strict, ...fn } = simplified.function;
1272
1208
  return { ...simplified, function: fn };
@@ -1275,6 +1211,17 @@ function adaptToolsForProvider(tools, provider, config = {}) {
1275
1211
  });
1276
1212
  }
1277
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;
1223
+ }
1224
+
1278
1225
  function normalizeToolCallArguments(name, argumentsText, toolSchema) {
1279
1226
  if (!argumentsText) return '';
1280
1227
  if (!isObject(toolSchema) || !isObject(toolSchema.properties)) return argumentsText;
@@ -1297,54 +1244,16 @@ function normalizeToolCallArguments(name, argumentsText, toolSchema) {
1297
1244
  }
1298
1245
  }
1299
1246
 
1300
- function normalizeReasoningEffortName(value) {
1301
- return value == null ? '' : String(value).toLowerCase().replaceAll('_', '-');
1302
- }
1303
-
1304
- function normalizeCodexSpawnAgentArguments(encodedName, argumentsText, config = {}) {
1305
- if (!isCodexSpawnAgentTool(encodedName) || !argumentsText) return argumentsText;
1306
- let parsed;
1307
- try {
1308
- parsed = JSON.parse(argumentsText);
1309
- } catch {
1310
- return argumentsText;
1311
- }
1312
- if (!isObject(parsed)) return argumentsText;
1313
- const next = { ...parsed };
1314
- let changed = false;
1315
- const allowedModels = deepSeekGatewayModelAliases(config);
1316
-
1317
- if (typeof next.model === 'string') {
1318
- const requestedModel = next.model;
1319
- if (allowedModels.length && !allowedModels.includes(requestedModel)) {
1320
- delete next.model;
1321
- changed = true;
1322
- }
1323
- }
1324
-
1325
- if (typeof next.reasoning_effort === 'string') {
1326
- const requestedEffort = normalizeReasoningEffortName(next.reasoning_effort);
1327
- if (!CODEX_REASONING_EFFORTS.includes(requestedEffort)) {
1328
- delete next.reasoning_effort;
1329
- changed = true;
1330
- return JSON.stringify(next);
1331
- }
1332
- }
1333
-
1334
- return changed ? JSON.stringify(next) : argumentsText;
1335
- }
1336
-
1337
- function normalizeFunctionCallItemArguments(item, toolSchemas, config = {}) {
1247
+ function normalizeFunctionCallItemArguments(item, toolSchemas) {
1338
1248
  if (!item || item.type !== 'function_call') return;
1339
1249
  const encodedName = encodeToolName(item.namespace, item.name);
1340
1250
  item.arguments = normalizeToolCallArguments(encodedName, item.arguments, toolSchemas?.get(encodedName));
1341
- item.arguments = normalizeCodexSpawnAgentArguments(encodedName, item.arguments, config);
1342
1251
  }
1343
1252
 
1344
- function functionCallItemNeedsArgumentNormalization(item, toolSchemas, config = {}) {
1253
+ function functionCallItemNeedsArgumentNormalization(item, toolSchemas) {
1345
1254
  if (!item || item.type !== 'function_call') return false;
1346
1255
  const encodedName = encodeToolName(item.namespace, item.name);
1347
- return Boolean(toolSchemas?.has(encodedName) || isCodexSpawnAgentTool(encodedName));
1256
+ return Boolean(toolSchemas?.has(encodedName));
1348
1257
  }
1349
1258
 
1350
1259
  function hasToolCallFunctionName(toolCall) {
@@ -1475,7 +1384,7 @@ function deepseekDefaultMaxTokens(config = {}) {
1475
1384
  return DEEPSEEK_DEFAULT_MAX_TOKENS;
1476
1385
  }
1477
1386
 
1478
- const DEEPSEEK_JSON_SCHEMA_INSTRUCTIONS_MARKER = 'Respond with a single valid json object';
1387
+ const DEEPSEEK_JSON_SCHEMA_INSTRUCTIONS_MARKER = 'Return only one valid JSON object';
1479
1388
 
1480
1389
  function jsonSchemaFromResponseFormat(responseFormat) {
1481
1390
  if (!isObject(responseFormat)) return null;
@@ -1490,10 +1399,10 @@ function addDeepSeekJsonSchemaInstructions(messages, responseFormat) {
1490
1399
  const schema = jsonSchemaFromResponseFormat(responseFormat);
1491
1400
  const schemaText = schema ? truncateRawText(JSON.stringify(schema), 6000) : '';
1492
1401
  const instructions = [
1493
- `${DEEPSEEK_JSON_SCHEMA_INSTRUCTIONS_MARKER} and nothing else: no prose, no markdown fences.`,
1402
+ `${DEEPSEEK_JSON_SCHEMA_INSTRUCTIONS_MARKER}, with no prose or Markdown.`,
1494
1403
  schemaText
1495
- ? `The json object must conform to this JSON Schema:\n${schemaText}`
1496
- : 'Use the json object shape requested in the conversation.',
1404
+ ? `It must match this JSON Schema:\n${schemaText}`
1405
+ : 'Use the object shape requested in the conversation.',
1497
1406
  ].join('\n');
1498
1407
  if (!messages.length || messages[0]?.role !== 'system') {
1499
1408
  return [{ role: 'system', content: instructions }, ...messages];
@@ -1656,12 +1565,14 @@ function normalizeInstructions(instructions) {
1656
1565
  return toText(instructions);
1657
1566
  }
1658
1567
 
1659
- export function normalizeResponsesRequest(request) {
1568
+ export function normalizeResponsesRequest(request, { restoreDiscoveredTools = true } = {}) {
1660
1569
  const messages = extractMessagesFromResponsesInput(request.input ?? request);
1661
1570
  const model = request.model || request.model_id || request.upstream_model || '';
1662
1571
  const instructions = normalizeInstructions(request.instructions);
1663
1572
  const responseFormat = request.response_format || request.text?.format;
1664
- const tools = mergeToolsWithToolSearchOutput(request.tools, request.input ?? request);
1573
+ const tools = restoreDiscoveredTools
1574
+ ? mergeToolsWithToolSearchOutput(request.tools, request.input ?? request)
1575
+ : request.tools;
1665
1576
  return {
1666
1577
  model,
1667
1578
  messages,
@@ -1683,10 +1594,8 @@ export function normalizeResponsesRequest(request) {
1683
1594
  text: request.text,
1684
1595
  truncation: request.truncation,
1685
1596
  user: request.user,
1686
- previous_response_id: request.previous_response_id,
1687
1597
  store: request.store,
1688
1598
  include: request.include,
1689
- conversation: request.conversation,
1690
1599
  };
1691
1600
  }
1692
1601
 
@@ -1848,7 +1757,10 @@ export function toProviderChatCompletionsRequest(chatRequest, config = {}) {
1848
1757
  if (provider === 'deepseek') {
1849
1758
  delete request.reasoning_effort;
1850
1759
  delete request.parallel_tool_calls;
1760
+ delete request.frequency_penalty;
1761
+ delete request.presence_penalty;
1851
1762
  request.tools = addInternalCommentaryTool(adaptToolsForProvider(request.tools, provider, config), chatRequest.tool_choice);
1763
+ assertDeepSeekToolCapacity(request.tools);
1852
1764
  request.messages = addDeepSeekToolInstructions(request.messages, request.tools);
1853
1765
  if (request.user !== undefined) {
1854
1766
  request.user_id = request.user;
@@ -1899,6 +1811,7 @@ function createBaseResponse({
1899
1811
  normalized,
1900
1812
  completedAt = null,
1901
1813
  incompleteReason = null,
1814
+ error = null,
1902
1815
  }) {
1903
1816
  return {
1904
1817
  id,
@@ -1907,7 +1820,7 @@ function createBaseResponse({
1907
1820
  completed_at: completedAt == null ? null : Math.floor(completedAt),
1908
1821
  status,
1909
1822
  background: false,
1910
- error: null,
1823
+ error,
1911
1824
  incomplete_details: incompleteReason ? { reason: incompleteReason } : null,
1912
1825
  instructions: normalized?.instructions || null,
1913
1826
  max_output_tokens: normalized?.max_tokens ?? null,
@@ -1930,6 +1843,37 @@ function createBaseResponse({
1930
1843
  };
1931
1844
  }
1932
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
+
1933
1877
  function normalizeResponsesUsage(usage) {
1934
1878
  if (!isObject(usage)) return null;
1935
1879
  const inputTokens = usage.input_tokens ?? usage.prompt_tokens ?? 0;
@@ -1991,7 +1935,7 @@ export function createResponseEnvelope({
1991
1935
  const PARALLEL_TOOL_WRAPPER_NAMES = new Set(['multi_tool_use.parallel', 'multi_tool_use_parallel']);
1992
1936
  const EMITTED_TOOL_NAME_PREFIX = 'functions.';
1993
1937
 
1994
- export function isParallelToolWrapperName(name) {
1938
+ function isParallelToolWrapperName(name) {
1995
1939
  const raw = String(name || '').toLowerCase();
1996
1940
  const stripped = raw.startsWith(EMITTED_TOOL_NAME_PREFIX) ? raw.slice(EMITTED_TOOL_NAME_PREFIX.length) : raw;
1997
1941
  return PARALLEL_TOOL_WRAPPER_NAMES.has(stripped);
@@ -2001,7 +1945,7 @@ function toolNameMatchKey(name) {
2001
1945
  return String(name || '').toLowerCase().replace(/\./g, '__');
2002
1946
  }
2003
1947
 
2004
- export function resolveEmittedToolName(name, knownNames) {
1948
+ function resolveEmittedToolName(name, knownNames) {
2005
1949
  const raw = String(name || '');
2006
1950
  if (!raw || !knownNames) return name;
2007
1951
  const known = knownNames instanceof Set ? knownNames : new Set(knownNames);
@@ -2074,7 +2018,7 @@ function parallelToolUsesFromArguments(argumentsText) {
2074
2018
  return calls;
2075
2019
  }
2076
2020
 
2077
- export function expandParallelToolCalls(toolCalls) {
2021
+ function expandParallelToolCalls(toolCalls) {
2078
2022
  if (!Array.isArray(toolCalls)) return toolCalls;
2079
2023
  if (!toolCalls.some((toolCall) => isParallelToolWrapperName(toolCall?.function?.name))) return toolCalls;
2080
2024
  const expanded = [];
@@ -2120,6 +2064,7 @@ export function convertChatCompletionToResponses({ completion: rawCompletion, mo
2120
2064
  );
2121
2065
  const createdAt = completion.created || Date.now() / 1000;
2122
2066
  const choice = Array.isArray(completion.choices) ? completion.choices[0] : null;
2067
+ const outcome = deepSeekFinishReasonOutcome(choice?.finish_reason);
2123
2068
  const message = choice?.message || {};
2124
2069
  const content = chatContentToResponseOutputParts(message.content);
2125
2070
  const messageHasToolCalls = hasChatToolCalls(message);
@@ -2133,7 +2078,7 @@ export function convertChatCompletionToResponses({ completion: rawCompletion, mo
2133
2078
  id: generateId('msg'),
2134
2079
  role: 'assistant',
2135
2080
  content,
2136
- status: 'completed',
2081
+ status: outcome.status,
2137
2082
  phase: message.phase || (messageHasToolCalls ? 'commentary' : 'final_answer'),
2138
2083
  });
2139
2084
  }
@@ -2156,6 +2101,7 @@ export function convertChatCompletionToResponses({ completion: rawCompletion, mo
2156
2101
  continue;
2157
2102
  }
2158
2103
  const item = responseItemFromChatToolCall(toolCall, toolNames, customToolNames);
2104
+ item.status = outcome.status;
2159
2105
  normalizeFunctionCallItemArguments(item, toolSchemas, config);
2160
2106
  output.push(item);
2161
2107
  }
@@ -2170,7 +2116,7 @@ export function convertChatCompletionToResponses({ completion: rawCompletion, mo
2170
2116
  content: [],
2171
2117
  reasoning_content: reasoningText,
2172
2118
  encrypted_content: encodeGatewayReasoning(reasoningText),
2173
- status: 'completed',
2119
+ status: outcome.status,
2174
2120
  });
2175
2121
  }
2176
2122
 
@@ -2178,13 +2124,14 @@ export function convertChatCompletionToResponses({ completion: rawCompletion, mo
2178
2124
  id: responseId,
2179
2125
  model,
2180
2126
  createdAt,
2181
- status: choice?.finish_reason === 'length' ? 'incomplete' : 'completed',
2127
+ status: outcome.status,
2182
2128
  output,
2183
2129
  previousResponseId: previousResponseId ?? null,
2184
2130
  usage: normalizeResponsesUsage(completion.usage),
2185
2131
  normalized,
2186
2132
  completedAt: Date.now() / 1000,
2187
- incompleteReason: choice?.finish_reason === 'length' ? 'max_output_tokens' : null,
2133
+ incompleteReason: outcome.incompleteReason,
2134
+ error: outcome.error,
2188
2135
  });
2189
2136
  }
2190
2137
 
@@ -2224,6 +2171,7 @@ export class ResponsesStreamMapper {
2224
2171
  this.pendingUsage = null;
2225
2172
  this.completedAt = null;
2226
2173
  this.finalized = false;
2174
+ this.terminalStatus = null;
2227
2175
  this.messageItemAdded = false;
2228
2176
  this.messageContentAdded = false;
2229
2177
  this.messageItemClosed = false;
@@ -2253,6 +2201,7 @@ export class ResponsesStreamMapper {
2253
2201
  }
2254
2202
 
2255
2203
  response(status = 'in_progress') {
2204
+ const outcome = deepSeekFinishReasonOutcome(this.finishReason);
2256
2205
  return createBaseResponse({
2257
2206
  id: this.responseId,
2258
2207
  model: this.model,
@@ -2263,7 +2212,8 @@ export class ResponsesStreamMapper {
2263
2212
  usage: this.usage,
2264
2213
  normalized: this.normalized,
2265
2214
  completedAt: this.completedAt,
2266
- incompleteReason: this.finishReason === 'length' ? 'max_output_tokens' : null,
2215
+ incompleteReason: status === 'in_progress' ? null : outcome.incompleteReason,
2216
+ error: status === 'failed' ? outcome.error : null,
2267
2217
  });
2268
2218
  }
2269
2219
 
@@ -2761,7 +2711,9 @@ export class ResponsesStreamMapper {
2761
2711
  if (this.finalized) return [];
2762
2712
  this.finalized = true;
2763
2713
  this.expandParallelToolItems();
2764
- const status = finishReason === 'length' ? 'incomplete' : 'completed';
2714
+ const outcome = deepSeekFinishReasonOutcome(finishReason);
2715
+ const status = outcome.status;
2716
+ this.terminalStatus = status;
2765
2717
  this.finishReason = finishReason;
2766
2718
  this.usage = normalizeResponsesUsage(usage);
2767
2719
  this.completedAt = Date.now() / 1000;
@@ -2876,10 +2828,11 @@ export class ResponsesStreamMapper {
2876
2828
  usage: normalizeResponsesUsage(usage),
2877
2829
  normalized: this.normalized,
2878
2830
  completedAt: Date.now() / 1000,
2879
- incompleteReason: finishReason === 'length' ? 'max_output_tokens' : null,
2831
+ incompleteReason: outcome.incompleteReason,
2832
+ error: outcome.error,
2880
2833
  });
2881
2834
  events.push({
2882
- type: status === 'incomplete' ? 'response.incomplete' : 'response.completed',
2835
+ type: status === 'completed' ? 'response.completed' : status === 'incomplete' ? 'response.incomplete' : 'response.failed',
2883
2836
  sequence_number: this.nextSequence(),
2884
2837
  response,
2885
2838
  });
@@ -3060,6 +3013,7 @@ export class ResponsesStreamMapper {
3060
3013
  streamFailed(message) {
3061
3014
  if (this.finalized) return [];
3062
3015
  this.finalized = true;
3016
+ this.terminalStatus = 'failed';
3063
3017
  this.completedAt = Date.now() / 1000;
3064
3018
  const response = this.response('failed');
3065
3019
  response.error = { code: 'upstream_error', message: String(message || 'upstream stream failed') };