modelmix 3.8.4 → 3.8.6

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
@@ -7,6 +7,7 @@ const path = require('path');
7
7
  const generateJsonSchema = require('./schema');
8
8
  const { Client } = require("@modelcontextprotocol/sdk/client/index.js");
9
9
  const { StdioClientTransport } = require("@modelcontextprotocol/sdk/client/stdio.js");
10
+ const { MCPToolsManager } = require('./mcp-tools');
10
11
 
11
12
  class ModelMix {
12
13
 
@@ -16,6 +17,7 @@ class ModelMix {
16
17
  this.tools = {};
17
18
  this.toolClient = {};
18
19
  this.mcp = {};
20
+ this.mcpToolsManager = new MCPToolsManager();
19
21
  this.options = {
20
22
  max_tokens: 5000,
21
23
  temperature: 1, // 1 --> More creative, 0 --> More deterministic.
@@ -99,11 +101,10 @@ class ModelMix {
99
101
  gpt5nano({ options = {}, config = {} } = {}) {
100
102
  return this.attach('gpt-5-nano', new MixOpenAI({ options, config }));
101
103
  }
102
- gptOss({ options = {}, config = {}, mix = { together: false, cerebras: false, groq: true, lmstudio: false } } = {}) {
104
+ gptOss({ options = {}, config = {}, mix = { together: false, cerebras: false, groq: true } } = {}) {
103
105
  if (mix.together) return this.attach('openai/gpt-oss-120b', new MixTogether({ options, config }));
104
106
  if (mix.cerebras) return this.attach('gpt-oss-120b', new MixCerebras({ options, config }));
105
107
  if (mix.groq) return this.attach('openai/gpt-oss-120b', new MixGroq({ options, config }));
106
- if (mix.lmstudio) return this.attach('openai/gpt-oss-120b', new MixLMStudio({ options, config }));
107
108
  return this;
108
109
  }
109
110
 
@@ -199,11 +200,15 @@ class ModelMix {
199
200
  }
200
201
 
201
202
  kimiK2({ options = {}, config = {}, mix = { together: false, groq: true } } = {}) {
202
- if (mix.together) this.attach('moonshotai/Kimi-K2-Instruct', new MixTogether({ options, config }));
203
- if (mix.groq) this.attach('moonshotai/kimi-k2-instruct', new MixGroq({ options, config }));
203
+ if (mix.together) this.attach('moonshotai/Kimi-K2-Instruct-0905', new MixTogether({ options, config }));
204
+ if (mix.groq) this.attach('moonshotai/kimi-k2-instruct-0905', new MixGroq({ options, config }));
204
205
  return this;
205
206
  }
206
207
 
208
+ lmstudio({ options = {}, config = {} } = {}) {
209
+ return this.attach('lmstudio', new MixLMStudio({ options, config }));
210
+ }
211
+
207
212
  addText(text, { role = "user" } = {}) {
208
213
  const content = [{
209
214
  type: "text",
@@ -468,7 +473,28 @@ class ModelMix {
468
473
  async prepareMessages() {
469
474
  await this.processImages();
470
475
  this.applyTemplate();
471
- this.messages = this.messages.slice(-this.config.max_history);
476
+
477
+ // Smart message slicing to preserve tool call sequences
478
+ if (this.config.max_history > 0) {
479
+ let sliceStart = Math.max(0, this.messages.length - this.config.max_history);
480
+
481
+ // If we're slicing and there's a tool message at the start,
482
+ // ensure we include the preceding assistant message with tool_calls
483
+ while (sliceStart > 0 &&
484
+ sliceStart < this.messages.length &&
485
+ this.messages[sliceStart].role === 'tool') {
486
+ sliceStart--;
487
+ // Also need to include the assistant message with tool_calls
488
+ if (sliceStart > 0 &&
489
+ this.messages[sliceStart].role === 'assistant' &&
490
+ this.messages[sliceStart].tool_calls) {
491
+ break;
492
+ }
493
+ }
494
+
495
+ this.messages = this.messages.slice(sliceStart);
496
+ }
497
+
472
498
  this.messages = this.groupByRoles(this.messages);
473
499
  this.options.messages = this.messages;
474
500
  }
@@ -490,7 +516,7 @@ class ModelMix {
490
516
 
491
517
  async execute({ config = {}, options = {} } = {}) {
492
518
  if (!this.models || this.models.length === 0) {
493
- throw new Error("No models specified. Use methods like .gpt41mini(), .sonnet4() first.");
519
+ throw new Error("No models specified. Use methods like .gpt5(), .sonnet4() first.");
494
520
  }
495
521
 
496
522
  return this.limiter.schedule(async () => {
@@ -552,7 +578,7 @@ class ModelMix {
552
578
  }
553
579
  }
554
580
 
555
- this.messages.push({ role: "assistant", content: result.toolCalls, tool_calls: result.toolCalls });
581
+ this.messages.push({ role: "assistant", content: null, tool_calls: result.toolCalls });
556
582
 
557
583
  const content = await this.processToolCalls(result.toolCalls);
558
584
  this.messages.push({ role: 'tool', content });
@@ -593,18 +619,67 @@ class ModelMix {
593
619
  const result = []
594
620
 
595
621
  for (const toolCall of toolCalls) {
596
- const client = this.toolClient[toolCall.function.name];
622
+ // Handle different tool call formats more robustly
623
+ let toolName, toolArgs, toolId;
624
+
625
+ try {
626
+ if (toolCall.function) {
627
+ // Formato OpenAI/normalizado
628
+ toolName = toolCall.function.name;
629
+ toolArgs = typeof toolCall.function.arguments === 'string'
630
+ ? JSON.parse(toolCall.function.arguments)
631
+ : toolCall.function.arguments;
632
+ toolId = toolCall.id;
633
+ } else if (toolCall.name) {
634
+ // Formato directo (posible formato alternativo)
635
+ toolName = toolCall.name;
636
+ toolArgs = toolCall.input || toolCall.arguments || {};
637
+ toolId = toolCall.id;
638
+ } else {
639
+ console.error('Unknown tool call format:', JSON.stringify(toolCall, null, 2));
640
+ continue;
641
+ }
597
642
 
598
- const response = await client.callTool({
599
- name: toolCall.function.name,
600
- arguments: JSON.parse(toolCall.function.arguments)
601
- });
643
+ // Validar que tenemos los datos necesarios
644
+ if (!toolName) {
645
+ console.error('Tool call missing name:', JSON.stringify(toolCall, null, 2));
646
+ continue;
647
+ }
602
648
 
603
- result.push({
604
- name: toolCall.function.name,
605
- tool_call_id: toolCall.id,
606
- content: response.content.map(item => item.text).join("\n")
607
- });
649
+ // Verificar si es una herramienta local registrada
650
+ if (this.mcpToolsManager.hasTool(toolName)) {
651
+ const response = await this.mcpToolsManager.executeTool(toolName, toolArgs);
652
+ result.push({
653
+ name: toolName,
654
+ tool_call_id: toolId,
655
+ content: response.content.map(item => item.text).join("\n")
656
+ });
657
+ } else {
658
+ // Usar el cliente MCP externo
659
+ const client = this.toolClient[toolName];
660
+ if (!client) {
661
+ throw new Error(`No client found for tool: ${toolName}`);
662
+ }
663
+
664
+ const response = await client.callTool({
665
+ name: toolName,
666
+ arguments: toolArgs
667
+ });
668
+
669
+ result.push({
670
+ name: toolName,
671
+ tool_call_id: toolId,
672
+ content: response.content.map(item => item.text).join("\n")
673
+ });
674
+ }
675
+ } catch (error) {
676
+ console.error(`Error processing tool call ${toolName}:`, error);
677
+ result.push({
678
+ name: toolName || 'unknown',
679
+ tool_call_id: toolId || 'unknown',
680
+ content: `Error: ${error.message}`
681
+ });
682
+ }
608
683
  }
609
684
  return result;
610
685
  }
@@ -651,6 +726,56 @@ class ModelMix {
651
726
  }
652
727
 
653
728
  }
729
+
730
+ addTool(toolDefinition, callback) {
731
+
732
+ if (this.config.max_history < 3) {
733
+ log.warn(`MCP ${toolDefinition.name} requires at least 3 max_history. Setting to 3.`);
734
+ this.config.max_history = 3;
735
+ }
736
+
737
+ this.mcpToolsManager.registerTool(toolDefinition, callback);
738
+
739
+ // Agregar la herramienta al sistema de tools para que sea incluida en las requests
740
+ if (!this.tools.local) {
741
+ this.tools.local = [];
742
+ }
743
+ this.tools.local.push({
744
+ name: toolDefinition.name,
745
+ description: toolDefinition.description,
746
+ inputSchema: toolDefinition.inputSchema
747
+ });
748
+
749
+ return this;
750
+ }
751
+
752
+ addTools(toolsWithCallbacks) {
753
+ for (const { tool, callback } of toolsWithCallbacks) {
754
+ this.addTool(tool, callback);
755
+ }
756
+ return this;
757
+ }
758
+
759
+ removeTool(toolName) {
760
+ this.mcpToolsManager.removeTool(toolName);
761
+
762
+ // Also remove from the tools system
763
+ if (this.tools.local) {
764
+ this.tools.local = this.tools.local.filter(tool => tool.name !== toolName);
765
+ }
766
+
767
+ return this;
768
+ }
769
+
770
+ listTools() {
771
+ const localTools = this.mcpToolsManager.getToolsForMCP();
772
+ const mcpTools = Object.values(this.tools).flat();
773
+
774
+ return {
775
+ local: localTools,
776
+ mcp: mcpTools.filter(tool => !localTools.find(local => local.name === tool.name))
777
+ };
778
+ }
654
779
  }
655
780
 
656
781
  class MixCustom {
@@ -658,7 +783,7 @@ class MixCustom {
658
783
  this.config = this.getDefaultConfig(config);
659
784
  this.options = this.getDefaultOptions(options);
660
785
  this.headers = this.getDefaultHeaders(headers);
661
- this.streamCallback = null; // Definimos streamCallback aquí
786
+ this.streamCallback = null; // Define streamCallback here
662
787
  }
663
788
 
664
789
  getDefaultOptions(customOptions) {
@@ -878,14 +1003,18 @@ class MixOpenAI extends MixCustom {
878
1003
 
879
1004
  if (message.role === 'tool') {
880
1005
  for (const content of message.content) {
881
- results.push({ role: 'tool', ...content })
1006
+ results.push({
1007
+ role: 'tool',
1008
+ tool_call_id: content.tool_call_id,
1009
+ content: content.content
1010
+ })
882
1011
  }
883
1012
  continue;
884
1013
  }
885
1014
 
886
1015
  if (Array.isArray(message.content)) {
887
- message.content = message.content.map(content => {
888
- if (content.type === 'image') {
1016
+ message.content = message.content.filter(content => content !== null && content !== undefined).map(content => {
1017
+ if (content && content.type === 'image') {
889
1018
  const { media_type, data } = content.source;
890
1019
  return {
891
1020
  type: 'image_url',
@@ -900,6 +1029,7 @@ class MixOpenAI extends MixCustom {
900
1029
 
901
1030
  results.push(message);
902
1031
  }
1032
+
903
1033
  return results;
904
1034
  }
905
1035
 
@@ -964,7 +1094,16 @@ class MixAnthropic extends MixCustom {
964
1094
  delete options.response_format;
965
1095
 
966
1096
  options.system = config.system;
967
- return super.create({ config, options });
1097
+
1098
+ try {
1099
+ return await super.create({ config, options });
1100
+ } catch (error) {
1101
+ // Log the error details for debugging
1102
+ if (error.response && error.response.data) {
1103
+ console.error('Anthropic API Error:', JSON.stringify(error.response.data, null, 2));
1104
+ }
1105
+ throw error;
1106
+ }
968
1107
  }
969
1108
 
970
1109
  convertMessages(messages, config) {
@@ -972,7 +1111,27 @@ class MixAnthropic extends MixCustom {
972
1111
  }
973
1112
 
974
1113
  static convertMessages(messages, config) {
975
- return messages.map(message => {
1114
+ // Filter out orphaned tool results for Anthropic
1115
+ const filteredMessages = [];
1116
+ for (let i = 0; i < messages.length; i++) {
1117
+ if (messages[i].role === 'tool') {
1118
+ // Check if there's a preceding assistant message with tool_calls
1119
+ let foundToolCall = false;
1120
+ for (let j = i - 1; j >= 0; j--) {
1121
+ if (messages[j].role === 'assistant' && messages[j].tool_calls) {
1122
+ foundToolCall = true;
1123
+ break;
1124
+ }
1125
+ }
1126
+ if (!foundToolCall) {
1127
+ // Skip orphaned tool results
1128
+ continue;
1129
+ }
1130
+ }
1131
+ filteredMessages.push(messages[i]);
1132
+ }
1133
+
1134
+ return filteredMessages.map(message => {
976
1135
  if (message.role === 'tool') {
977
1136
  return {
978
1137
  role: "user",
@@ -984,17 +1143,31 @@ class MixAnthropic extends MixCustom {
984
1143
  }
985
1144
  }
986
1145
 
987
- message.content = message.content.map(content => {
988
- if (content.type === 'function') {
989
- return {
990
- type: 'tool_use',
991
- id: content.id,
992
- name: content.function.name,
993
- input: JSON.parse(content.function.arguments)
1146
+ // Handle messages with tool_calls (assistant messages that call tools)
1147
+ if (message.tool_calls) {
1148
+ const content = message.tool_calls.map(call => ({
1149
+ type: 'tool_use',
1150
+ id: call.id,
1151
+ name: call.function.name,
1152
+ input: JSON.parse(call.function.arguments)
1153
+ }));
1154
+ return { role: 'assistant', content };
1155
+ }
1156
+
1157
+ // Handle content conversion for other messages
1158
+ if (message.content && Array.isArray(message.content)) {
1159
+ message.content = message.content.filter(content => content !== null && content !== undefined).map(content => {
1160
+ if (content && content.type === 'function') {
1161
+ return {
1162
+ type: 'tool_use',
1163
+ id: content.id,
1164
+ name: content.function.name,
1165
+ input: JSON.parse(content.function.arguments)
1166
+ }
994
1167
  }
995
- }
996
- return content;
997
- });
1168
+ return content;
1169
+ });
1170
+ }
998
1171
 
999
1172
  return message;
1000
1173
  });
@@ -1065,7 +1238,6 @@ class MixAnthropic extends MixCustom {
1065
1238
  for (const tool in tools) {
1066
1239
  for (const item of tools[tool]) {
1067
1240
  options.tools.push({
1068
- type: 'custom',
1069
1241
  name: item.name,
1070
1242
  description: item.description,
1071
1243
  input_schema: item.inputSchema
@@ -1326,6 +1498,19 @@ class MixGoogle extends MixCustom {
1326
1498
  static convertMessages(messages, config) {
1327
1499
  return messages.map(message => {
1328
1500
 
1501
+ // Handle assistant messages with tool_calls (content is null)
1502
+ if (message.role === 'assistant' && message.tool_calls) {
1503
+ return {
1504
+ role: 'model',
1505
+ parts: message.tool_calls.map(toolCall => ({
1506
+ functionCall: {
1507
+ name: toolCall.function.name,
1508
+ args: JSON.parse(toolCall.function.arguments)
1509
+ }
1510
+ }))
1511
+ }
1512
+ }
1513
+
1329
1514
  if (!Array.isArray(message.content)) return message;
1330
1515
  const role = (message.role === 'assistant' || message.role === 'tool') ? 'model' : 'user'
1331
1516
 
package/mcp-tools.js ADDED
@@ -0,0 +1,96 @@
1
+ const log = require('lemonlog')('ModelMix:MCP-Tools');
2
+
3
+ class MCPToolsManager {
4
+ constructor() {
5
+ this.tools = new Map();
6
+ this.callbacks = new Map();
7
+ }
8
+
9
+ registerTool(toolDefinition, callback) {
10
+ const { name, description, inputSchema } = toolDefinition;
11
+
12
+ if (!name || !description || !inputSchema) {
13
+ throw new Error('Tool definition must include name, description, and inputSchema');
14
+ }
15
+
16
+ if (typeof callback !== 'function') {
17
+ throw new Error('Callback must be a function');
18
+ }
19
+
20
+ // Registrar la herramienta
21
+ this.tools.set(name, {
22
+ name,
23
+ description,
24
+ inputSchema
25
+ });
26
+
27
+ // Registrar el callback
28
+ this.callbacks.set(name, callback);
29
+
30
+ log.debug(`Tool registered: ${name}`);
31
+ }
32
+
33
+ registerTools(toolsWithCallbacks) {
34
+ for (const { tool, callback } of toolsWithCallbacks) {
35
+ this.registerTool(tool, callback);
36
+ }
37
+ }
38
+
39
+ async executeTool(name, args) {
40
+ const callback = this.callbacks.get(name);
41
+ if (!callback) {
42
+ throw new Error(`Tool not found: ${name}`);
43
+ }
44
+
45
+ try {
46
+ const result = await callback(args);
47
+ // For primitive values (numbers, booleans), convert to string
48
+ // For objects/arrays, stringify them
49
+ let textResult;
50
+ if (typeof result === 'string') {
51
+ textResult = result;
52
+ } else if (typeof result === 'number' || typeof result === 'boolean') {
53
+ textResult = String(result);
54
+ } else {
55
+ textResult = JSON.stringify(result, null, 2);
56
+ }
57
+
58
+ return {
59
+ content: [{
60
+ type: "text",
61
+ text: textResult
62
+ }]
63
+ };
64
+ } catch (error) {
65
+ log.error(`Error executing tool ${name}:`, error);
66
+ return {
67
+ content: [{
68
+ type: "text",
69
+ text: `Error executing ${name}: ${error.message}`
70
+ }]
71
+ };
72
+ }
73
+ }
74
+
75
+ getToolsForMCP() {
76
+ return Array.from(this.tools.values());
77
+ }
78
+
79
+ hasTool(name) {
80
+ return this.tools.has(name);
81
+ }
82
+
83
+ removeTool(name) {
84
+ this.tools.delete(name);
85
+ this.callbacks.delete(name);
86
+ log.debug(`Tool removed: ${name}`);
87
+ }
88
+
89
+ clear() {
90
+ this.tools.clear();
91
+ this.callbacks.clear();
92
+ log.debug('All tools cleared');
93
+ }
94
+ }
95
+
96
+ module.exports = { MCPToolsManager };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modelmix",
3
- "version": "3.8.4",
3
+ "version": "3.8.6",
4
4
  "description": "🧬 ModelMix - Unified API for Diverse AI LLM.",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -52,7 +52,7 @@
52
52
  "bottleneck": "^2.19.5",
53
53
  "file-type": "^16.5.4",
54
54
  "form-data": "^4.0.4",
55
- "lemonlog": "^1.1.2"
55
+ "lemonlog": "^1.2.0"
56
56
  },
57
57
  "devDependencies": {
58
58
  "chai": "^5.2.1",
@@ -69,6 +69,7 @@
69
69
  "test:templates": "mocha test/templates.test.js --timeout 10000 --require test/setup.js",
70
70
  "test:images": "mocha test/images.test.js --timeout 10000 --require test/setup.js",
71
71
  "test:bottleneck": "mocha test/bottleneck.test.js --timeout 10000 --require test/setup.js",
72
- "test:live": "mocha test/live.test.js --timeout 10000 --require dotenv/config --require test/setup.js"
72
+ "test:live": "mocha test/live.test.js --timeout 10000 --require dotenv/config --require test/setup.js",
73
+ "test:live.mcp": "mocha test/live.mcp.js --timeout 60000 --require dotenv/config --require test/setup.js"
73
74
  }
74
75
  }