modelmix 5.1.9 → 5.1.12

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
@@ -119,10 +119,9 @@ console.log(ETH.price);
119
119
 
120
120
  **This example uses providers with free quotas (Groq, Cerebras, and Together). OpenRouter is disabled because its GPT-OSS 120B route is no longer free. If one model runs out of quota, ModelMix automatically falls back to the next model in the chain.**
121
121
  ```javascript
122
- ModelMix.new({ mix: { openrouter: false } })
122
+ ModelMix.new()
123
123
  .gptOss()
124
124
  .kimiK25()
125
- .hermes3()
126
125
  .addText('What is the capital of France?');
127
126
  ```
128
127
 
@@ -143,15 +142,20 @@ ModelMix provides convenient shorthand methods for quickly accessing different A
143
142
  | `gpt56terra()` | OpenAI | gpt-5.6-terra | [\$2.00][1] | [\$12.00][1] |
144
143
  | `gpt56luna()` | OpenAI | gpt-5.6-luna | [\$0.20][1] | [\$1.20][1] |
145
144
  | `gpt55()` | OpenAI | gpt-5.5 | [\$5.00][1] | [\$30.00][1] |
145
+ | `gpt55pro()` | OpenAI | gpt-5.5-pro | [\$30.00][1] | [\$180.00][1] |
146
146
  | `gpt54()` | OpenAI | gpt-5.4 | [\$2.50][1] | [\$15.00][1] |
147
147
  | `gpt54mini()` | OpenAI | gpt-5.4-mini | [\$0.75][1] | [\$4.50][1] |
148
148
  | `gpt54nano()` | OpenAI | gpt-5.4-nano | [\$0.20][1] | [\$1.25][1] |
149
- | `gpt53codex()` | OpenAI | gpt-5.3-codex | [\$1.25][1] | [\$14.00][1] |
149
+ | `gpt54pro()` | OpenAI | gpt-5.4-pro | [\$30.00][1] | [\$180.00][1] |
150
+ | `gpt53codex()` | OpenAI | gpt-5.3-codex | [\$1.75][1] | [\$14.00][1] |
151
+ | `gpt53chat()` | OpenAI | gpt-5.3-chat-latest | [\$1.75][1] | [\$14.00][1] |
150
152
  | `gpt52()` | OpenAI | gpt-5.2 | [\$1.75][1] | [\$14.00][1] |
151
153
  | `gpt51()` | OpenAI | gpt-5.1 | [\$1.25][1] | [\$10.00][1] |
154
+ | `gpt5()` | OpenAI | gpt-5 | [\$1.25][1] | [\$10.00][1] |
152
155
  | `gpt5mini()` | OpenAI | gpt-5-mini | [\$0.25][1] | [\$2.00][1] |
153
156
  | `gpt5nano()` | OpenAI | gpt-5-nano | [\$0.05][1] | [\$0.40][1] |
154
- | `gptOss()` | Cerebras/Groq/OpenRouter | gpt-oss-120B | [\$0.15][7] | [\$0.60][7] |
157
+ | `gptOss()` | Multi-provider | gpt-oss-120B | [\$0.15][7] | [\$0.60][7] |
158
+ | `fable51()` | Anthropic | claude-fable-5-1 | [\$10.00][2] | [\$50.00][2] |
155
159
  | `fable5()` | Anthropic | claude-fable-5 | [\$10.00][2] | [\$50.00][2] |
156
160
  | `opus5()` | Anthropic | claude-opus-5 | [\$5.00][2] | [\$25.00][2] |
157
161
  | `opus48()` | Anthropic | claude-opus-4-8 | [\$5.00][2] | [\$25.00][2] |
@@ -172,6 +176,7 @@ ModelMix provides convenient shorthand methods for quickly accessing different A
172
176
  | `grok420multiAgent()` | Grok | grok-4.20-multi-agent-0309 | [\$1.25][6] | [\$2.50][6] |
173
177
  | `grok420()` | Grok | grok-4.20-0309 (†) | [\$1.25][6] | [\$2.50][6] |
174
178
  | `museGlimmer30b()` | Fireworks | models/muse-glimmer-30b | [\$0.35][17] | [\$1.50][17] |
179
+ | `museSpark12Contributor()` | OpenRouter | meta/muse-spark-1.2-contributor | [\$0.10][22] | [\$0.20][22] |
175
180
  | `qwen35397b()` | OpenRouter | qwen/qwen3.5-397b-a17b | [\$0.385][14] | [\$2.45][14] |
176
181
  | `qwen36plus()` | OpenRouter | qwen/qwen3.6-plus | [\$0.325][18] | [\$1.95][18] |
177
182
  | `qwen37plus()` | Fireworks | models/qwen3p7-plus | [\$0.40][10] | [\$1.60][10] |
@@ -197,9 +202,13 @@ ModelMix provides convenient shorthand methods for quickly accessing different A
197
202
 
198
203
  Gemini 3.7 Flash and 3.6 Flash use Google's introductory standard pricing through December 31, 2026; standard rates double on January 1, 2027.
199
204
 
