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/.gitignore +138 -0
- package/README.md +69 -2
- package/demo/benchmark.js +83 -0
- package/demo/package.json +2 -0
- package/demo/prompts/story.txt +35 -0
- package/demo/prompts/template-engine.txt +41 -0
- package/demo/short.js +2 -0
- package/effort.js +3 -1
- package/index.d.ts +9 -5
- package/index.js +75 -12
- package/lib/model-chain.js +1 -1
- package/lib/parse-json-response.js +14 -0
- package/lib/providers/anthropic.js +3 -1
- package/lib/providers/base.js +65 -17
- package/lib/providers/google.js +13 -2
- package/lib/providers/openai-compatible.js +36 -0
- package/lib/providers/openai.js +52 -8
- package/lib/token-usage.js +5 -0
- package/package.json +5 -2
- package/plugins/benchmark/index.d.ts +112 -0
- package/plugins/benchmark/index.js +575 -0
- package/plugins/benchmark/test/benchmark.test.js +518 -0
- package/plugins/skills/index.d.ts +9 -0
- package/plugins/skills/index.js +107 -0
- package/plugins/skills/test/skills.test.js +182 -0
- package/pnpm-workspace.yaml +2 -2
- package/skills/modelmix/SKILL.md +32 -2
- package/test/deepseek.test.js +273 -1
- package/test/fallback.test.js +57 -0
- package/test/google.test.js +55 -0
- package/test/history.test.js +7 -4
- package/test/json.test.js +29 -1
- package/test/plugins.test.js +150 -1
- package/test/public-api.test.js +2 -0
- package/test/tokens.test.js +75 -3
- package/RLM_PLUGIN_SPEC.md +0 -465
- package/demo/package-lock.json +0 -516
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
|
-
|
|
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
|
-
...
|
|
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:
|
|
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
|
|
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 };
|
package/lib/model-chain.js
CHANGED
|
@@ -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,
|
package/lib/providers/base.js
CHANGED
|
@@ -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]' ||
|
|
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
|
-
|
|
184
|
+
reject(error);
|
|
185
|
+
response.data.destroy();
|
|
186
|
+
return;
|
|
172
187
|
}
|
|
173
188
|
}
|
|
174
189
|
});
|
|
175
190
|
|
|
176
|
-
response.data.on('end', () =>
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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]
|
|
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:
|
|
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
|
|
318
|
+
message,
|
|
271
319
|
think: MixCustom.extractThink(response.data),
|
|
272
|
-
toolCalls
|
|
273
|
-
tokens:
|
|
320
|
+
toolCalls,
|
|
321
|
+
tokens: this.constructor.extractTokens(response.data),
|
|
274
322
|
response: response.data
|
|
275
323
|
}
|
|
276
324
|
}
|
package/lib/providers/google.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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,
|
package/lib/providers/openai.js
CHANGED
|
@@ -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:
|
|
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.
|
|
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
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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;
|
package/lib/token-usage.js
CHANGED
|
@@ -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
|
|
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
|
}
|