modelmix 4.2.6 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/demo/package-lock.json +0 -221
- package/demo/package.json +4 -2
- package/demo/parallel-strategy.js +479 -0
- package/demo/rlm-basic.js +183 -0
- package/demo/rlm-fast.js +239 -0
- package/demo/rlm-simple.js +271 -0
- package/demo/round-robin.js +26 -0
- package/index.js +112 -71
- package/package.json +3 -3
package/index.js
CHANGED
|
@@ -34,9 +34,9 @@ class ModelMix {
|
|
|
34
34
|
this.config = {
|
|
35
35
|
system: 'You are an assistant.',
|
|
36
36
|
max_history: 1, // Default max history
|
|
37
|
-
debug:
|
|
38
|
-
verbose: 2, // 0=silent, 1=minimal, 2=readable summary, 3=full details
|
|
37
|
+
debug: 0, // 0=silent, 1=minimal, 2=readable summary, 3=full details
|
|
39
38
|
bottleneck: defaultBottleneckConfig,
|
|
39
|
+
roundRobin: false, // false=fallback mode, true=round robin rotation
|
|
40
40
|
...config
|
|
41
41
|
}
|
|
42
42
|
const freeMix = { openrouter: true, cerebras: true, groq: true, together: false, lambda: false };
|
|
@@ -56,7 +56,9 @@ class ModelMix {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
new({ options = {}, config = {}, mix = {} } = {}) {
|
|
59
|
-
|
|
59
|
+
const instance = new ModelMix({ options: { ...this.options, ...options }, config: { ...this.config, ...config }, mix: { ...this.mix, ...mix } });
|
|
60
|
+
instance.models = this.models; // Share models array for round-robin rotation
|
|
61
|
+
return instance;
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
static formatJSON(obj) {
|
|
@@ -79,24 +81,12 @@ class ModelMix {
|
|
|
79
81
|
}
|
|
80
82
|
}
|
|
81
83
|
|
|
82
|
-
//
|
|
84
|
+
// debug logging helpers
|
|
83
85
|
static truncate(str, maxLen = 100) {
|
|
84
86
|
if (!str || typeof str !== 'string') return str;
|
|
85
87
|
return str.length > maxLen ? str.substring(0, maxLen) + '...' : str;
|
|
86
88
|
}
|
|
87
89
|
|
|
88
|
-
static getVerboseLevel(config) {
|
|
89
|
-
// debug=true acts as verbose level 3
|
|
90
|
-
return config.verbose || 0;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
static verboseLog(level, config, ...args) {
|
|
94
|
-
const verboseLevel = ModelMix.getVerboseLevel(config);
|
|
95
|
-
if (verboseLevel >= level) {
|
|
96
|
-
console.log(...args);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
90
|
static formatInputSummary(messages, system) {
|
|
101
91
|
const lastMessage = messages[messages.length - 1];
|
|
102
92
|
let inputText = '';
|
|
@@ -111,18 +101,18 @@ class ModelMix {
|
|
|
111
101
|
const systemStr = `System: ${ModelMix.truncate(system, 50)}`;
|
|
112
102
|
const inputStr = `Input: ${ModelMix.truncate(inputText, 120)}`;
|
|
113
103
|
const msgCount = `(${messages.length} msg${messages.length !== 1 ? 's' : ''})`;
|
|
114
|
-
|
|
104
|
+
|
|
115
105
|
return `${systemStr} \n| ${inputStr} ${msgCount}`;
|
|
116
106
|
}
|
|
117
107
|
|
|
118
|
-
static formatOutputSummary(result,
|
|
108
|
+
static formatOutputSummary(result, debug) {
|
|
119
109
|
const parts = [];
|
|
120
110
|
if (result.message) {
|
|
121
111
|
// Try to parse as JSON for better formatting
|
|
122
112
|
try {
|
|
123
113
|
const parsed = JSON.parse(result.message.trim());
|
|
124
|
-
// If it's valid JSON and
|
|
125
|
-
if (
|
|
114
|
+
// If it's valid JSON and debug >= 2, show it formatted
|
|
115
|
+
if (debug >= 2) {
|
|
126
116
|
parts.push(`Output (JSON):\n${ModelMix.formatJSON(parsed)}`);
|
|
127
117
|
} else {
|
|
128
118
|
parts.push(`Output: ${ModelMix.truncate(result.message, 150)}`);
|
|
@@ -133,7 +123,7 @@ class ModelMix {
|
|
|
133
123
|
}
|
|
134
124
|
}
|
|
135
125
|
if (result.think) {
|
|
136
|
-
parts.push(`
|
|
126
|
+
parts.push(`Think: ${ModelMix.truncate(result.think, 80)}`);
|
|
137
127
|
}
|
|
138
128
|
if (result.toolCalls && result.toolCalls.length > 0) {
|
|
139
129
|
const toolNames = result.toolCalls.map(t => t.function?.name || t.name).join(', ');
|
|
@@ -321,6 +311,14 @@ class ModelMix {
|
|
|
321
311
|
return this;
|
|
322
312
|
}
|
|
323
313
|
|
|
314
|
+
kimiK25think({ options = {}, config = {}, mix = { together: true } } = {}) {
|
|
315
|
+
mix = { ...this.mix, ...mix };
|
|
316
|
+
if (mix.together) this.attach('moonshotai/Kimi-K2.5', new MixTogether({ options, config }));
|
|
317
|
+
if (mix.fireworks) this.attach('accounts/fireworks/models/kimi-k2p5', new MixFireworks({ options, config }));
|
|
318
|
+
if (mix.openrouter) this.attach('moonshotai/kimi-k2.5', new MixOpenRouter({ options, config }));
|
|
319
|
+
return this;
|
|
320
|
+
}
|
|
321
|
+
|
|
324
322
|
kimiK2think({ options = {}, config = {}, mix = { together: true } } = {}) {
|
|
325
323
|
mix = { ...this.mix, ...mix };
|
|
326
324
|
if (mix.together) this.attach('moonshotai/Kimi-K2-Thinking', new MixTogether({ options, config }));
|
|
@@ -328,8 +326,8 @@ class ModelMix {
|
|
|
328
326
|
return this;
|
|
329
327
|
}
|
|
330
328
|
|
|
331
|
-
lmstudio({ options = {}, config = {} } = {}) {
|
|
332
|
-
return this.attach(
|
|
329
|
+
lmstudio(model = 'lmstudio', { options = {}, config = {} } = {}) {
|
|
330
|
+
return this.attach(model, new MixLMStudio({ options, config }));
|
|
333
331
|
}
|
|
334
332
|
|
|
335
333
|
minimaxM2({ options = {}, config = {} } = {}) {
|
|
@@ -562,7 +560,7 @@ class ModelMix {
|
|
|
562
560
|
|
|
563
561
|
_extractBlock(response) {
|
|
564
562
|
const block = response.match(/```(?:\w+)?\s*([\s\S]*?)```/);
|
|
565
|
-
return block ? block[1].trim() : response;
|
|
563
|
+
return block ? block[1].trim() : response.trim();
|
|
566
564
|
}
|
|
567
565
|
|
|
568
566
|
async block({ addSystemExtra = true } = {}) {
|
|
@@ -610,7 +608,13 @@ class ModelMix {
|
|
|
610
608
|
|
|
611
609
|
groupByRoles(messages) {
|
|
612
610
|
return messages.reduce((acc, currentMessage, index) => {
|
|
613
|
-
|
|
611
|
+
// Don't group tool messages or assistant messages with tool_calls
|
|
612
|
+
// Each tool response must be separate with its own tool_call_id
|
|
613
|
+
const shouldNotGroup = currentMessage.role === 'tool' ||
|
|
614
|
+
currentMessage.tool_calls ||
|
|
615
|
+
currentMessage.tool_call_id;
|
|
616
|
+
|
|
617
|
+
if (index === 0 || currentMessage.role !== messages[index - 1].role || shouldNotGroup) {
|
|
614
618
|
// acc.push({
|
|
615
619
|
// role: currentMessage.role,
|
|
616
620
|
// content: currentMessage.content
|
|
@@ -697,11 +701,23 @@ class ModelMix {
|
|
|
697
701
|
throw new Error("No user messages have been added. Use addText(prompt), addTextFromFile(filePath), addImage(filePath), or addImageFromUrl(url) to add a prompt.");
|
|
698
702
|
}
|
|
699
703
|
|
|
704
|
+
// Merge config to get final roundRobin value
|
|
705
|
+
const finalConfig = { ...this.config, ...config };
|
|
706
|
+
|
|
707
|
+
// Try all models in order (first is primary, rest are fallbacks)
|
|
708
|
+
const modelsToTry = this.models.map((model, index) => ({ model, index }));
|
|
709
|
+
|
|
710
|
+
// Round robin: rotate models array AFTER using current for next request
|
|
711
|
+
if (finalConfig.roundRobin && this.models.length > 1) {
|
|
712
|
+
const firstModel = this.models.shift();
|
|
713
|
+
this.models.push(firstModel);
|
|
714
|
+
}
|
|
715
|
+
|
|
700
716
|
let lastError = null;
|
|
701
717
|
|
|
702
|
-
for (let i = 0; i <
|
|
718
|
+
for (let i = 0; i < modelsToTry.length; i++) {
|
|
703
719
|
|
|
704
|
-
const currentModel =
|
|
720
|
+
const { model: currentModel, index: originalIndex } = modelsToTry[i];
|
|
705
721
|
const currentModelKey = currentModel.key;
|
|
706
722
|
const providerInstance = currentModel.provider;
|
|
707
723
|
const optionsTools = providerInstance.getOptionsTools(this.tools);
|
|
@@ -721,15 +737,17 @@ class ModelMix {
|
|
|
721
737
|
...config,
|
|
722
738
|
};
|
|
723
739
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
if (verboseLevel >= 1) {
|
|
740
|
+
if (currentConfig.debug >= 1) {
|
|
727
741
|
const isPrimary = i === 0;
|
|
728
742
|
const prefix = isPrimary ? '→' : '↻';
|
|
729
|
-
const suffix = isPrimary
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
743
|
+
const suffix = isPrimary
|
|
744
|
+
? (currentConfig.roundRobin ? ` (round-robin #${originalIndex + 1})` : '')
|
|
745
|
+
: ' (fallback)';
|
|
746
|
+
// Extract provider name from class name (e.g., "MixOpenRouter" -> "openrouter")
|
|
747
|
+
const providerName = providerInstance.constructor.name.replace(/^Mix/, '').toLowerCase();
|
|
748
|
+
const header = `\n${prefix} [${providerName}:${currentModelKey}] #${originalIndex + 1}${suffix}`;
|
|
749
|
+
|
|
750
|
+
if (currentConfig.debug >= 2) {
|
|
733
751
|
console.log(`${header} | ${ModelMix.formatInputSummary(this.messages, currentConfig.system)}`);
|
|
734
752
|
} else {
|
|
735
753
|
console.log(header);
|
|
@@ -761,24 +779,30 @@ class ModelMix {
|
|
|
761
779
|
|
|
762
780
|
this.messages.push({ role: "assistant", content: null, tool_calls: result.toolCalls });
|
|
763
781
|
|
|
764
|
-
const
|
|
765
|
-
|
|
782
|
+
const toolResults = await this.processToolCalls(result.toolCalls);
|
|
783
|
+
for (const toolResult of toolResults) {
|
|
784
|
+
this.messages.push({
|
|
785
|
+
role: 'tool',
|
|
786
|
+
tool_call_id: toolResult.tool_call_id,
|
|
787
|
+
content: toolResult.content
|
|
788
|
+
});
|
|
789
|
+
}
|
|
766
790
|
|
|
767
791
|
return this.execute();
|
|
768
792
|
}
|
|
769
793
|
|
|
770
|
-
//
|
|
771
|
-
if (
|
|
794
|
+
// debug level 1: Just success indicator
|
|
795
|
+
if (currentConfig.debug === 1) {
|
|
772
796
|
console.log(`✓ Success`);
|
|
773
797
|
}
|
|
774
798
|
|
|
775
|
-
//
|
|
776
|
-
if (
|
|
777
|
-
console.log(`✓ ${ModelMix.formatOutputSummary(result,
|
|
799
|
+
// debug level 2: Readable summary of output
|
|
800
|
+
if (currentConfig.debug >= 2) {
|
|
801
|
+
console.log(`✓ ${ModelMix.formatOutputSummary(result, currentConfig.debug).trim()}`);
|
|
778
802
|
}
|
|
779
803
|
|
|
780
|
-
//
|
|
781
|
-
if (
|
|
804
|
+
// debug level 3 (debug): Full response details
|
|
805
|
+
if (currentConfig.debug >= 3) {
|
|
782
806
|
if (result.response) {
|
|
783
807
|
console.log('\n[RAW RESPONSE]');
|
|
784
808
|
console.log(ModelMix.formatJSON(result.response));
|
|
@@ -795,22 +819,22 @@ class ModelMix {
|
|
|
795
819
|
}
|
|
796
820
|
}
|
|
797
821
|
|
|
798
|
-
if (
|
|
822
|
+
if (currentConfig.debug >= 1) console.log('');
|
|
799
823
|
|
|
800
824
|
return result;
|
|
801
825
|
|
|
802
826
|
} catch (error) {
|
|
803
827
|
lastError = error;
|
|
804
|
-
log.warn(`Model ${currentModelKey} failed (Attempt #${i + 1}/${
|
|
828
|
+
log.warn(`Model ${currentModelKey} failed (Attempt #${i + 1}/${modelsToTry.length}).`);
|
|
805
829
|
if (error.message) log.warn(`Error: ${error.message}`);
|
|
806
830
|
if (error.statusCode) log.warn(`Status Code: ${error.statusCode}`);
|
|
807
831
|
if (error.details) log.warn(`Details:\n${ModelMix.formatJSON(error.details)}`);
|
|
808
832
|
|
|
809
|
-
if (i ===
|
|
810
|
-
console.error(`All ${
|
|
833
|
+
if (i === modelsToTry.length - 1) {
|
|
834
|
+
console.error(`All ${modelsToTry.length} model(s) failed. Throwing last error from ${currentModelKey}.`);
|
|
811
835
|
throw lastError;
|
|
812
836
|
} else {
|
|
813
|
-
const nextModelKey =
|
|
837
|
+
const nextModelKey = modelsToTry[i + 1].model.key;
|
|
814
838
|
log.info(`-> Proceeding to next model: ${nextModelKey}`);
|
|
815
839
|
}
|
|
816
840
|
}
|
|
@@ -1024,16 +1048,13 @@ class MixCustom {
|
|
|
1024
1048
|
|
|
1025
1049
|
options.messages = this.convertMessages(options.messages, config);
|
|
1026
1050
|
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
// Verbose level 3 (debug): Full request details
|
|
1030
|
-
if (verboseLevel >= 3) {
|
|
1051
|
+
// debug level 3 (debug): Full request details
|
|
1052
|
+
if (config.debug >= 3) {
|
|
1031
1053
|
console.log('\n[REQUEST DETAILS]');
|
|
1032
1054
|
|
|
1033
1055
|
console.log('\n[CONFIG]');
|
|
1034
1056
|
const configToLog = { ...config };
|
|
1035
1057
|
delete configToLog.debug;
|
|
1036
|
-
delete configToLog.verbose;
|
|
1037
1058
|
console.log(ModelMix.formatJSON(configToLog));
|
|
1038
1059
|
|
|
1039
1060
|
console.log('\n[OPTIONS]');
|
|
@@ -1222,12 +1243,23 @@ class MixOpenAI extends MixCustom {
|
|
|
1222
1243
|
}
|
|
1223
1244
|
|
|
1224
1245
|
if (message.role === 'tool') {
|
|
1225
|
-
|
|
1246
|
+
// Handle new format: tool_call_id directly on message
|
|
1247
|
+
if (message.tool_call_id) {
|
|
1226
1248
|
results.push({
|
|
1227
1249
|
role: 'tool',
|
|
1228
|
-
tool_call_id:
|
|
1229
|
-
content:
|
|
1230
|
-
})
|
|
1250
|
+
tool_call_id: message.tool_call_id,
|
|
1251
|
+
content: message.content
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
// Handle old format: content is an array
|
|
1255
|
+
else if (Array.isArray(message.content)) {
|
|
1256
|
+
for (const content of message.content) {
|
|
1257
|
+
results.push({
|
|
1258
|
+
role: 'tool',
|
|
1259
|
+
tool_call_id: content.tool_call_id,
|
|
1260
|
+
content: content.content
|
|
1261
|
+
})
|
|
1262
|
+
}
|
|
1231
1263
|
}
|
|
1232
1264
|
continue;
|
|
1233
1265
|
}
|
|
@@ -1255,10 +1287,10 @@ class MixOpenAI extends MixCustom {
|
|
|
1255
1287
|
|
|
1256
1288
|
static getOptionsTools(tools) {
|
|
1257
1289
|
const options = {};
|
|
1258
|
-
|
|
1290
|
+
const toolsArray = [];
|
|
1259
1291
|
for (const tool in tools) {
|
|
1260
1292
|
for (const item of tools[tool]) {
|
|
1261
|
-
|
|
1293
|
+
toolsArray.push({
|
|
1262
1294
|
type: 'function',
|
|
1263
1295
|
function: {
|
|
1264
1296
|
name: item.name,
|
|
@@ -1269,7 +1301,11 @@ class MixOpenAI extends MixCustom {
|
|
|
1269
1301
|
}
|
|
1270
1302
|
}
|
|
1271
1303
|
|
|
1272
|
-
//
|
|
1304
|
+
// Solo incluir tools si el array no está vacío
|
|
1305
|
+
if (toolsArray.length > 0) {
|
|
1306
|
+
options.tools = toolsArray;
|
|
1307
|
+
// options.tool_choice = "auto";
|
|
1308
|
+
}
|
|
1273
1309
|
|
|
1274
1310
|
return options;
|
|
1275
1311
|
}
|
|
@@ -1458,10 +1494,10 @@ class MixAnthropic extends MixCustom {
|
|
|
1458
1494
|
|
|
1459
1495
|
static getOptionsTools(tools) {
|
|
1460
1496
|
const options = {};
|
|
1461
|
-
|
|
1497
|
+
const toolsArray = [];
|
|
1462
1498
|
for (const tool in tools) {
|
|
1463
1499
|
for (const item of tools[tool]) {
|
|
1464
|
-
|
|
1500
|
+
toolsArray.push({
|
|
1465
1501
|
name: item.name,
|
|
1466
1502
|
description: item.description,
|
|
1467
1503
|
input_schema: item.inputSchema
|
|
@@ -1469,6 +1505,11 @@ class MixAnthropic extends MixCustom {
|
|
|
1469
1505
|
}
|
|
1470
1506
|
}
|
|
1471
1507
|
|
|
1508
|
+
// Solo incluir tools si el array no está vacío
|
|
1509
|
+
if (toolsArray.length > 0) {
|
|
1510
|
+
options.tools = toolsArray;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1472
1513
|
return options;
|
|
1473
1514
|
}
|
|
1474
1515
|
}
|
|
@@ -1855,16 +1896,13 @@ class MixGoogle extends MixCustom {
|
|
|
1855
1896
|
};
|
|
1856
1897
|
|
|
1857
1898
|
try {
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
// Verbose level 3 (debug): Full request details
|
|
1861
|
-
if (verboseLevel >= 3) {
|
|
1899
|
+
// debug level 3 (debug): Full request details
|
|
1900
|
+
if (config.debug >= 3) {
|
|
1862
1901
|
console.log('\n[REQUEST DETAILS - GOOGLE]');
|
|
1863
1902
|
|
|
1864
1903
|
console.log('\n[CONFIG]');
|
|
1865
1904
|
const configToLog = { ...config };
|
|
1866
1905
|
delete configToLog.debug;
|
|
1867
|
-
delete configToLog.verbose;
|
|
1868
1906
|
console.log(ModelMix.formatJSON(configToLog));
|
|
1869
1907
|
|
|
1870
1908
|
console.log('\n[PAYLOAD]');
|
|
@@ -1925,11 +1963,14 @@ class MixGoogle extends MixCustom {
|
|
|
1925
1963
|
}
|
|
1926
1964
|
}
|
|
1927
1965
|
|
|
1928
|
-
const options = {
|
|
1929
|
-
|
|
1966
|
+
const options = {};
|
|
1967
|
+
|
|
1968
|
+
// Solo incluir tools si el array no está vacío
|
|
1969
|
+
if (functionDeclarations.length > 0) {
|
|
1970
|
+
options.tools = [{
|
|
1930
1971
|
functionDeclarations
|
|
1931
|
-
}]
|
|
1932
|
-
}
|
|
1972
|
+
}];
|
|
1973
|
+
}
|
|
1933
1974
|
|
|
1934
1975
|
return options;
|
|
1935
1976
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "modelmix",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"description": "🧬 Reliable interface with automatic fallback for AI LLMs.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"repository": {
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
"homepage": "https://github.com/clasen/ModelMix#readme",
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@modelcontextprotocol/sdk": "^1.25.
|
|
49
|
+
"@modelcontextprotocol/sdk": "^1.25.3",
|
|
50
50
|
"axios": "^1.12.2",
|
|
51
51
|
"bottleneck": "^2.19.5",
|
|
52
52
|
"file-type": "^16.5.4",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"chai": "^5.2.1",
|
|
58
58
|
"dotenv": "^17.2.1",
|
|
59
|
-
"mocha": "^11.
|
|
59
|
+
"mocha": "^11.3.0",
|
|
60
60
|
"nock": "^14.0.9",
|
|
61
61
|
"sinon": "^21.0.0"
|
|
62
62
|
},
|