modelmix 4.2.4 → 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/demo/verbose.js +103 -0
- package/index.js +136 -89
- 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 };
|
|
@@ -46,7 +46,6 @@ class ModelMix {
|
|
|
46
46
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
|
|
50
49
|
replace(keyValues) {
|
|
51
50
|
this.config.replace = { ...this.config.replace, ...keyValues };
|
|
52
51
|
return this;
|
|
@@ -57,7 +56,9 @@ class ModelMix {
|
|
|
57
56
|
}
|
|
58
57
|
|
|
59
58
|
new({ options = {}, config = {}, mix = {} } = {}) {
|
|
60
|
-
|
|
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;
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
static formatJSON(obj) {
|
|
@@ -80,24 +81,12 @@ class ModelMix {
|
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
83
|
|
|
83
|
-
//
|
|
84
|
+
// debug logging helpers
|
|
84
85
|
static truncate(str, maxLen = 100) {
|
|
85
86
|
if (!str || typeof str !== 'string') return str;
|
|
86
87
|
return str.length > maxLen ? str.substring(0, maxLen) + '...' : str;
|
|
87
88
|
}
|
|
88
89
|
|
|
89
|
-
static getVerboseLevel(config) {
|
|
90
|
-
// debug=true acts as verbose level 3
|
|
91
|
-
return config.verbose || 0;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
static verboseLog(level, config, ...args) {
|
|
95
|
-
const verboseLevel = ModelMix.getVerboseLevel(config);
|
|
96
|
-
if (verboseLevel >= level) {
|
|
97
|
-
console.log(...args);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
90
|
static formatInputSummary(messages, system) {
|
|
102
91
|
const lastMessage = messages[messages.length - 1];
|
|
103
92
|
let inputText = '';
|
|
@@ -109,26 +98,38 @@ class ModelMix {
|
|
|
109
98
|
inputText = lastMessage.content;
|
|
110
99
|
}
|
|
111
100
|
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
return
|
|
101
|
+
const systemStr = `System: ${ModelMix.truncate(system, 50)}`;
|
|
102
|
+
const inputStr = `Input: ${ModelMix.truncate(inputText, 120)}`;
|
|
103
|
+
const msgCount = `(${messages.length} msg${messages.length !== 1 ? 's' : ''})`;
|
|
104
|
+
|
|
105
|
+
return `${systemStr} \n| ${inputStr} ${msgCount}`;
|
|
117
106
|
}
|
|
118
107
|
|
|
119
|
-
static formatOutputSummary(result) {
|
|
120
|
-
const
|
|
108
|
+
static formatOutputSummary(result, debug) {
|
|
109
|
+
const parts = [];
|
|
121
110
|
if (result.message) {
|
|
122
|
-
|
|
111
|
+
// Try to parse as JSON for better formatting
|
|
112
|
+
try {
|
|
113
|
+
const parsed = JSON.parse(result.message.trim());
|
|
114
|
+
// If it's valid JSON and debug >= 2, show it formatted
|
|
115
|
+
if (debug >= 2) {
|
|
116
|
+
parts.push(`Output (JSON):\n${ModelMix.formatJSON(parsed)}`);
|
|
117
|
+
} else {
|
|
118
|
+
parts.push(`Output: ${ModelMix.truncate(result.message, 150)}`);
|
|
119
|
+
}
|
|
120
|
+
} catch (e) {
|
|
121
|
+
// Not JSON, show truncated as before
|
|
122
|
+
parts.push(`Output: ${ModelMix.truncate(result.message, 150)}`);
|
|
123
|
+
}
|
|
123
124
|
}
|
|
124
125
|
if (result.think) {
|
|
125
|
-
|
|
126
|
+
parts.push(`Think: ${ModelMix.truncate(result.think, 80)}`);
|
|
126
127
|
}
|
|
127
128
|
if (result.toolCalls && result.toolCalls.length > 0) {
|
|
128
129
|
const toolNames = result.toolCalls.map(t => t.function?.name || t.name).join(', ');
|
|
129
|
-
|
|
130
|
+
parts.push(`Tools: ${toolNames}`);
|
|
130
131
|
}
|
|
131
|
-
return
|
|
132
|
+
return parts.join(' | ');
|
|
132
133
|
}
|
|
133
134
|
|
|
134
135
|
attach(key, provider) {
|
|
@@ -317,8 +318,8 @@ class ModelMix {
|
|
|
317
318
|
return this;
|
|
318
319
|
}
|
|
319
320
|
|
|
320
|
-
lmstudio({ options = {}, config = {} } = {}) {
|
|
321
|
-
return this.attach(
|
|
321
|
+
lmstudio(model = 'lmstudio', { options = {}, config = {} } = {}) {
|
|
322
|
+
return this.attach(model, new MixLMStudio({ options, config }));
|
|
322
323
|
}
|
|
323
324
|
|
|
324
325
|
minimaxM2({ options = {}, config = {} } = {}) {
|
|
@@ -599,7 +600,13 @@ class ModelMix {
|
|
|
599
600
|
|
|
600
601
|
groupByRoles(messages) {
|
|
601
602
|
return messages.reduce((acc, currentMessage, index) => {
|
|
602
|
-
|
|
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) {
|
|
603
610
|
// acc.push({
|
|
604
611
|
// role: currentMessage.role,
|
|
605
612
|
// content: currentMessage.content
|
|
@@ -686,11 +693,23 @@ class ModelMix {
|
|
|
686
693
|
throw new Error("No user messages have been added. Use addText(prompt), addTextFromFile(filePath), addImage(filePath), or addImageFromUrl(url) to add a prompt.");
|
|
687
694
|
}
|
|
688
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
|
+
|
|
689
708
|
let lastError = null;
|
|
690
709
|
|
|
691
|
-
for (let i = 0; i <
|
|
710
|
+
for (let i = 0; i < modelsToTry.length; i++) {
|
|
692
711
|
|
|
693
|
-
const currentModel =
|
|
712
|
+
const { model: currentModel, index: originalIndex } = modelsToTry[i];
|
|
694
713
|
const currentModelKey = currentModel.key;
|
|
695
714
|
const providerInstance = currentModel.provider;
|
|
696
715
|
const optionsTools = providerInstance.getOptionsTools(this.tools);
|
|
@@ -710,16 +729,21 @@ class ModelMix {
|
|
|
710
729
|
...config,
|
|
711
730
|
};
|
|
712
731
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
if (verboseLevel >= 1) {
|
|
732
|
+
if (currentConfig.debug >= 1) {
|
|
716
733
|
const isPrimary = i === 0;
|
|
717
|
-
const
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
734
|
+
const prefix = isPrimary ? '→' : '↻';
|
|
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) {
|
|
743
|
+
console.log(`${header} | ${ModelMix.formatInputSummary(this.messages, currentConfig.system)}`);
|
|
744
|
+
} else {
|
|
745
|
+
console.log(header);
|
|
746
|
+
}
|
|
723
747
|
}
|
|
724
748
|
|
|
725
749
|
try {
|
|
@@ -747,56 +771,62 @@ class ModelMix {
|
|
|
747
771
|
|
|
748
772
|
this.messages.push({ role: "assistant", content: null, tool_calls: result.toolCalls });
|
|
749
773
|
|
|
750
|
-
const
|
|
751
|
-
|
|
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
|
+
}
|
|
752
782
|
|
|
753
783
|
return this.execute();
|
|
754
784
|
}
|
|
755
785
|
|
|
756
|
-
//
|
|
757
|
-
if (
|
|
758
|
-
console.log(
|
|
786
|
+
// debug level 1: Just success indicator
|
|
787
|
+
if (currentConfig.debug === 1) {
|
|
788
|
+
console.log(`✓ Success`);
|
|
759
789
|
}
|
|
760
790
|
|
|
761
|
-
//
|
|
762
|
-
if (
|
|
763
|
-
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()}`);
|
|
764
794
|
}
|
|
765
795
|
|
|
766
|
-
//
|
|
767
|
-
if (
|
|
796
|
+
// debug level 3 (debug): Full response details
|
|
797
|
+
if (currentConfig.debug >= 3) {
|
|
768
798
|
if (result.response) {
|
|
769
|
-
console.log('\n
|
|
799
|
+
console.log('\n[RAW RESPONSE]');
|
|
770
800
|
console.log(ModelMix.formatJSON(result.response));
|
|
771
801
|
}
|
|
772
802
|
|
|
773
803
|
if (result.message) {
|
|
774
|
-
console.log('\n
|
|
804
|
+
console.log('\n[FULL MESSAGE]');
|
|
775
805
|
console.log(ModelMix.formatMessage(result.message));
|
|
776
806
|
}
|
|
777
807
|
|
|
778
808
|
if (result.think) {
|
|
779
|
-
console.log('\n
|
|
809
|
+
console.log('\n[FULL THINKING]');
|
|
780
810
|
console.log(result.think);
|
|
781
811
|
}
|
|
782
812
|
}
|
|
783
813
|
|
|
784
|
-
if (
|
|
814
|
+
if (currentConfig.debug >= 1) console.log('');
|
|
785
815
|
|
|
786
816
|
return result;
|
|
787
817
|
|
|
788
818
|
} catch (error) {
|
|
789
819
|
lastError = error;
|
|
790
|
-
log.warn(`Model ${currentModelKey} failed (Attempt #${i + 1}/${
|
|
820
|
+
log.warn(`Model ${currentModelKey} failed (Attempt #${i + 1}/${modelsToTry.length}).`);
|
|
791
821
|
if (error.message) log.warn(`Error: ${error.message}`);
|
|
792
822
|
if (error.statusCode) log.warn(`Status Code: ${error.statusCode}`);
|
|
793
823
|
if (error.details) log.warn(`Details:\n${ModelMix.formatJSON(error.details)}`);
|
|
794
824
|
|
|
795
|
-
if (i ===
|
|
796
|
-
console.error(`All ${
|
|
825
|
+
if (i === modelsToTry.length - 1) {
|
|
826
|
+
console.error(`All ${modelsToTry.length} model(s) failed. Throwing last error from ${currentModelKey}.`);
|
|
797
827
|
throw lastError;
|
|
798
828
|
} else {
|
|
799
|
-
const nextModelKey =
|
|
829
|
+
const nextModelKey = modelsToTry[i + 1].model.key;
|
|
800
830
|
log.info(`-> Proceeding to next model: ${nextModelKey}`);
|
|
801
831
|
}
|
|
802
832
|
}
|
|
@@ -1010,19 +1040,16 @@ class MixCustom {
|
|
|
1010
1040
|
|
|
1011
1041
|
options.messages = this.convertMessages(options.messages, config);
|
|
1012
1042
|
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
if (verboseLevel >= 3) {
|
|
1017
|
-
console.log('\n 📡 REQUEST DETAILS:');
|
|
1043
|
+
// debug level 3 (debug): Full request details
|
|
1044
|
+
if (config.debug >= 3) {
|
|
1045
|
+
console.log('\n[REQUEST DETAILS]');
|
|
1018
1046
|
|
|
1019
|
-
console.log('\n
|
|
1047
|
+
console.log('\n[CONFIG]');
|
|
1020
1048
|
const configToLog = { ...config };
|
|
1021
1049
|
delete configToLog.debug;
|
|
1022
|
-
delete configToLog.verbose;
|
|
1023
1050
|
console.log(ModelMix.formatJSON(configToLog));
|
|
1024
1051
|
|
|
1025
|
-
console.log('\n
|
|
1052
|
+
console.log('\n[OPTIONS]');
|
|
1026
1053
|
console.log(ModelMix.formatJSON(options));
|
|
1027
1054
|
}
|
|
1028
1055
|
|
|
@@ -1208,12 +1235,23 @@ class MixOpenAI extends MixCustom {
|
|
|
1208
1235
|
}
|
|
1209
1236
|
|
|
1210
1237
|
if (message.role === 'tool') {
|
|
1211
|
-
|
|
1238
|
+
// Handle new format: tool_call_id directly on message
|
|
1239
|
+
if (message.tool_call_id) {
|
|
1212
1240
|
results.push({
|
|
1213
1241
|
role: 'tool',
|
|
1214
|
-
tool_call_id:
|
|
1215
|
-
content:
|
|
1216
|
-
})
|
|
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
|
+
}
|
|
1217
1255
|
}
|
|
1218
1256
|
continue;
|
|
1219
1257
|
}
|
|
@@ -1241,10 +1279,10 @@ class MixOpenAI extends MixCustom {
|
|
|
1241
1279
|
|
|
1242
1280
|
static getOptionsTools(tools) {
|
|
1243
1281
|
const options = {};
|
|
1244
|
-
|
|
1282
|
+
const toolsArray = [];
|
|
1245
1283
|
for (const tool in tools) {
|
|
1246
1284
|
for (const item of tools[tool]) {
|
|
1247
|
-
|
|
1285
|
+
toolsArray.push({
|
|
1248
1286
|
type: 'function',
|
|
1249
1287
|
function: {
|
|
1250
1288
|
name: item.name,
|
|
@@ -1255,7 +1293,11 @@ class MixOpenAI extends MixCustom {
|
|
|
1255
1293
|
}
|
|
1256
1294
|
}
|
|
1257
1295
|
|
|
1258
|
-
//
|
|
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
|
+
}
|
|
1259
1301
|
|
|
1260
1302
|
return options;
|
|
1261
1303
|
}
|
|
@@ -1444,10 +1486,10 @@ class MixAnthropic extends MixCustom {
|
|
|
1444
1486
|
|
|
1445
1487
|
static getOptionsTools(tools) {
|
|
1446
1488
|
const options = {};
|
|
1447
|
-
|
|
1489
|
+
const toolsArray = [];
|
|
1448
1490
|
for (const tool in tools) {
|
|
1449
1491
|
for (const item of tools[tool]) {
|
|
1450
|
-
|
|
1492
|
+
toolsArray.push({
|
|
1451
1493
|
name: item.name,
|
|
1452
1494
|
description: item.description,
|
|
1453
1495
|
input_schema: item.inputSchema
|
|
@@ -1455,6 +1497,11 @@ class MixAnthropic extends MixCustom {
|
|
|
1455
1497
|
}
|
|
1456
1498
|
}
|
|
1457
1499
|
|
|
1500
|
+
// Solo incluir tools si el array no está vacío
|
|
1501
|
+
if (toolsArray.length > 0) {
|
|
1502
|
+
options.tools = toolsArray;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1458
1505
|
return options;
|
|
1459
1506
|
}
|
|
1460
1507
|
}
|
|
@@ -1841,19 +1888,16 @@ class MixGoogle extends MixCustom {
|
|
|
1841
1888
|
};
|
|
1842
1889
|
|
|
1843
1890
|
try {
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
if (verboseLevel >= 3) {
|
|
1848
|
-
console.log('\n 📡 REQUEST DETAILS (GOOGLE):');
|
|
1891
|
+
// debug level 3 (debug): Full request details
|
|
1892
|
+
if (config.debug >= 3) {
|
|
1893
|
+
console.log('\n[REQUEST DETAILS - GOOGLE]');
|
|
1849
1894
|
|
|
1850
|
-
console.log('\n
|
|
1895
|
+
console.log('\n[CONFIG]');
|
|
1851
1896
|
const configToLog = { ...config };
|
|
1852
1897
|
delete configToLog.debug;
|
|
1853
|
-
delete configToLog.verbose;
|
|
1854
1898
|
console.log(ModelMix.formatJSON(configToLog));
|
|
1855
1899
|
|
|
1856
|
-
console.log('\n
|
|
1900
|
+
console.log('\n[PAYLOAD]');
|
|
1857
1901
|
console.log(ModelMix.formatJSON(payload));
|
|
1858
1902
|
}
|
|
1859
1903
|
|
|
@@ -1911,11 +1955,14 @@ class MixGoogle extends MixCustom {
|
|
|
1911
1955
|
}
|
|
1912
1956
|
}
|
|
1913
1957
|
|
|
1914
|
-
const options = {
|
|
1915
|
-
|
|
1958
|
+
const options = {};
|
|
1959
|
+
|
|
1960
|
+
// Solo incluir tools si el array no está vacío
|
|
1961
|
+
if (functionDeclarations.length > 0) {
|
|
1962
|
+
options.tools = [{
|
|
1916
1963
|
functionDeclarations
|
|
1917
|
-
}]
|
|
1918
|
-
}
|
|
1964
|
+
}];
|
|
1965
|
+
}
|
|
1919
1966
|
|
|
1920
1967
|
return options;
|
|
1921
1968
|
}
|