modelmix 3.8.6 → 3.8.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/README.md CHANGED
@@ -126,9 +126,8 @@ Here's a comprehensive list of available methods:
126
126
  | `o3()` | OpenAI | o3 | [\$10.00 / \$40.00][1] |
127
127
  | `gptOss()` | Together | gpt-oss-120B | [\$0.15 / \$0.60][7] |
128
128
  | `opus41[think]()` | Anthropic | claude-opus-4-1-20250805 | [\$15.00 / \$75.00][2] |
129
+ | `sonnet45[think]()`| Anthropic | claude-sonnet-4-5-20250929 | [\$3.00 / \$15.00][2] |
129
130
  | `sonnet4[think]()` | Anthropic | claude-sonnet-4-20250514 | [\$3.00 / \$15.00][2] |
130
- | `sonnet37[think]()`| Anthropic | claude-3-7-sonnet-20250219 | [\$3.00 / \$15.00][2] |
131
- | `sonnet35()` | Anthropic | claude-3-5-sonnet-20241022 | [\$3.00 / \$15.00][2] |
132
131
  | `haiku35()` | Anthropic | claude-3-5-haiku-20241022 | [\$0.80 / \$4.00][2] |
133
132
  | `gemini25flash()` | Google | gemini-2.5-flash-preview-04-17 | [\$0.00 / \$0.00][3] |
134
133
  | `gemini25proExp()` | Google | gemini-2.5-pro-exp-03-25 | [\$0.00 / \$0.00][3] |
@@ -331,7 +330,6 @@ new ModelMix(args = { options: {}, config: {} })
331
330
  - **options**: This object contains default options that are applied to all models. These options can be overridden when creating a specific model instance. Examples of default options include:
332
331
  - `max_tokens`: Sets the maximum number of tokens to generate, e.g., 2000.
333
332
  - `temperature`: Controls the randomness of the model's output, e.g., 1.
334
- - `top_p`: Controls the diversity of the output, e.g., 1.
335
333
  - ...(Additional default options can be added as needed)
336
334
  - **config**: This object contains configuration settings that control the behavior of the `ModelMix` instance. These settings can also be overridden for specific model instances. Examples of configuration settings include:
337
335
  - `system`: Sets the default system message for the model, e.g., "You are an assistant."
package/index.js CHANGED
@@ -21,7 +21,6 @@ class ModelMix {
21
21
  this.options = {
22
22
  max_tokens: 5000,
23
23
  temperature: 1, // 1 --> More creative, 0 --> More deterministic.
24
- top_p: 1, // 100% --> The model considers all possible tokens.
25
24
  ...options
26
25
  };
27
26
 
@@ -100,21 +99,14 @@ class ModelMix {
100
99
  }
101
100
  gpt5nano({ options = {}, config = {} } = {}) {
102
101
  return this.attach('gpt-5-nano', new MixOpenAI({ options, config }));
103
- }
102
+ }
104
103
  gptOss({ options = {}, config = {}, mix = { together: false, cerebras: false, groq: true } } = {}) {
105
104
  if (mix.together) return this.attach('openai/gpt-oss-120b', new MixTogether({ options, config }));
106
105
  if (mix.cerebras) return this.attach('gpt-oss-120b', new MixCerebras({ options, config }));
107
106
  if (mix.groq) return this.attach('openai/gpt-oss-120b', new MixGroq({ options, config }));
108
107
  return this;
109
108
  }
110
-
111
- opus4think({ options = {}, config = {} } = {}) {
112
- options = { ...MixAnthropic.thinkingOptions, ...options };
113
- return this.attach('claude-opus-4-20250514', new MixAnthropic({ options, config }));
114
- }
115
- opus4({ options = {}, config = {} } = {}) {
116
- return this.attach('claude-opus-4-20250514', new MixAnthropic({ options, config }));
117
- }
109
+
118
110
  opus41({ options = {}, config = {} } = {}) {
119
111
  return this.attach('claude-opus-4-1-20250805', new MixAnthropic({ options, config }));
120
112
  }
@@ -129,6 +121,13 @@ class ModelMix {
129
121
  options = { ...MixAnthropic.thinkingOptions, ...options };
130
122
  return this.attach('claude-sonnet-4-20250514', new MixAnthropic({ options, config }));
131
123
  }
124
+ sonnet45({ options = {}, config = {} } = {}) {
125
+ return this.attach('claude-sonnet-4-5-20250929', new MixAnthropic({ options, config }));
126
+ }
127
+ sonnet45think({ options = {}, config = {} } = {}) {
128
+ options = { ...MixAnthropic.thinkingOptions, ...options };
129
+ return this.attach('claude-sonnet-4-5-20250929', new MixAnthropic({ options, config }));
130
+ }
132
131
  sonnet37({ options = {}, config = {} } = {}) {
133
132
  return this.attach('claude-3-7-sonnet-20250219', new MixAnthropic({ options, config }));
134
133
  }
