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.
@@ -0,0 +1,555 @@
1
+ const { expect } = require('chai');
2
+ const { ModelMix } = require('../index.js');
3
+ const nock = require('nock');
4
+
5
+ // Completely disable nock for live tests
6
+ nock.restore();
7
+ nock.cleanAll();
8
+
9
+ const setup = {
10
+ options: { temperature: 0 },
11
+ config: { debug: false, max_history: 3 }
12
+ };
13
+
14
+ describe('Live MCP Integration Tests', function () {
15
+ // Increase timeout for real API calls
16
+ this.timeout(60000);
17
+
18
+ // Ensure nock doesn't interfere with live tests
19
+ before(() => {
20
+ nock.restore();
21
+ nock.cleanAll();
22
+ });
23
+
24
+ beforeEach(() => {
25
+ nock.restore();
26
+ nock.cleanAll();
27
+ });
28
+
29
+ after(() => {
30
+ nock.restore();
31
+ });
32
+
33
+ describe('Basic MCP Tool Integration', function () {
34
+
35
+ it('should use custom MCP tools with GPT-4.1', async function () {
36
+ const model = ModelMix.new(setup).gpt41();
37
+
38
+ // Add custom calculator tool
39
+ model.addTool({
40
+ name: "calculate",
41
+ description: "Perform mathematical calculations",
42
+ inputSchema: {
43
+ type: "object",
44
+ properties: {
45
+ expression: {
46
+ type: "string",
47
+ description: "Mathematical expression to evaluate (e.g., '2 + 2', '10 * 5')"
48
+ }
49
+ },
50
+ required: ["expression"]
51
+ }
52
+ }, async ({ expression }) => {
53
+ try {
54
+ const sanitized = expression.replace(/[^0-9+\-*/().\s]/g, '');
55
+ const result = eval(sanitized);
56
+ return `${expression} = ${result}`;
57
+ } catch (error) {
58
+ throw new Error(`Invalid mathematical expression: ${expression}`);
59
+ }
60
+ });
61
+
62
+ model.setSystem('You are a helpful calculator. Use the calculate tool for math operations.');
63
+ model.addText('What is 15 * 23?');
64
+
65
+ const response = await model.message();
66
+ console.log(`GPT-4.1 with MCP tools: ${response}`);
67
+
68
+ expect(response).to.be.a('string');
69
+ expect(response).to.include('345');
70
+ });
71
+
72
+ it('should use custom MCP tools with Claude Sonnet 4', async function () {
73
+ const model = ModelMix.new(setup).sonnet4();
74
+
75
+ // Add time tool
76
+ model.addTool({
77
+ name: "get_current_time",
78
+ description: "Get the current date and time",
79
+ inputSchema: {
80
+ type: "object",
81
+ properties: {
82
+ timezone: {
83
+ type: "string",
84
+ description: "Timezone (optional, defaults to UTC)"
85
+ }
86
+ }
87
+ }
88
+ }, async ({ timezone = 'UTC' }) => {
89
+ const now = new Date();
90
+ if (timezone === 'UTC') {
91
+ return `Current time (UTC): ${now.toISOString()}`;
92
+ }
93
+ return `Current time (${timezone}): ${now.toLocaleString('en-US', { timeZone: timezone })}`;
94
+ });
95
+
96
+ model.setSystem('You are a helpful assistant that can tell time. Use the get_current_time tool when asked about time.');
97
+ model.addText('What time is it right now?');
98
+
99
+ try {
100
+ const response = await model.message();
101
+ console.log(`Claude Sonnet 4 with MCP tools: ${response}`);
102
+
103
+ expect(response).to.be.a('string');
104
+ expect(response.toLowerCase()).to.match(/(time|clock|hour|minute|second|am|pm|utc|\d{4})/);
105
+ } catch (error) {
106
+ console.error('Full error:', error);
107
+ if (error.response && error.response.data) {
108
+ console.error('Response data:', JSON.stringify(error.response.data, null, 2));
109
+ }
110
+ throw error;
111
+ }
112
+ });
113
+
114
+ it('should use custom MCP tools with Gemini 2.5 Flash', async function () {
115
+ const model = ModelMix.new(setup).gemini25flash();
116
+
117
+ // Add password generator tool
118
+ model.addTool({
119
+ name: "generate_password",
120
+ description: "Generate a secure password",
121
+ inputSchema: {
122
+ type: "object",
123
+ properties: {
124
+ length: {
125
+ type: "integer",
126
+ description: "Password length",
127
+ default: 12
128
+ },
129
+ includeSymbols: {
130
+ type: "boolean",
131
+ description: "Include special symbols",
132
+ default: true
133
+ }
134
+ }
135
+ }
136
+ }, async ({ length = 12, includeSymbols = true }) => {
137
+ const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
138
+ const symbols = '!@#$%^&*()_+-=[]{}|;:,.<>?';
139
+ const charset = includeSymbols ? chars + symbols : chars;
140
+
141
+ let password = '';
142
+ for (let i = 0; i < length; i++) {
143
+ password += charset.charAt(Math.floor(Math.random() * charset.length));
144
+ }
145
+
146
+ return `Generated password (${length} characters): ${password}`;
147
+ });
148
+
149
+ model.setSystem('You are a security assistant. Use the generate_password tool to create secure passwords.');
150
+ model.addText('Generate a secure password of 16 characters with symbols.');
151
+
152
+ const response = await model.message();
153
+ console.log(`Gemini 2.5 Flash with MCP tools: ${response}`);
154
+
155
+ expect(response).to.be.a('string');
156
+ expect(response).to.include('password');
157
+ expect(response).to.include('16');
158
+ });
159
+
160
+ });
161
+
162
+ describe('Advanced MCP Tool Integration', function () {
163
+
164
+ it('should use multiple MCP tools with Grok 3 Mini', async function () {
165
+ const model = ModelMix.new(setup).grok3mini();
166
+
167
+ // Add multiple tools
168
+ model.addTools([
169
+ {
170
+ tool: {
171
+ name: "calculate",
172
+ description: "Perform mathematical calculations",
173
+ inputSchema: {
174
+ type: "object",
175
+ properties: {
176
+ expression: {
177
+ type: "string",
178
+ description: "Mathematical expression"
179
+ }
180
+ },
181
+ required: ["expression"]
182
+ }
183
+ },
184
+ callback: async ({ expression }) => {
185
+ const sanitized = expression.replace(/[^0-9+\-*/().\s]/g, '');
186
+ const result = eval(sanitized);
187
+ return `${expression} = ${result}`;
188
+ }
189
+ },
190
+ {
191
+ tool: {
192
+ name: "generate_uuid",
193
+ description: "Generate a UUID",
194
+ inputSchema: {
195
+ type: "object",
196
+ properties: {
197
+ version: {
198
+ type: "string",
199
+ enum: ["v4"],
200
+ default: "v4"
201
+ }
202
+ }
203
+ }
204
+ },
205
+ callback: async ({ version = "v4" }) => {
206
+ const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
207
+ const r = Math.random() * 16 | 0;
208
+ const v = c === 'x' ? r : (r & 0x3 | 0x8);
209
+ return v.toString(16);
210
+ });
211
+ return `Generated UUID (${version}): ${uuid}`;
212
+ }
213
+ }
214
+ ]);
215
+
216
+ model.setSystem('You are a helpful assistant with calculation and UUID generation capabilities.');
217
+ model.addText('Calculate 25 * 4 and generate a UUID.');
218
+
219
+ const response = await model.message();
220
+ console.log(`Grok 3 Mini with multiple MCP tools: ${response}`);
221
+
222
+ expect(response).to.be.a('string');
223
+ expect(response).to.include('100');
224
+ expect(response).to.match(/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/i);
225
+ });
226
+
227
+ it('should use MCP tools with Scout model', async function () {
228
+ const model = ModelMix.new(setup).scout();
229
+
230
+ // Add text processing tool
231
+ model.addTool({
232
+ name: "text_analysis",
233
+ description: "Analyze text and provide statistics",
234
+ inputSchema: {
235
+ type: "object",
236
+ properties: {
237
+ text: {
238
+ type: "string",
239
+ description: "Text to analyze"
240
+ }
241
+ },
242
+ required: ["text"]
243
+ }
244
+ }, async ({ text }) => {
245
+ const words = text.split(/\s+/).filter(word => word.length > 0);
246
+ const chars = text.length;
247
+ const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 0).length;
248
+
249
+ return `Text Analysis:
250
+ - Characters: ${chars}
251
+ - Words: ${words.length}
252
+ - Sentences: ${sentences}
253
+ - Average word length: ${(chars / words.length).toFixed(1)} characters`;
254
+ });
255
+
256
+ model.setSystem('You are a text analysis assistant. Use the text_analysis tool to analyze text.');
257
+ model.addText('Analyze this text: "Hello world! This is a test sentence for analysis. How many words are there?"');
258
+
259
+ const response = await model.message();
260
+ console.log(`Scout with MCP tools: ${response}`);
261
+
262
+ expect(response).to.be.a('string');
263
+ expect(response).to.include('Characters:');
264
+ expect(response).to.include('Words:');
265
+ });
266
+
267
+ it('should use MCP tools with GPT-5 Mini', async function () {
268
+ const model = ModelMix.new(setup).gpt5mini();
269
+
270
+ // Add data formatting tool
271
+ model.addTool({
272
+ name: "format_data",
273
+ description: "Format data into different formats",
274
+ inputSchema: {
275
+ type: "object",
276
+ properties: {
277
+ data: {
278
+ type: "string",
279
+ description: "Data to format"
280
+ },
281
+ format: {
282
+ type: "string",
283
+ enum: ["json", "csv", "xml"],
284
+ description: "Output format"
285
+ }
286
+ },
287
+ required: ["data", "format"]
288
+ }
289
+ }, async ({ data, format }) => {
290
+ const items = data.split(',').map(item => item.trim());
291
+
292
+ switch (format) {
293
+ case 'json':
294
+ return JSON.stringify({ items }, null, 2);
295
+ case 'csv':
296
+ return `item\n${items.join('\n')}`;
297
+ case 'xml':
298
+ return `<items>\n${items.map(item => ` <item>${item}</item>`).join('\n')}\n</items>`;
299
+ default:
300
+ return `Unsupported format: ${format}`;
301
+ }
302
+ });
303
+
304
+ model.setSystem('You are a data formatting assistant. Use the format_data tool to format data.');
305
+ model.addText('Format this data as JSON: "apple, banana, cherry, date"');
306
+
307
+ const response = await model.message();
308
+ console.log(`GPT-4.1 Mini with MCP tools: ${response}`);
309
+
310
+ expect(response).to.be.a('string');
311
+ expect(response).to.include('apple');
312
+ expect(response).to.include('banana');
313
+ });
314
+
315
+ });
316
+
317
+ describe('MCP Tools with JSON Output', function () {
318
+
319
+ it('should combine MCP tools with JSON output using GPT-4.1 Nano', async function () {
320
+ const model = ModelMix.new(setup).gpt41nano();
321
+
322
+ // Add calculation tool
323
+ model.addTool({
324
+ name: "advanced_math",
325
+ description: "Perform advanced mathematical operations",
326
+ inputSchema: {
327
+ type: "object",
328
+ properties: {
329
+ operation: {
330
+ type: "string",
331
+ enum: ["sqrt", "power", "factorial"],
332
+ description: "Type of operation"
333
+ },
334
+ value: {
335
+ type: "number",
336
+ description: "Input value"
337
+ },
338
+ exponent: {
339
+ type: "number",
340
+ description: "Exponent for power operation"
341
+ }
342
+ },
343
+ required: ["operation", "value"]
344
+ }
345
+ }, async ({ operation, value, exponent }) => {
346
+ switch (operation) {
347
+ case 'sqrt':
348
+ return Math.sqrt(value);
349
+ case 'power':
350
+ if (exponent === undefined) throw new Error('Exponent required for power operation');
351
+ return Math.pow(value, exponent);
352
+ case 'factorial':
353
+ if (value < 0 || !Number.isInteger(value)) throw new Error('Invalid input for factorial');
354
+ let result = 1;
355
+ for (let i = 2; i <= value; i++) result *= i;
356
+ return result;
357
+ default:
358
+ throw new Error(`Unknown operation: ${operation}`);
359
+ }
360
+ });
361
+
362
+ model.setSystem('You are a mathematical assistant. Use the advanced_math tool to calculate values. When asked to return JSON, format your response as valid JSON with the requested structure.');
363
+ model.addText('Calculate the square root of 144 and the factorial of 5. Return the results in JSON format with keys "sqrt_result" and "factorial_result".');
364
+
365
+ const result = await model.json({
366
+ sqrt_result: 0,
367
+ factorial_result: 0,
368
+ calculations: []
369
+ });
370
+
371
+ console.log(`GPT-4.1 Nano with MCP tools JSON result:`, JSON.stringify(result, null, 2));
372
+
373
+ expect(result).to.be.an('object');
374
+ expect(result.sqrt_result).to.equal(12);
375
+ expect(result.factorial_result).to.equal(120);
376
+ });
377
+
378
+ it('should use MCP tools with JSON output using Gemini 2.5 Flash', async function () {
379
+ const model = ModelMix.new(setup).gemini25flash();
380
+
381
+ // Add system info tool
382
+ model.addTool({
383
+ name: "system_info",
384
+ description: "Get system information",
385
+ inputSchema: {
386
+ type: "object",
387
+ properties: {
388
+ info_type: {
389
+ type: "string",
390
+ enum: ["timestamp", "random", "platform"],
391
+ description: "Type of system info to get"
392
+ }
393
+ },
394
+ required: ["info_type"]
395
+ }
396
+ }, async ({ info_type }) => {
397
+ switch (info_type) {
398
+ case 'timestamp':
399
+ return Date.now();
400
+ case 'random':
401
+ return Math.floor(Math.random() * 1000);
402
+ case 'platform':
403
+ return process.platform || 'unknown';
404
+ default:
405
+ throw new Error(`Unknown info type: ${info_type}`);
406
+ }
407
+ });
408
+
409
+ model.setSystem('You are a system assistant. Use the system_info tool and return structured data.');
410
+ model.addText('Get the current timestamp and a random number, format as JSON.');
411
+
412
+ const result = await model.json({
413
+ timestamp: 0,
414
+ random_number: 0,
415
+ generated_at: ""
416
+ });
417
+
418
+ console.log(`Gemini 2.5 Flash with MCP tools JSON result:`, result);
419
+
420
+ expect(result).to.be.an('object');
421
+ expect(result.timestamp).to.be.a('number');
422
+ expect(result.random_number).to.be.a('number');
423
+ expect(result.timestamp).to.be.greaterThan(1700000000000);
424
+ });
425
+
426
+ });
427
+
428
+ describe('MCP Tools Error Handling', function () {
429
+
430
+ it('should handle MCP tool errors gracefully with GPT-5 Nano', async function () {
431
+ const model = ModelMix.new(setup).gpt5nano();
432
+
433
+ // Add tool that can fail
434
+ model.addTool({
435
+ name: "risky_operation",
436
+ description: "An operation that might fail",
437
+ inputSchema: {
438
+ type: "object",
439
+ properties: {
440
+ should_fail: {
441
+ type: "boolean",
442
+ description: "Whether the operation should fail"
443
+ }
444
+ },
445
+ required: ["should_fail"]
446
+ }
447
+ }, async ({ should_fail }) => {
448
+ if (should_fail) {
449
+ throw new Error('Operation failed as requested');
450
+ }
451
+ return 'Operation completed successfully';
452
+ });
453
+
454
+ model.setSystem('You are a resilient assistant. Handle tool errors gracefully.');
455
+ model.addText('Try the risky operation with should_fail set to true, then explain what happened.');
456
+
457
+ const response = await model.message();
458
+ console.log(`GPT-5 Nano with error handling: ${response}`);
459
+
460
+ expect(response).to.be.a('string');
461
+ expect(response.toLowerCase()).to.match(/(error|fail|problem|issue)/);
462
+ });
463
+
464
+ });
465
+
466
+ describe('Multiple Models with Same MCP Tools', function () {
467
+
468
+ const createCommonTools = (model) => {
469
+ model.addTool({
470
+ name: "string_utils",
471
+ description: "String utility functions",
472
+ inputSchema: {
473
+ type: "object",
474
+ properties: {
475
+ text: {
476
+ type: "string",
477
+ description: "Input text"
478
+ },
479
+ operation: {
480
+ type: "string",
481
+ enum: ["uppercase", "lowercase", "reverse", "length"],
482
+ description: "Operation to perform"
483
+ }
484
+ },
485
+ required: ["text", "operation"]
486
+ }
487
+ }, async ({ text, operation }) => {
488
+ switch (operation) {
489
+ case 'uppercase':
490
+ return text.toUpperCase();
491
+ case 'lowercase':
492
+ return text.toLowerCase();
493
+ case 'reverse':
494
+ return text.split('').reverse().join('');
495
+ case 'length':
496
+ return text.length.toString();
497
+ default:
498
+ throw new Error(`Unknown operation: ${operation}`);
499
+ }
500
+ });
501
+ };
502
+
503
+ it('should work with same MCP tools across different OpenAI models', async function () {
504
+ const models = [
505
+ { name: 'GPT-5 Mini', model: ModelMix.new(setup).gpt5mini() },
506
+ { name: 'GPT-5 Nano', model: ModelMix.new(setup).gpt5nano() },
507
+ { name: 'GPT-4.1', model: ModelMix.new(setup).gpt41() }
508
+ ];
509
+
510
+ const results = [];
511
+
512
+ for (const { name, model } of models) {
513
+ createCommonTools(model);
514
+ model.setSystem('You are a string processing assistant. Use string_utils tool.');
515
+ model.addText('Convert "Hello World" to uppercase using the string utility.');
516
+
517
+ const response = await model.message();
518
+ results.push({ model: name, response });
519
+ console.log(`${name}: ${response}`);
520
+
521
+ expect(response).to.be.a('string');
522
+ expect(response).to.include('HELLO WORLD');
523
+ }
524
+
525
+ expect(results).to.have.length(3);
526
+ });
527
+
528
+ it('should work with same MCP tools across different Anthropic models', async function () {
529
+ const models = [
530
+ { name: 'Sonnet 4', model: ModelMix.new(setup).sonnet4() },
531
+ { name: 'Sonnet 3.7', model: ModelMix.new(setup).sonnet37() },
532
+ { name: 'Haiku 3.5', model: ModelMix.new(setup).haiku35() }
533
+ ];
534
+
535
+ const results = [];
536
+
537
+ for (const { name, model } of models) {
538
+ createCommonTools(model);
539
+ model.setSystem('You are a string processing assistant. Use string_utils tool.');
540
+ model.addText('Get the length of "ModelMix" using the string utility.');
541
+
542
+ const response = await model.message();
543
+ results.push({ model: name, response });
544
+ console.log(`${name}: ${response}`);
545
+
546
+ expect(response).to.be.a('string');
547
+ expect(response).to.include('8');
548
+ }
549
+
550
+ expect(results).to.have.length(3);
551
+ });
552
+
553
+ });
554
+
555
+ });
@@ -23,6 +23,7 @@ const testFiles = [
23
23
  'fallback.test.js',
24
24
  'templates.test.js',
25
25
  'images.test.js',
26
+ 'live.mcp.js',
26
27
  ];
27
28
 
28
29
  console.log('🧬 ModelMix Test Suite Runner');
@@ -51,6 +52,7 @@ function runTests() {
51
52
  console.log('- ✅ File Operations & Templates');
52
53
  console.log('- ✅ Image Processing & Multimodal');
53
54
  console.log('- ✅ MCP Integration');
55
+ console.log('- ✅ Live MCP Tools Testing');
54
56
  console.log('- ✅ Rate Limiting with Bottleneck');
55
57
  console.log('- ✅ Integration & Edge Cases');
56
58
  } else {