modelmix 3.8.2 → 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/.claude/settings.local.json +5 -1
- package/README.md +8 -7
- package/demo/demo.mjs +8 -8
- package/demo/images.mjs +9 -0
- package/demo/img.png +0 -0
- package/demo/mcp-simple.mjs +166 -0
- package/demo/mcp-tools.mjs +344 -0
- package/index.js +284 -50
- package/mcp-tools.js +96 -0
- package/package.json +22 -7
- package/test/README.md +158 -0
- package/test/bottleneck.test.js +483 -0
- package/test/fallback.test.js +387 -0
- package/test/fixtures/data.json +36 -0
- package/test/fixtures/img.png +0 -0
- package/test/fixtures/template.txt +15 -0
- package/test/images.test.js +87 -0
- package/test/json.test.js +295 -0
- package/test/live.mcp.js +555 -0
- package/test/live.test.js +356 -0
- package/test/mocha.opts +5 -0
- package/test/setup.js +176 -0
- package/test/templates.test.js +473 -0
- package/test/test-runner.js +75 -0
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.
|
|
@@ -41,6 +43,7 @@ class ModelMix {
|
|
|
41
43
|
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
|
|
44
47
|
replace(keyValues) {
|
|
45
48
|
this.config.replace = { ...this.config.replace, ...keyValues };
|
|
46
49
|
return this;
|
|
@@ -68,7 +71,6 @@ class ModelMix {
|
|
|
68
71
|
return this;
|
|
69
72
|
}
|
|
70
73
|
|
|
71
|
-
// --- Model addition methods ---
|
|
72
74
|
gpt41({ options = {}, config = {} } = {}) {
|
|
73
75
|
return this.attach('gpt-4.1', new MixOpenAI({ options, config }));
|
|
74
76
|
}
|
|
@@ -90,13 +92,22 @@ class ModelMix {
|
|
|
90
92
|
gpt45({ options = {}, config = {} } = {}) {
|
|
91
93
|
return this.attach('gpt-4.5-preview', new MixOpenAI({ options, config }));
|
|
92
94
|
}
|
|
93
|
-
|
|
95
|
+
gpt5({ options = {}, config = {} } = {}) {
|
|
96
|
+
return this.attach('gpt-5', new MixOpenAI({ options, config }));
|
|
97
|
+
}
|
|
98
|
+
gpt5mini({ options = {}, config = {} } = {}) {
|
|
99
|
+
return this.attach('gpt-5-mini', new MixOpenAI({ options, config }));
|
|
100
|
+
}
|
|
101
|
+
gpt5nano({ options = {}, config = {} } = {}) {
|
|
102
|
+
return this.attach('gpt-5-nano', new MixOpenAI({ options, config }));
|
|
103
|
+
}
|
|
104
|
+
gptOss({ options = {}, config = {}, mix = { together: false, cerebras: false, groq: true } } = {}) {
|
|
94
105
|
if (mix.together) return this.attach('openai/gpt-oss-120b', new MixTogether({ options, config }));
|
|
95
106
|
if (mix.cerebras) return this.attach('gpt-oss-120b', new MixCerebras({ options, config }));
|
|
96
107
|
if (mix.groq) return this.attach('openai/gpt-oss-120b', new MixGroq({ options, config }));
|
|
97
|
-
if (mix.lmstudio) return this.attach('openai/gpt-oss-120b', new MixLMStudio({ options, config }));
|
|
98
108
|
return this;
|
|
99
109
|
}
|
|
110
|
+
|
|
100
111
|
opus4think({ options = {}, config = {} } = {}) {
|
|
101
112
|
options = { ...MixAnthropic.thinkingOptions, ...options };
|
|
102
113
|
return this.attach('claude-opus-4-20250514', new MixAnthropic({ options, config }));
|
|
@@ -132,7 +143,7 @@ class ModelMix {
|
|
|
132
143
|
return this.attach('claude-3-5-haiku-20241022', new MixAnthropic({ options, config }));
|
|
133
144
|
}
|
|
134
145
|
gemini25flash({ options = {}, config = {} } = {}) {
|
|
135
|
-
return this.attach('gemini-2.5-flash
|
|
146
|
+
return this.attach('gemini-2.5-flash', new MixGoogle({ options, config }));
|
|
136
147
|
}
|
|
137
148
|
gemini25proExp({ options = {}, config = {} } = {}) {
|
|
138
149
|
return this.attach('gemini-2.5-pro-exp-03-25', new MixGoogle({ options, config }));
|
|
@@ -189,11 +200,15 @@ class ModelMix {
|
|
|
189
200
|
}
|
|
190
201
|
|
|
191
202
|
kimiK2({ options = {}, config = {}, mix = { together: false, groq: true } } = {}) {
|
|
192
|
-
if (mix.together) this.attach('moonshotai/Kimi-K2-Instruct', new MixTogether({ options, config }));
|
|
193
|
-
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 }));
|
|
194
205
|
return this;
|
|
195
206
|
}
|
|
196
207
|
|
|
208
|
+
lmstudio({ options = {}, config = {} } = {}) {
|
|
209
|
+
return this.attach('lmstudio', new MixLMStudio({ options, config }));
|
|
210
|
+
}
|
|
211
|
+
|
|
197
212
|
addText(text, { role = "user" } = {}) {
|
|
198
213
|
const content = [{
|
|
199
214
|
type: "text",
|
|
@@ -236,6 +251,12 @@ class ModelMix {
|
|
|
236
251
|
}
|
|
237
252
|
|
|
238
253
|
addImage(filePath, { role = "user" } = {}) {
|
|
254
|
+
const absolutePath = path.resolve(filePath);
|
|
255
|
+
|
|
256
|
+
if (!fs.existsSync(absolutePath)) {
|
|
257
|
+
throw new Error(`Image file not found: ${filePath}`);
|
|
258
|
+
}
|
|
259
|
+
|
|
239
260
|
this.messages.push({
|
|
240
261
|
role,
|
|
241
262
|
content: [{
|
|
@@ -250,24 +271,41 @@ class ModelMix {
|
|
|
250
271
|
}
|
|
251
272
|
|
|
252
273
|
addImageFromUrl(url, { role = "user" } = {}) {
|
|
274
|
+
let source;
|
|
275
|
+
if (url.startsWith('data:')) {
|
|
276
|
+
// Parse data URL: data:image/jpeg;base64,/9j/4AAQ...
|
|
277
|
+
const match = url.match(/^data:([^;]+);base64,(.+)$/);
|
|
278
|
+
if (match) {
|
|
279
|
+
source = {
|
|
280
|
+
type: "base64",
|
|
281
|
+
media_type: match[1],
|
|
282
|
+
data: match[2]
|
|
283
|
+
};
|
|
284
|
+
} else {
|
|
285
|
+
throw new Error('Invalid data URL format');
|
|
286
|
+
}
|
|
287
|
+
} else {
|
|
288
|
+
source = {
|
|
289
|
+
type: "url",
|
|
290
|
+
data: url
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
253
294
|
this.messages.push({
|
|
254
295
|
role,
|
|
255
296
|
content: [{
|
|
256
297
|
type: "image",
|
|
257
|
-
source
|
|
258
|
-
type: "url",
|
|
259
|
-
data: url
|
|
260
|
-
}
|
|
298
|
+
source
|
|
261
299
|
}]
|
|
262
300
|
});
|
|
301
|
+
|
|
263
302
|
return this;
|
|
264
303
|
}
|
|
265
304
|
|
|
266
305
|
async processImages() {
|
|
267
|
-
// Process images that are in messages
|
|
268
306
|
for (let i = 0; i < this.messages.length; i++) {
|
|
269
307
|
const message = this.messages[i];
|
|
270
|
-
if (!message.content) continue;
|
|
308
|
+
if (!Array.isArray(message.content)) continue;
|
|
271
309
|
|
|
272
310
|
for (let j = 0; j < message.content.length; j++) {
|
|
273
311
|
const content = message.content[j];
|
|
@@ -381,8 +419,13 @@ class ModelMix {
|
|
|
381
419
|
}
|
|
382
420
|
|
|
383
421
|
replaceKeyFromFile(key, filePath) {
|
|
384
|
-
|
|
385
|
-
|
|
422
|
+
try {
|
|
423
|
+
const content = this.readFile(filePath);
|
|
424
|
+
this.replace({ [key]: this._template(content, this.config.replace) });
|
|
425
|
+
} catch (error) {
|
|
426
|
+
// Gracefully handle file read errors without throwing
|
|
427
|
+
log.warn(`replaceKeyFromFile: ${error.message}`);
|
|
428
|
+
}
|
|
386
429
|
return this;
|
|
387
430
|
}
|
|
388
431
|
|
|
@@ -430,7 +473,28 @@ class ModelMix {
|
|
|
430
473
|
async prepareMessages() {
|
|
431
474
|
await this.processImages();
|
|
432
475
|
this.applyTemplate();
|
|
433
|
-
|
|
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
|
+
|
|
434
498
|
this.messages = this.groupByRoles(this.messages);
|
|
435
499
|
this.options.messages = this.messages;
|
|
436
500
|
}
|
|
@@ -452,7 +516,7 @@ class ModelMix {
|
|
|
452
516
|
|
|
453
517
|
async execute({ config = {}, options = {} } = {}) {
|
|
454
518
|
if (!this.models || this.models.length === 0) {
|
|
455
|
-
throw new Error("No models specified. Use methods like .
|
|
519
|
+
throw new Error("No models specified. Use methods like .gpt5(), .sonnet4() first.");
|
|
456
520
|
}
|
|
457
521
|
|
|
458
522
|
return this.limiter.schedule(async () => {
|
|
@@ -514,7 +578,7 @@ class ModelMix {
|
|
|
514
578
|
}
|
|
515
579
|
}
|
|
516
580
|
|
|
517
|
-
this.messages.push({ role: "assistant", content:
|
|
581
|
+
this.messages.push({ role: "assistant", content: null, tool_calls: result.toolCalls });
|
|
518
582
|
|
|
519
583
|
const content = await this.processToolCalls(result.toolCalls);
|
|
520
584
|
this.messages.push({ role: 'tool', content });
|
|
@@ -555,18 +619,67 @@ class ModelMix {
|
|
|
555
619
|
const result = []
|
|
556
620
|
|
|
557
621
|
for (const toolCall of toolCalls) {
|
|
558
|
-
|
|
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
|
+
}
|
|
559
642
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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
|
+
}
|
|
564
648
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
+
}
|
|
570
683
|
}
|
|
571
684
|
return result;
|
|
572
685
|
}
|
|
@@ -613,6 +726,56 @@ class ModelMix {
|
|
|
613
726
|
}
|
|
614
727
|
|
|
615
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
|
+
}
|
|
616
779
|
}
|
|
617
780
|
|
|
618
781
|
class MixCustom {
|
|
@@ -620,7 +783,7 @@ class MixCustom {
|
|
|
620
783
|
this.config = this.getDefaultConfig(config);
|
|
621
784
|
this.options = this.getDefaultOptions(options);
|
|
622
785
|
this.headers = this.getDefaultHeaders(headers);
|
|
623
|
-
this.streamCallback = null; //
|
|
786
|
+
this.streamCallback = null; // Define streamCallback here
|
|
624
787
|
}
|
|
625
788
|
|
|
626
789
|
getDefaultOptions(customOptions) {
|
|
@@ -812,6 +975,15 @@ class MixOpenAI extends MixCustom {
|
|
|
812
975
|
delete options.max_tokens;
|
|
813
976
|
delete options.temperature;
|
|
814
977
|
}
|
|
978
|
+
|
|
979
|
+
// Use max_completion_tokens and remove temperature for GPT-5 models
|
|
980
|
+
if (options.model?.includes('gpt-5')) {
|
|
981
|
+
if (options.max_tokens) {
|
|
982
|
+
options.max_completion_tokens = options.max_tokens;
|
|
983
|
+
delete options.max_tokens;
|
|
984
|
+
}
|
|
985
|
+
delete options.temperature;
|
|
986
|
+
}
|
|
815
987
|
|
|
816
988
|
return super.create({ config, options });
|
|
817
989
|
}
|
|
@@ -831,26 +1003,33 @@ class MixOpenAI extends MixCustom {
|
|
|
831
1003
|
|
|
832
1004
|
if (message.role === 'tool') {
|
|
833
1005
|
for (const content of message.content) {
|
|
834
|
-
results.push({
|
|
1006
|
+
results.push({
|
|
1007
|
+
role: 'tool',
|
|
1008
|
+
tool_call_id: content.tool_call_id,
|
|
1009
|
+
content: content.content
|
|
1010
|
+
})
|
|
835
1011
|
}
|
|
836
1012
|
continue;
|
|
837
1013
|
}
|
|
838
1014
|
|
|
839
|
-
if (Array.isArray(message.content))
|
|
840
|
-
|
|
841
|
-
if (content.type === 'image') {
|
|
842
|
-
const {
|
|
843
|
-
|
|
1015
|
+
if (Array.isArray(message.content)) {
|
|
1016
|
+
message.content = message.content.filter(content => content !== null && content !== undefined).map(content => {
|
|
1017
|
+
if (content && content.type === 'image') {
|
|
1018
|
+
const { media_type, data } = content.source;
|
|
1019
|
+
return {
|
|
844
1020
|
type: 'image_url',
|
|
845
1021
|
image_url: {
|
|
846
|
-
url: `data:${media_type}
|
|
1022
|
+
url: `data:${media_type};base64,${data}`
|
|
847
1023
|
}
|
|
848
|
-
}
|
|
1024
|
+
};
|
|
849
1025
|
}
|
|
850
|
-
|
|
1026
|
+
return content;
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
851
1029
|
|
|
852
1030
|
results.push(message);
|
|
853
1031
|
}
|
|
1032
|
+
|
|
854
1033
|
return results;
|
|
855
1034
|
}
|
|
856
1035
|
|
|
@@ -915,7 +1094,16 @@ class MixAnthropic extends MixCustom {
|
|
|
915
1094
|
delete options.response_format;
|
|
916
1095
|
|
|
917
1096
|
options.system = config.system;
|
|
918
|
-
|
|
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
|
+
}
|
|
919
1107
|
}
|
|
920
1108
|
|
|
921
1109
|
convertMessages(messages, config) {
|
|
@@ -923,7 +1111,27 @@ class MixAnthropic extends MixCustom {
|
|
|
923
1111
|
}
|
|
924
1112
|
|
|
925
1113
|
static convertMessages(messages, config) {
|
|
926
|
-
|
|
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 => {
|
|
927
1135
|
if (message.role === 'tool') {
|
|
928
1136
|
return {
|
|
929
1137
|
role: "user",
|
|
@@ -935,17 +1143,31 @@ class MixAnthropic extends MixCustom {
|
|
|
935
1143
|
}
|
|
936
1144
|
}
|
|
937
1145
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
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
|
+
}
|
|
945
1167
|
}
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
}
|
|
1168
|
+
return content;
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
949
1171
|
|
|
950
1172
|
return message;
|
|
951
1173
|
});
|
|
@@ -1016,7 +1238,6 @@ class MixAnthropic extends MixCustom {
|
|
|
1016
1238
|
for (const tool in tools) {
|
|
1017
1239
|
for (const item of tools[tool]) {
|
|
1018
1240
|
options.tools.push({
|
|
1019
|
-
type: 'custom',
|
|
1020
1241
|
name: item.name,
|
|
1021
1242
|
description: item.description,
|
|
1022
1243
|
input_schema: item.inputSchema
|
|
@@ -1277,6 +1498,19 @@ class MixGoogle extends MixCustom {
|
|
|
1277
1498
|
static convertMessages(messages, config) {
|
|
1278
1499
|
return messages.map(message => {
|
|
1279
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
|
+
|
|
1280
1514
|
if (!Array.isArray(message.content)) return message;
|
|
1281
1515
|
const role = (message.role === 'assistant' || message.role === 'tool') ? 'model' : 'user'
|
|
1282
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.
|
|
3
|
+
"version": "3.8.6",
|
|
4
4
|
"description": "🧬 ModelMix - Unified API for Diverse AI LLM.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"repository": {
|
|
@@ -15,13 +15,14 @@
|
|
|
15
15
|
"openai",
|
|
16
16
|
"anthropic",
|
|
17
17
|
"agent",
|
|
18
|
-
"perplexity",
|
|
19
18
|
"grok4",
|
|
20
19
|
"gpt",
|
|
21
20
|
"claude",
|
|
22
21
|
"llama",
|
|
22
|
+
"fallback",
|
|
23
23
|
"kimi",
|
|
24
24
|
"chat",
|
|
25
|
+
"gpt5",
|
|
25
26
|
"opus",
|
|
26
27
|
"sonnet",
|
|
27
28
|
"multimodal",
|
|
@@ -29,18 +30,14 @@
|
|
|
29
30
|
"gemini",
|
|
30
31
|
"ollama",
|
|
31
32
|
"lmstudio",
|
|
32
|
-
"together",
|
|
33
33
|
"nano",
|
|
34
34
|
"deepseek",
|
|
35
35
|
"oss",
|
|
36
|
-
"4.1",
|
|
37
|
-
"qwen",
|
|
38
36
|
"nousresearch",
|
|
39
37
|
"reasoning",
|
|
40
38
|
"bottleneck",
|
|
41
39
|
"cerebras",
|
|
42
40
|
"scout",
|
|
43
|
-
"fallback",
|
|
44
41
|
"clasen"
|
|
45
42
|
],
|
|
46
43
|
"author": "Martin Clasen",
|
|
@@ -55,6 +52,24 @@
|
|
|
55
52
|
"bottleneck": "^2.19.5",
|
|
56
53
|
"file-type": "^16.5.4",
|
|
57
54
|
"form-data": "^4.0.4",
|
|
58
|
-
"lemonlog": "^1.
|
|
55
|
+
"lemonlog": "^1.2.0"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"chai": "^5.2.1",
|
|
59
|
+
"dotenv": "^17.2.1",
|
|
60
|
+
"mocha": "^11.7.1",
|
|
61
|
+
"nock": "^14.0.9",
|
|
62
|
+
"sinon": "^21.0.0"
|
|
63
|
+
},
|
|
64
|
+
"scripts": {
|
|
65
|
+
"test": "mocha test/**/*.js --timeout 10000 --require dotenv/config --require test/setup.js",
|
|
66
|
+
"test:watch": "mocha test/**/*.js --watch --timeout 10000 --require test/setup.js",
|
|
67
|
+
"test:json": "mocha test/json.test.js --timeout 10000 --require test/setup.js",
|
|
68
|
+
"test:fallback": "mocha test/fallback.test.js --timeout 10000 --require test/setup.js",
|
|
69
|
+
"test:templates": "mocha test/templates.test.js --timeout 10000 --require test/setup.js",
|
|
70
|
+
"test:images": "mocha test/images.test.js --timeout 10000 --require test/setup.js",
|
|
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",
|
|
73
|
+
"test:live.mcp": "mocha test/live.mcp.js --timeout 60000 --require dotenv/config --require test/setup.js"
|
|
59
74
|
}
|
|
60
75
|
}
|