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/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: false,
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
- return new ModelMix({ options: { ...this.options, ...options }, config: { ...this.config, ...config }, mix: { ...this.mix, ...mix } });
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
- // Verbose logging helpers
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 lines = [];
113
- lines.push(` 📝 System: ${ModelMix.truncate(system, 60)}`);
114
- lines.push(` 💬 Input: ${ModelMix.truncate(inputText, 150)}`);
115
- lines.push(` 📊 Messages: ${messages.length}`);
116
- return lines.join('\n');
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 lines = [];
108
+ static formatOutputSummary(result, debug) {
109
+ const parts = [];
121
110
  if (result.message) {
122
- lines.push(` 📤 Output: ${ModelMix.truncate(result.message, 200)}`);
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
- lines.push(` 🧠 Thinking: ${ModelMix.truncate(result.think, 100)}`);
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
- lines.push(` 🔧 Tools: ${toolNames}`);
130
+ parts.push(`Tools: ${toolNames}`);
130
131
  }
131
- return lines.join('\n');
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('lmstudio', new MixLMStudio({ options, config }));
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
- if (index === 0 || currentMessage.role !== messages[index - 1].role) {
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 < this.models.length; i++) {
710
+ for (let i = 0; i < modelsToTry.length; i++) {
692
711
 
693
- const currentModel = this.models[i];
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
- const verboseLevel = ModelMix.getVerboseLevel(currentConfig);
714
-
715
- if (verboseLevel >= 1) {
732
+ if (currentConfig.debug >= 1) {
716
733
  const isPrimary = i === 0;
717
- const tag = isPrimary ? '🚀' : '🔄';
718
- console.log(`\n${tag} [${currentModelKey}] Attempt #${i + 1}` + (isPrimary ? '' : ' (Fallback)'));
719
- }
720
-
721
- if (verboseLevel >= 2) {
722
- console.log(ModelMix.formatInputSummary(this.messages, currentConfig.system));
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 content = await this.processToolCalls(result.toolCalls);
751
- this.messages.push({ role: 'tool', content });
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
- // Verbose level 1: Just success indicator
757
- if (verboseLevel >= 1) {
758
- console.log(` ✅ Success`);
786
+ // debug level 1: Just success indicator
787
+ if (currentConfig.debug === 1) {
788
+ console.log(`✓ Success`);
759
789
  }
760
790
 
761
- // Verbose level 2: Readable summary of output
762
- if (verboseLevel >= 2) {
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
- // Verbose level 3 (debug): Full response details
767
- if (verboseLevel >= 3) {
796
+ // debug level 3 (debug): Full response details
797
+ if (currentConfig.debug >= 3) {
768
798
  if (result.response) {
769
- console.log('\n 📦 RAW RESPONSE:');
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 💬 FULL MESSAGE:');
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 🧠 FULL THINKING:');
809
+ console.log('\n[FULL THINKING]');
780
810
  console.log(result.think);
781
811
  }
782
812
  }
783
813
 
784
- if (verboseLevel >= 1) console.log('');
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}/${this.models.length}).`);
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 === this.models.length - 1) {
796
- console.error(`All ${this.models.length} model(s) failed. Throwing last error from ${currentModelKey}.`);
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 = this.models[i + 1].key;
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
- const verboseLevel = ModelMix.getVerboseLevel(config);
1014
-
1015
- // Verbose level 3 (debug): Full request details
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 ⚙️ CONFIG:');
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 📋 OPTIONS:');
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
- for (const content of message.content) {
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: content.tool_call_id,
1215
- content: content.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
- options.tools = [];
1282
+ const toolsArray = [];
1245
1283
  for (const tool in tools) {
1246
1284
  for (const item of tools[tool]) {
1247
- options.tools.push({
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
- // options.tool_choice = "auto";
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
- options.tools = [];
1489
+ const toolsArray = [];
1448
1490
  for (const tool in tools) {
1449
1491
  for (const item of tools[tool]) {
1450
- options.tools.push({
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
- const verboseLevel = ModelMix.getVerboseLevel(config);
1845
-
1846
- // Verbose level 3 (debug): Full request details
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 ⚙️ CONFIG:');
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 📋 PAYLOAD:');
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
- tools: [{
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modelmix",
3
- "version": "4.2.4",
3
+ "version": "4.2.8",
4
4
  "description": "🧬 Reliable interface with automatic fallback for AI LLMs.",
5
5
  "main": "index.js",
6
6
  "repository": {