modelmix 3.2.2 → 3.3.2

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
@@ -6,159 +6,11 @@ const Bottleneck = require('bottleneck');
6
6
  const path = require('path');
7
7
  const generateJsonSchema = require('./schema');
8
8
 
9
- class ModelMixBuilder {
10
- constructor(args = {}) {
11
- this.models = []; // Array of { key: string, providerClass: class, options: {}, config: {} }
12
- this.mix = new ModelMix(args);
13
- this.handler = null;
14
- this._messageHandlerMethods = [ // Methods to delegate after handler creation
15
- 'new', 'addText', 'addTextFromFile', 'setSystem', 'setSystemFromFile',
16
- 'addImage', 'addImageFromUrl', 'message', 'json', 'block', 'raw',
17
- 'stream', 'replace', 'replaceKeyFromFile'
18
- ];
19
- }
20
-
21
- addModel(key, providerClass, { options = {}, config = {} } = {}) {
22
- if (this.handler) {
23
- throw new Error("Cannot add models after message generation has started.");
24
- }
25
-
26
- // Attach provider if not already attached
27
- const providerInstance = new providerClass();
28
- const mainPrefix = providerInstance.config.prefix[0];
29
- if (!Object.values(this.mix.models).some(p => p.config.prefix.includes(mainPrefix))) {
30
- this.mix.attach(providerInstance);
31
- }
32
-
33
- if (!key) {
34
- throw new Error(`Model key is required when adding a model via ${providerClass.name}.`);
35
- }
36
- this.models.push({ key, providerClass, options, config });
37
- return this;
38
- }
39
-
40
- _getHandler() {
41
- if (!this.handler) {
42
- if (!this.mix || this.models.length === 0) {
43
- throw new Error("No models specified. Use methods like .gpt(), .sonnet() first.");
44
- }
45
-
46
- // Pass all model definitions. The create method will handle it appropriately
47
- this.handler = this.mix.createByDef(this.models);
48
-
49
- // Delegate chainable methods to the handler
50
- this._messageHandlerMethods.forEach(methodName => {
51
- if (typeof this.handler[methodName] === 'function') {
52
- this[methodName] = (...args) => {
53
- const result = this.handler[methodName](...args);
54
- // Return the handler instance for chainable methods, otherwise the result
55
- return result === this.handler ? this : result;
56
- };
57
- }
58
- });
59
- // Special handling for async methods that return results
60
- ['message', 'json', 'block', 'raw', 'stream'].forEach(asyncMethodName => {
61
- if (typeof this.handler[asyncMethodName] === 'function') {
62
- this[asyncMethodName] = async (...args) => {
63
- return await this.handler[asyncMethodName](...args);
64
- };
65
- }
66
- });
67
- }
68
- return this.handler;
69
- }
70
-
71
- // --- Instance methods for adding models (primary or fallback) ---
72
- // These will be mirrored by static methods on ModelMix
73
- gpt41({ model = 'gpt-4.1', options = {}, config = {} } = {}) {
74
- return this.addModel(model, MixOpenAI, { options, config });
75
- }
76
- gpt41mini({ model = 'gpt-4.1-mini', options = {}, config = {} } = {}) {
77
- return this.addModel(model, MixOpenAI, { options, config });
78
- }
79
- gpt41nano({ model = 'gpt-4.1-nano', options = {}, config = {} } = {}) {
80
- return this.addModel(model, MixOpenAI, { options, config });
81
- }
82
- gpt4o({ model = 'gpt-4o', options = {}, config = {} } = {}) {
83
- return this.addModel(model, MixOpenAI, { options, config });
84
- }
85
- o4mini({ model = 'o4-mini', options = {}, config = {} } = {}) {
86
- return this.addModel(model, MixOpenAI, { options, config });
87
- }
88
- o3({ model = 'o3', options = {}, config = {} } = {}) {
89
- return this.addModel(model, MixOpenAI, { options, config });
90
- }
91
- sonnet37({ model = 'claude-3-7-sonnet-20250219', options = {}, config = {} } = {}) {
92
- return this.addModel(model, MixAnthropic, { options, config });
93
- }
94
- sonnet37think({ model = 'claude-3-7-sonnet-20250219', options = {
95
- thinking: {
96
- "type": "enabled",
97
- "budget_tokens": 1024
98
- },
99
- temperature: 1
100
- }, config = {} } = {}) {
101
- return this.addModel(model, MixAnthropic, { options, config });
102
- }
103
- sonnet35({ model = 'claude-3-5-sonnet-20241022', options = {}, config = {} } = {}) {
104
- return this.addModel(model, MixAnthropic, { options, config });
105
- }
106
- haiku35({ model = 'claude-3-5-haiku-20241022', options = {}, config = {} } = {}) {
107
- return this.addModel(model, MixAnthropic, { options, config });
108
- }
109
- gemini25flash({ model = 'gemini-2.5-flash-preview-04-17', options = {}, config = {} } = {}) {
110
- return this.addModel(model, MixGoogle, { options, config });
111
- }
112
- gemini25proExp({ model = 'gemini-2.5-pro-exp-03-25', options = {}, config = {} } = {}) {
113
- return this.addModel(model, MixGoogle, { options, config });
114
- }
115
- gemini25pro({ model = 'gemini-2.5-pro-preview-05-06', options = {}, config = {} } = {}) {
116
- return this.addModel(model, MixGoogle, { options, config });
117
- }
118
- sonar({ model = 'sonar-pro', options = {}, config = {} } = {}) {
119
- return this.addModel(model, MixPerplexity, { options, config });
120
- }
121
- qwen3({ model = 'Qwen/Qwen3-235B-A22B-fp8-tput', options = {}, config = {} } = {}) {
122
- return this.addModel(model, MixTogether, { options, config });
123
- }
124
- grok2({ model = 'grok-2-latest', options = {}, config = {} } = {}) {
125
- return this.addModel(model, MixGrok, { options, config });
126
- }
127
- grok3({ model = 'grok-3-beta', options = {}, config = {} } = {}) {
128
- return this.addModel(model, MixGrok, { options, config });
129
- }
130
- grok3mini({ model = 'grok-3-mini-beta', options = {}, config = {} } = {}) {
131
- return this.addModel(model, MixGrok, { options, config });
132
- }
133
- scout({ model = 'llama-4-scout-17b-16e-instruct', options = {}, config = {} } = {}) {
134
- return this.addModel(model, MixCerebras, { options, config });
135
- }
136
-
137
- // --- Methods delegated to MessageHandler after creation ---
138
- // Define stubs that will call _getHandler first
139
-
140
- new() { this._getHandler(); return this.new(...arguments); }
141
- addText() { this._getHandler(); return this.addText(...arguments); }
142
- addTextFromFile() { this._getHandler(); return this.addTextFromFile(...arguments); }
143
- setSystem() { this._getHandler(); return this.setSystem(...arguments); }
144
- setSystemFromFile() { this._getHandler(); return this.setSystemFromFile(...arguments); }
145
- addImage() { this._getHandler(); return this.addImage(...arguments); }
146
- addImageFromUrl() { this._getHandler(); return this.addImageFromUrl(...arguments); }
147
- replace() { this._getHandler(); return this.replace(...arguments); }
148
- replaceKeyFromFile() { this._getHandler(); return this.replaceKeyFromFile(...arguments); }
149
-
150
- // Async methods need await
151
- async message() { this._getHandler(); return await this.message(...arguments); }
152
- async json() { this._getHandler(); return await this.json(...arguments); }
153
- async block() { this._getHandler(); return await this.block(...arguments); }
154
- async raw() { this._getHandler(); return await this.raw(...arguments); }
155
- async stream() { this._getHandler(); return await this.stream(...arguments); }
156
- }
157
-
158
9
  class ModelMix {
159
10
  constructor({ options = {}, config = {} } = {}) {
160
- this.models = {};
161
- this.defaultOptions = {
11
+ this.models = [];
12
+ this.messages = [];
13
+ this.options = {
162
14
  max_tokens: 5000,
163
15
  temperature: 1, // 1 --> More creative, 0 --> More deterministic.
164
16
  top_p: 1, // 100% --> The model considers all possible tokens.
@@ -181,6 +33,7 @@ class ModelMix {
181
33
  }
182
34
 
183
35
  this.limiter = new Bottleneck(this.config.bottleneck);
36
+
184
37
  }
185
38
 
186
39
  replace(keyValues) {
@@ -188,165 +41,131 @@ class ModelMix {
188
41
  return this;
189
42
  }
190
43
 
191
- attach(...modelInstances) {
192
- for (const modelInstance of modelInstances) {
193
- const key = modelInstance.config.prefix.join("_");
194
- this.models[key] = modelInstance;
195
- }
196
- return this;
44
+ static new({ options = {}, config = {} } = {}) {
45
+ return new ModelMix({ options, config });
197
46
  }
198
47
 
199
- static create(args = {}) {
200
- return new ModelMixBuilder(args);
48
+ new() {
49
+ return new ModelMix({ options: this.options, config: this.config });
201
50
  }
202
51
 
203
- createByDef(modelDefinitions, { config: explicitOverallConfig = {}, options: explicitOverallOptions = {} } = {}) {
52
+ attach(key, provider) {
204
53
 
205
- // modelDefinitions is expected to be the array from ModelMixBuilder.models
206
- // e.g., [{ key, providerClass, options, config }, ...]
207
- const allModelsInfo = modelDefinitions;
208
- const modelKeys = allModelsInfo.map(m => m.key);
209
-
210
- if (modelKeys.length === 0) {
211
- throw new Error('No model keys provided in modelDefinitions.');
54
+ if (this.models.some(model => model.key === key)) {
55
+ return this;
212
56
  }
213
57
 
214
- // Verificar que todos los modelos estén disponibles
215
- const unavailableModels = modelKeys.filter(modelKey => {
216
- return !Object.values(this.models).some(entry =>
217
- entry.config.prefix.some(p => modelKey.startsWith(p))
218
- );
219
- });
220
-
221
- if (unavailableModels.length > 0) {
222
- throw new Error(`The following models are not available: ${unavailableModels.join(', ')}`);
223
- }
224
-
225
- // Una vez verificado que todos están disponibles, obtener el primer modelo (primary)
226
- const primaryModelInfo = allModelsInfo[0];
227
- const primaryModelKey = primaryModelInfo.key;
228
- const primaryModelEntry = Object.values(this.models).find(entry =>
229
- entry.config.prefix.some(p => primaryModelKey.startsWith(p))
230
- );
231
-
232
- if (!primaryModelEntry) { // Should be caught by unavailableModels, but good for robustness
233
- throw new Error(`Primary model provider for key ${primaryModelKey} not found or attached.`);
58
+ if (this.messages.length > 0) {
59
+ throw new Error("Cannot add models after message generation has started.");
234
60
  }
235
61
 
236
- // Options/config for the MessageHandler instance (session-level)
237
- // These are based on the primary model's specification.
238
- const optionsHandler = {
239
- ...this.defaultOptions, // ModelMix global defaults
240
- ...(primaryModelEntry.options || {}), // Primary provider class defaults
241
- ...(primaryModelInfo.options || {}), // Options from addModel for primary
242
- ...explicitOverallOptions, // Explicit options to .create() if any
243
- model: primaryModelKey // Ensure primary model key is set
244
- };
245
-
246
- const configHandler = {
247
- ...this.config, // ModelMix global config
248
- ...(primaryModelEntry.config || {}), // Primary provider class config
249
- ...(primaryModelInfo.config || {}), // Config from addModel for primary
250
- ...explicitOverallConfig // Explicit config to .create()
251
- };
252
-
253
- // Pass the entire allModelsInfo array for fallback/iteration
254
- return new MessageHandler(this, primaryModelEntry, optionsHandler, configHandler, allModelsInfo);
62
+ this.models.push({ key, provider });
63
+ return this;
255
64
  }
256
65
 
257
- create(modelKeys = [], { config = {}, options = {} } = {}) {
258
-
259
- // Backward compatibility for string model keys
260
- if (!modelKeys || (Array.isArray(modelKeys) && modelKeys.length === 0)) {
261
- return new ModelMixBuilder({ config: { ...this.config, ...config }, options: { ...this.defaultOptions, ...options } });
262
- }
263
-
264
- // If modelKeys is a string, convert it to an array for backward compatibility
265
- const modelArray = Array.isArray(modelKeys) ? modelKeys : [modelKeys];
266
-
267
- if (modelArray.length === 0) {
268
- throw new Error('No model keys provided');
269
- }
270
-
271
- // Create model definitions based on string keys
272
- const modelDefinitions = modelArray.map(key => {
273
- // Find the provider for this model key
274
- const providerEntry = Object.values(this.models).find(entry =>
275
- entry.config.prefix.some(p => key.startsWith(p))
276
- );
277
-
278
- if (!providerEntry) {
279
- throw new Error(`Model provider not found for key: ${key}`);
280
- }
281
-
282
- // Return a synthesized model definition with just the key and options/config from the create call
283
- return {
284
- key,
285
- providerClass: null, // Not needed for our purpose
286
- options, // Use the options from create call for all models
287
- config // Use the config from create call for all models
288
- };
289
- });
290
-
291
- // Pass to the new implementation
292
- return this.createByDef(modelDefinitions, { config, options });
66
+ // --- Model addition methods ---
67
+ gpt41({ options = {}, config = {} } = {}) {
68
+ return this.attach('gpt-4.1', new MixOpenAI({ options, config }));
293
69
  }
294
-
295
- setSystem(text) {
296
- this.config.system = text;
297
- return this;
70
+ gpt41mini({ options = {}, config = {} } = {}) {
71
+ return this.attach('gpt-4.1-mini', new MixOpenAI({ options, config }));
72
+ }
73
+ gpt41nano({ options = {}, config = {} } = {}) {
74
+ return this.attach('gpt-4.1-nano', new MixOpenAI({ options, config }));
75
+ }
76
+ gpt4o({ options = {}, config = {} } = {}) {
77
+ return this.attach('gpt-4o', new MixOpenAI({ options, config }));
78
+ }
79
+ o4mini({ options = {}, config = {} } = {}) {
80
+ return this.attach('o4-mini', new MixOpenAI({ options, config }));
81
+ }
82
+ o3({ options = {}, config = {} } = {}) {
83
+ return this.attach('o3', new MixOpenAI({ options, config }));
84
+ }
85
+ gpt45({ options = {}, config = {} } = {}) {
86
+ return this.attach('gpt-4.5-preview', new MixOpenAI({ options, config }));
87
+ }
88
+ sonnet37({ options = {}, config = {} } = {}) {
89
+ return this.attach('claude-3-7-sonnet-20250219', new MixAnthropic({ options, config }));
90
+ }
91
+ sonnet37think({ options = {
92
+ thinking: {
93
+ "type": "enabled",
94
+ "budget_tokens": 1024
95
+ },
96
+ temperature: 1
97
+ }, config = {} } = {}) {
98
+ return this.attach('claude-3-7-sonnet-20250219', new MixAnthropic({ options, config }));
99
+ }
100
+ sonnet35({ options = {}, config = {} } = {}) {
101
+ return this.attach('claude-3-5-sonnet-20241022', new MixAnthropic({ options, config }));
102
+ }
103
+ haiku35({ options = {}, config = {} } = {}) {
104
+ return this.attach('claude-3-5-haiku-20241022', new MixAnthropic({ options, config }));
105
+ }
106
+ gemini25flash({ options = {}, config = {} } = {}) {
107
+ return this.attach('gemini-2.5-flash-preview-04-17', new MixGoogle({ options, config }));
108
+ }
109
+ gemini25proExp({ options = {}, config = {} } = {}) {
110
+ return this.attach('gemini-2.5-pro-exp-03-25', new MixGoogle({ options, config }));
111
+ }
112
+ gemini25pro({ options = {}, config = {} } = {}) {
113
+ return this.attach('gemini-2.5-pro-preview-05-06', new MixGoogle({ options, config }));
114
+ }
115
+ sonarPro({ options = {}, config = {} } = {}) {
116
+ return this.attach('sonar-pro', new MixPerplexity({ options, config }));
117
+ }
118
+ sonar({ options = {}, config = {} } = {}) {
119
+ return this.attach('sonar', new MixPerplexity({ options, config }));
298
120
  }
299
121
 
300
- setSystemFromFile(filePath) {
301
- const content = this.readFile(filePath);
302
- this.setSystem(content);
303
- return this;
122
+ grok2({ options = {}, config = {} } = {}) {
123
+ return this.attach('grok-2-latest', new MixGrok({ options, config }));
124
+ }
125
+ grok3({ options = {}, config = {} } = {}) {
126
+ return this.attach('grok-3-beta', new MixGrok({ options, config }));
127
+ }
128
+ grok3mini({ options = {}, config = {} } = {}) {
129
+ return this.attach('grok-3-mini-beta', new MixGrok({ options, config }));
304
130
  }
305
131
 
306
- readFile(filePath, { encoding = 'utf8' } = {}) {
307
- try {
308
- const absolutePath = path.resolve(filePath);
309
- return fs.readFileSync(absolutePath, { encoding });
310
- } catch (error) {
311
- if (error.code === 'ENOENT') {
312
- throw new Error(`File not found: ${filePath}`);
313
- } else if (error.code === 'EACCES') {
314
- throw new Error(`Permission denied: ${filePath}`);
315
- } else {
316
- throw new Error(`Error reading file ${filePath}: ${error.message}`);
317
- }
318
- }
132
+ qwen3({ options = {}, config = {}, mix = { groq: true, together: false } } = {}) {
133
+ if (mix.groq) this.attach('qwen-qwq-32b', new MixGroq({ options, config }));
134
+ if (mix.together) this.attach('Qwen/Qwen3-235B-A22B-fp8-tput', new MixTogether({ options, config }));
135
+ return this;
319
136
  }
320
- }
321
137
 
322
- class MessageHandler {
323
- constructor(mix, modelEntry, options, config, allModelsInfo = []) {
324
- this.mix = mix;
325
- this.modelEntry = modelEntry; // Primary model's provider instance
326
- this.options = options; // Session-level options, based on primary
327
- this.config = config; // Session-level config, based on primary
328
- this.messages = [];
329
- this.allModelsInfo = allModelsInfo; // Store the full info array [{ key, providerClass, options, config }, ...]
330
- this.imagesToProcess = [];
138
+ scout({ options = {}, config = {}, mix = { groq: true, together: false, cerebras: false } } = {}) {
139
+ if (mix.groq) this.attach('meta-llama/llama-4-scout-17b-16e-instruct', new MixGroq({ options, config }));
140
+ if (mix.together) this.attach('meta-llama/Llama-4-Scout-17B-16E-Instruct', new MixTogether({ options, config }));
141
+ if (mix.cerebras) this.attach('llama-4-scout-17b-16e-instruct', new MixCerebras({ options, config }));
142
+ return this;
143
+ }
144
+ maverick({ options = {}, config = {}, mix = { groq: true, together: false } } = {}) {
145
+ if (mix.groq) this.attach('meta-llama/llama-4-maverick-17b-128e-instruct', new MixGroq({ options, config }));
146
+ if (mix.together) this.attach('meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8', new MixTogether({ options, config }));
147
+ return this;
331
148
  }
332
149
 
333
- new() {
334
- this.messages = [];
150
+ deepseekR1({ options = {}, config = {}, mix = { groq: true, together: false, cerebras: false } } = {}) {
151
+ if (mix.groq) this.attach('deepseek-r1-distill-llama-70b', new MixGroq({ options, config }));
152
+ if (mix.together) this.attach('deepseek-ai/DeepSeek-R1', new MixTogether({ options, config }));
153
+ if (mix.cerebras) this.attach('deepseek-r1-distill-llama-70b', new MixCerebras({ options, config }));
335
154
  return this;
336
155
  }
337
156
 
338
- addText(text, config = { role: "user" }) {
157
+ addText(text, { role = "user" } = {}) {
339
158
  const content = [{
340
159
  type: "text",
341
160
  text
342
161
  }];
343
162
 
344
- this.messages.push({ ...config, content });
163
+ this.messages.push({ role, content });
345
164
  return this;
346
165
  }
347
166
 
348
167
  addTextFromFile(filePath, { role = "user" } = {}) {
349
- const content = this.mix.readFile(filePath);
168
+ const content = this.readFile(filePath);
350
169
  this.addText(content, { role });
351
170
  return this;
352
171
  }
@@ -357,13 +176,13 @@ class MessageHandler {
357
176
  }
358
177
 
359
178
  setSystemFromFile(filePath) {
360
- const content = this.mix.readFile(filePath);
179
+ const content = this.readFile(filePath);
361
180
  this.setSystem(content);
362
181
  return this;
363
182
  }
364
183
 
365
184
  addImage(filePath, { role = "user" } = {}) {
366
- const imageBuffer = this.mix.readFile(filePath, { encoding: null });
185
+ const imageBuffer = this.readFile(filePath, { encoding: null });
367
186
  const mimeType = mime.lookup(filePath);
368
187
 
369
188
  if (!mimeType || !mimeType.startsWith('image/')) {
@@ -387,16 +206,20 @@ class MessageHandler {
387
206
  };
388
207
 
389
208
  this.messages.push(imageMessage);
390
-
391
209
  return this;
392
210
  }
393
211
 
394
212
  addImageFromUrl(url, config = { role: "user" }) {
213
+ if (!this.imagesToProcess) {
214
+ this.imagesToProcess = [];
215
+ }
395
216
  this.imagesToProcess.push({ url, config });
396
217
  return this;
397
218
  }
398
219
 
399
220
  async processImageUrls() {
221
+ if (!this.imagesToProcess) return;
222
+
400
223
  const imageContents = await Promise.all(
401
224
  this.imagesToProcess.map(async (image) => {
402
225
  try {
@@ -405,7 +228,7 @@ class MessageHandler {
405
228
  const mimeType = response.headers['content-type'];
406
229
  return { base64, mimeType, config: image.config };
407
230
  } catch (error) {
408
- console.error(`Error descargando imagen desde ${image.url}:`, error);
231
+ console.error(`Error downloading image from ${image.url}:`, error);
409
232
  return null;
410
233
  }
411
234
  })
@@ -434,22 +257,18 @@ class MessageHandler {
434
257
  async message() {
435
258
  this.options.stream = false;
436
259
  let raw = await this.execute();
437
- if (!raw.message && raw.response?.content?.[1]?.text) {
438
- return raw.response.content[1].text;
439
- }
440
-
441
260
  return raw.message;
442
261
  }
443
262
 
444
263
  async json(schemaExample = null, schemaDescription = {}, { type = 'json_object', addExample = false, addSchema = true } = {}) {
445
264
  this.options.response_format = { type };
265
+
446
266
  if (schemaExample) {
267
+ this.config.schema = generateJsonSchema(schemaExample, schemaDescription);
447
268
 
448
269
  if (addSchema) {
449
- const schema = generateJsonSchema(schemaExample, schemaDescription);
450
- this.config.systemExtra = "\nOutput JSON Schema: \n```\n" + JSON.stringify(schema) + "\n```";
270
+ this.config.systemExtra = "\nOutput JSON Schema: \n```\n" + JSON.stringify(this.config.schema) + "\n```";
451
271
  }
452
-
453
272
  if (addExample) {
454
273
  this.config.systemExtra += "\nOutput JSON Example: \n```\n" + JSON.stringify(schemaExample) + "\n```";
455
274
  }
@@ -480,22 +299,18 @@ class MessageHandler {
480
299
 
481
300
  async stream(callback) {
482
301
  this.options.stream = true;
483
- this.modelEntry.streamCallback = callback;
302
+ this.streamCallback = callback;
484
303
  return this.execute();
485
304
  }
486
305
 
487
- replace(keyValues) {
488
- this.config.replace = { ...this.config.replace, ...keyValues };
489
- return this;
490
- }
491
-
492
306
  replaceKeyFromFile(key, filePath) {
493
- const content = this.mix.readFile(filePath);
307
+ const content = this.readFile(filePath);
494
308
  this.replace({ [key]: this.template(content, this.config.replace) });
495
309
  return this;
496
310
  }
497
311
 
498
312
  template(input, replace) {
313
+ if (!replace) return input;
499
314
  for (const k in replace) {
500
315
  input = input.split(/([¿?¡!,"';:\(\)\.\s])/).map(x => x === k ? replace[k] : x).join("");
501
316
  }
@@ -519,13 +334,13 @@ class MessageHandler {
519
334
  applyTemplate() {
520
335
  if (!this.config.replace) return;
521
336
 
522
- this.config.system = this.template(this.config.system, this.config.replace)
337
+ this.config.system = this.template(this.config.system, this.config.replace);
523
338
 
524
339
  this.messages = this.messages.map(message => {
525
340
  if (message.content instanceof Array) {
526
341
  message.content = message.content.map(content => {
527
342
  if (content.type === 'text') {
528
- content.text = this.template(content.text, this.config.replace)
343
+ content.text = this.template(content.text, this.config.replace);
529
344
  }
530
345
  return content;
531
346
  });
@@ -542,140 +357,96 @@ class MessageHandler {
542
357
  this.options.messages = this.messages;
543
358
  }
544
359
 
360
+ readFile(filePath, { encoding = 'utf8' } = {}) {
361
+ try {
362
+ const absolutePath = path.resolve(filePath);
363
+ return fs.readFileSync(absolutePath, { encoding });
364
+ } catch (error) {
365
+ if (error.code === 'ENOENT') {
366
+ throw new Error(`File not found: ${filePath}`);
367
+ } else if (error.code === 'EACCES') {
368
+ throw new Error(`Permission denied: ${filePath}`);
369
+ } else {
370
+ throw new Error(`Error reading file ${filePath}: ${error.message}`);
371
+ }
372
+ }
373
+ }
374
+
545
375
  async execute() {
546
- return this.mix.limiter.schedule(async () => {
547
- await this.prepareMessages(); // Prepare messages once, outside the loop
376
+ if (!this.models || this.models.length === 0) {
377
+ throw new Error("No models specified. Use methods like .gpt(), .sonnet() first.");
378
+ }
379
+
380
+ return this.limiter.schedule(async () => {
381
+ await this.prepareMessages();
548
382
 
549
383
  if (this.messages.length === 0) {
550
384
  throw new Error("No user messages have been added. Use addText(prompt), addTextFromFile(filePath), addImage(filePath), or addImageFromUrl(url) to add a prompt.");
551
385
  }
552
386
 
553
387
  let lastError = null;
554
- const modelIterationList = this.allModelsInfo; // Use the full info for iteration
555
-
556
- // Iterate through the models defined in the handler's list
557
- for (let i = 0; i < modelIterationList.length; i++) {
558
- const currentModelDetail = modelIterationList[i];
559
- const currentModelKey = currentModelDetail.key;
560
- const currentModelBuilderOptions = currentModelDetail.options || {};
561
- const currentModelBuilderConfig = currentModelDetail.config || {};
562
-
563
- // Find the corresponding model provider instance in the ModelMix instance
564
- const currentModelProviderInstance = Object.values(this.mix.models).find(entry =>
565
- entry.config.prefix.some(p => currentModelKey.startsWith(p))
566
- );
567
-
568
- if (!currentModelProviderInstance) {
569
- log.warn(`Model provider not found or attached for key: ${currentModelKey}. Skipping.`);
570
- if (!lastError) {
571
- lastError = new Error(`Model provider not found for key: ${currentModelKey}`);
572
- }
573
- continue; // Try the next model
574
- }
575
-
576
- // Construct effective options and config for THIS attempt
577
- const attemptOptions = {
578
- ...this.mix.defaultOptions, // 1. ModelMix global defaults
579
- ...(currentModelProviderInstance.options || {}), // 2. Provider class defaults for current model
580
- ...this.options, // 3. MessageHandler current general options (from primary + handler changes)
581
- ...currentModelBuilderOptions, // 4. Specific options from addModel for THIS model
582
- model: currentModelKey // 5. Crucial: set current model key
583
- };
584
388
 
585
- const attemptConfig = {
586
- ...this.mix.config, // 1. ModelMix global config
587
- ...(currentModelProviderInstance.config || {}), // 2. Provider class config for current model
588
- ...this.config, // 3. MessageHandler current general config
589
- ...currentModelBuilderConfig // 4. Specific config from addModel for THIS model
590
- };
389
+ for (let i = 0; i < this.models.length; i++) {
591
390
 
592
- // Determine the effective debug flag for this attempt (for logging and API call context)
593
- // Precedence: model-specific builder config -> handler config -> mix config
594
- const effectiveDebugForAttempt = attemptConfig.hasOwnProperty('debug') ? attemptConfig.debug :
595
- this.config.hasOwnProperty('debug') ? this.config.debug :
596
- this.mix.config.debug;
391
+ const currentModel = this.models[i];
392
+ const currentModelKey = currentModel.key;
393
+ const providerInstance = currentModel.provider;
597
394
 
598
- // Update attemptConfig with the finally resolved debug flag for the API call
599
- const apiCallConfig = { ...attemptConfig, debug: effectiveDebugForAttempt };
395
+ let options = {
396
+ ...this.options,
397
+ ...providerInstance.options,
398
+ model: currentModelKey
399
+ };
600
400
 
401
+ const config = {
402
+ ...this.config,
403
+ ...providerInstance.config,
404
+ };
601
405
 
602
- if (effectiveDebugForAttempt) {
406
+ if (config.debug) {
603
407
  const isPrimary = i === 0;
604
- log.debug(`Attempt #${i + 1}: Using model ${currentModelKey}` + (isPrimary ? ' (Primary)' : ' (Fallback)'));
605
- log.debug("Effective attemptOptions for " + currentModelKey + ":");
606
- log.inspect(attemptOptions);
607
- log.debug("Effective apiCallConfig for " + currentModelKey + ":");
608
- log.inspect(apiCallConfig);
408
+ log.debug(`[${currentModelKey}] Attempt #${i + 1}` + (isPrimary ? ' (Primary)' : ' (Fallback)'));
609
409
  }
610
410
 
611
-
612
- // Apply model-specific adjustments to a copy of options for this attempt
613
- let finalAttemptOptions = { ...attemptOptions };
614
- if (currentModelProviderInstance instanceof MixOpenAI && finalAttemptOptions.model?.startsWith('o')) {
615
- delete finalAttemptOptions.max_tokens;
616
- delete finalAttemptOptions.temperature;
617
- }
618
- if (currentModelProviderInstance instanceof MixAnthropic) {
619
- if (finalAttemptOptions.thinking) {
620
- delete finalAttemptOptions.top_p;
621
- // if (finalAttemptOptions.temperature < 1) {
622
- // finalAttemptOptions.temperature = 1;
623
- // }
624
- }
625
- delete finalAttemptOptions.response_format; // Anthropic doesn't use this top-level option
626
- }
627
- // ... add other potential model-specific option adjustments here ...
628
-
629
411
  try {
630
- // Attach the stream callback to the *current* model entry for this attempt
631
- // this.modelEntry is the primary model's provider instance where streamCallback was stored by MessageHandler.stream()
632
- if (finalAttemptOptions.stream && this.modelEntry && this.modelEntry.streamCallback) {
633
- currentModelProviderInstance.streamCallback = this.modelEntry.streamCallback;
412
+ if (options.stream && this.streamCallback) {
413
+ providerInstance.streamCallback = this.streamCallback;
634
414
  }
635
415
 
636
- // Pass the adjusted options/config for this specific attempt
637
- const result = await currentModelProviderInstance.create({ options: finalAttemptOptions, config: apiCallConfig });
638
-
639
- // Add successful response to history *before* returning
640
- let messageContentToAdd = result.message;
641
- if (currentModelProviderInstance instanceof MixAnthropic && result.response?.content?.[0]?.text) {
642
- messageContentToAdd = result.response.content[0].text;
643
- } else if (currentModelProviderInstance instanceof MixOllama && result.response?.message?.content) {
644
- messageContentToAdd = result.response.message.content;
645
- } // Add more cases if other providers have different structures
416
+ const result = await providerInstance.create({ options, config });
646
417
 
647
- this.messages.push({ role: "assistant", content: messageContentToAdd });
418
+ this.messages.push({ role: "assistant", content: result.message });
648
419
 
649
- if (effectiveDebugForAttempt) {
420
+ if (config.debug) {
650
421
  log.debug(`Request successful with model: ${currentModelKey}`);
651
422
  log.inspect(result.response);
652
423
  }
653
- return result; // Success!
424
+
425
+ return result;
426
+
654
427
  } catch (error) {
655
- lastError = error; // Store the most recent error
656
- log.warn(`Model ${currentModelKey} failed (Attempt #${i + 1}/${modelIterationList.length}).`);
428
+ lastError = error;
429
+ log.warn(`Model ${currentModelKey} failed (Attempt #${i + 1}/${this.models.length}).`);
657
430
  if (error.message) log.warn(`Error: ${error.message}`);
658
431
  if (error.statusCode) log.warn(`Status Code: ${error.statusCode}`);
659
432
  if (error.details) log.warn(`Details: ${JSON.stringify(error.details)}`);
660
433
 
661
- // Check if this is the last model in the list
662
- if (i === modelIterationList.length - 1) {
663
- log.error(`All ${modelIterationList.length} model(s) failed. Throwing last error from ${currentModelKey}.`);
664
- throw lastError; // Re-throw the last encountered error
434
+ if (i === this.models.length - 1) {
435
+ log.error(`All ${this.models.length} model(s) failed. Throwing last error from ${currentModelKey}.`);
436
+ throw lastError;
665
437
  } else {
666
- const nextModelKey = modelIterationList[i + 1].key;
438
+ const nextModelKey = this.models[i + 1].key;
667
439
  log.info(`-> Proceeding to next model: ${nextModelKey}`);
668
440
  }
669
441
  }
670
442
  }
671
443
 
672
- // This point should theoretically not be reached if there's at least one model key
673
- // and the loop either returns a result or throws an error.
674
444
  log.error("Fallback logic completed without success or throwing the final error.");
675
445
  throw lastError || new Error("Failed to get response from any model, and no specific error was caught.");
676
446
  });
677
447
  }
678
448
  }
449
+
679
450
  class MixCustom {
680
451
  constructor({ config = {}, options = {}, headers = {} } = {}) {
681
452
  this.config = this.getDefaultConfig(config);
@@ -694,7 +465,6 @@ class MixCustom {
694
465
  return {
695
466
  url: '',
696
467
  apiKey: '',
697
- prefix: [],
698
468
  ...customConfig
699
469
  };
700
470
  }
@@ -797,8 +567,16 @@ class MixCustom {
797
567
  return '';
798
568
  }
799
569
 
570
+ extractMessage(data) {
571
+ if (data.choices && data.choices[0].message.content) return data.choices[0].message.content;
572
+ return '';
573
+ }
574
+
800
575
  processResponse(response) {
801
- return { response: response.data, message: response.data.choices[0].message.content };
576
+ return {
577
+ response: response.data,
578
+ message: this.extractMessage(response.data)
579
+ };
802
580
  }
803
581
  }
804
582
 
@@ -806,7 +584,6 @@ class MixOpenAI extends MixCustom {
806
584
  getDefaultConfig(customConfig) {
807
585
  return super.getDefaultConfig({
808
586
  url: 'https://api.openai.com/v1/chat/completions',
809
- prefix: ['gpt', 'ft:', 'o'],
810
587
  apiKey: process.env.OPENAI_API_KEY,
811
588
  ...customConfig
812
589
  });
@@ -854,7 +631,6 @@ class MixAnthropic extends MixCustom {
854
631
  getDefaultConfig(customConfig) {
855
632
  return super.getDefaultConfig({
856
633
  url: 'https://api.anthropic.com/v1/messages',
857
- prefix: ['claude'],
858
634
  apiKey: process.env.ANTHROPIC_API_KEY,
859
635
  ...customConfig
860
636
  });
@@ -889,8 +665,18 @@ class MixAnthropic extends MixCustom {
889
665
  return '';
890
666
  }
891
667
 
892
- processResponse(response) {
893
- return { response: response.data, message: response.data.content[0].text };
668
+ extractMessage(data) {
669
+ if (data.content) {
670
+ // thinking
671
+ if (data.content?.[1]?.text) {
672
+ return data.content[1].text;
673
+ }
674
+
675
+ if (data.content[0].text) {
676
+ return data.content[0].text;
677
+ }
678
+ }
679
+ return '';
894
680
  }
895
681
  }
896
682
 
@@ -898,13 +684,22 @@ class MixPerplexity extends MixCustom {
898
684
  getDefaultConfig(customConfig) {
899
685
  return super.getDefaultConfig({
900
686
  url: 'https://api.perplexity.ai/chat/completions',
901
- prefix: ['sonar'],
902
687
  apiKey: process.env.PPLX_API_KEY,
903
688
  ...customConfig
904
689
  });
905
690
  }
906
691
 
907
692
  async create({ config = {}, options = {} } = {}) {
693
+
694
+ if (config.schema) {
695
+ config.systemExtra = '';
696
+
697
+ options.response_format = {
698
+ type: 'json_schema',
699
+ json_schema: { schema: config.schema }
700
+ };
701
+ }
702
+
908
703
  if (!this.config.apiKey) {
909
704
  throw new Error('Perplexity API key not found. Please provide it in config or set PPLX_API_KEY environment variable.');
910
705
  }
@@ -943,8 +738,8 @@ class MixOllama extends MixCustom {
943
738
  return super.create({ config, options });
944
739
  }
945
740
 
946
- processResponse(response) {
947
- return { response: response.data, message: response.data.message.content.trim() };
741
+ extractMessage(data) {
742
+ return data.message.content.trim();
948
743
  }
949
744
 
950
745
  static convertMessages(messages) {
@@ -973,7 +768,6 @@ class MixGrok extends MixOpenAI {
973
768
  getDefaultConfig(customConfig) {
974
769
  return super.getDefaultConfig({
975
770
  url: 'https://api.x.ai/v1/chat/completions',
976
- prefix: ['grok'],
977
771
  apiKey: process.env.XAI_API_KEY,
978
772
  ...customConfig
979
773
  });
@@ -1000,7 +794,6 @@ class MixGroq extends MixCustom {
1000
794
  getDefaultConfig(customConfig) {
1001
795
  return super.getDefaultConfig({
1002
796
  url: 'https://api.groq.com/openai/v1/chat/completions',
1003
- prefix: ["llama", "mixtral", "gemma", "deepseek-r1-distill"],
1004
797
  apiKey: process.env.GROQ_API_KEY,
1005
798
  ...customConfig
1006
799
  });
@@ -1022,7 +815,6 @@ class MixTogether extends MixCustom {
1022
815
  getDefaultConfig(customConfig) {
1023
816
  return super.getDefaultConfig({
1024
817
  url: 'https://api.together.xyz/v1/chat/completions',
1025
- prefix: ["meta-llama", "google", "NousResearch", "deepseek-ai", "Qwen"],
1026
818
  apiKey: process.env.TOGETHER_API_KEY,
1027
819
  ...customConfig
1028
820
  });
@@ -1061,7 +853,6 @@ class MixCerebras extends MixCustom {
1061
853
  getDefaultConfig(customConfig) {
1062
854
  return super.getDefaultConfig({
1063
855
  url: 'https://api.cerebras.ai/v1/chat/completions',
1064
- prefix: ["llama"],
1065
856
  apiKey: process.env.CEREBRAS_API_KEY,
1066
857
  ...customConfig
1067
858
  });
@@ -1079,14 +870,12 @@ class MixGoogle extends MixCustom {
1079
870
  getDefaultConfig(customConfig) {
1080
871
  return super.getDefaultConfig({
1081
872
  url: 'https://generativelanguage.googleapis.com/v1beta/models',
1082
- prefix: ['gemini'],
1083
873
  apiKey: process.env.GOOGLE_API_KEY,
1084
874
  ...customConfig
1085
875
  });
1086
876
  }
1087
877
 
1088
878
  getDefaultHeaders(customHeaders) {
1089
- // Remove the authorization header as we'll use the API key as a query parameter
1090
879
  return {
1091
880
  'Content-Type': 'application/json',
1092
881
  ...customHeaders
@@ -1105,7 +894,7 @@ class MixGoogle extends MixCustom {
1105
894
  static convertMessages(messages) {
1106
895
  return messages.map(message => {
1107
896
  const parts = [];
1108
-
897
+
1109
898
  if (message.content instanceof Array) {
1110
899
  message.content.forEach(content => {
1111
900
  if (content.type === 'text') {
@@ -1137,13 +926,13 @@ class MixGoogle extends MixCustom {
1137
926
 
1138
927
  const modelId = options.model || 'gemini-2.5-flash-preview-04-17';
1139
928
  const generateContentApi = options.stream ? 'streamGenerateContent' : 'generateContent';
1140
-
929
+
1141
930
  // Construct the full URL with model ID, API endpoint, and API key
1142
931
  const fullUrl = `${this.config.url}/${modelId}:${generateContentApi}?key=${this.config.apiKey}`;
1143
932
 
1144
933
  // Convert messages to Gemini format
1145
934
  const contents = MixGoogle.convertMessages(options.messages);
1146
-
935
+
1147
936
  // Add system message if present
1148
937
  if (config.system || config.systemExtra) {
1149
938
  contents.unshift({
@@ -1171,21 +960,8 @@ class MixGoogle extends MixCustom {
1171
960
  }
1172
961
  }
1173
962
 
1174
- extractDelta(data) {
1175
- try {
1176
- const parsed = JSON.parse(data);
1177
- if (parsed.candidates?.[0]?.content?.parts?.[0]?.text) {
1178
- return parsed.candidates[0].content.parts[0].text;
1179
- }
1180
- } catch (e) {
1181
- // If parsing fails, return empty string
1182
- }
1183
- return '';
1184
- }
1185
-
1186
- processResponse(response) {
1187
- const content = response.data.candidates?.[0]?.content?.parts?.[0]?.text || '';
1188
- return { response: response.data, message: content };
963
+ extractMessage(data) {
964
+ return data.candidates?.[0]?.content?.parts?.[0]?.text;
1189
965
  }
1190
966
  }
1191
967