@@ -136,9 +135,6 @@ class ModelMix {
136
135
  options = { ...MixAnthropic.thinkingOptions, ...options };
137
136
  return this.attach('claude-3-7-sonnet-20250219', new MixAnthropic({ options, config }));
138
137
  }
139
- sonnet35({ options = {}, config = {} } = {}) {
140
- return this.attach('claude-3-5-sonnet-20241022', new MixAnthropic({ options, config }));
141
- }
142
138
  haiku35({ options = {}, config = {} } = {}) {
143
139
  return this.attach('claude-3-5-haiku-20241022', new MixAnthropic({ options, config }));
144
140
  }
@@ -252,7 +248,7 @@ class ModelMix {
252
248
 
253
249
  addImage(filePath, { role = "user" } = {}) {
254
250
  const absolutePath = path.resolve(filePath);
255
-
251
+
256
252
  if (!fs.existsSync(absolutePath)) {
257
253
  throw new Error(`Image file not found: ${filePath}`);
258
254
  }
@@ -286,11 +282,11 @@ class ModelMix {
286
282
  }
287
283
  } else {
288
284
  source = {
289
- type: "url",
285
+ type: "url",
290
286
  data: url
291
287
  };
292
288
  }
293
-
289
+
294
290
  this.messages.push({
295
291
  role,
296
292
  content: [{
@@ -298,7 +294,7 @@ class ModelMix {
298
294
  source
299
295
  }]
300
296
  });
301
-
297
+
302
298
  return this;
303
299
  }
304
300
 
@@ -473,28 +469,28 @@ class ModelMix {
473
469
  async prepareMessages() {
474
470
  await this.processImages();
475
471
  this.applyTemplate();
476
-
472
+
477
473
  // Smart message slicing to preserve tool call sequences
478
474
  if (this.config.max_history > 0) {
479
475
  let sliceStart = Math.max(0, this.messages.length - this.config.max_history);
480
-
476
+
481
477
  // If we're slicing and there's a tool message at the start,
482
478
  // 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') {
479
+ while (sliceStart > 0 &&
480
+ sliceStart < this.messages.length &&
481
+ this.messages[sliceStart].role === 'tool') {
486
482
  sliceStart--;
487
483
  // Also need to include the assistant message with tool_calls
488
- if (sliceStart > 0 &&
489
- this.messages[sliceStart].role === 'assistant' &&
484
+ if (sliceStart > 0 &&
485
+ this.messages[sliceStart].role === 'assistant' &&
490
486
  this.messages[sliceStart].tool_calls) {
491
487
  break;
492
488
  }
493
489
  }
494
-
490
+
495
491
  this.messages = this.messages.slice(sliceStart);
496
492
  }
497
-
493
+
498
494
  this.messages = this.groupByRoles(this.messages);
499
495
  this.options.messages = this.messages;
500
496
  }
@@ -621,13 +617,13 @@ class ModelMix {
621
617
  for (const toolCall of toolCalls) {
622
618
  // Handle different tool call formats more robustly
623
619
  let toolName, toolArgs, toolId;
624
-
620
+
625
621
  try {
626
622
  if (toolCall.function) {
627
623
  // Formato OpenAI/normalizado
628
624
  toolName = toolCall.function.name;
629
- toolArgs = typeof toolCall.function.arguments === 'string'
630
- ? JSON.parse(toolCall.function.arguments)
625
+ toolArgs = typeof toolCall.function.arguments === 'string'
626
+ ? JSON.parse(toolCall.function.arguments)
631
627
  : toolCall.function.arguments;
632
628
  toolId = toolCall.id;
633
629
  } else if (toolCall.name) {
@@ -735,7 +731,7 @@ class ModelMix {
735
731
  }
736
732
 
737
733
  this.mcpToolsManager.registerTool(toolDefinition, callback);
738
-
734
+
739
735
  // Agregar la herramienta al sistema de tools para que sea incluida en las requests
740
736
  if (!this.tools.local) {
741
737
  this.tools.local = [];
@@ -745,7 +741,7 @@ class ModelMix {
745
741
  description: toolDefinition.description,
746
742
  inputSchema: toolDefinition.inputSchema
747
743
  });
748
-
744
+
749
745
  return this;
750
746
  }
751
747
 
@@ -758,19 +754,19 @@ class ModelMix {
758
754
 
759
755
  removeTool(toolName) {
760
756
  this.mcpToolsManager.removeTool(toolName);
761
-
757
+
762
758
  // Also remove from the tools system
763
759
  if (this.tools.local) {
764
760
  this.tools.local = this.tools.local.filter(tool => tool.name !== toolName);
765
761
  }
766
-
762
+
767
763
  return this;
768
764
  }
769
765
 
770
766
  listTools() {
771
767
  const localTools = this.mcpToolsManager.getToolsForMCP();
772
768
  const mcpTools = Object.values(this.tools).flat();
773
-
769
+
774
770
  return {
775
771
  local: localTools,
776
772
  mcp: mcpTools.filter(tool => !localTools.find(local => local.name === tool.name))
@@ -975,7 +971,7 @@ class MixOpenAI extends MixCustom {
975
971
  delete options.max_tokens;
976
972
  delete options.temperature;
977
973
  }
978
-
974
+
979
975
  // Use max_completion_tokens and remove temperature for GPT-5 models
980
976
  if (options.model?.includes('gpt-5')) {
981
977
  if (options.max_tokens) {
@@ -1003,10 +999,10 @@ class MixOpenAI extends MixCustom {
1003
999
 
1004
1000
  if (message.role === 'tool') {
1005
1001
  for (const content of message.content) {
1006
- results.push({
1007
- role: 'tool',
1002
+ results.push({
1003
+ role: 'tool',
1008
1004
  tool_call_id: content.tool_call_id,
1009
- content: content.content
1005
+ content: content.content
1010
1006
  })
1011
1007
  }
1012
1008
  continue;
@@ -1029,7 +1025,7 @@ class MixOpenAI extends MixCustom {
1029
1025
 
1030
1026
  results.push(message);
1031
1027
  }
1032
-
1028
+
1033
1029
  return results;
1034
1030
  }
1035
1031
 
@@ -1080,21 +1076,10 @@ class MixAnthropic extends MixCustom {
1080
1076
 
1081
1077
  async create({ config = {}, options = {} } = {}) {
1082
1078
 
1083
- // Remove top_p for thinking
1084
- if (options.thinking) {
1085
- delete options.top_p;
1086
- }
1087
-
1088
- if (options.model && options.model.includes('claude-opus-4-1')) {
1089
- if (options.temperature !== undefined && options.top_p !== undefined) {
1090
- delete options.top_p;
1091
- }
1092
- }
1093
-
1094
1079
  delete options.response_format;
1095
1080
 
1096
1081
  options.system = config.system;
1097
-
1082
+
1098
1083
  try {
1099
1084
  return await super.create({ config, options });
1100
1085
  } catch (error) {
@@ -1130,7 +1115,7 @@ class MixAnthropic extends MixCustom {
1130
1115
  }
1131
1116
  filteredMessages.push(messages[i]);
1132
1117
  }
1133
-
1118
+
1134
1119
  return filteredMessages.map(message => {
1135
1120
  if (message.role === 'tool') {
1136
1121
  return {
@@ -1575,10 +1560,13 @@ class MixGoogle extends MixCustom {
1575
1560
  options.messages = MixGoogle.convertMessages(options.messages);
1576
1561
 
1577
1562
  const generationConfig = {
1578
- topP: options.top_p,
1579
1563
  maxOutputTokens: options.max_tokens,
1580
1564
  }
1581
1565
 
1566
+ if (options.top_p) {
1567
+ generationConfig.topP = options.top_p;
1568
+ }
1569
+
1582
1570
  generationConfig.responseMimeType = "text/plain";
1583
1571
 
1584
1572
  const payload = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modelmix",
3
- "version": "3.8.6",
3
+ "version": "3.8.8",
4
4
  "description": "🧬 ModelMix - Unified API for Diverse AI LLM.",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -6,6 +6,11 @@ const Bottleneck = require('bottleneck');
6
6
 
7
7
  describe('Rate Limiting with Bottleneck Tests', () => {
8
8
 
9
+ // Setup test hooks
10
+ if (global.setupTestHooks) {
11
+ global.setupTestHooks();
12
+ }
13
+
9
14
  afterEach(() => {
10
15
  nock.cleanAll();
11
16
  sinon.restore();
@@ -57,6 +62,13 @@ describe('Rate Limiting with Bottleneck Tests', () => {
57
62
  });
58
63
  });
59
64
 
65
+ afterEach(async () => {
66
+ // Clean up bottleneck state
67
+ if (model && model.limiter) {
68
+ await model.limiter.stop({ dropWaitingJobs: true });
69
+ }
70
+ });
71
+
60
72
  it('should enforce minimum time between requests', async () => {
61
73
  const startTimes = [];
62
74
 
@@ -162,6 +174,13 @@ describe('Rate Limiting with Bottleneck Tests', () => {
162
174
  });
163
175
  });
164
176
 
177
+ afterEach(async () => {
178
+ // Clean up bottleneck state
179
+ if (model && model.limiter) {
180
+ await model.limiter.stop({ dropWaitingJobs: true });
181
+ }
182
+ });
183
+
165
184
  it('should apply rate limiting to OpenAI requests', async () => {
166
185
  const requestTimes = [];
167
186
 
@@ -240,6 +259,13 @@ describe('Rate Limiting with Bottleneck Tests', () => {
240
259
  });
241
260
  });
242
261
 
262
+ afterEach(async () => {
263
+ // Clean up bottleneck state
264
+ if (model && model.limiter) {
265
+ await model.limiter.stop({ dropWaitingJobs: true });
266
+ }
267
+ });
268
+
243
269
  it('should handle rate limiting with API errors', async () => {
244
270
  model.gpt4o();
245
271
 
@@ -4,6 +4,11 @@ const nock = require('nock');
4
4
  const { ModelMix } = require('../index.js');
5
5
 
6
6
  describe('Provider Fallback Chain Tests', () => {
7
+
8
+ // Setup test hooks
9
+ if (global.setupTestHooks) {
10
+ global.setupTestHooks();
11
+ }
7
12
 
8
13
  afterEach(() => {
9
14
  nock.cleanAll();
package/test/live.mcp.js CHANGED
@@ -528,7 +528,7 @@ describe('Live MCP Integration Tests', function () {
528
528
  it('should work with same MCP tools across different Anthropic models', async function () {
529
529
  const models = [
530
530
  { name: 'Sonnet 4', model: ModelMix.new(setup).sonnet4() },
531
- { name: 'Sonnet 3.7', model: ModelMix.new(setup).sonnet37() },
531
+ { name: 'Sonnet 4.5', model: ModelMix.new(setup).sonnet45() },
532
532
  { name: 'Haiku 3.5', model: ModelMix.new(setup).haiku35() }
533
533
  ];
534
534
 
package/test/live.test.js CHANGED
@@ -31,7 +31,7 @@ describe('Live Integration Tests', function () {
31
31
  });
32
32
 
33
33
  it('should process images with Anthropic Claude', async function () {
34
- const model = ModelMix.new(setup).sonnet35();
34
+ const model = ModelMix.new(setup).sonnet45();
35
35
 
36
36
  model.addImageFromUrl(blueSquareBase64)
37
37
  .addText('What color is this image? Answer in one word only.');
@@ -82,8 +82,8 @@ describe('Live Integration Tests', function () {
82
82
  expect(result.skills).to.be.an('array');
83
83
  });
84
84
 
85
- it('should return structured JSON with Sonnet Think', async function () {
86
- const model = ModelMix.new(setup).sonnet4think();
85
+ it('should return structured JSON with Sonnet 4.5 thinking', async function () {
86
+ const model = ModelMix.new(setup).sonnet45think();
87
87
 
88
88
  model.addText('Generate information about a fictional city.');
89
89
 
package/test/setup.js CHANGED
@@ -39,8 +39,24 @@ global.TEST_CONFIG = {
39
39
 
40
40
  // Global cleanup function
41
41
  global.cleanup = function() {
42
+ // More thorough nock cleanup
42
43
  nock.cleanAll();
44
+ nock.restore();
45
+ nock.activate();
43
46
  sinon.restore();
47
+
48
+ // Clear any pending timers and intervals
49
+ const highestTimeoutId = setTimeout(() => {}, 0);
50
+ clearTimeout(highestTimeoutId);
51
+ for (let i = 0; i < highestTimeoutId; i++) {
52
+ clearTimeout(i);
53
+ clearInterval(i);
54
+ }
55
+
56
+ // Force garbage collection if available
57
+ if (global.gc) {
58
+ global.gc();
59
+ }
44
60
  };
45
61
 
46
62
  // Console override for testing (suppress debug logs unless needed)
@@ -151,6 +167,10 @@ global.testUtils = {
151
167
  // Export hooks for tests to use
152
168
  global.setupTestHooks = function() {
153
169
  beforeEach(() => {
170
+ // Ensure clean state before each test
171
+ nock.cleanAll();
172
+ sinon.restore();
173
+
154
174
  // Disable real HTTP requests
155
175
  if (global.TEST_CONFIG.MOCK_APIS) {
156
176
  nock.disableNetConnect();
@@ -165,6 +185,11 @@ global.setupTestHooks = function() {
165
185
  if (global.TEST_CONFIG.MOCK_APIS) {
166
186
  nock.enableNetConnect();
167
187
  }
188
+
189
+ // Force garbage collection if available
190
+ if (global.gc) {
191
+ global.gc();
192
+ }
168
193
  });
169
194
  };
170
195
 
@@ -6,6 +6,11 @@ const path = require('path');
6
6
  const { ModelMix } = require('../index.js');
7
7
 
8
8
  describe('Template and File Operations Tests', () => {
9
+
10
+ // Setup test hooks
11
+ if (global.setupTestHooks) {
12
+ global.setupTestHooks();
13
+ }
9
14
 
10
15
  afterEach(() => {
11
16
  nock.cleanAll();