200
- `museGlimmer30b()` uses Fireworks first and OpenRouter as its default fallback. NVIDIA NIM and Together are also available through `mix.nvidia` and `mix.together`; disable either default provider with `mix.fireworks: false` or `mix.openrouter: false`.
205
+ `museGlimmer30b()` uses Fireworks by default. OpenRouter, NVIDIA NIM, and Together are available through `mix.openrouter`, `mix.nvidia`, and `mix.together`.
201
206
 
202
- The multi-provider shortcuts also expose the current catalog alternatives: `gptOss()` supports NVIDIA and Fireworks; `qwen37plus()` supports Together; `kimiK27Code()` supports Fireworks and OpenRouter; `kimiK3()` supports Fireworks, OpenRouter, and Together; `GLM52()` supports Fireworks and OpenRouter; and both MiniMax shortcuts support Fireworks. `minimaxM27()` keeps every enabled provider in its fallback chain. `qwen36plus()` now defaults to OpenRouter because Fireworks retired its serverless deployment; Fireworks remains available explicitly for private or on-demand deployments.
207
+ `fable51()` uses the official Anthropic API by default (`claude-fable-5-1`). Pass `mix: { openrouter: true }` to append [`anthropic/claude-fable-5.1`][21] as its fallback.
208
+
209
+ Every textual GPT-5 shortcut in the table uses the official OpenAI API by default. Pass `mix: { openrouter: true }` to `ModelMix.new()` or to an individual shortcut to append the matching [`openai/*` OpenRouter route][23] as its fallback. `gpt53chat()` maps the official `gpt-5.3-chat-latest` alias to `openai/gpt-5.3-chat`. Realtime shortcuts remain official-only because they use OpenAI's WebSocket transport.
210
+
211
+ OpenRouter fallbacks are disabled globally by default and are appended only with `mix.openrouter: true`. Shortcuts whose primary provider is OpenRouter, such as `qwen36plus()`, are unaffected. The multi-provider shortcuts also expose the current catalog alternatives: `gptOss()` supports NVIDIA and Fireworks; `qwen37plus()` supports Together; `kimiK27Code()` supports Fireworks and OpenRouter; `kimiK3()` supports Fireworks, OpenRouter, and Together; `GLM52()` supports Fireworks and OpenRouter; and both MiniMax shortcuts support Fireworks. `minimaxM27()` keeps every explicitly enabled provider in its fallback chain.
203
212
 
204
213
  [1]: https://platform.openai.com/docs/pricing "Pricing | OpenAI"
205
214
  [2]: https://docs.anthropic.com/en/docs/about-claude/pricing "Pricing - Anthropic"
