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/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 };
@@ -56,7 +56,9 @@ class ModelMix {
56
56
  }
57
57
 
58
58
  new({ options = {}, config = {}, mix = {} } = {}) {
59
- 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;
60
62
  }
61
63
 
62
64
  static formatJSON(obj) {
@@ -79,24 +81,12 @@ class ModelMix {
79
81
  }
80
82
  }
81
83
 
82
- // Verbose logging helpers
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, verboseLevel = 2) {
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 verbose >= 2, show it formatted
125
- if (verboseLevel >= 2) {
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(`Thinking: ${ModelMix.truncate(result.think, 80)}`);
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('lmstudio', new MixLMStudio({ options, config }));
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
- 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) {
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 < this.models.length; i++) {
710
+ for (let i = 0; i < modelsToTry.length; i++) {
703
711
 
704
- const currentModel = this.models[i];
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
- const verboseLevel = ModelMix.getVerboseLevel(currentConfig);
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 ? '' : ' (fallback)';
730
- const header = `\n${prefix} [${currentModelKey}] #${i + 1}${suffix}`;
731
-
732
- if (verboseLevel >= 2) {
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 content = await this.processToolCalls(result.toolCalls);
765
- 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
+ }
766
782
 
767
783
  return this.execute();
768
784
  }
769
785
 
770
- // Verbose level 1: Just success indicator
771
- if (verboseLevel === 1) {
786
+ // debug level 1: Just success indicator
787
+ if (currentConfig.debug === 1) {
772
788
  console.log(`✓ Success`);
773
789
  }
774
790
 
775
- // Verbose level 2: Readable summary of output
776
- if (verboseLevel >= 2) {
777
- console.log(`✓ ${ModelMix.formatOutputSummary(result, verboseLevel).trim()}`);
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
- // Verbose level 3 (debug): Full response details
781
- if (verboseLevel >= 3) {
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 (verboseLevel >= 1) console.log('');
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}/${this.models.length}).`);
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 === this.models.length - 1) {
810
- 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}.`);
811
827
  throw lastError;
812
828
  } else {
813
- const nextModelKey = this.models[i + 1].key;
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
- const verboseLevel = ModelMix.getVerboseLevel(config);
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
- for (const content of message.content) {
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: content.tool_call_id,
1229
- content: content.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
- options.tools = [];
1282
+ const toolsArray = [];
1259
1283
  for (const tool in tools) {
1260
1284
  for (const item of tools[tool]) {
1261
- options.tools.push({
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
- // 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
+ }
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
- options.tools = [];
1489
+ const toolsArray = [];
1462
1490
  for (const tool in tools) {
1463
1491
  for (const item of tools[tool]) {
1464
- options.tools.push({
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
- const verboseLevel = ModelMix.getVerboseLevel(config);
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
- tools: [{
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modelmix",
3
- "version": "4.2.6",
3
+ "version": "4.2.8",
4
4
  "description": "🧬 Reliable interface with automatic fallback for AI LLMs.",
5
5
  "main": "index.js",
6
6
  "repository": {