modelmix 5.1.20 → 5.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/index.js CHANGED
@@ -8,6 +8,7 @@ const log = require('lemonlog')('ModelMix');
8
8
  const Bottleneck = require('bottleneck');
9
9
  const path = require('path');
10
10
  const generateJsonSchema = require('./schema');
11
+ const parseJsonResponse = require('./lib/parse-json-response');
11
12
  const { Client } = require("@modelcontextprotocol/sdk/client/index.js");
12
13
  const { StdioClientTransport } = require("@modelcontextprotocol/sdk/client/stdio.js");
13
14
  const { MCPToolsManager } = require('./mcp-tools');
@@ -47,6 +48,7 @@ let MixKimi;
47
48
  let MixAnthropic;
48
49
  let MixMiniMax;
49
50
  let MixMiMo;
51
+ let MixDeepSeek;
50
52
  let MixPerplexity;
51
53
  let MixOllama;
52
54
  let MixGrok;
@@ -764,6 +766,10 @@ class ModelMix {
764
766
  return this;
765
767
  }
766
768
 
769
+ deepseekPro({ options = {}, config = {} } = {}) {
770
+ return this.attach('deepseek/deepseek-v4-pro-0813', new MixOpenRouter({ options, config }));
771
+ }
772
+
767
773
  deepseekV4Pro({ options = {}, config = {}, mix = { fireworks: true } } = {}) {
768
774
  mix = { ...this.mix, ...mix };
769
775
  if (mix.nvidia) this.attach('deepseek-ai/deepseek-v4-pro', new MixNVIDIA({ options, config }));
@@ -782,6 +788,14 @@ class ModelMix {
782
788
  return this;
783
789
  }
784
790
 
791
+ deepseekV41Flash({ options = {}, config = {}, mix = { deepseek: true } } = {}) {
792
+ mix = { ...this.mix, ...mix };
793
+ if (mix.deepseek) this.attach('deepseek-flash', new MixDeepSeek({ options, config }));
794
+ if (mix.fireworks) this.attach('accounts/fireworks/models/deepseek-v4p1-flash', new MixFireworks({ options, config }));
795
+ if (mix.openrouter) this.attach('deepseek/deepseek-v4.1-flash', new MixOpenRouter({ options, config }));
796
+ return this;
797
+ }
798
+
785
799
  GLM52({ options = {}, config = {}, mix = { together: true } } = {}) {
786
800
  mix = { ...this.mix, ...mix };
787
801
  if (mix.together) this.attach('zai-org/GLM-5.2', new MixTogether({ options, config }));
@@ -1000,7 +1014,13 @@ class ModelMix {
1000
1014
  }
1001
1015
  }
1002
1016
  const { message } = await this.execute({ options, config, systemSuffix, outputMode: 'json', signal });
1003
- const parsed = JSON.parse(this._extractBlock(message));
1017
+ let parsed;
1018
+ try {
1019
+ parsed = parseJsonResponse(message);
1020
+ } catch (error) {
1021
+ if (!(error instanceof SyntaxError)) throw error;
1022
+ parsed = JSON.parse(this._extractBlock(message));
1023
+ }
1004
1024
  return isArrayWrap ? parsed.out : parsed;
1005
1025
  }
1006
1026
 