@@ -221,6 +230,9 @@ The multi-provider shortcuts also expose the current catalog alternatives: `gptO
221
230
  [18]: https://openrouter.ai/qwen/qwen3.6-plus "Qwen 3.6 Plus on OpenRouter"
222
231
  [19]: https://openrouter.ai/qwen/qwen3.8-flash "Qwen3.8 Flash on OpenRouter"
223
232
  [20]: https://openrouter.ai/z-ai/glm-5.3-flash "GLM 5.3 Flash on OpenRouter"
233
+ [21]: https://openrouter.ai/anthropic/claude-fable-5.1 "Claude Fable 5.1 on OpenRouter"
234
+ [22]: https://openrouter.ai/meta/muse-spark-1.2-contributor "Muse Spark 1.2 Contributor on OpenRouter"
235
+ [23]: https://openrouter.ai/openai "OpenAI Models on OpenRouter"
224
236
 
225
237
  Each method accepts optional `options`, `config`, and (for multi-provider methods) `mix` parameters to customize behavior.
226
238
 
package/demo/free.js CHANGED
@@ -1,12 +1,10 @@
1
1
  import { ModelMix } from '../index.js';
2
2
  try { process.loadEnvFile(); } catch {}
3
3
 
4
- const ai = ModelMix.new({ config: { debug: 2 }, mix: { openrouter: false } })
4
+ const ai = ModelMix.new({ config: { debug: 2 } })
5
5
  .gptOss()
6
6
  .kimiK25()
7
- .hermes3()
8
7
  .addText('What is the capital of France?');
9
8
 
10
9
  const response = await ai.message();
11
10
  console.log('Response from a free-tier provider:', response);
12
-
package/demo/short.js CHANGED
@@ -9,19 +9,21 @@ const setup = {
9
9
  };
10
10
 
11
11
  const mmix = await ModelMix.new(setup)
12
- .sonnet46() // (main model) Anthropic claude-sonnet-4-6
13
- .gpt56luna() // (fallback 1) OpenAI gpt-5.6-luna
14
- .gemini37flash() // (fallback 2) Google gemini-3.7-flash
15
- .gpt5nano() // (fallback 3) OpenAI gpt-5-nano
16
- .grok46() // (fallback 4) Grok grok-4.6
17
- .qwen35397b() // (fallback 5) OpenRouter qwen/qwen3.5-397b-a17b
18
- .qwen3827b() // (fallback 6) OpenRouter qwen/qwen3.8-27b
19
- .qwen38flash() // (fallback 7) OpenRouter qwen/qwen3.8-flash
20
- .GLM53() // (fallback 8) OpenRouter z-ai/glm-5.3
21
- .GLM53Flash() // (fallback 9) OpenRouter z-ai/glm-5.3-flash
22
- .museGlimmer30b({ mix: { fireworks: false, openrouter: true } }) // (fallback 10) OpenRouter meta/muse-glimmer-30b
23
- .hermes470b() // (fallback 11) OpenRouter nousresearch/hermes-4-70b
24
- .hermes4405b() // (fallback 12) OpenRouter nousresearch/hermes-4-405b
12
+ .fable51({ mix: { openrouter: true } }) // (main + provider fallback) Anthropic/OpenRouter Claude Fable 5.1
13
+ .sonnet46() // (fallback 2) Anthropic claude-sonnet-4-6
14
+ .gpt56luna({ mix: { openrouter: true } }) // (fallback 3 + provider fallback) OpenAI/OpenRouter gpt-5.6-luna
15
+ .gemini37flash() // (fallback 4) Google gemini-3.7-flash
16
+ .gpt5nano({ mix: { openrouter: true } }) // (fallback 5 + provider fallback) OpenAI/OpenRouter gpt-5-nano
17
+ .grok46() // (fallback 6) Grok grok-4.6
18
+ .qwen35397b() // (fallback 7) OpenRouter qwen/qwen3.5-397b-a17b
19
+ .qwen3827b() // (fallback 8) OpenRouter qwen/qwen3.8-27b
20
+ .qwen38flash() // (fallback 9) OpenRouter qwen/qwen3.8-flash
21
+ .GLM53() // (fallback 10) OpenRouter z-ai/glm-5.3
22
+ .GLM53Flash() // (fallback 11) OpenRouter z-ai/glm-5.3-flash
23
+ .museGlimmer30b({ mix: { fireworks: false, openrouter: true } }) // (fallback 12) OpenRouter meta/muse-glimmer-30b
24
+ .museSpark12Contributor() // (fallback 13) OpenRouter meta/muse-spark-1.2-contributor
25
+ .hermes470b() // (fallback 14) OpenRouter nousresearch/hermes-4-70b
26
+ .hermes4405b() // (fallback 15) OpenRouter nousresearch/hermes-4-405b
25
27
  .addText("What's your name?");
26
28
 
27
29
  console.log(await mmix.message());
package/effort.js CHANGED
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  const OPENAI_LEVELS = ['none', 'low', 'medium', 'high', 'xhigh'];
10
- const OPENAI_LEVEL_LADDER = [...OPENAI_LEVELS, 'max'];
10
+ const OPENAI_LEVEL_LADDER = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
11
11
  const ANTHROPIC_LEVELS = ['low', 'medium', 'high', 'xhigh', 'max'];
12
12
  const GEMINI_LEVELS = ['minimal', 'low', 'medium', 'high'];
13
13
 
@@ -45,9 +45,11 @@ const OPENAI_MODEL_LEVELS = {
45
45
  'gpt-5.6-sol': ['none', 'low', 'medium', 'high', 'xhigh', 'max'],
46
46
  'gpt-5.6-terra': ['none', 'low', 'medium', 'high', 'xhigh', 'max'],
47
47
  'gpt-5.6-luna': ['none', 'low', 'medium', 'high', 'xhigh', 'max'],
48
+ 'anthropic/claude-fable-5.1': ['low', 'medium', 'high', 'xhigh', 'max'],
48
49
  'meta/muse-glimmer-30b': ['low', 'medium', 'high'],
49
50
  'accounts/fireworks/models/muse-glimmer-30b': ['low', 'medium', 'high', 'xhigh'],
50
51
  'meta-models/Muse-Glimmer-30B': ['low', 'medium', 'high', 'xhigh'],
52
+ 'meta/muse-spark-1.2-contributor': ['minimal', 'low', 'medium', 'high', 'xhigh'],
51
53
  'accounts/fireworks/models/qwen3p8-2p4t-a95b': ['none', 'low', 'medium', 'high'],
52
54
  'qwen/qwen3.8-27b': ['low', 'medium', 'xhigh'],
53
55
  'qwen/qwen3.8-flash': ['low', 'medium', 'xhigh'],
@@ -87,7 +89,6 @@ const DEEPSEEK_BANDS = [
87
89
  * MiniMax M3 (OpenAI-compatible): thinking.type disabled | adaptive.
88
90
  * @see https://platform.minimax.io/docs/api-reference/text-openai-api
89
91
  */
90
- const MINIMAX_LEVELS = ['disabled', 'adaptive'];
91
92
  const MINIMAX_BANDS = [
92
93
  [0, 19, 'disabled'],
93
94
  [20, 100, 'adaptive'],
@@ -203,8 +204,11 @@ function pickNearestLevel(desired, ladder, supported) {
203
204
  }
204
205
 
205
206
  function supportedOpenAILevels(modelKey) {
206
- if (modelKey && OPENAI_MODEL_LEVELS[modelKey]) {
207
- return OPENAI_MODEL_LEVELS[modelKey];
207
+ const canonicalModelKey = typeof modelKey === 'string' && modelKey.startsWith('openai/')
208
+ ? modelKey.slice('openai/'.length)
209
+ : modelKey;
210
+ if (canonicalModelKey && OPENAI_MODEL_LEVELS[canonicalModelKey]) {
211
+ return OPENAI_MODEL_LEVELS[canonicalModelKey];
208
212
  }
209
213
  return OPENAI_LEVELS;
210
214
  }
@@ -400,9 +404,11 @@ function mapEffort(providerFamily, effort, modelKey) {
400
404
  const supported = supportedOpenAILevels(modelKey);
401
405
  const desired = modelKey === 'z-ai/glm-5.3' || modelKey === 'z-ai/glm-5.3-flash'
402
406
  ? levelFromBands(normalized, GLM53_BANDS)
403
- : normalized === 100 && supported.includes('max')
404
- ? 'max'
405
- : levelFromBands(normalized, OPENAI_BANDS);
407
+ : modelKey === 'anthropic/claude-fable-5.1'
408
+ ? levelFromBands(normalized, ANTHROPIC_BANDS)
409
+ : normalized === 100 && supported.includes('max')
410
+ ? 'max'
411
+ : levelFromBands(normalized, OPENAI_BANDS);
406
412
  const level = pickNearestLevel(desired, OPENAI_LEVEL_LADDER, supported);
407
413
  return { reasoning_effort: level };
408
414
  }
package/index.d.ts CHANGED
@@ -76,6 +76,7 @@ export interface ModelMixConfig {
76
76
  }
77
77
 
78
78
  export interface ModelMixMixFlags {
79
+ anthropic?: boolean;
79
80
  openrouter?: boolean;
80
81
  cerebras?: boolean;
81
82
  groq?: boolean;
@@ -462,6 +463,7 @@ export declare class ModelMix {
462
463
  gptOss(args?: ModelAttachArgs): this;
463
464
 
464
465
  // Anthropic
466
+ fable51(args?: ModelAttachArgs): this;
465
467
  fable50(args?: ModelAttachArgs): this;
466
468
  fable5(args?: ModelAttachArgs): this;
467
469
  opus50(args?: ModelAttachArgs): this;
@@ -497,6 +499,7 @@ export declare class ModelMix {
497
499
 
498
500
  // Multi-provider
499
501
  museGlimmer30b(args?: ModelAttachArgs): this;
502
+ museSpark12Contributor(args?: ModelAttachArgs): this;
500
503
  qwen35397b(args?: ModelAttachArgs): this;
501
504
  qwen36plus(args?: ModelAttachArgs): this;
502
505
  qwen37plus(args?: ModelAttachArgs): this;
package/index.js CHANGED
@@ -144,7 +144,7 @@ class ModelMix {
144
144
  if (this.config.effort !== undefined && this.config.effort !== null) {
145
145
  this.config.effort = normalizeEffort(this.config.effort);
146
146
  }
147
- const freeMix = { openrouter: true, cerebras: true, groq: true, together: false, lambda: false };
147
+ const freeMix = { openrouter: false, cerebras: true, groq: true, together: false, lambda: false };
148
148
  this.mix = { ...freeMix, ...mix };
149
149
 
150
150
  this.limiter = new Bottleneck(this.config.bottleneck);
@@ -434,47 +434,59 @@ class ModelMix {
434
434
  return this;
435
435
  }
436
436
 
437
- gpt5({ options = {}, config = {} } = {}) {
438
- return this.attach('gpt-5', new MixOpenAI({ options, config }));
437
+ _attachOpenAIWithOpenRouter(officialKey, Provider, {
438
+ options = {},
439
+ config = {},
440
+ mix = {},
441
+ openRouterKey = `openai/${officialKey}`
442
+ } = {}) {
443
+ mix = { ...this.mix, ...mix };
444
+ this.attach(officialKey, new Provider({ options, config }));
445
+ if (mix.openrouter) this.attach(openRouterKey, new MixOpenRouter({ options, config }));
446
+ return this;
439
447
  }
440
- gpt5mini({ options = {}, config = {} } = {}) {
441
- return this.attach('gpt-5-mini', new MixOpenAI({ options, config }));
448
+
449
+ gpt5(args = {}) {
450
+ return this._attachOpenAIWithOpenRouter('gpt-5', MixOpenAI, args);
451
+ }
452
+ gpt5mini(args = {}) {
453
+ return this._attachOpenAIWithOpenRouter('gpt-5-mini', MixOpenAI, args);
454
+ }
455
+ gpt5nano(args = {}) {
456
+ return this._attachOpenAIWithOpenRouter('gpt-5-nano', MixOpenAI, args);
442
457
  }
443
- gpt5nano({ options = {}, config = {} } = {}) {
444
- return this.attach('gpt-5-nano', new MixOpenAI({ options, config }));
458
+ gpt51(args = {}) {
459
+ return this._attachOpenAIWithOpenRouter('gpt-5.1', MixOpenAIResponses, args);
445
460
  }
446
- gpt51({ options = {}, config = {} } = {}) {
447
- return this.attach('gpt-5.1', new MixOpenAIResponses({ options, config }));
461
+ gpt52(args = {}) {
462
+ return this._attachOpenAIWithOpenRouter('gpt-5.2', MixOpenAIResponses, args);
448
463
  }
449
- gpt52({ options = {}, config = {} } = {}) {
450
- return this.attach('gpt-5.2', new MixOpenAIResponses({ options, config }));
464
+ gpt54(args = {}) {
465
+ return this._attachOpenAIWithOpenRouter('gpt-5.4', MixOpenAIResponses, args);
451
466
  }
452
- gpt54({ options = {}, config = {} } = {}) {
453
- return this.attach('gpt-5.4', new MixOpenAIResponses({ options, config }));
467
+ gpt54mini(args = {}) {
468
+ return this._attachOpenAIWithOpenRouter('gpt-5.4-mini', MixOpenAIResponses, args);
454
469
  }
455
- gpt54mini({ options = {}, config = {} } = {}) {
456
- return this.attach('gpt-5.4-mini', new MixOpenAIResponses({ options, config }));
470
+ gpt54nano(args = {}) {
471
+ return this._attachOpenAIWithOpenRouter('gpt-5.4-nano', MixOpenAIResponses, args);
457
472
  }
458
- gpt54nano({ options = {}, config = {} } = {}) {
459
- return this.attach('gpt-5.4-nano', new MixOpenAIResponses({ options, config }));
460
- }
461
- gpt54pro({ options = {}, config = {} } = {}) {
462
- return this.attach('gpt-5.4-pro', new MixOpenAIResponses({ options, config }));
473
+ gpt54pro(args = {}) {
474
+ return this._attachOpenAIWithOpenRouter('gpt-5.4-pro', MixOpenAIResponses, args);
463
475
  }
464
- gpt55({ options = {}, config = {} } = {}) {
465
- return this.attach('gpt-5.5', new MixOpenAIResponses({ options, config }));
476
+ gpt55(args = {}) {
477
+ return this._attachOpenAIWithOpenRouter('gpt-5.5', MixOpenAIResponses, args);
466
478
  }
467
- gpt55pro({ options = {}, config = {} } = {}) {
468
- return this.attach('gpt-5.5-pro', new MixOpenAIResponses({ options, config }));
479
+ gpt55pro(args = {}) {
480
+ return this._attachOpenAIWithOpenRouter('gpt-5.5-pro', MixOpenAIResponses, args);
469
481
  }
470
- gpt56sol({ options = {}, config = {} } = {}) {
471
- return this.attach('gpt-5.6-sol', new MixOpenAIResponses({ options, config }));
482
+ gpt56sol(args = {}) {
483
+ return this._attachOpenAIWithOpenRouter('gpt-5.6-sol', MixOpenAIResponses, args);
472
484
  }
473
- gpt56terra({ options = {}, config = {} } = {}) {
474
- return this.attach('gpt-5.6-terra', new MixOpenAIResponses({ options, config }));
485
+ gpt56terra(args = {}) {
486
+ return this._attachOpenAIWithOpenRouter('gpt-5.6-terra', MixOpenAIResponses, args);
475
487
  }
476
- gpt56luna({ options = {}, config = {} } = {}) {
477
- return this.attach('gpt-5.6-luna', new MixOpenAIResponses({ options, config }));
488
+ gpt56luna(args = {}) {
489
+ return this._attachOpenAIWithOpenRouter('gpt-5.6-luna', MixOpenAIResponses, args);
478
490
  }
479
491
  gptRealtime({ options = {}, config = {} } = {}) {
480
492
  return this.attach('gpt-realtime', new MixOpenAIWebSocket({ options, config }));
@@ -482,11 +494,14 @@ class ModelMix {
482
494
  gptRealtimeMini({ options = {}, config = {} } = {}) {
483
495
  return this.attach('gpt-realtime-mini', new MixOpenAIWebSocket({ options, config }));
484
496
  }
485
- gpt53codex({ options = {}, config = {} } = {}) {
486
- return this.attach('gpt-5.3-codex', new MixOpenAIResponses({ options, config }));
487
- }
488
- gpt53chat({ options = {}, config = {} } = {}) {
489
- return this.attach('gpt-5.3-chat-latest', new MixOpenAIResponses({ options, config }));
497
+ gpt53codex(args = {}) {
498
+ return this._attachOpenAIWithOpenRouter('gpt-5.3-codex', MixOpenAIResponses, args);
499
+ }
500
+ gpt53chat(args = {}) {
501
+ return this._attachOpenAIWithOpenRouter('gpt-5.3-chat-latest', MixOpenAIResponses, {
502
+ ...args,
503
+ openRouterKey: 'openai/gpt-5.3-chat'
504
+ });
490
505
  }
491
506
  gptOss({ options = {}, config = {}, mix = {} } = {}) {
492
507
  mix = { ...this.mix, ...mix };
@@ -504,6 +519,12 @@ class ModelMix {
504
519
  fable5(args = {}) {
505
520
  return this.fable50(args);
506
521
  }
522
+ fable51({ options = {}, config = {}, mix = {} } = {}) {
523
+ mix = { anthropic: true, ...this.mix, ...mix };
524
+ if (mix.anthropic) this.attach('claude-fable-5-1', new MixAnthropic({ options, config }));
525
+ if (mix.openrouter) this.attach('anthropic/claude-fable-5.1', new MixOpenRouter({ options, config }));
526
+ return this;
527
+ }
507
528
  opus50({ options = {}, config = {} } = {}) {
508
529
  return this.attach('claude-opus-5', new MixAnthropic({ options, config }));
509
530
  }
@@ -585,6 +606,10 @@ class ModelMix {
585
606
  return this;
586
607
  }
587
608
 
609
+ museSpark12Contributor({ options = {}, config = {} } = {}) {
610
+ return this.attach('meta/muse-spark-1.2-contributor', new MixOpenRouter({ options, config }));
611
+ }
612
+
588
613
  qwen35397b({ options = {}, config = {} } = {}) {
589
614
  return this.attach('qwen/qwen3.5-397b-a17b', new MixOpenRouter({ options, config }));
590
615
  }
@@ -628,7 +653,7 @@ class ModelMix {
628
653
  return this.attach('nousresearch/hermes-4-405b', new MixOpenRouter({ options, config }));
629
654
  }
630
655
 
631
- hermes3({ options = {}, config = {}, mix = {} } = {}) {
656
+ hermes3({ options = {}, config = {}, mix = { openrouter: true } } = {}) {
632
657
  mix = { ...this.mix, ...mix };
633
658
  if (mix.lambda) this.attach('Hermes-3-Llama-3.1-405B-FP8', new MixLambda({ options, config }));
634
659
  if (mix.openrouter) this.attach('nousresearch/hermes-3-llama-3.1-405b:free', new MixOpenRouter({ options, config }));
@@ -673,7 +698,7 @@ class ModelMix {
673
698
  }
674
699
 
675
700
 
676
- minimaxM27({ options = {}, config = {}, mix = { openrouter: true, minimax: true } } = {}) {
701
+ minimaxM27({ options = {}, config = {}, mix = { minimax: true } } = {}) {
677
702
  mix = { ...this.mix, ...mix };
678
703
  if (mix.nvidia) this.attach('minimaxai/minimax-m2.7', new MixNVIDIA({ options, config }));
679
704
  if (mix.fireworks) this.attach('accounts/fireworks/models/minimax-m2p7', new MixFireworks({ options, config }));
@@ -786,22 +811,23 @@ class ModelMix {
786
811
  return this;
787
812
  }
788
813
 
789
- addImageFromBuffer(buffer, { role = "user", cache } = {}) {
814
+ _addImageSource(source, { role = "user", cache } = {}) {
790
815
  const contentCache = normalizeContentCache(cache);
791
816
  this.messages.push({
792
817
  role,
793
818
  content: [{
794
819
  type: "image",
795
- source: {
796
- type: "buffer",
797
- data: buffer
798
- },
820
+ source,
799
821
  ...(contentCache !== undefined && { cache: contentCache })
800
822
  }]
801
823
  });
802
824
  return this;
803
825
  }
804
826
 
827
+ addImageFromBuffer(buffer, { role = "user", cache } = {}) {
828
+ return this._addImageSource({ type: "buffer", data: buffer }, { role, cache });
829
+ }
830
+
805
831
  addImage(filePath, { role = "user", cache } = {}) {
806
832
  const absolutePath = path.resolve(filePath);
807
833
 
@@ -809,19 +835,7 @@ class ModelMix {
809
835
  throw new Error(`Image file not found: ${filePath}`);
810
836
  }
811
837
 
812
- const contentCache = normalizeContentCache(cache);
813
- this.messages.push({
814
- role,
815
- content: [{
816
- type: "image",
817
- source: {
818
- type: "file",
819
- data: filePath
820
- },
821
- ...(contentCache !== undefined && { cache: contentCache })
822
- }]
823
- });
824
- return this;
838
+ return this._addImageSource({ type: "file", data: filePath }, { role, cache });
825
839
  }
826
840
 
827
841
  addImageFromUrl(url, { role = "user", cache } = {}) {
@@ -845,17 +859,7 @@ class ModelMix {
845
859
  };
846
860
  }
847
861
 
848
- const contentCache = normalizeContentCache(cache);
849
- this.messages.push({
850
- role,
851
- content: [{
852
- type: "image",
853
- source,
854
- ...(contentCache !== undefined && { cache: contentCache })
855
- }]
856
- });
857
-
858
- return this;
862
+ return this._addImageSource(source, { role, cache });
859
863
  }
860
864
 
861
865
  async processImages() {
@@ -1096,10 +1100,6 @@ class ModelMix {
1096
1100
  const shouldNotGroup = ModelMix.hasToolInteraction(currentMessage);
1097
1101
 
1098
1102
  if (index === 0 || currentMessage.role !== messages[index - 1].role || shouldNotGroup) {
1099
- // acc.push({
1100
- // role: currentMessage.role,
1101
- // content: currentMessage.content
1102
- // });
1103
1103
  acc.push(currentMessage);
1104
1104
  } else {
1105
1105
  acc[acc.length - 1].content = acc[acc.length - 1].content.concat(currentMessage.content);
@@ -1865,4 +1865,4 @@ class ModelMix {
1865
1865
  log
1866
1866
  }));
1867
1867
 
1868
- module.exports = { MixCustom, ModelMix, ModerationMix, MixModeration, MixAnthropic, MixKimi, MixMiniMax, MixMiMo, MixOpenAI, MixOpenAIResponses, MixOpenAIModeration, MixOpenAIWebSocket, MixOpenRouter, MixPerplexity, MixOllama, MixLMStudio, MixGroq, MixTogether, MixGrok, MixCerebras, MixGoogle, MixFireworks, MixNVIDIA, normalizeEffort, applyUnifiedEffort, resolveProviderFamily };
1868
+ module.exports = { MixCustom, ModelMix, ModerationMix, MixModeration, MixAnthropic, MixKimi, MixMiniMax, MixMiMo, MixOpenAI, MixOpenAIResponses, MixOpenAIModeration, MixOpenAIWebSocket, MixOpenRouter, MixPerplexity, MixOllama, MixLambda, MixLMStudio, MixGroq, MixTogether, MixGrok, MixCerebras, MixGoogle, MixFireworks, MixNVIDIA, normalizeEffort, applyUnifiedEffort, resolveProviderFamily };
@@ -5,12 +5,12 @@ const CHAIN_MODEL_SHORTCUTS = new Set([
5
5
  'gpt51', 'gpt52', 'gpt54', 'gpt54mini', 'gpt54nano', 'gpt54pro',
6
6
  'gpt55', 'gpt55pro', 'gpt56sol', 'gpt56terra', 'gpt56luna',
7
7
  'gptRealtime', 'gptRealtimeMini', 'gpt53codex', 'gpt53chat', 'gptOss',
8
- 'fable50', 'fable5', 'opus50', 'opus5', 'opus48', 'opus47', 'opus46',
8
+ 'fable51', 'fable50', 'fable5', 'opus50', 'opus5', 'opus48', 'opus47', 'opus46',
9
9
  'sonnet50', 'sonnet5', 'sonnet46', 'sonnet45', 'haiku45',
10
10
  'gemini31pro', 'gemini37flash', 'gemini36flash', 'gemini35flash',
11
11
  'gemini35flashLite', 'gemini31flashLite', 'sonarPro', 'sonar',
12
12
  'grok46', 'grok45', 'grok43', 'grok420multiAgent', 'grok420',
13
- 'museGlimmer30b',
13
+ 'museGlimmer30b', 'museSpark12Contributor',
14
14
  'qwen35397b', 'qwen36plus', 'qwen37plus', 'qwen38max', 'qwen3827b', 'qwen38flash',
15
15
  'hermes470b', 'hermes4405b', 'hermes3',
16
16
  'kimiK26', 'kimiK27Code', 'kimiK3', 'kimiK25',
@@ -0,0 +1,9 @@
1
+ function requireProviderApiKey(customConfig, environmentVariable, providerName) {
2
+ const apiKey = customConfig.apiKey || process.env[environmentVariable];
3
+ if (!apiKey) {
4
+ throw new Error(`${providerName} API key not found. Please provide it in config or set ${environmentVariable} environment variable.`);
5
+ }
6
+ return apiKey;
7
+ }
8
+
9
+ module.exports = { requireProviderApiKey };
@@ -4,6 +4,25 @@ const {
4
4
  stripContentCacheMetadata,
5
5
  hasNeutralCacheBreakpoint
6
6
  } = require('../content-cache');
7
+ const { requireProviderApiKey } = require('../provider-api-key');
8
+
9
+ function rejectsAnthropicSamplingParams(model = '') {
10
+ const id = String(model).toLowerCase();
11
+ if (!id.includes('claude')) return false;
12
+ if (id.includes('mythos') || id.includes('fable')) return true;
13
+
14
+ const opus = id.match(/claude-opus-(\d+)(?:-(\d+))?/);
15
+ if (opus) {
16
+ const major = Number(opus[1]);
17
+ const minor = opus[2] !== undefined ? Number(opus[2]) : 0;
18
+ return major > 4 || (major === 4 && minor >= 7);
19
+ }
20
+
21
+ const sonnet = id.match(/claude-sonnet-(\d+)/);
22
+ if (sonnet) return Number(sonnet[1]) >= 5;
23
+
24
+ return false;
25
+ }
7
26
 
8
27
  function createAnthropicProviders({ ModelMix, MixCustom, log }) {
9
28
  class MixAnthropic extends MixCustom {
@@ -30,28 +49,11 @@ function createAnthropicProviders({ ModelMix, MixCustom, log }) {
30
49
  * See: https://platform.claude.com/docs/en/about-claude/models/migration-guide
31
50
  */
32
51
  static rejectsSamplingParams(model = '') {
33
- const id = String(model).toLowerCase();
34
- if (!id.includes('claude')) return false;
35
- if (id.includes('mythos') || id.includes('fable')) return true;
36
-
37
- const opus = id.match(/claude-opus-(\d+)(?:-(\d+))?/);
38
- if (opus) {
39
- const major = Number(opus[1]);
40
- const minor = opus[2] !== undefined ? Number(opus[2]) : 0;
41
- return major > 4 || (major === 4 && minor >= 7);
42
- }
43
-
44
- const sonnet = id.match(/claude-sonnet-(\d+)/);
45
- if (sonnet) return Number(sonnet[1]) >= 5;
46
-
47
- return false;
52
+ return rejectsAnthropicSamplingParams(model);
48
53
  }
49
54
 
50
55
  getDefaultConfig(customConfig) {
51
- const apiKey = customConfig.apiKey || process.env.ANTHROPIC_API_KEY;
52
- if (!apiKey) {
53
- throw new Error('Anthropic API key not found. Please provide it in config or set ANTHROPIC_API_KEY environment variable.');
54
- }
56
+ const apiKey = requireProviderApiKey(customConfig, 'ANTHROPIC_API_KEY', 'Anthropic');
55
57
 
56
58
  return super.getDefaultConfig({
57
59
  url: 'https://api.anthropic.com/v1/messages',
@@ -334,4 +336,4 @@ function createAnthropicProviders({ ModelMix, MixCustom, log }) {
334
336
  return { MixAnthropic };
335
337
  }
336
338
 
337
- module.exports = createAnthropicProviders;
339
+ module.exports = { createAnthropicProviders, rejectsAnthropicSamplingParams };
@@ -11,6 +11,8 @@ const {
11
11
  stripContentCacheMetadata
12
12
  } = require('../content-cache');
13
13
  const { configForDebug, redactSecret } = require('../provider-debug');
14
+ const { requireProviderApiKey } = require('../provider-api-key');
15
+ const { normalizeOpenAIOptions } = require('./openai-options');
14
16
 
15
17
  function createBaseProviders({ ModelMix }) {
16
18
  class MixCustom {
@@ -274,10 +276,7 @@ function createBaseProviders({ ModelMix }) {
274
276
  }
275
277
 
276
278
  getDefaultConfig(customConfig) {
277
- const apiKey = customConfig.apiKey || process.env.OPENAI_API_KEY;
278
- if (!apiKey) {
279
- throw new Error('OpenAI API key not found. Please provide it in config or set OPENAI_API_KEY environment variable.');
280
- }
279
+ const apiKey = requireProviderApiKey(customConfig, 'OPENAI_API_KEY', 'OpenAI');
281
280
 
282
281
  return super.getDefaultConfig({
283
282
  url: 'https://api.openai.com/v1/chat/completions',
@@ -287,22 +286,7 @@ function createBaseProviders({ ModelMix }) {
287
286
  }
288
287
 
289
288
  async create({ config = {}, options = {} } = {}) {
290
-
291
- // Remove max_tokens and temperature for o1/o3 models
292
- if (options.model?.startsWith('o')) {
293
- delete options.max_tokens;
294
- delete options.temperature;
295
- }
296
-
297
- // Use max_completion_tokens and remove temperature for GPT-5 models
298
- if (options.model?.includes('gpt-5')) {
299
- if (options.max_tokens) {
300
- options.max_completion_tokens = options.max_tokens;
301
- delete options.max_tokens;
302
- }
303
- delete options.temperature;
304
- }
305
-
289
+ normalizeOpenAIOptions(options);
306
290
  return super.create({ config, options });
307
291
  }
308
292
 
@@ -387,10 +371,8 @@ function createBaseProviders({ ModelMix }) {
387
371
  }
388
372
  }
389
373
 
390
- // Solo incluir tools si el array no está vacío
391
374
  if (toolsArray.length > 0) {
392
375
  options.tools = toolsArray;
393
- // options.tool_choice = "auto";
394
376
  }
395
377
 
396
378
  return options;
@@ -1,10 +1,11 @@
1
1
  const { fetchJsonResponse } = require('../../http-client');
2
2
  const { configForDebug } = require('../provider-debug');
3
+ const { requireProviderApiKey } = require('../provider-api-key');
3
4
 
4
5
  function createGoogleProviders({ ModelMix, MixCustom }) {
5
6
  class MixGoogle extends MixCustom {
6
7
  getDefaultConfig(customConfig) {
7
- const apiKey = customConfig.apiKey || process.env.GEMINI_API_KEY;
8
+ const apiKey = requireProviderApiKey(customConfig, 'GEMINI_API_KEY', 'Gemini');
8
9
  return super.getDefaultConfig({
9
10
  url: 'https://generativelanguage.googleapis.com/v1beta/models',
10
11
  apiKey,
@@ -19,7 +20,7 @@ function createGoogleProviders({ ModelMix, MixCustom }) {
19
20
  };
20
21
  }
21
22
 
22
- static convertMessages(messages, config) {
23
+ static convertMessages(messages) {
23
24
  return messages.map(message => {
24
25
 
25
26
  // Handle assistant messages with tool_calls (content is null)
@@ -122,10 +123,6 @@ function createGoogleProviders({ ModelMix, MixCustom }) {
122
123
  }
123
124
 
124
125
  async create({ config = {}, options = {} } = {}) {
125
- if (!this.config.apiKey) {
126
- throw new Error('Gemini API key not found. Please provide it in config or set GEMINI_API_KEY environment variable.');
127
- }
128
-
129
126
  const generateContentApi = options.stream ? 'streamGenerateContent' : 'generateContent';
130
127
 
131
128
  const fullUrl = `${this.config.url}/${options.model}:${generateContentApi}?key=${this.config.apiKey}`;