modelmix 4.2.6 → 4.2.8
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 +2 -0
- 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 +103 -70
- package/package.json +1 -1
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(', ');
|
|
@@ -328,8 +318,8 @@ class ModelMix {
|
|
|
328
318
|
return this;
|
|
329
319
|
}
|
|
330
320
|
|
|
331
|
-
lmstudio({ options = {}, config = {} } = {}) {
|
|
332
|
-
return this.attach(
|
|
321
|
+
lmstudio(model = 'lmstudio', { options = {}, config = {} } = {}) {
|
|
322
|
+
return this.attach(model, new MixLMStudio({ options, config }));
|
|
333
323
|
}
|
|
334
324
|
|
|
335
325
|
minimaxM2({ options = {}, config = {} } = {}) {
|
|
@@ -610,7 +600,13 @@ class ModelMix {
|
|
|
610
600
|
|
|
611
601
|
groupByRoles(messages) {
|
|
612
602
|
return messages.reduce((acc, currentMessage, index) => {
|
|
613
|
-
|
|
603
|
+
// Don't group tool messages or assistant messages with tool_calls
|
|
604
|
+
// Each tool response must be separate with its own tool_call_id
|
|
605
|
+
const shouldNotGroup = currentMessage.role === 'tool' ||
|
|
606
|
+
currentMessage.tool_calls ||
|
|
607
|
+
currentMessage.tool_call_id;
|
|
608
|
+
|
|
609
|
+
if (index === 0 || currentMessage.role !== messages[index - 1].role || shouldNotGroup) {
|
|
614
610
|
// acc.push({
|
|
615
611
|
// role: currentMessage.role,
|
|
616
612
|
// content: currentMessage.content
|
|
@@ -697,11 +693,23 @@ class ModelMix {
|
|
|
697
693
|
throw new Error("No user messages have been added. Use addText(prompt), addTextFromFile(filePath), addImage(filePath), or addImageFromUrl(url) to add a prompt.");
|
|
698
694
|
}
|
|
699
695
|
|
|
696
|
+
// Merge config to get final roundRobin value
|
|
697
|
+
const finalConfig = { ...this.config, ...config };
|
|
698
|
+
|
|
699
|
+
// Try all models in order (first is primary, rest are fallbacks)
|
|
700
|
+
const modelsToTry = this.models.map((model, index) => ({ model, index }));
|
|
701
|
+
|
|
702
|
+
// Round robin: rotate models array AFTER using current for next request
|
|
703
|
+
if (finalConfig.roundRobin && this.models.length > 1) {
|
|
704
|
+
const firstModel = this.models.shift();
|
|
705
|
+
this.models.push(firstModel);
|
|
706
|
+
}
|
|
707
|
+
|
|
700
708
|
let lastError = null;
|
|
701
709
|
|
|
702
|
-
for (let i = 0; i <
|
|
710
|
+
for (let i = 0; i < modelsToTry.length; i++) {
|
|
703
711
|
|
|
704
|
-
const currentModel =
|
|
712
|
+
const { model: currentModel, index: originalIndex } = modelsToTry[i];
|
|
705
713
|
const currentModelKey = currentModel.key;
|
|
706
714
|
const providerInstance = currentModel.provider;
|
|
707
715
|
const optionsTools = providerInstance.getOptionsTools(this.tools);
|
|
@@ -721,15 +729,17 @@ class ModelMix {
|
|
|
721
729
|
...config,
|
|
722
730
|
};
|
|
723
731
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
if (verboseLevel >= 1) {
|
|
732
|
+
if (currentConfig.debug >= 1) {
|
|
727
733
|
const isPrimary = i === 0;
|
|
728
734
|
const prefix = isPrimary ? '→' : '↻';
|
|
729
|
-
const suffix = isPrimary
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
735
|
+
const suffix = isPrimary
|
|
736
|
+
? (currentConfig.roundRobin ? ` (round-robin #${originalIndex + 1})` : '')
|
|
737
|
+
: ' (fallback)';
|
|
738
|
+
// Extract provider name from class name (e.g., "MixOpenRouter" -> "openrouter")
|
|
739
|
+
const providerName = providerInstance.constructor.name.replace(/^Mix/, '').toLowerCase();
|
|
740
|
+
const header = `\n${prefix} [${providerName}:${currentModelKey}] #${originalIndex + 1}${suffix}`;
|
|
741
|
+
|
|
742
|
+
if (currentConfig.debug >= 2) {
|
|
733
743
|
console.log(`${header} | ${ModelMix.formatInputSummary(this.messages, currentConfig.system)}`);
|
|
734
744
|
} else {
|
|
735
745
|
console.log(header);
|
|
@@ -761,24 +771,30 @@ class ModelMix {
|
|
|
761
771
|
|
|
762
772
|
this.messages.push({ role: "assistant", content: null, tool_calls: result.toolCalls });
|
|
763
773
|
|
|
764
|
-
const
|
|
765
|
-
|
|
774
|
+
const toolResults = await this.processToolCalls(result.toolCalls);
|
|
775
|
+
for (const toolResult of toolResults) {
|
|
776
|
+
this.messages.push({
|
|
777
|
+
role: 'tool',
|
|
778
|
+
tool_call_id: toolResult.tool_call_id,
|
|
779
|
+
content: toolResult.content
|
|
780
|
+
});
|
|
781
|
+
}
|
|
766
782
|
|
|
767
783
|
return this.execute();
|
|
768
784
|
}
|
|
769
785
|
|
|
770
|
-
//
|
|
771
|
-
if (
|
|
786
|
+
// debug level 1: Just success indicator
|
|
787
|
+
if (currentConfig.debug === 1) {
|
|
772
788
|
console.log(`✓ Success`);
|
|
773
789
|
}
|
|
774
790
|
|
|
775
|
-
//
|
|
776
|
-
if (
|
|
777
|
-
console.log(`✓ ${ModelMix.formatOutputSummary(result,
|
|
791
|
+
// debug level 2: Readable summary of output
|
|
792
|
+
if (currentConfig.debug >= 2) {
|
|
793
|
+
console.log(`✓ ${ModelMix.formatOutputSummary(result, currentConfig.debug).trim()}`);
|
|
778
794
|
}
|
|
779
795
|
|
|
780
|
-
//
|
|
781
|
-
if (
|
|
796
|
+
// debug level 3 (debug): Full response details
|
|
797
|
+
if (currentConfig.debug >= 3) {
|
|
782
798
|
if (result.response) {
|
|
783
799
|
console.log('\n[RAW RESPONSE]');
|
|
784
800
|
console.log(ModelMix.formatJSON(result.response));
|
|
@@ -795,22 +811,22 @@ class ModelMix {
|
|
|
795
811
|
}
|
|
796
812
|
}
|
|
797
813
|
|
|
798
|
-
if (
|
|
814
|
+
if (currentConfig.debug >= 1) console.log('');
|
|
799
815
|
|
|
800
816
|
return result;
|
|
801
817
|
|
|
802
818
|
} catch (error) {
|
|
803
819
|
lastError = error;
|
|
804
|
-
log.warn(`Model ${currentModelKey} failed (Attempt #${i + 1}/${
|
|
820
|
+
log.warn(`Model ${currentModelKey} failed (Attempt #${i + 1}/${modelsToTry.length}).`);
|
|
805
821
|
if (error.message) log.warn(`Error: ${error.message}`);
|
|
806
822
|
if (error.statusCode) log.warn(`Status Code: ${error.statusCode}`);
|
|
807
823
|
if (error.details) log.warn(`Details:\n${ModelMix.formatJSON(error.details)}`);
|
|
808
824
|
|
|
809
|
-
if (i ===
|
|
810
|
-
console.error(`All ${
|
|
825
|
+
if (i === modelsToTry.length - 1) {
|
|
826
|
+
console.error(`All ${modelsToTry.length} model(s) failed. Throwing last error from ${currentModelKey}.`);
|
|
811
827
|
throw lastError;
|
|
812
828
|
} else {
|
|
813
|
-
const nextModelKey =
|
|
829
|
+
const nextModelKey = modelsToTry[i + 1].model.key;
|
|
814
830
|
log.info(`-> Proceeding to next model: ${nextModelKey}`);
|
|
815
831
|
}
|
|
816
832
|
}
|
|
@@ -1024,16 +1040,13 @@ class MixCustom {
|
|
|
1024
1040
|
|
|
1025
1041
|
options.messages = this.convertMessages(options.messages, config);
|
|
1026
1042
|
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
// Verbose level 3 (debug): Full request details
|
|
1030
|
-
if (verboseLevel >= 3) {
|
|
1043
|
+
// debug level 3 (debug): Full request details
|
|
1044
|
+
if (config.debug >= 3) {
|
|
1031
1045
|
console.log('\n[REQUEST DETAILS]');
|
|
1032
1046
|
|
|
1033
1047
|
console.log('\n[CONFIG]');
|
|
1034
1048
|
const configToLog = { ...config };
|
|
1035
1049
|
delete configToLog.debug;
|
|
1036
|
-
delete configToLog.verbose;
|
|
1037
1050
|
console.log(ModelMix.formatJSON(configToLog));
|
|
1038
1051
|
|
|
1039
1052
|
console.log('\n[OPTIONS]');
|
|
@@ -1222,12 +1235,23 @@ class MixOpenAI extends MixCustom {
|
|
|
1222
1235
|
}
|
|
1223
1236
|
|
|
1224
1237
|
if (message.role === 'tool') {
|
|
1225
|
-
|
|
1238
|
+
// Handle new format: tool_call_id directly on message
|
|
1239
|
+
if (message.tool_call_id) {
|
|
1226
1240
|
results.push({
|
|
1227
1241
|
role: 'tool',
|
|
1228
|
-
tool_call_id:
|
|
1229
|
-
content:
|
|
1230
|
-
})
|
|
1242
|
+
tool_call_id: message.tool_call_id,
|
|
1243
|
+
content: message.content
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
// Handle old format: content is an array
|
|
1247
|
+
else if (Array.isArray(message.content)) {
|
|
1248
|
+
for (const content of message.content) {
|
|
1249
|
+
results.push({
|
|
1250
|
+
role: 'tool',
|
|
1251
|
+
tool_call_id: content.tool_call_id,
|
|
1252
|
+
content: content.content
|
|
1253
|
+
})
|
|
1254
|
+
}
|
|
1231
1255
|
}
|
|
1232
1256
|
continue;
|
|
1233
1257
|
}
|
|
@@ -1255,10 +1279,10 @@ class MixOpenAI extends MixCustom {
|
|
|
1255
1279
|
|
|
1256
1280
|
static getOptionsTools(tools) {
|
|
1257
1281
|
const options = {};
|
|
1258
|
-
|
|
1282
|
+
const toolsArray = [];
|
|
1259
1283
|
for (const tool in tools) {
|
|
1260
1284
|
for (const item of tools[tool]) {
|
|
1261
|
-
|
|
1285
|
+
toolsArray.push({
|
|
1262
1286
|
type: 'function',
|
|
1263
1287
|
function: {
|
|
1264
1288
|
name: item.name,
|
|
@@ -1269,7 +1293,11 @@ class MixOpenAI extends MixCustom {
|
|
|
1269
1293
|
}
|
|
1270
1294
|
}
|
|
1271
1295
|
|
|
1272
|
-
//
|
|
1296
|
+
// Solo incluir tools si el array no está vacío
|
|
1297
|
+
if (toolsArray.length > 0) {
|
|
1298
|
+
options.tools = toolsArray;
|
|
1299
|
+
// options.tool_choice = "auto";
|
|
1300
|
+
}
|
|
1273
1301
|
|
|
1274
1302
|
return options;
|
|
1275
1303
|
}
|
|
@@ -1458,10 +1486,10 @@ class MixAnthropic extends MixCustom {
|
|
|
1458
1486
|
|
|
1459
1487
|
static getOptionsTools(tools) {
|
|
1460
1488
|
const options = {};
|
|
1461
|
-
|
|
1489
|
+
const toolsArray = [];
|
|
1462
1490
|
for (const tool in tools) {
|
|
1463
1491
|
for (const item of tools[tool]) {
|
|
1464
|
-
|
|
1492
|
+
toolsArray.push({
|
|
1465
1493
|
name: item.name,
|
|
1466
1494
|
description: item.description,
|
|
1467
1495
|
input_schema: item.inputSchema
|
|
@@ -1469,6 +1497,11 @@ class MixAnthropic extends MixCustom {
|
|
|
1469
1497
|
}
|
|
1470
1498
|
}
|
|
1471
1499
|
|
|
1500
|
+
// Solo incluir tools si el array no está vacío
|
|
1501
|
+
if (toolsArray.length > 0) {
|
|
1502
|
+
options.tools = toolsArray;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1472
1505
|
return options;
|
|
1473
1506
|
}
|
|
1474
1507
|
}
|
|
@@ -1855,16 +1888,13 @@ class MixGoogle extends MixCustom {
|
|
|
1855
1888
|
};
|
|
1856
1889
|
|
|
1857
1890
|
try {
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
// Verbose level 3 (debug): Full request details
|
|
1861
|
-
if (verboseLevel >= 3) {
|
|
1891
|
+
// debug level 3 (debug): Full request details
|
|
1892
|
+
if (config.debug >= 3) {
|
|
1862
1893
|
console.log('\n[REQUEST DETAILS - GOOGLE]');
|
|
1863
1894
|
|
|
1864
1895
|
console.log('\n[CONFIG]');
|
|
1865
1896
|
const configToLog = { ...config };
|
|
1866
1897
|
delete configToLog.debug;
|
|
1867
|
-
delete configToLog.verbose;
|
|
1868
1898
|
console.log(ModelMix.formatJSON(configToLog));
|
|
1869
1899
|
|
|
1870
1900
|
console.log('\n[PAYLOAD]');
|
|
@@ -1925,11 +1955,14 @@ class MixGoogle extends MixCustom {
|
|
|
1925
1955
|
}
|
|
1926
1956
|
}
|
|
1927
1957
|
|
|
1928
|
-
const options = {
|
|
1929
|
-
|
|
1958
|
+
const options = {};
|
|
1959
|
+
|
|
1960
|
+
// Solo incluir tools si el array no está vacío
|
|
1961
|
+
if (functionDeclarations.length > 0) {
|
|
1962
|
+
options.tools = [{
|
|
1930
1963
|
functionDeclarations
|
|
1931
|
-
}]
|
|
1932
|
-
}
|
|
1964
|
+
}];
|
|
1965
|
+
}
|
|
1933
1966
|
|
|
1934
1967
|
return options;
|
|
1935
1968
|
}
|