@@ -1292,6 +1312,7 @@ class ModelMix {
1292
1312
  const request = {
1293
1313
  system: this._renderSystem(config, {}, systemSuffix, templateContext),
1294
1314
  messages: clonePluginValue(preparedMessages),
1315
+ tools: [],
1295
1316
  options: clonePluginValue({ ...this.options, ...options }),
1296
1317
  config: clonePluginValue(this._mergeRequestConfig(config)),
1297
1318
  outputMode
@@ -1363,15 +1384,34 @@ class ModelMix {
1363
1384
  templateContext
1364
1385
  }) {
1365
1386
  const provider = currentModel.provider;
1387
+ const tools = pluginRequest?.tools.length ? {
1388
+ ...this.tools,
1389
+ local: [...(this.tools.local || []), ...pluginRequest.tools.map(entry => entry.tool)]
1390
+ } : this.tools;
1391
+ const toolOptions = provider.getOptionsTools(tools);
1366
1392
  const currentOptions = {
1367
1393
  ...this.options,
1368
1394
  messages: preparedMessages,
1369
1395
  ...provider.options,
1370
- ...provider.getOptionsTools(this.tools),
1396
+ ...toolOptions,
1371
1397
  ...options,
1372
1398
  ...(pluginRequest?.options || {}),
1373
1399
  model: currentModel.key
1374
1400
  };
1401
+ if (pluginRequest?.tools.length && currentOptions.tools !== toolOptions.tools) {
1402
+ if (!Array.isArray(currentOptions.tools)) {
1403
+ throw new TypeError('Request options.tools must be an array when using plugin tools.');
1404
+ }
1405
+ currentOptions.tools = [...(toolOptions.tools || []), ...currentOptions.tools];
1406
+ const names = new Set();
1407
+ for (const tool of currentOptions.tools) {
1408
+ for (const definition of tool.functionDeclarations || [tool.function || tool]) {
1409
+ if (!definition.name) continue;
1410
+ if (names.has(definition.name)) throw new Error(`Duplicate tool name: ${definition.name}`);
1411
+ names.add(definition.name);
1412
+ }
1413
+ }
1414
+ }
1375
1415
  const currentConfig = pluginRequest
1376
1416
  ? {
1377
1417
  ...provider.config,
@@ -1471,24 +1511,28 @@ class ModelMix {
1471
1511
  }
1472
1512
  }
1473
1513
 
1474
- _enrichResultTokens(result, resolvedModelKey, elapsedMs) {
1514
+ _enrichResultTokens(result, resolvedModelKey, elapsedMs, provider) {
1475
1515
  if (!result.tokens) return;
1476
1516
 
1477
1517
  const normalizedTokens = ModelMix.normalizeTokenUsage(result.tokens);
1478
1518
  const costBreakdown = ModelMix.calculateCostBreakdown(resolvedModelKey, normalizedTokens);
1479
1519
  const cacheMetrics = ModelMix.calculateCacheMetrics(resolvedModelKey, normalizedTokens);
1520
+ const response = Array.isArray(result.response)
1521
+ ? result.response.findLast(chunk => chunk.usage)
1522
+ : result.response;
1523
+ const reportedCost = provider instanceof MixOpenRouter ? response?.usage?.cost : undefined;
1480
1524
  result.tokens = {
1481
1525
  ...result.tokens,
1482
1526
  ...normalizedTokens,
1483
1527
  ...cacheMetrics,
1484
- cost: tokenUsage.hasModelPricing(resolvedModelKey) ? costBreakdown.total : 0,
1528
+ cost: Number.isFinite(reportedCost) && reportedCost >= 0 ? reportedCost : costBreakdown.total,
1485
1529
  costBreakdown
1486
1530
  };
1487
1531
  const elapsedSec = elapsedMs / 1000;
1488
1532
  result.tokens.speed = elapsedSec > 0 ? Math.round(result.tokens.output / elapsedSec) : 0;
1489
1533
  }
1490
1534
 
1491
- async _continueToolCalls(result, pluginRequest, execution) {
1535
+ async _continueToolCalls(result, pluginRequest, execution, pluginTools) {
1492
1536
  const originalMessages = this.messages;
1493
1537
  const toolMessages = pluginRequest
1494
1538
  ? clonePluginValue(pluginRequest.messages)
@@ -1516,7 +1560,7 @@ class ModelMix {
1516
1560
  if (!result.assistantMessage) {
1517
1561
  toolMessages.push({ role: 'assistant', content: null, tool_calls: result.toolCalls });
1518
1562
  }
1519
- const toolResults = await this.processToolCalls(result.toolCalls, execution.signal);
1563
+ const toolResults = await this.processToolCalls(result.toolCalls, execution.signal, pluginTools);
1520
1564
  for (const toolResult of toolResults) {
1521
1565
  toolMessages.push({
1522
1566
  role: 'tool',
@@ -1626,6 +1670,23 @@ class ModelMix {
1626
1670
  this._requirePreparedMessages(preparedMessages);
1627
1671
 
1628
1672
  const finalConfig = pluginRequest ? pluginRequest.config : this._mergeRequestConfig(config);
1673
+ const pluginTools = pluginRequest ? new MCPToolsManager() : null;
1674
+ if (pluginRequest) {
1675
+ if (!Array.isArray(pluginRequest.tools)) {
1676
+ throw new TypeError('Plugin request tools must be an array.');
1677
+ }
1678
+ const names = new Set(Object.values(this.tools).flat().map(tool => tool.name));
1679
+ for (const entry of pluginRequest.tools) {
1680
+ if (!isPlainObject(entry) || !isPlainObject(entry.tool)) {
1681
+ throw new TypeError('Plugin request tools must contain { tool, callback }.');
1682
+ }
1683
+ if (names.has(entry.tool.name)) {
1684
+ throw new Error(`Duplicate tool name: ${entry.tool.name}`);
1685
+ }
1686
+ pluginTools.registerTool(entry.tool, entry.callback);
1687
+ names.add(entry.tool.name);
1688
+ }
1689
+ }
1629
1690
  const modelsToTry = this.models.map((model, index) => ({ model, index }));
1630
1691
  if (finalConfig.roundRobin && this.models.length > 1) {
1631
1692
  this.models.push(this.models.shift());
@@ -1659,7 +1720,7 @@ class ModelMix {
1659
1720
  providerAttempt.resolvedModelKey,
1660
1721
  signal
1661
1722
  );
1662
- this._enrichResultTokens(result, providerAttempt.resolvedModelKey, elapsedMs);
1723
+ this._enrichResultTokens(result, providerAttempt.resolvedModelKey, elapsedMs, providerAttempt.provider);
1663
1724
 
1664
1725
  if (result.toolCalls && result.toolCalls.length > 0) {
1665
1726
  return this._continueToolCalls(result, pluginRequest, {
@@ -1671,7 +1732,7 @@ class ModelMix {
1671
1732
  _templateContext: templateContext,
1672
1733
  _executionMetadata: executionMetadata,
1673
1734
  _pluginsApplied: pluginsApplied
1674
- });
1735
+ }, pluginTools);
1675
1736
  }
1676
1737
 
1677
1738
  this._logProviderSuccess(result, providerAttempt.currentConfig);
@@ -1747,7 +1808,7 @@ class ModelMix {
1747
1808
  if (isRootExecution) this._commitTemplateRenderContext(templateContext);
1748
1809
  return result;
1749
1810
  }
1750
- async processToolCalls(toolCalls, signal) {
1811
+ async processToolCalls(toolCalls, signal, pluginTools) {
1751
1812
  assertAbortSignal(signal);
1752
1813
  const result = []
1753
1814
 
@@ -1780,8 +1841,9 @@ class ModelMix {
1780
1841
  }
1781
1842
 
1782
1843
  // Verificar si es una herramienta local registrada
1783
- if (this.mcpToolsManager.hasTool(toolName)) {
1784
- const response = await this.mcpToolsManager.executeTool(toolName, toolArgs, signal);
1844
+ if (pluginTools?.hasTool(toolName) || this.mcpToolsManager.hasTool(toolName)) {
1845
+ const manager = pluginTools?.hasTool(toolName) ? pluginTools : this.mcpToolsManager;
1846
+ const response = await manager.executeTool(toolName, toolArgs, signal);
1785
1847
  throwIfAborted(signal);
1786
1848
  result.push({
1787
1849
  name: toolName,
@@ -1926,6 +1988,7 @@ class ModelMix {
1926
1988
  MixAnthropic,
1927
1989
  MixMiniMax,
1928
1990
  MixMiMo,
1991
+ MixDeepSeek,
1929
1992
  MixPerplexity,
1930
1993
  MixOllama,
1931
1994
  MixGrok,
@@ -1943,4 +2006,4 @@ class ModelMix {
1943
2006
  log
1944
2007
  }));
1945
2008
 
1946
- module.exports = { MixCustom, ModelMix, ModerationMix, MixModeration, MixAnthropic, MixKimi, MixMiniMax, MixMiMo, MixOpenAI, MixOpenAIResponses, MixOpenAIModeration, MixOpenAIWebSocket, MixOpenRouter, MixPerplexity, MixOllama, MixLambda, MixLMStudio, MixGroq, MixTogether, MixGrok, MixCerebras, MixGoogle, MixFireworks, MixNVIDIA, normalizeEffort, applyUnifiedEffort, resolveProviderFamily };
2009
+ module.exports = { MixCustom, ModelMix, ModerationMix, MixModeration, MixAnthropic, MixKimi, MixMiniMax, MixMiMo, MixDeepSeek, MixOpenAI, MixOpenAIResponses, MixOpenAIModeration, MixOpenAIWebSocket, MixOpenRouter, MixPerplexity, MixOllama, MixLambda, MixLMStudio, MixGroq, MixTogether, MixGrok, MixCerebras, MixGoogle, MixFireworks, MixNVIDIA, normalizeEffort, applyUnifiedEffort, resolveProviderFamily };
@@ -15,7 +15,7 @@ const CHAIN_MODEL_SHORTCUTS = new Set([
15
15
  'hermes470b', 'hermes4405b', 'hermes3',
16
16
  'kimiK26', 'kimiK27Code', 'kimiK3', 'kimiK25',
17
17
  'minimaxM27', 'minimaxM3', 'mimo25', 'mimo25pro',
18
- 'deepseekV4Pro', 'deepseekV4Flash', 'GLM52', 'GLM53', 'GLM53Flash'
18
+ 'deepseekV4Pro', 'deepseekPro', 'deepseekV4Flash', 'deepseekV41Flash', 'GLM52', 'GLM53', 'GLM53Flash'
19
19
  ]);
20
20
 
21
21
  function parseChainModels(modelSpecs) {
@@ -0,0 +1,14 @@
1
+ function parseJsonResponse(message) {
2
+ try {
3
+ return JSON.parse(message);
4
+ } catch (error) {
5
+ if (!(error instanceof SyntaxError) || typeof message !== 'string') throw error;
6
+ const text = message.trim();
7
+ const fenced = text.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
8
+ if (fenced) return JSON.parse(fenced[1]);
9
+ if (text.endsWith('```')) return JSON.parse(text.slice(0, -3));
10
+ throw error;
11
+ }
12
+ }
13
+
14
+ module.exports = parseJsonResponse;
@@ -278,9 +278,11 @@ function createAnthropicProviders({ ModelMix, MixCustom, log }) {
278
278
  );
279
279
  const input = (data.usage.input_tokens || 0) + cached + cacheWrite;
280
280
  const output = data.usage.output_tokens || 0;
281
+ const thinking = data.usage.output_tokens_details?.thinking_tokens || 0;
281
282
  return ModelMix.normalizeTokenUsage({
282
283
  input,
283
- output,
284
+ output: output - thinking,
285
+ thinking,
284
286
  total: input + output,
285
287
  cached,
286
288
  cacheWrite,
@@ -146,6 +146,10 @@ function createBaseProviders({ ModelMix }) {
146
146
  let raw = [];
147
147
  let message = '';
148
148
  let buffer = '';
149
+ let completed = false;
150
+ let chatCompletion = false;
151
+ let usage = {};
152
+ let usageMetadata;
149
153
 
150
154
  response.data.on('data', chunk => {
151
155
  buffer += chunk.toString();
@@ -156,39 +160,75 @@ function createBaseProviders({ ModelMix }) {
156
160
  buffer = buffer.slice(boundary + 1);
157
161
 
158
162
  const firstBraceIndex = dataStr.indexOf('{');
159
- if (dataStr === '[DONE]' || firstBraceIndex === -1) continue;
163
+ if (dataStr === 'data: [DONE]' || dataStr === '[DONE]') {
164
+ completed = true;
165
+ continue;
166
+ }
167
+ if (firstBraceIndex === -1) continue;
160
168
 
161
169
  const jsonStr = dataStr.slice(firstBraceIndex);
162
170
  try {
163
171
  const data = JSON.parse(jsonStr);
172
+ MixCustom.assertResponse(data);
173
+ chatCompletion ||= Array.isArray(data.choices);
174
+ completed ||= Boolean(data.choices?.[0]?.finish_reason);
175
+ raw.push(data);
176
+ usage = { ...usage, ...data.message?.usage, ...data.usage };
177
+ if (data.usageMetadata) usageMetadata = data.usageMetadata;
178
+ const delta = this.extractDelta(data);
179
+ message += delta;
164
180
  if (this.streamCallback) {
165
- const delta = this.extractDelta(data);
166
- message += delta;
167
181
  this.streamCallback({ response: data, message, delta });
168
- raw.push(data);
169
182
  }
170
183
  } catch (error) {
171
- console.error('Error parsing JSON:', error);
184
+ reject(error);
185
+ response.data.destroy();
186
+ return;
172
187
  }
173
188
  }
174
189
  });
175
190
 
176
- response.data.on('end', () => resolve({
177
- response: raw,
178
- message: message.trim(),
179
- toolCalls: [],
180
- think: null,
181
- tokens: raw.length > 0 ? MixCustom.extractTokens(raw[raw.length - 1]) : { input: 0, output: 0, total: 0, cached: 0 }
182
- }));
191
+ response.data.on('end', () => {
192
+ if (chatCompletion && !completed) {
193
+ reject(MixCustom.responseError(raw.at(-1), 'Provider stream ended without completing.'));
194
+ return;
195
+ }
196
+ resolve({
197
+ response: raw,
198
+ message: message.trim(),
199
+ toolCalls: [],
200
+ think: null,
201
+ tokens: this.constructor.extractTokens({ usage, usageMetadata })
202
+ });
203
+ });
183
204
  response.data.on('error', reject);
184
205
  });
185
206
  }
186
207
 
187
208
  extractDelta(data) {
188
- return data.choices[0].delta.content;
209
+ return data.choices?.[0]?.delta?.content || '';
210
+ }
211
+
212
+ static responseError(data, message, statusCode = 502) {
213
+ const error = new Error(message);
214
+ error.statusCode = statusCode;
215
+ error.details = data;
216
+ return error;
217
+ }
218
+
219
+ static assertResponse(data) {
220
+ const failure = data?.error || data?.choices?.find(choice => choice.error)?.error;
221
+ if (failure || data?.choices?.some(choice => choice.finish_reason === 'error') || data?.status === 'failed') {
222
+ throw MixCustom.responseError(
223
+ data,
224
+ failure?.message || 'Provider failed to complete the response.',
225
+ Number.isInteger(failure?.code) && failure.code >= 400 && failure.code <= 599 ? failure.code : 502
226
+ );
227
+ }
189
228
  }
190
229
 
191
230
  static extractMessage(data) {
231
+ MixCustom.assertResponse(data);
192
232
  const choice = data?.choices?.[0] || {};
193
233
  const messageObj = choice.message || {};
194
234
  const finishReason = choice.finish_reason;
@@ -254,9 +294,12 @@ function createBaseProviders({ ModelMix }) {
254
294
  static extractTokens(data) {
255
295
  // OpenAI/Groq/Together/Lambda/Cerebras/Fireworks format
256
296
  if (data.usage) {
297
+ const output = data.usage.completion_tokens || 0;
298
+ const thinking = data.usage.completion_tokens_details?.reasoning_tokens || 0;
257
299
  return ModelMix.normalizeTokenUsage({
258
300
  input: data.usage.prompt_tokens || 0,
259
- output: data.usage.completion_tokens || 0,
301
+ output: output - thinking,
302
+ thinking,
260
303
  total: data.usage.total_tokens,
261
304
  cached: ModelMix.extractCacheTokens(data.usage),
262
305
  cacheWrite: ModelMix.extractCacheWriteTokens(data.usage)
@@ -266,11 +309,16 @@ function createBaseProviders({ ModelMix }) {
266
309
  }
267
310
 
268
311
  processResponse(response) {
312
+ const message = MixCustom.extractMessage(response.data);
313
+ const toolCalls = MixCustom.extractToolCalls(response.data);
314
+ if (!message && toolCalls.length === 0 && response.data?.choices?.[0]?.finish_reason !== 'length') {
315
+ throw MixCustom.responseError(response.data, 'Provider returned no text or tool calls without completing a usable response.');
316
+ }
269
317
  return {
270
- message: MixCustom.extractMessage(response.data),
318
+ message,
271
319
  think: MixCustom.extractThink(response.data),
272
- toolCalls: MixCustom.extractToolCalls(response.data),
273
- tokens: MixCustom.extractTokens(response.data),
320
+ toolCalls,
321
+ tokens: this.constructor.extractTokens(response.data),
274
322
  response: response.data
275
323
  }
276
324
  }
@@ -61,6 +61,12 @@ function createGoogleProviders({ ModelMix, MixCustom }) {
61
61
  }
62
62
  }
63
63
 
64
+ if (typeof message.content === 'string') {
65
+ return {
66
+ role: message.role === 'assistant' ? 'model' : 'user',
67
+ parts: [{ text: message.content }]
68
+ };
69
+ }
64
70
  if (!Array.isArray(message.content)) return message;
65
71
  const role = (message.role === 'assistant' || message.role === 'tool') ? 'model' : 'user'
66
72
 
@@ -164,7 +170,10 @@ function createGoogleProviders({ ModelMix, MixCustom }) {
164
170
  options.tools.some(t => t.functionDeclarations && t.functionDeclarations.length > 0);
165
171
 
166
172
  if (!hasTools) {
167
- generationConfig.responseMimeType = "text/plain";
173
+ generationConfig.responseMimeType = options.response_format?.type === 'json_object'
174
+ || options.response_format?.type === 'json_schema'
175
+ ? 'application/json'
176
+ : 'text/plain';
168
177
  }
169
178
 
170
179
  const payload = {
@@ -230,7 +239,9 @@ function createGoogleProviders({ ModelMix, MixCustom }) {
230
239
  }
231
240
 
232
241
  static extractMessage(data) {
233
- return data.candidates?.[0]?.content?.parts?.[0]?.text;
242
+ const parts = data.candidates?.[0]?.content?.parts;
243
+ const textParts = parts?.filter(part => !part.thought && typeof part.text === 'string');
244
+ return textParts?.length ? textParts.map(part => part.text).join('') : undefined;
234
245
  }
235
246
 
236
247
  static extractTokens(data) {
@@ -5,6 +5,29 @@ const {
5
5
  const { requireProviderApiKey } = require('../provider-api-key');
6
6
 
7
7
  function createCompatibleProviders({ MixCustom, MixOpenAI }) {
8
+ class MixDeepSeek extends MixOpenAI {
9
+ getDefaultConfig(customConfig) {
10
+ const apiKey = requireProviderApiKey(customConfig, 'DEEPSEEK_API_KEY', 'DeepSeek');
11
+
12
+ return MixCustom.prototype.getDefaultConfig.call(this, {
13
+ url: 'https://api.deepseek.com/chat/completions',
14
+ apiKey,
15
+ ...customConfig
16
+ });
17
+ }
18
+
19
+ extractDelta(data) {
20
+ return data?.choices?.[0]?.delta?.content || '';
21
+ }
22
+
23
+ processResponse(response) {
24
+ return {
25
+ ...super.processResponse(response),
26
+ assistantMessage: response.data?.choices?.[0]?.message
27
+ };
28
+ }
29
+ }
30
+
8
31
  class MixMiniMax extends MixOpenAI {
9
32
  getDefaultConfig(customConfig) {
10
33
  const apiKey = requireProviderApiKey(customConfig, 'MINIMAX_API_KEY', 'MiniMax');
@@ -124,6 +147,18 @@ function createCompatibleProviders({ MixCustom, MixOpenAI }) {
124
147
  }
125
148
 
126
149
  class MixGrok extends MixOpenAI {
150
+ static extractTokens(data) {
151
+ if (!data.usage) return MixCustom.extractTokens(data);
152
+ return MixCustom.extractTokens({
153
+ ...data,
154
+ usage: {
155
+ ...data.usage,
156
+ completion_tokens: (data.usage.completion_tokens || 0)
157
+ + (data.usage.completion_tokens_details?.reasoning_tokens || 0)
158
+ }
159
+ });
160
+ }
161
+
127
162
  getDefaultConfig(customConfig) {
128
163
  const apiKey = requireProviderApiKey(customConfig, 'XAI_API_KEY', 'Grok');
129
164
 
@@ -292,6 +327,7 @@ function createCompatibleProviders({ MixCustom, MixOpenAI }) {
292
327
  }
293
328
 
294
329
  return {
330
+ MixDeepSeek,
295
331
  MixMiniMax,
296
332
  MixMiMo,
297
333
  MixPerplexity,
@@ -54,6 +54,17 @@ function createOpenAIProviders({
54
54
 
55
55
  if (options.reasoning_effort) request.reasoning = { effort: options.reasoning_effort };
56
56
  if (options.verbosity) request.text = { verbosity: options.verbosity };
57
+ if (options.tools !== undefined) {
58
+ request.tools = options.tools.map(tool => tool.type === 'function' && tool.function
59
+ ? { type: 'function', strict: false, ...tool.function }
60
+ : tool);
61
+ }
62
+ if (options.tool_choice !== undefined) {
63
+ request.tool_choice = options.tool_choice?.function
64
+ ? { type: 'function', name: options.tool_choice.function.name }
65
+ : options.tool_choice;
66
+ }
67
+ if (options.parallel_tool_calls !== undefined) request.parallel_tool_calls = options.parallel_tool_calls;
57
68
 
58
69
  if (options.response_format) {
59
70
  const rf = options.response_format;
@@ -136,11 +147,27 @@ function createOpenAIProviders({
136
147
  }
137
148
 
138
149
  static processResponsesResponse(response) {
150
+ MixCustom.assertResponse(response.data);
139
151
  const message = MixOpenAIResponses.extractResponsesMessage(response.data);
152
+ const toolCalls = (response.data.output || [])
153
+ .filter(item => item.type === 'function_call')
154
+ .map(item => ({
155
+ id: item.call_id,
156
+ type: 'function',
157
+ function: { name: item.name, arguments: item.arguments }
158
+ }));
140
159
  return {
141
160
  message,
142
161
  think: null,
143
- toolCalls: [],
162
+ toolCalls,
163
+ ...(toolCalls.length > 0 && {
164
+ assistantMessage: {
165
+ role: 'assistant',
166
+ content: message || null,
167
+ tool_calls: toolCalls,
168
+ _responsesOutput: response.data.output
169
+ }
170
+ }),
144
171
  tokens: MixOpenAIResponses.extractResponsesTokens(response.data),
145
172
  response: response.data
146
173
  };
@@ -148,9 +175,12 @@ function createOpenAIProviders({
148
175
 
149
176
  static extractResponsesTokens(data) {
150
177
  if (data.usage) {
178
+ const output = data.usage.output_tokens || 0;
179
+ const thinking = data.usage.output_tokens_details?.reasoning_tokens || 0;
151
180
  return ModelMix.normalizeTokenUsage({
152
181
  input: data.usage.input_tokens || 0,
153
- output: data.usage.output_tokens || 0,
182
+ output: output - thinking,
183
+ thinking,
154
184
  total: data.usage.total_tokens,
155
185
  cached: ModelMix.extractCacheTokens(data.usage),
156
186
  cacheWrite: ModelMix.extractCacheWriteTokens(data.usage)
@@ -175,7 +205,14 @@ function createOpenAIProviders({
175
205
 
176
206
  for (const message of messages) {
177
207
  if (!message || !message.role) continue;
178
- if (message.tool_calls || message.role === 'tool') continue;
208
+ if (message.role === 'assistant' && Array.isArray(message._responsesOutput)) {
209
+ mapped.push(...message._responsesOutput);
210
+ continue;
211
+ }
212
+ if (message.role === 'tool') {
213
+ mapped.push({ type: 'function_call_output', call_id: message.tool_call_id, output: message.content });
214
+ continue;
215
+ }
179
216
 
180
217
  const content = [];
181
218
  const isAssistant = message.role === 'assistant';
@@ -230,11 +267,18 @@ function createOpenAIProviders({
230
267
  }
231
268
  }
232
269
 
233
- if (content.length === 0) continue;
234
- mapped.push({
235
- role: message.role,
236
- content
237
- });
270
+ if (content.length > 0) {
271
+ mapped.push({ role: message.role, content });
272
+ }
273
+ for (const call of message.tool_calls || []) {
274
+ const args = call.function ? call.function.arguments : call.input ?? call.arguments ?? {};
275
+ mapped.push({
276
+ type: 'function_call',
277
+ call_id: call.id,
278
+ name: call.function ? call.function.name : call.name,
279
+ arguments: typeof args === 'string' ? args : JSON.stringify(args)
280
+ });
281
+ }
238
282
  }
239
283
 
240
284
  return mapped;
@@ -96,6 +96,10 @@ const MODEL_PRICING = {
96
96
  'deepseek-ai/DeepSeek-V4-Flash': { input: 0.14, output: 0.28 },
97
97
  'deepseek-ai/DeepSeek-V4-Pro': { input: 2.10, output: 4.40 },
98
98
  'deepseek/deepseek-v4-flash': { input: 0.09, output: 0.18 },
99
+ 'deepseek/deepseek-v4.1-flash': { input: 0.15, cachedInput: 0.015, output: 0.60 },
100
+ 'accounts/fireworks/models/deepseek-v4p1-flash': { input: 0.22, cachedInput: 0.007, output: 0.66 },
101
+ 'deepseek-flash': { input: 0.30, cachedInput: 0.006, output: 1.20 },
102
+ 'deepseek/deepseek-v4-pro-0813': { input: 0.5808, cachedInput: 0.05808, output: 1.7424 },
99
103
  'accounts/fireworks/models/glm-4p7': { input: 0.55, output: 2.19 },
100
104
  'zai-org/GLM-5.2': { input: 1.40, cachedInput: 0.26, output: 4.40 },
101
105
  'accounts/fireworks/models/glm-5p2': { input: 1.40, cachedInput: 0.14, output: 4.40 },
@@ -322,6 +326,7 @@ function getModelPricing(modelKey) {
322
326
  function extractCacheTokens(usage = {}) {
323
327
  return usage.input_tokens_details?.cached_tokens
324
328
  ?? usage.prompt_tokens_details?.cached_tokens
329
+ ?? usage.prompt_cache_hit_tokens
325
330
  ?? usage.cache_read_input_tokens
326
331
  ?? usage.cachedContentTokenCount
327
332
  ?? usage.cached_content_token_count
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modelmix",
3
- "version": "5.1.20",
3
+ "version": "5.2.1",
4
4
  "description": "🧬 Reliable interface with automatic fallback for AI LLMs.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -56,6 +56,7 @@
56
56
  "bottleneck": "^2.19.5",
57
57
  "ejs": "6.0.1",
58
58
  "file-type": "^21.3.4",
59
+ "js-yaml": "4.3.2",
59
60
  "lemonlog": "^1.2.2",
60
61
  "ws": "^8.21.1"
61
62
  },
@@ -77,7 +78,9 @@
77
78
  "test:live.mcp": "mocha test/live.mcp.js --timeout 60000 --require test/setup.js",
78
79
  "test:tokens": "mocha test/tokens.test.js --timeout 10000 --require test/setup.js",
79
80
  "test:plugins": "mocha test/plugins.test.js --timeout 10000 --require test/setup.js",
81
+ "test:skills": "mocha plugins/skills/test/**/*.test.js --timeout 10000 --require test/setup.js",
82
+ "test:benchmark": "mocha plugins/benchmark/test/**/*.test.js --timeout 10000 --require test/setup.js",
80
83
  "test:rlm": "mocha plugins/rlm/test/**/*.test.js --timeout 10000 --require test/setup.js",
81
- "test:offline": "mocha test/abort.test.js test/json.test.js test/fallback.test.js test/templates.test.js test/images.test.js test/bottleneck.test.js test/tokens.test.js test/history.test.js test/anthropic.test.js test/effort.test.js test/grok.test.js test/moderation.test.js test/plugins.test.js plugins/rlm/test/**/*.test.js --timeout 10000 --require test/setup.js"
84
+ "test:offline": "mocha test/abort.test.js test/json.test.js test/fallback.test.js test/templates.test.js test/images.test.js test/bottleneck.test.js test/tokens.test.js test/history.test.js test/anthropic.test.js test/effort.test.js test/grok.test.js test/google.test.js test/moderation.test.js test/plugins.test.js plugins/skills/test/**/*.test.js plugins/benchmark/test/**/*.test.js plugins/rlm/test/**/*.test.js --timeout 10000 --require test/setup.js"
82
85
  }
83
86
  }