modelmix 4.6.15 → 4.7.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/README.md CHANGED
@@ -89,7 +89,7 @@ console.log(ETH.price);
89
89
  ```javascript
90
90
  ModelMix.new()
91
91
  .gptOss()
92
- .kimiK25think()
92
+ .kimiK25()
93
93
  .hermes3()
94
94
  .addText('What is the capital of France?');
95
95
  ```
@@ -124,7 +124,9 @@ ModelMix.new().effort(-1).minimaxM3().addText('...').message();
124
124
  | DeepSeek V4 | off | `low`↑ | `high`↑ | `high`↑ | `max`↑ | — |
125
125
  | MiniMax M3 | off | adaptive | adaptive | adaptive | adaptive | adaptive |
126
126
 
127
- \* Gemini bands: 0–24 / 25–49 / 50–74 / 75–100. DeepSeek `↑` = thinking on; `off` = thinking disabled. MiniMax `off`/`adaptive` = `thinking.disabled` / `thinking.type=adaptive`. Gemini 2.5 maps 0–100 to `thinkingBudget`. `-1` = provider adaptive/dynamic when available, else no-op. Levels clamp to what each model supports.
127
+ \* Gemini bands: 0–24 / 25–49 / 50–74 / 75–100. DeepSeek `↑` = thinking on; `off` = thinking disabled. MiniMax `off`/`adaptive` = `thinking.disabled` / `thinking.type=adaptive`. Gemini 2.5 maps 0–100 to `thinkingBudget`. Anthropic maps adaptive thinking + `output_config.effort` on Claude 5 / Fable / Opus 4.6+ / Sonnet 4.6+; older models (Sonnet 4.5, Haiku 4.5) get `thinking.type=enabled` + `budget_tokens`. `-1` = provider adaptive/dynamic when available, else no-op. Levels clamp to what each model supports.
128
+
129
+ Migration: former `*think()` shorthands are removed — use `.effort(n).<model>()` (or any 0–100 / `-1`). Kimi: `kimiK25()` / `kimiK26()`. Grok 4.20: `.grok420()` is non-reasoning; `.effort(20+).grok420()` (or `-1`) selects the reasoning model.
128
130
 
129
131
  ## 🔧 Model Context Protocol (MCP) Integration
130
132
 
@@ -177,14 +179,14 @@ Here's a comprehensive list of available methods:
177
179
  | `gpt41mini()` | OpenAI | gpt-4.1-mini | [\$0.40 / \$1.60][1] |
178
180
  | `gpt41nano()` | OpenAI | gpt-4.1-nano | [\$0.10 / \$0.40][1] |
179
181
  | `gptOss()` | Together | gpt-oss-120B | [\$0.15 / \$0.60][7] |
180
- | `fable5[think]()` | Anthropic | claude-fable-5 | [\$10.00 / \$50.00][2] |
181
- | `opus5[think]()` | Anthropic | claude-opus-5 | [\$5.00 / \$25.00][2] |
182
- | `opus48[think]()` | Anthropic | claude-opus-4-8 | [\$5.00 / \$25.00][2] |
183
- | `opus47[think]()` | Anthropic | claude-opus-4-7 | [\$5.00 / \$25.00][2] |
184
- | `opus46[think]()` | Anthropic | claude-opus-4-6 | [\$5.00 / \$25.00][2] |
185
- | `sonnet5[think]()` | Anthropic | claude-sonnet-5 | [\$3.00 / \$15.00][2] |
186
- | `sonnet46[think]()` | Anthropic | claude-sonnet-4-6 | [\$3.00 / \$15.00][2] |
187
- | `haiku45[think]()` | Anthropic | claude-haiku-4-5-20251001 | [\$1.00 / \$5.00][2] |
182
+ | `fable5()` | Anthropic | claude-fable-5 | [\$10.00 / \$50.00][2] |
183
+ | `opus5()` | Anthropic | claude-opus-5 | [\$5.00 / \$25.00][2] |
184
+ | `opus48()` | Anthropic | claude-opus-4-8 | [\$5.00 / \$25.00][2] |
185
+ | `opus47()` | Anthropic | claude-opus-4-7 | [\$5.00 / \$25.00][2] |
186
+ | `opus46()` | Anthropic | claude-opus-4-6 | [\$5.00 / \$25.00][2] |
187
+ | `sonnet5()` | Anthropic | claude-sonnet-5 | [\$3.00 / \$15.00][2] |
188
+ | `sonnet46()` | Anthropic | claude-sonnet-4-6 | [\$3.00 / \$15.00][2] |
189
+ | `haiku45()` | Anthropic | claude-haiku-4-5-20251001 | [\$1.00 / \$5.00][2] |
188
190
  | `gemini31pro()` | Google | gemini-3.1-pro-preview | [\$2.00 / \$12.00][3] |
189
191
  | `gemini36flash()` | Google | gemini-3.6-flash | [\$1.50 / \$7.50][3] |
190
192
  | `gemini35flash()` | Google | gemini-3.5-flash | [\$0.75 / \$4.50][3] |
@@ -192,8 +194,7 @@ Here's a comprehensive list of available methods:
192
194
  | `grok45()` | Grok | grok-4.5 | [\$2.00 / \$6.00][6] |
193
195
  | `grok43()` | Grok | grok-4.3 | [\$1.25 / \$2.50][6] |
194
196
  | `grok420multiAgent()`| Grok | grok-4.20-multi-agent-0309 | [\$1.25 / \$2.50][6] |
195
- | `grok420[think]()` | Grok | grok-4.20-0309 | [\$1.25 / \$2.50][6] |
196
- | `grok41[think]()` | Grok | grok-4-1-fast | [\$0.20 / \$0.50][6] |
197
+ | `grok420()` | Grok | grok-4.20-0309 (†) | [\$1.25 / \$2.50][6] |
197
198
  | `qwen36plus()` | Fireworks/Together | qwen3p6-plus / Qwen3.6-Plus | [\$0.50 / \$3.00][10] |
198
199
  | `qwen37plus()` | Fireworks | models/qwen3p7-plus | [\$0.40 / \$1.60][10] |
199
200
  | `qwen38max()` | OpenRouter | qwen/qwen3.8-max | [\$2.00 / \$6.00][12] |
@@ -207,8 +208,8 @@ Here's a comprehensive list of available methods:
207
208
  | `sonarPro()` | Perplexity | sonar-pro | [\$3.00 / \$15.00][4] |
208
209
  | `hermes3()` | Lambda | Hermes-3-Llama-3.1-405B-FP8 | [\$0.80 / \$0.80][8] |
209
210
  | `kimiK3()` | Moonshot | kimi-k3 | [\$3.00 / \$15.00][11] |
210
- | `kimiK25think()` | Together | Kimi-K2.5 | [\$0.50 / \$2.80][7] |
211
- | `kimiK26think()` | Fireworks | models/kimi-k2p6 | [\$0.95 / \$4.00][10] |
211
+ | `kimiK25()` | Together | Kimi-K2.5 | [\$0.50 / \$2.80][7] |
212
+ | `kimiK26()` | Fireworks | models/kimi-k2p6 | [\$0.95 / \$4.00][10] |
212
213
 
213
214
  [1]: https://platform.openai.com/docs/pricing "Pricing | OpenAI"
214
215
  [2]: https://docs.anthropic.com/en/docs/about-claude/pricing "Pricing - Anthropic"
@@ -224,6 +225,7 @@ Here's a comprehensive list of available methods:
224
225
  [12]: https://openrouter.ai/qwen/qwen3.8-max "OpenRouter Pricing"
225
226
 
226
227
  Each method accepts optional `options`, `config`, and (for multi-provider methods) `mix` parameters to customize behavior.
228
+ † `grok420()` resolves to `grok-4.20-0309-non-reasoning` by default, or `grok-4.20-0309-reasoning` when `.effort(20+)` / `-1` (or native non-`none` `reasoning_effort`) is set.
227
229
  For NVIDIA on DeepSeek V4 Flash/Pro, use `deepseekV4Flash({ mix: { nvidia: true } })` or `deepseekV4Pro({ mix: { nvidia: true } })`.
228
230
  For Together on Qwen 3.6 Plus, use `qwen36plus({ mix: { fireworks: false, together: true } })`.
229
231
  For OpenRouter on Qwen 3.7 Plus, use `qwen37plus({ mix: { fireworks: false, openrouter: true } })`.
package/demo/free.js CHANGED
@@ -3,7 +3,7 @@ try { process.loadEnvFile(); } catch {}
3
3
 
4
4
  const ai = ModelMix.new({ config: { debug: 2 } })
5
5
  .gptOss()
6
- .kimiK25think()
6
+ .kimiK25()
7
7
  .hermes3()
8
8
  .addText('What is the capital of France?');
9
9
 
package/demo/opus5.js CHANGED
@@ -15,9 +15,9 @@ opus.addText("Explain quantum entanglement in simple terms.");
15
15
  const response = await opus.message();
16
16
  console.log(response);
17
17
 
18
- console.log("\n" + '--------| opus5think() |--------');
18
+ console.log("\n" + '--------| effort(100).opus5() |--------');
19
19
 
20
- const opusThink = mmix.new().opus5think();
20
+ const opusThink = mmix.new().effort(100).opus5();
21
21
  opusThink.addText("A bat and a ball cost $1.10 in total. The bat costs $1.00 more than the ball. How much does the ball cost?");
22
22
  const thinkResponse = await opusThink.raw();
23
23
  console.log(thinkResponse);
package/demo/short.js CHANGED
@@ -9,9 +9,9 @@ const setup = {
9
9
  };
10
10
 
11
11
  const mmix = await ModelMix.new(setup)
12
- .sonnet4() // (main model) Anthropic claude-sonnet-4-20250514
13
- .o4mini() // (fallback 1) OpenAI o4-mini
14
- .gemini25proExp({ config: { temperature: 0 } }) // (fallback 2) Google gemini-2.5-pro-exp-03-25
12
+ .sonnet46() // (main model) Anthropic claude-sonnet-4-6
13
+ .gpt56luna() // (fallback 1) OpenAI gpt-5.6-luna
14
+ .gemini36flash({ config: { temperature: 0 } }) // (fallback 2) Google gemini-3.6-flash
15
15
  .gpt41nano() // (fallback 3) OpenAI gpt-4.1-nano
16
16
  .grok43() // (fallback 4) Grok grok-4.3
17
17
  .addText("What's your name?");
package/demo/stream.js CHANGED
@@ -6,7 +6,7 @@ await ModelMix.new().gpt41nano()
6
6
  .addText('describe')
7
7
  .stream((data) => { console.log(data.message); });
8
8
 
9
- await ModelMix.new().haiku35()
9
+ await ModelMix.new().haiku45()
10
10
  .addImageFromUrl('https://pbs.twimg.com/media/F6-GsjraAAADDGy?format=jpg')
11
11
  .addText('describe')
12
12
  .stream((data) => { console.log(data.message); });
package/demo/tokens.js CHANGED
@@ -25,7 +25,7 @@ console.log('-'.repeat(60));
25
25
 
26
26
  const providers = [
27
27
  { name: 'OpenAI GPT-5-nano', fn: (m) => m.gpt5nano() },
28
- { name: 'Anthropic Haiku', fn: (m) => m.haiku35() },
28
+ { name: 'Anthropic Haiku', fn: (m) => m.haiku45() },
29
29
  { name: 'Google Gemini', fn: (m) => m.gemini25flash() }
30
30
  ];
31
31
 
package/effort.js CHANGED
@@ -105,6 +105,39 @@ const PROVIDER_FAMILY_BY_CLASS = {
105
105
  MixCustom: null,
106
106
  };
107
107
 
108
+ /** Logical alias from `.grok420()` — resolved to reasoning / non-reasoning at request time. */
109
+ const GROK420_ALIAS = 'grok-4.20-0309';
110
+ const GROK420_REASONING = 'grok-4.20-0309-reasoning';
111
+ const GROK420_NON_REASONING = 'grok-4.20-0309-non-reasoning';
112
+
113
+ function isGrok420Alias(modelKey) {
114
+ return modelKey === GROK420_ALIAS;
115
+ }
116
+
117
+ /**
118
+ * Pick Grok 4.20 concrete model from unified effort (and native reasoning_effort).
119
+ * - no effort / OpenAI band `none` (0–19) / native `none` → non-reasoning
120
+ * - effort -1 or 20–100 / native non-none reasoning_effort → reasoning
121
+ */
122
+ function resolveGrok420ModelKey(modelKey, effort, options = {}) {
123
+ if (!isGrok420Alias(modelKey)) return modelKey;
124
+
125
+ const native = options.reasoning_effort;
126
+ if (native != null && native !== '') {
127
+ return native === 'none' ? GROK420_NON_REASONING : GROK420_REASONING;
128
+ }
129
+
130
+ if (effort === undefined || effort === null) {
131
+ return GROK420_NON_REASONING;
132
+ }
133
+
134
+ const normalized = normalizeEffort(effort);
135
+ if (normalized === -1) return GROK420_REASONING;
136
+
137
+ const level = levelFromBands(normalized, OPENAI_BANDS);
138
+ return level === 'none' ? GROK420_NON_REASONING : GROK420_REASONING;
139
+ }
140
+
108
141
  function normalizeEffort(value) {
109
142
  if (typeof value !== 'number' || !Number.isFinite(value) || !Number.isInteger(value)) {
110
143
  throw new Error(`Invalid effort: expected integer -1 or 0..100, got ${JSON.stringify(value)}`);
@@ -170,6 +203,62 @@ function isMiniMax(modelKey) {
170
203
  return typeof modelKey === 'string' && modelKey.toLowerCase().includes('minimax');
171
204
  }
172
205
 
206
+ /** Max budget_tokens when mapping unified effort onto manual Anthropic thinking. */
207
+ const ANTHROPIC_MANUAL_BUDGET_MAX = 16384;
208
+
209
+ /**
210
+ * Models that use adaptive thinking + output_config.effort (Claude 5 / Fable /
211
+ * Opus 4.6+ / Sonnet 4.6+). Older ones (Sonnet 4.5, Haiku 4.5, Opus 4.5) use
212
+ * thinking.type=enabled + budget_tokens.
213
+ */
214
+ function usesAnthropicAdaptiveThinking(modelKey) {
215
+ const id = String(modelKey || '').toLowerCase();
216
+ if (!id) return true;
217
+ if (!id.includes('claude')) return true;
218
+ if (id.includes('fable') || id.includes('mythos')) return true;
219
+
220
+ const opus = id.match(/claude-opus-(\d+)(?:-(\d+))?/);
221
+ if (opus) {
222
+ const major = Number(opus[1]);
223
+ const minor = opus[2] !== undefined ? Number(opus[2]) : 0;
224
+ return major > 4 || (major === 4 && minor >= 6);
225
+ }
226
+
227
+ const sonnet = id.match(/claude-sonnet-(\d+)(?:-(\d+))?/);
228
+ if (sonnet) {
229
+ const major = Number(sonnet[1]);
230
+ const minor = sonnet[2] !== undefined ? Number(sonnet[2]) : 0;
231
+ return major > 4 || (major === 4 && minor >= 6);
232
+ }
233
+
234
+ // Haiku 4.5 and earlier: manual extended thinking only
235
+ if (id.includes('haiku')) return false;
236
+
237
+ return true;
238
+ }
239
+
240
+ function mapAnthropicManualBudget(normalized) {
241
+ return Math.max(1024, Math.round((normalized / 100) * ANTHROPIC_MANUAL_BUDGET_MAX));
242
+ }
243
+
244
+ function mapAnthropicEffort(normalized, modelKey) {
245
+ if (!usesAnthropicAdaptiveThinking(modelKey)) {
246
+ return {
247
+ thinking: {
248
+ type: 'enabled',
249
+ budget_tokens: mapAnthropicManualBudget(normalized)
250
+ }
251
+ };
252
+ }
253
+
254
+ const desired = levelFromBands(normalized, ANTHROPIC_BANDS);
255
+ const level = pickNearestLevel(desired, ANTHROPIC_LEVELS, ANTHROPIC_LEVELS);
256
+ return {
257
+ thinking: { type: 'adaptive', display: 'summarized' },
258
+ output_config: { effort: level }
259
+ };
260
+ }
261
+
173
262
  function mapDeepSeekEffort(normalized) {
174
263
  // DeepSeek V4 has no adaptive mode (only enabled/disabled + low|high|max)
175
264
  if (normalized === -1) return null;
@@ -276,15 +365,17 @@ function mapEffort(providerFamily, effort, modelKey) {
276
365
  if (isMiniMax(modelKey)) {
277
366
  return mapMiniMaxEffort(normalized);
278
367
  }
368
+ // Non-reasoning Grok 4.20 has no reasoning_effort control
369
+ if (modelKey === GROK420_NON_REASONING) {
370
+ return null;
371
+ }
279
372
  const desired = levelFromBands(normalized, OPENAI_BANDS);
280
373
  const level = pickNearestLevel(desired, OPENAI_LEVELS, supportedOpenAILevels(modelKey));
281
374
  return { reasoning_effort: level };
282
375
  }
283
376
 
284
377
  if (providerFamily === 'anthropic') {
285
- const desired = levelFromBands(normalized, ANTHROPIC_BANDS);
286
- const level = pickNearestLevel(desired, ANTHROPIC_LEVELS, ANTHROPIC_LEVELS);
287
- return { output_config: { effort: level } };
378
+ return mapAnthropicEffort(normalized, modelKey);
288
379
  }
289
380
 
290
381
  if (providerFamily === 'google') {
@@ -355,8 +446,11 @@ module.exports = {
355
446
  applyUnifiedEffort,
356
447
  hasNativeEffort,
357
448
  resolveProviderFamily,
449
+ resolveGrok420ModelKey,
450
+ isGrok420Alias,
358
451
  isDeepSeekV4,
359
452
  isMiniMax,
453
+ usesAnthropicAdaptiveThinking,
360
454
  levelFromBands,
361
455
  pickNearestLevel,
362
456
  OPENAI_BANDS,
@@ -367,4 +461,8 @@ module.exports = {
367
461
  ANTHROPIC_LEVELS,
368
462
  GEMINI_LEVELS,
369
463
  DEEPSEEK_LEVELS,
464
+ ANTHROPIC_MANUAL_BUDGET_MAX,
465
+ GROK420_ALIAS,
466
+ GROK420_REASONING,
467
+ GROK420_NON_REASONING,
370
468
  };
package/index.d.ts CHANGED
@@ -286,28 +286,14 @@ export declare class ModelMix {
286
286
 
287
287
  // Anthropic
288
288
  fable5(args?: ModelAttachArgs): this;
289
- fable5think(args?: ModelAttachArgs): this;
290
289
  opus5(args?: ModelAttachArgs): this;
291
- opus5think(args?: ModelAttachArgs): this;
292
- opus48think(args?: ModelAttachArgs): this;
293
- opus47think(args?: ModelAttachArgs): this;
294
- opus46think(args?: ModelAttachArgs): this;
295
290
  opus48(args?: ModelAttachArgs): this;
296
291
  opus47(args?: ModelAttachArgs): this;
297
292
  opus46(args?: ModelAttachArgs): this;
298
- opus41(args?: ModelAttachArgs): this;
299
- opus41think(args?: ModelAttachArgs): this;
300
293
  sonnet5(args?: ModelAttachArgs): this;
301
- sonnet5think(args?: ModelAttachArgs): this;
302
- sonnet4(args?: ModelAttachArgs): this;
303
- sonnet4think(args?: ModelAttachArgs): this;
304
294
  sonnet46(args?: ModelAttachArgs): this;
305
- sonnet46think(args?: ModelAttachArgs): this;
306
295
  sonnet45(args?: ModelAttachArgs): this;
307
- sonnet45think(args?: ModelAttachArgs): this;
308
- haiku35(args?: ModelAttachArgs): this;
309
296
  haiku45(args?: ModelAttachArgs): this;
310
- haiku45think(args?: ModelAttachArgs): this;
311
297
 
312
298
  // Google
313
299
  gemini25flash(args?: ModelAttachArgs): this;
@@ -327,10 +313,8 @@ export declare class ModelMix {
327
313
  grok45(args?: ModelAttachArgs): this;
328
314
  grok43(args?: ModelAttachArgs): this;
329
315
  grok420multiAgent(args?: ModelAttachArgs): this;
330
- grok420think(args?: ModelAttachArgs): this;
316
+ /** Non-reasoning by default; with `.effort(20+)` / `-1` uses the reasoning model at request time. */
331
317
  grok420(args?: ModelAttachArgs): this;
332
- grok41think(args?: ModelAttachArgs): this;
333
- grok41(args?: ModelAttachArgs): this;
334
318
 
335
319
  // Multi-provider
336
320
  qwen3(args?: ModelAttachArgs): this;
@@ -338,10 +322,10 @@ export declare class ModelMix {
338
322
  qwen37plus(args?: ModelAttachArgs): this;
339
323
  qwen38max(args?: ModelAttachArgs): this;
340
324
  hermes3(args?: ModelAttachArgs): this;
341
- kimiK26think(args?: ModelAttachArgs): this;
325
+ kimiK26(args?: ModelAttachArgs): this;
342
326
  kimiK27Code(args?: ModelAttachArgs): this;
343
327
  kimiK3(args?: ModelAttachArgs): this;
344
- kimiK25think(args?: ModelAttachArgs): this;
328
+ kimiK25(args?: ModelAttachArgs): this;
345
329
  lmstudio(model?: string, args?: ModelAttachArgs): this;
346
330
  minimaxM25(args?: ModelAttachArgs): this;
347
331
  minimaxM27(args?: ModelAttachArgs): this;
@@ -352,7 +336,6 @@ export declare class ModelMix {
352
336
  deepseekV4Flash(args?: ModelAttachArgs): this;
353
337
  GLM51(args?: ModelAttachArgs): this;
354
338
  GLM52(args?: ModelAttachArgs): this;
355
- GLM5(args?: ModelAttachArgs): this;
356
339
 
357
340
  addText(text: string, options?: RoleOptions): this;
358
341
  addTextFromFile(filePath: string, options?: RoleOptions): this;
package/index.js CHANGED
@@ -23,7 +23,8 @@ const {
23
23
  const {
24
24
  normalizeEffort,
25
25
  applyUnifiedEffort,
26
- resolveProviderFamily
26
+ resolveProviderFamily,
27
+ resolveGrok420ModelKey
27
28
  } = require('./effort');
28
29
 
29
30
  const DEFAULT_RETRYABLE_STATUS_CODES = [408, 425, 429, 500, 502, 503, 504, 529];
@@ -72,11 +73,8 @@ const MODEL_PRICING = {
72
73
  'claude-opus-4-8': [5.00, 25.00],
73
74
  'claude-opus-4-7': [5.00, 25.00],
74
75
  'claude-opus-4-6': [5.00, 25.00],
75
- 'claude-opus-4-1-20250805': [15.00, 75.00],
76
76
  'claude-sonnet-4-6': [3.00, 15.00],
77
77
  'claude-sonnet-4-5-20250929': [3.00, 15.00],
78
- 'claude-sonnet-4-20250514': [3.00, 15.00],
79
- 'claude-3-5-haiku-20241022': [0.80, 4.00],
80
78
  'claude-haiku-4-5-20251001': [1.00, 5.00],
81
79
  // Google
82
80
  'gemini-3.1-pro-preview':[2.00, 12.00],
@@ -91,10 +89,9 @@ const MODEL_PRICING = {
91
89
  'grok-4.5': [2.00, 6.00],
92
90
  'grok-4.3': [1.25, 2.50],
93
91
  'grok-4.20-multi-agent-0309': [1.25, 2.50],
92
+ 'grok-4.20-0309': [1.25, 2.50],
94
93
  'grok-4.20-0309-reasoning': [1.25, 2.50],
95
94
  'grok-4.20-0309-non-reasoning': [1.25, 2.50],
96
- 'grok-4-1-fast-reasoning': [0.20, 0.50],
97
- 'grok-4-1-fast-non-reasoning': [0.20, 0.50],
98
95
  // Fireworks
99
96
  'accounts/fireworks/models/deepseek-v4-flash': [0.14, 0.28],
100
97
  'accounts/fireworks/models/deepseek-v4-pro': [1.74, 3.48],
@@ -110,12 +107,10 @@ const MODEL_PRICING = {
110
107
  'accounts/fireworks/models/qwen3p7-plus': [0.40, 1.60],
111
108
  'qwen/qwen3.7-plus': [0.32, 1.28],
112
109
  'qwen/qwen3.8-max': [2.00, 6.00],
113
- 'fireworks/glm-5': [1.00, 3.20],
114
110
  // MiniMax
115
111
  'MiniMax-M2.5': [0.30, 1.20],
116
112
  'MiniMax-M2.7': [0.30, 1.20],
117
113
  'MiniMax-M3': [0.30, 1.20],
118
- 'fireworks/minimax-m2p5': [0.30, 1.20],
119
114
  'minimax/minimax-m2.7': [0.30, 1.20],
120
115
  'minimax/minimax-m3': [0.30, 1.20],
121
116
  'MiniMaxAI/MiniMax-M3': [0.30, 1.20],
@@ -392,29 +387,9 @@ class ModelMix {
392
387
  fable5({ options = {}, config = {} } = {}) {
393
388
  return this.attach('claude-fable-5', new MixAnthropic({ options, config }));
394
389
  }
395
- fable5think({ options = {}, config = {} } = {}) {
396
- options = { ...MixAnthropic.maxEffortThinkingOptions, ...options };
397
- return this.attach('claude-fable-5', new MixAnthropic({ options, config }));
398
- }
399
390
  opus5({ options = {}, config = {} } = {}) {
400
391
  return this.attach('claude-opus-5', new MixAnthropic({ options, config }));
401
392
  }
402
- opus5think({ options = {}, config = {} } = {}) {
403
- options = { ...MixAnthropic.maxEffortThinkingOptions, ...options };
404
- return this.attach('claude-opus-5', new MixAnthropic({ options, config }));
405
- }
406
- opus48think({ options = {}, config = {} } = {}) {
407
- options = { ...MixAnthropic.thinkingOptions, ...options };
408
- return this.attach('claude-opus-4-8', new MixAnthropic({ options, config }));
409
- }
410
- opus47think({ options = {}, config = {} } = {}) {
411
- options = { ...MixAnthropic.thinkingOptions, ...options };
412
- return this.attach('claude-opus-4-7', new MixAnthropic({ options, config }));
413
- }
414
- opus46think({ options = {}, config = {} } = {}) {
415
- options = { ...MixAnthropic.thinkingOptions, ...options };
416
- return this.attach('claude-opus-4-6', new MixAnthropic({ options, config }));
417
- }
418
393
  opus48({ options = {}, config = {} } = {}) {
419
394
  return this.attach('claude-opus-4-8', new MixAnthropic({ options, config }));
420
395
  }
@@ -424,52 +399,18 @@ class ModelMix {
424
399
  opus46({ options = {}, config = {} } = {}) {
425
400
  return this.attach('claude-opus-4-6', new MixAnthropic({ options, config }));
426
401
  }
427
- opus41({ options = {}, config = {} } = {}) {
428
- return this.attach('claude-opus-4-1-20250805', new MixAnthropic({ options, config }));
429
- }
430
- opus41think({ options = {}, config = {} } = {}) {
431
- options = { ...MixAnthropic.thinkingOptions, ...options };
432
- return this.attach('claude-opus-4-1-20250805', new MixAnthropic({ options, config }));
433
- }
434
402
  sonnet5({ options = {}, config = {} } = {}) {
435
403
  return this.attach('claude-sonnet-5', new MixAnthropic({ options, config }));
436
404
  }
437
- sonnet5think({ options = {}, config = {} } = {}) {
438
- options = { ...MixAnthropic.thinkingOptions, ...options };
439
- return this.attach('claude-sonnet-5', new MixAnthropic({ options, config }));
440
- }
441
- sonnet4({ options = {}, config = {} } = {}) {
442
- return this.attach('claude-sonnet-4-20250514', new MixAnthropic({ options, config }));
443
- }
444
- sonnet4think({ options = {}, config = {} } = {}) {
445
- options = { ...MixAnthropic.thinkingOptions, ...options };
446
- return this.attach('claude-sonnet-4-20250514', new MixAnthropic({ options, config }));
447
- }
448
405
  sonnet46({ options = {}, config = {} } = {}) {
449
406
  return this.attach('claude-sonnet-4-6', new MixAnthropic({ options, config }));
450
407
  }
451
- sonnet46think({ options = {}, config = {} } = {}) {
452
- options = { ...MixAnthropic.thinkingOptions, ...options };
453
- return this.attach('claude-sonnet-4-6', new MixAnthropic({ options, config }));
454
- }
455
-
456
408
  sonnet45({ options = {}, config = {} } = {}) {
457
409
  return this.attach('claude-sonnet-4-5-20250929', new MixAnthropic({ options, config }));
458
410
  }
459
- sonnet45think({ options = {}, config = {} } = {}) {
460
- options = { ...MixAnthropic.thinkingOptions, ...options };
461
- return this.attach('claude-sonnet-4-5-20250929', new MixAnthropic({ options, config }));
462
- }
463
- haiku35({ options = {}, config = {} } = {}) {
464
- return this.attach('claude-3-5-haiku-20241022', new MixAnthropic({ options, config }));
465
- }
466
411
  haiku45({ options = {}, config = {} } = {}) {
467
412
  return this.attach('claude-haiku-4-5-20251001', new MixAnthropic({ options, config }));
468
413
  }
469
- haiku45think({ options = {}, config = {} } = {}) {
470
- options = { ...MixAnthropic.thinkingOptions, ...options };
471
- return this.attach('claude-haiku-4-5-20251001', new MixAnthropic({ options, config }));
472
- }
473
414
  gemini25flash({ options = {}, config = {} } = {}) {
474
415
  return this.attach('gemini-2.5-flash', new MixGoogle({ options, config }));
475
416
  }
@@ -510,17 +451,9 @@ class ModelMix {
510
451
  grok420multiAgent({ options = {}, config = {} } = {}) {
511
452
  return this.attach('grok-4.20-multi-agent-0309', new MixGrok({ options, config }));
512
453
  }
513
- grok420think({ options = {}, config = {} } = {}) {
514
- return this.attach('grok-4.20-0309-reasoning', new MixGrok({ options, config }));
515
- }
454
+ /** Non-reasoning by default; with `.effort(20+)` / `-1` resolves to the reasoning model at request time. */
516
455
  grok420({ options = {}, config = {} } = {}) {
517
- return this.attach('grok-4.20-0309-non-reasoning', new MixGrok({ options, config }));
518
- }
519
- grok41think({ options = {}, config = {} } = {}) {
520
- return this.attach('grok-4-1-fast-reasoning', new MixGrok({ options, config }));
521
- }
522
- grok41({ options = {}, config = {} } = {}) {
523
- return this.attach('grok-4-1-fast-non-reasoning', new MixGrok({ options, config }));
456
+ return this.attach('grok-4.20-0309', new MixGrok({ options, config }));
524
457
  }
525
458
 
526
459
  qwen3({ options = {}, config = {}, mix = { together: true, cerebras: false } } = {}) {
@@ -556,7 +489,7 @@ class ModelMix {
556
489
  return this;
557
490
  }
558
491
 
559
- kimiK26think({ options = {}, config = {}, mix = { fireworks: true } } = {}) {
492
+ kimiK26({ options = {}, config = {}, mix = { fireworks: true } } = {}) {
560
493
  mix = { ...this.mix, ...mix };
561
494
  if (mix.fireworks) this.attach('accounts/fireworks/models/kimi-k2p6', new MixFireworks({ options, config }));
562
495
  if (mix.openrouter) this.attach('moonshotai/kimi-k2.6', new MixOpenRouter({ options, config }));
@@ -577,7 +510,7 @@ class ModelMix {
577
510
  return this;
578
511
  }
579
512
 
580
- kimiK25think({ options = {}, config = {}, mix = { together: true } } = {}) {
513
+ kimiK25({ options = {}, config = {}, mix = { together: true } } = {}) {
581
514
  mix = { ...this.mix, ...mix };
582
515
  if (mix.together) this.attach('moonshotai/Kimi-K2.5', new MixTogether({ options, config }));
583
516
  if (mix.fireworks) this.attach('accounts/fireworks/models/kimi-k2p5', new MixFireworks({ options, config }));
@@ -593,7 +526,6 @@ class ModelMix {
593
526
  minimaxM25({ options = {}, config = {}, mix = { minimax: true } } = {}) {
594
527
  mix = { ...this.mix, ...mix };
595
528
  if (mix.minimax) this.attach('MiniMax-M2.5', new MixMiniMax({ options, config }));
596
- if (mix.fireworks) this.attach('fireworks/minimax-m2p5', new MixFireworks({ options, config }));
597
529
  return this;
598
530
  }
599
531
 
@@ -661,12 +593,6 @@ class ModelMix {
661
593
  return this;
662
594
  }
663
595
 
664
- GLM5({ options = {}, config = {}, mix = { fireworks: true } } = {}) {
665
- mix = { ...this.mix, ...mix };
666
- if (mix.fireworks) this.attach('fireworks/glm-5', new MixFireworks({ options, config }));
667
- return this;
668
- }
669
-
670
596
  addText(text, { role = "user" } = {}) {
671
597
  const content = [{
672
598
  type: "text",
@@ -1007,7 +933,7 @@ class ModelMix {
1007
933
 
1008
934
  async execute({ config = {}, options = {} } = {}) {
1009
935
  if (!this.models || this.models.length === 0) {
1010
- throw new Error("No models specified. Use methods like .gpt5(), .sonnet4() first.");
936
+ throw new Error("No models specified. Use methods like .gpt5(), .sonnet46() first.");
1011
937
  }
1012
938
 
1013
939
  return this.limiter.schedule(async () => {
@@ -1065,9 +991,17 @@ class ModelMix {
1065
991
  }
1066
992
  };
1067
993
 
994
+ // Grok 4.20 alias → reasoning / non-reasoning from unified effort
995
+ const resolvedModelKey = resolveGrok420ModelKey(
996
+ currentModelKey,
997
+ currentConfig.effort,
998
+ currentOptions
999
+ );
1000
+ currentOptions.model = resolvedModelKey;
1001
+
1068
1002
  // Unified effort → native provider fields (skipped if native already set)
1069
1003
  const providerFamily = resolveProviderFamily(providerInstance);
1070
- applyUnifiedEffort(currentOptions, currentConfig, providerFamily, currentModelKey);
1004
+ applyUnifiedEffort(currentOptions, currentConfig, providerFamily, resolvedModelKey);
1071
1005
 
1072
1006
  if (currentConfig.debug >= 1) {
1073
1007
  const isPrimary = i === 0;
@@ -1077,7 +1011,7 @@ class ModelMix {
1077
1011
  : ' (fallback)';
1078
1012
  // Extract provider name from class name (e.g., "MixOpenRouter" -> "openrouter")
1079
1013
  const providerName = providerInstance.constructor.name.replace(/^Mix/, '').toLowerCase();
1080
- const header = `\n${prefix} [${providerName}:${currentModelKey}] #${originalIndex + 1}${suffix}`;
1014
+ const header = `\n${prefix} [${providerName}:${resolvedModelKey}] #${originalIndex + 1}${suffix}`;
1081
1015
 
1082
1016
  if (currentConfig.debug >= 2) {
1083
1017
  console.log(`${header}\n${ModelMix.formatInputSummary(this.messages, currentConfig.system, currentConfig.debug)}`);
@@ -1122,7 +1056,7 @@ class ModelMix {
1122
1056
  if (currentConfig.debug >= 1) {
1123
1057
  const nextAttempt = attempt + 2;
1124
1058
  const totalAttempts = retries + 1;
1125
- console.log(`↺ Retrying [${currentModelKey}] due to status ${statusCode} (${nextAttempt}/${totalAttempts})`);
1059
+ console.log(`↺ Retrying [${resolvedModelKey}] due to status ${statusCode} (${nextAttempt}/${totalAttempts})`);
1126
1060
  }
1127
1061
 
1128
1062
  const delay = Math.min(baseDelayMs * Math.pow(2, attempt), maxDelayMs);
@@ -1134,7 +1068,7 @@ class ModelMix {
1134
1068
  const elapsedMs = Date.now() - startTime;
1135
1069
 
1136
1070
  if (result.tokens) {
1137
- result.tokens.cost = ModelMix.calculateCost(currentModelKey, result.tokens);
1071
+ result.tokens.cost = ModelMix.calculateCost(resolvedModelKey, result.tokens);
1138
1072
  const elapsedSec = elapsedMs / 1000;
1139
1073
  result.tokens.speed = elapsedSec > 0 ? Math.round(result.tokens.output / elapsedSec) : 0;
1140
1074
  }
@@ -2217,19 +2151,6 @@ class MixKimi extends MixOpenAI {
2217
2151
 
2218
2152
  class MixAnthropic extends MixCustom {
2219
2153
 
2220
- static thinkingOptions = {
2221
- thinking: {
2222
- "type": "enabled",
2223
- "budget_tokens": 1638
2224
- },
2225
- temperature: 1
2226
- };
2227
-
2228
- static maxEffortThinkingOptions = {
2229
- output_config: { effort: 'max' },
2230
- thinking: { display: 'summarized' }
2231
- };
2232
-
2233
2154
  /**
2234
2155
  * Opus 4.7+ and Claude 5 family reject sampling params (temperature/top_p/top_k).
2235
2156
  * See: https://platform.claude.com/docs/en/about-claude/models/migration-guide
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modelmix",
3
- "version": "4.6.15",
3
+ "version": "4.7.2",
4
4
  "description": "🧬 Reliable interface with automatic fallback for AI LLMs.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -72,6 +72,6 @@
72
72
  "test:live": "mocha test/live.test.js --timeout 10000 --require test/setup.js",
73
73
  "test:live.mcp": "mocha test/live.mcp.js --timeout 60000 --require test/setup.js",
74
74
  "test:tokens": "mocha test/tokens.test.js --timeout 10000 --require test/setup.js",
75
- "test:offline": "mocha test/json.test.js test/fallback.test.js test/templates.test.js test/images.test.js test/bottleneck.test.js test/tokens.test.js test/history.test.js test/anthropic.test.js test/effort.test.js --timeout 10000 --require test/setup.js"
75
+ "test:offline": "mocha test/json.test.js test/fallback.test.js test/templates.test.js test/images.test.js test/bottleneck.test.js test/tokens.test.js test/history.test.js test/anthropic.test.js test/effort.test.js test/grok.test.js --timeout 10000 --require test/setup.js"
76
76
  }
77
77
  }
@@ -114,7 +114,7 @@ ModelMix.new({ config: { effort: 80 } })
114
114
  | DeepSeek V4 | off | `low`↑ | `high`↑ | `high`↑ | `max`↑ | — |
115
115
  | MiniMax M3 | off | adaptive | adaptive | adaptive | adaptive | adaptive |
116
116
 
117
- \* Gemini bands: 0–24 / 25–49 / 50–74 / 75–100. DeepSeek `↑` = thinking on; `off` = thinking disabled. MiniMax `off`/`adaptive` = `thinking.disabled` / `thinking.type=adaptive`. Gemini 2.5 maps 0–100 to `thinkingBudget`. `-1` = adaptive/dynamic when available, else no-op. Levels clamp per model. `*think()` shorthands still win over unified `effort`.
117
+ \* Gemini bands: 0–24 / 25–49 / 50–74 / 75–100. DeepSeek `↑` = thinking on; `off` = thinking disabled. MiniMax `off`/`adaptive` = `thinking.disabled` / `thinking.type=adaptive`. Gemini 2.5 maps 0–100 to `thinkingBudget`. Anthropic: adaptive + `output_config.effort` on Claude 5 / Fable / Opus 4.6+ / Sonnet 4.6+; Sonnet 4.5 / Haiku 4.5 use `thinking.type=enabled` + `budget_tokens`. `-1` = adaptive/dynamic when available, else no-op. Levels clamp per model. Former `*think()` methods are removed use `.effort(n).<model>()`. Kimi: `kimiK25()` / `kimiK26()`. Grok 4.20: `.grok420()` non-reasoning; `.effort(20+|-1).grok420()` selects reasoning.
118
118
 
119
119
  ## Available Model Shorthands
120
120
 
@@ -122,21 +122,21 @@ ModelMix.new({ config: { effort: 80 } })
122
122
  `gpt52()` `gpt52chat()` `gpt51()` `gpt5()` `gpt5mini()` `gpt5nano()` `gpt45()` `gpt41()` `gpt41mini()` `gpt41nano()` `o3()` `o4mini()`
123
123
 
124
124
  ### Anthropic
125
- `fable5()` `opus5()` `opus48()` `opus47()` `opus46()` `opus41()` `sonnet5()` `sonnet46()` `sonnet45()` `sonnet4()` `haiku45()` `haiku35()`
125
+ `fable5()` `opus5()` `opus48()` `opus47()` `opus46()` `sonnet5()` `sonnet46()` `sonnet45()` `haiku45()`
126
126
 
127
- Thinking variants: append `think` — e.g. `fable5think()` `opus5think()` `opus48think()` `opus47think()` `opus46think()` `sonnet5think()` `sonnet46think()` `sonnet45think()` `sonnet4think()` `opus41think()` `haiku45think()`
127
+ Use `.effort(n)` (or `config.effort`) to enable Anthropic thinking e.g. `.effort(100).opus5()`.
128
128
 
129
129
  ### Google
130
130
  `gemini3pro()` `gemini3flash()` `gemini36flash()` `gemini35flash()` `gemini25pro()` `gemini25flash()`
131
131
 
132
132
  ### Grok
133
- `grok45()` `grok43()` `grok420multiAgent()` `grok420()` `grok420think()` `grok41()` `grok41think()`
133
+ `grok45()` `grok43()` `grok420multiAgent()` `grok420()`
134
134
 
135
135
  ### Perplexity
136
136
  `sonar()` `sonarPro()`
137
137
 
138
138
  ### Together
139
- `qwen36plus()` `GLM52()` `kimiK25think()` `gptOss()`
139
+ `qwen36plus()` `GLM52()` `kimiK25()` `kimiK26()` `gptOss()`
140
140
 
141
141
  ### Moonshot
142
142
  `kimiK3()` — requires `MOONSHOT_API_KEY`; use `{ mix: { moonshot: false, openrouter: true } }` for OpenRouter.
@@ -145,7 +145,7 @@ Thinking variants: append `think` — e.g. `fable5think()` `opus5think()` `opus4
145
145
  `minimaxM25()` `minimaxM27()` `minimaxM3()`
146
146
 
147
147
  ### Fireworks
148
- `qwen36plus()` `qwen37plus()` `deepseekV4Flash()` `deepseekV4Pro()` `GLM5()`
148
+ `qwen36plus()` `qwen37plus()` `deepseekV4Flash()` `deepseekV4Pro()` `kimiK26()`
149
149
 
150
150
  ### Cerebras
151
151
  `GLM46()`
@@ -154,7 +154,7 @@ Thinking variants: append `think` — e.g. `fable5think()` `opus5think()` `opus4
154
154
  `qwen38max()` `GLM45()`
155
155
 
156
156
  ### Multi-provider (auto-fallback across free/paid tiers)
157
- `hermes3()` `kimiK25think()`
157
+ `hermes3()` `kimiK25()`
158
158
 
159
159
  ### Local
160
160
  `lmstudio()` — for LM Studio local models
@@ -286,7 +286,8 @@ const code = await ModelMix.new()
286
286
 
287
287
  ```javascript
288
288
  const raw = await ModelMix.new()
289
- .sonnet45think()
289
+ .effort(100)
290
+ .sonnet45()
290
291
  .addText("Solve this step by step: 2+2*3")
291
292
  .raw();
292
293
  // raw.message, raw.think, raw.tokens, raw.toolCalls, raw.response
@@ -428,7 +429,7 @@ For full debug output, also set: `DEBUG=ModelMix* node script.js`
428
429
  ```javascript
429
430
  const model = ModelMix.new()
430
431
  .gptOss()
431
- .kimiK25think()
432
+ .kimiK25()
432
433
  .hermes3()
433
434
  .addText("What is the capital of France?");
434
435
  console.log(await model.message());
@@ -451,7 +452,7 @@ const model = ModelMix.new({
451
452
  minimax: false, // default: false
452
453
  fireworks: false // default: false
453
454
  }
454
- }).kimiK25think();
455
+ }).kimiK25();
455
456
  ```
456
457
 
457
458
  ## Agent Usage Rules
@@ -463,8 +464,7 @@ const model = ModelMix.new({
463
464
  - When using MCP tools or `addTool()`, set `max_history` to at least 3 — tool call/response pairs consume history slots.
464
465
  - Use `.json()` for structured output instead of parsing text manually. Use descriptor objects `{ description, required, enum, default, nullable }` for richer schema control.
465
466
  - Use `.message()` for simple text, `.raw()` when you need tokens/thinking/toolCalls.
466
- - For thinking models, append `think` to the method name (e.g. `sonnet45think()`).
467
- - For cross-provider reasoning intensity, use unified `effort` (`-1` or `0`–`100`) via `config.effort` or `.effort(n)` — never put it in `options`. Native fields win if already set.
467
+ - For Anthropic thinking, use unified `effort` (`-1` or `0`–`100`) via `config.effort` or `.effort(n)` — e.g. `.effort(100).opus5()`. Never put `effort` in `options`. Native fields win if already set.
468
468
  - Template placeholders use `{key}` syntax in both system prompts and user messages.
469
469
  - The library uses CommonJS internally but supports ESM import via `{ ModelMix }`.
470
470
  - GPT-5+ models automatically use `max_completion_tokens` instead of `max_tokens`.
@@ -12,15 +12,15 @@ describe('Anthropic Model Registration Tests', () => {
12
12
  expect(model.models[0].provider).to.be.instanceOf(MixAnthropic);
13
13
  });
14
14
 
15
- it('should register Claude Fable 5 with max effort thinking', () => {
16
- const model = ModelMix.new();
17
- model.fable5think();
18
-
19
- expect(model.models).to.have.length(1);
20
- expect(model.models[0].key).to.equal('claude-fable-5');
21
- expect(model.models[0].provider.options.output_config).to.deep.equal({ effort: 'max' });
22
- expect(model.models[0].provider.options.thinking).to.deep.equal({ display: 'summarized' });
23
- expect(model.models[0].provider.options).to.not.have.property('temperature');
15
+ it('should apply max effort thinking via .effort(100).fable5()', () => {
16
+ const model = ModelMix.new().effort(100).fable5();
17
+ const { applyUnifiedEffort } = require('../effort.js');
18
+
19
+ expect(model.config.effort).to.equal(100);
20
+ const options = { model: 'claude-fable-5' };
21
+ applyUnifiedEffort(options, model.config, 'anthropic', 'claude-fable-5');
22
+ expect(options.output_config).to.deep.equal({ effort: 'max' });
23
+ expect(options.thinking).to.deep.equal({ type: 'adaptive', display: 'summarized' });
24
24
  });
25
25
 
26
26
  it('should register Claude Opus 5', () => {
@@ -32,15 +32,15 @@ describe('Anthropic Model Registration Tests', () => {
32
32
  expect(model.models[0].provider).to.be.instanceOf(MixAnthropic);
33
33
  });
34
34
 
35
- it('should register Claude Opus 5 with max effort thinking', () => {
36
- const model = ModelMix.new();
37
- model.opus5think();
35
+ it('should apply max effort thinking via .effort(100).opus5()', () => {
36
+ const model = ModelMix.new().effort(100).opus5();
37
+ const { applyUnifiedEffort } = require('../effort.js');
38
38
 
39
- expect(model.models).to.have.length(1);
40
- expect(model.models[0].key).to.equal('claude-opus-5');
41
- expect(model.models[0].provider.options.output_config).to.deep.equal({ effort: 'max' });
42
- expect(model.models[0].provider.options.thinking).to.deep.equal({ display: 'summarized' });
43
- expect(model.models[0].provider.options).to.not.have.property('temperature');
39
+ expect(model.config.effort).to.equal(100);
40
+ const options = { model: 'claude-opus-5' };
41
+ applyUnifiedEffort(options, model.config, 'anthropic', 'claude-opus-5');
42
+ expect(options.output_config).to.deep.equal({ effort: 'max' });
43
+ expect(options.thinking).to.deep.equal({ type: 'adaptive', display: 'summarized' });
44
44
  });
45
45
 
46
46
  describe('Sampling params (temperature/top_p/top_k)', () => {
@@ -53,7 +53,6 @@ describe('Anthropic Model Registration Tests', () => {
53
53
  expect(MixAnthropic.rejectsSamplingParams('anthropic/claude-opus-5')).to.equal(true);
54
54
 
55
55
  expect(MixAnthropic.rejectsSamplingParams('claude-opus-4-6')).to.equal(false);
56
- expect(MixAnthropic.rejectsSamplingParams('claude-opus-4-1-20250805')).to.equal(false);
57
56
  expect(MixAnthropic.rejectsSamplingParams('claude-sonnet-4-6')).to.equal(false);
58
57
  expect(MixAnthropic.rejectsSamplingParams('claude-haiku-4-5-20251001')).to.equal(false);
59
58
  });
@@ -143,16 +142,14 @@ describe('Anthropic Model Registration Tests', () => {
143
142
  expect(model.models[0].provider).to.be.instanceOf(MixAnthropic);
144
143
  });
145
144
 
146
- it('should register Claude Opus 4.8 with thinking enabled', () => {
147
- const model = ModelMix.new();
148
- model.opus48think();
145
+ it('should apply adaptive thinking via .effort(100).opus48()', () => {
146
+ const model = ModelMix.new().effort(100).opus48();
147
+ const { applyUnifiedEffort } = require('../effort.js');
149
148
 
150
- expect(model.models).to.have.length(1);
151
- expect(model.models[0].key).to.equal('claude-opus-4-8');
152
- expect(model.models[0].provider.options.thinking).to.deep.equal({
153
- type: 'enabled',
154
- budget_tokens: 1638
155
- });
149
+ const options = { model: 'claude-opus-4-8' };
150
+ applyUnifiedEffort(options, model.config, 'anthropic', 'claude-opus-4-8');
151
+ expect(options.thinking).to.deep.equal({ type: 'adaptive', display: 'summarized' });
152
+ expect(options.output_config).to.deep.equal({ effort: 'max' });
156
153
  });
157
154
 
158
155
  it('should register Claude Sonnet 5', () => {
@@ -164,16 +161,14 @@ describe('Anthropic Model Registration Tests', () => {
164
161
  expect(model.models[0].provider).to.be.instanceOf(MixAnthropic);
165
162
  });
166
163
 
167
- it('should register Claude Sonnet 5 with thinking enabled', () => {
168
- const model = ModelMix.new();
169
- model.sonnet5think();
164
+ it('should apply adaptive thinking via .effort(100).sonnet5()', () => {
165
+ const model = ModelMix.new().effort(100).sonnet5();
166
+ const { applyUnifiedEffort } = require('../effort.js');
170
167
 
171
- expect(model.models).to.have.length(1);
172
- expect(model.models[0].key).to.equal('claude-sonnet-5');
173
- expect(model.models[0].provider.options.thinking).to.deep.equal({
174
- type: 'enabled',
175
- budget_tokens: 1638
176
- });
168
+ const options = { model: 'claude-sonnet-5' };
169
+ applyUnifiedEffort(options, model.config, 'anthropic', 'claude-sonnet-5');
170
+ expect(options.thinking).to.deep.equal({ type: 'adaptive', display: 'summarized' });
171
+ expect(options.output_config).to.deep.equal({ effort: 'max' });
177
172
  });
178
173
 
179
174
  describe('Thinking block extraction', () => {
@@ -81,9 +81,31 @@ describe('Unified effort scale', () => {
81
81
  expect(mapEffort('openai', 10, 'gpt-oss-120b')).to.deep.equal({ reasoning_effort: 'low' });
82
82
  });
83
83
 
84
- it('maps Anthropic effort to output_config.effort', () => {
85
- expect(mapEffort('anthropic', 10)).to.deep.equal({ output_config: { effort: 'low' } });
86
- expect(mapEffort('anthropic', 90)).to.deep.equal({ output_config: { effort: 'max' } });
84
+ it('maps Anthropic adaptive models to thinking + output_config.effort', () => {
85
+ expect(mapEffort('anthropic', 10, 'claude-opus-5')).to.deep.equal({
86
+ thinking: { type: 'adaptive', display: 'summarized' },
87
+ output_config: { effort: 'low' }
88
+ });
89
+ expect(mapEffort('anthropic', 90, 'claude-fable-5')).to.deep.equal({
90
+ thinking: { type: 'adaptive', display: 'summarized' },
91
+ output_config: { effort: 'max' }
92
+ });
93
+ expect(mapEffort('anthropic', 50, 'claude-sonnet-4-6')).to.deep.equal({
94
+ thinking: { type: 'adaptive', display: 'summarized' },
95
+ output_config: { effort: 'high' }
96
+ });
97
+ });
98
+
99
+ it('maps Anthropic manual models to thinking.type=enabled + budget_tokens', () => {
100
+ expect(mapEffort('anthropic', 50, 'claude-sonnet-4-5-20250929')).to.deep.equal({
101
+ thinking: { type: 'enabled', budget_tokens: 8192 }
102
+ });
103
+ expect(mapEffort('anthropic', 100, 'claude-haiku-4-5-20251001')).to.deep.equal({
104
+ thinking: { type: 'enabled', budget_tokens: 16384 }
105
+ });
106
+ expect(mapEffort('anthropic', 0, 'claude-sonnet-4-5-20250929')).to.deep.equal({
107
+ thinking: { type: 'enabled', budget_tokens: 1024 }
108
+ });
87
109
  });
88
110
 
89
111
  it('maps Anthropic adaptive to thinking.type=adaptive', () => {
@@ -223,6 +245,14 @@ describe('Unified effort scale', () => {
223
245
  const options = {};
224
246
  applyUnifiedEffort(options, { effort: 90 }, 'anthropic', 'claude-opus-5');
225
247
  expect(options.output_config).to.deep.equal({ effort: 'max' });
248
+ expect(options.thinking).to.deep.equal({ type: 'adaptive', display: 'summarized' });
249
+ });
250
+
251
+ it('applies Anthropic manual thinking for Sonnet 4.5', () => {
252
+ const options = {};
253
+ applyUnifiedEffort(options, { effort: 50 }, 'anthropic', 'claude-sonnet-4-5-20250929');
254
+ expect(options.thinking).to.deep.equal({ type: 'enabled', budget_tokens: 8192 });
255
+ expect(options.output_config).to.equal(undefined);
226
256
  });
227
257
 
228
258
  it('skips Anthropic mapping when output_config.effort is set', () => {
@@ -306,16 +336,29 @@ describe('Unified effort scale', () => {
306
336
  expect(request.reasoning).to.deep.equal({ effort: 'none' });
307
337
  });
308
338
 
309
- it('Anthropic *think() native effort wins over config.effort', () => {
310
- const model = ModelMix.new({ config: { effort: 20 } }).opus5think();
311
- expect(model.models[0].provider.options.output_config.effort).to.equal('max');
339
+ it('Anthropic config.effort maps through .effort().opus5()', () => {
340
+ const model = ModelMix.new().effort(100).opus5();
341
+ expect(model.config.effort).to.equal(100);
312
342
 
313
343
  const options = {
314
344
  ...model.models[0].provider.options,
315
345
  model: 'claude-opus-5'
316
346
  };
317
- applyUnifiedEffort(options, { effort: 20 }, 'anthropic', 'claude-opus-5');
347
+ applyUnifiedEffort(options, model.config, 'anthropic', 'claude-opus-5');
318
348
  expect(options.output_config.effort).to.equal('max');
349
+ expect(options.thinking).to.deep.equal({ type: 'adaptive', display: 'summarized' });
350
+ });
351
+
352
+ it('Anthropic .effort(50).sonnet45() maps to manual budget_tokens', () => {
353
+ const model = ModelMix.new().effort(50).sonnet45();
354
+ expect(model.config.effort).to.equal(50);
355
+
356
+ const options = {
357
+ ...model.models[0].provider.options,
358
+ model: 'claude-sonnet-4-5-20250929'
359
+ };
360
+ applyUnifiedEffort(options, model.config, 'anthropic', 'claude-sonnet-4-5-20250929');
361
+ expect(options.thinking).to.deep.equal({ type: 'enabled', budget_tokens: 8192 });
319
362
  });
320
363
 
321
364
  it('MixGoogle generationConfig includes thinkingConfig from options', async () => {
package/test/grok.test.js CHANGED
@@ -1,13 +1,18 @@
1
1
  const { expect } = require('chai');
2
2
  const { ModelMix } = require('../index.js');
3
+ const {
4
+ resolveGrok420ModelKey,
5
+ GROK420_ALIAS,
6
+ GROK420_REASONING,
7
+ GROK420_NON_REASONING
8
+ } = require('../effort.js');
3
9
 
4
10
  describe('Grok Model Registration Tests', () => {
5
11
  const grokModels = [
6
12
  { method: 'grok45', key: 'grok-4.5' },
7
13
  { method: 'grok43', key: 'grok-4.3' },
8
14
  { method: 'grok420multiAgent', key: 'grok-4.20-multi-agent-0309' },
9
- { method: 'grok420think', key: 'grok-4.20-0309-reasoning' },
10
- { method: 'grok420', key: 'grok-4.20-0309-non-reasoning' }
15
+ { method: 'grok420', key: GROK420_ALIAS }
11
16
  ];
12
17
 
13
18
  for (const grokModel of grokModels) {
@@ -20,3 +25,34 @@ describe('Grok Model Registration Tests', () => {
20
25
  });
21
26
  }
22
27
  });
28
+
29
+ describe('Grok 4.20 effort → model resolution', () => {
30
+ it('uses non-reasoning when effort is unset', () => {
31
+ expect(resolveGrok420ModelKey(GROK420_ALIAS, undefined)).to.equal(GROK420_NON_REASONING);
32
+ expect(resolveGrok420ModelKey(GROK420_ALIAS, null)).to.equal(GROK420_NON_REASONING);
33
+ });
34
+
35
+ it('uses non-reasoning for OpenAI none band (0–19)', () => {
36
+ expect(resolveGrok420ModelKey(GROK420_ALIAS, 0)).to.equal(GROK420_NON_REASONING);
37
+ expect(resolveGrok420ModelKey(GROK420_ALIAS, 19)).to.equal(GROK420_NON_REASONING);
38
+ });
39
+
40
+ it('uses reasoning for effort 20–100 and -1', () => {
41
+ expect(resolveGrok420ModelKey(GROK420_ALIAS, 20)).to.equal(GROK420_REASONING);
42
+ expect(resolveGrok420ModelKey(GROK420_ALIAS, 50)).to.equal(GROK420_REASONING);
43
+ expect(resolveGrok420ModelKey(GROK420_ALIAS, 100)).to.equal(GROK420_REASONING);
44
+ expect(resolveGrok420ModelKey(GROK420_ALIAS, -1)).to.equal(GROK420_REASONING);
45
+ });
46
+
47
+ it('honors native reasoning_effort over config.effort', () => {
48
+ expect(resolveGrok420ModelKey(GROK420_ALIAS, 0, { reasoning_effort: 'high' }))
49
+ .to.equal(GROK420_REASONING);
50
+ expect(resolveGrok420ModelKey(GROK420_ALIAS, 100, { reasoning_effort: 'none' }))
51
+ .to.equal(GROK420_NON_REASONING);
52
+ });
53
+
54
+ it('leaves non-alias keys unchanged', () => {
55
+ expect(resolveGrok420ModelKey(GROK420_REASONING, 50)).to.equal(GROK420_REASONING);
56
+ expect(resolveGrok420ModelKey('grok-4.3', 50)).to.equal('grok-4.3');
57
+ });
58
+ });
@@ -522,7 +522,7 @@ describe('Conversation History Tests', () => {
522
522
  const model = ModelMix.new({
523
523
  config: { debug: false, max_history: 10 }
524
524
  });
525
- model.opus5think();
525
+ model.effort(100).opus5();
526
526
 
527
527
  model.addText('2+2?');
528
528
  nock('https://api.anthropic.com')
package/test/live.test.js CHANGED
@@ -98,7 +98,7 @@ describe('Live Integration Tests', function () {
98
98
  });
99
99
 
100
100
  it('should return structured JSON with Sonnet 4.6 thinking', async function () {
101
- const model = ModelMix.new(setup).sonnet46think();
101
+ const model = ModelMix.new(setup).effort(100).sonnet46();
102
102
 
103
103
  model.addText('Generate information about a fictional city.');
104
104
 
@@ -192,22 +192,10 @@ describe('Live Integration Tests', function () {
192
192
  expect(response.toLowerCase()).to.include('gptoss test successful');
193
193
  });
194
194
 
195
- it('should work with Grok 4.1 model', async function () {
196
- const model = ModelMix.new(setup).grok41();
197
-
198
- model.addText('Say "grok41 test successful" and nothing else.');
199
-
200
- const response = await model.message();
201
- console.log(`Grok 4.1 response: ${response}`);
202
-
203
- expect(response).to.be.a('string');
204
- expect(response.toLowerCase()).to.include('grok41 test successful');
205
- });
206
-
207
195
  const grokSeriesTests = [
208
196
  { name: 'Grok 4.3', factory: (m) => m.grok43(), token: 'grok43' },
209
- { name: 'Grok 4.20 reasoning', factory: (m) => m.grok420think(), token: 'grok420think' },
210
- { name: 'Grok 4.20 non-reasoning', factory: (m) => m.grok420(), token: 'grok420' }
197
+ { name: 'Grok 4.20 reasoning', factory: (m) => m.effort(50).grok420(), token: 'grok420' },
198
+ { name: 'Grok 4.20 non-reasoning', factory: (m) => m.grok420(), token: 'grok420nr' }
211
199
  ];
212
200
 
213
201
  for (const grokModel of grokSeriesTests) {
@@ -254,7 +242,7 @@ describe('Live Integration Tests', function () {
254
242
  describe('JSON Structured Output for New Models', function () {
255
243
 
256
244
  it('should return structured JSON with KimiK25 Thinking', async function () {
257
- const model = ModelMix.new(setup).kimiK25think();
245
+ const model = ModelMix.new(setup).kimiK25();
258
246
 
259
247
  model.addText('Generate information about a fictional vehicle.');
260
248
 
@@ -275,8 +263,8 @@ describe('Live Integration Tests', function () {
275
263
  expect(result.features).to.be.an('array');
276
264
  });
277
265
 
278
- it('should return structured JSON with Grok3Mini', async function () {
279
- const model = ModelMix.new(setup).grok41();
266
+ it('should return structured JSON with Grok 4.3', async function () {
267
+ const model = ModelMix.new(setup).grok43();
280
268
 
281
269
  model.addText('Generate information about a fictional technology.');
282
270
 
@@ -287,7 +275,7 @@ describe('Live Integration Tests', function () {
287
275
  power: "1000 qubits"
288
276
  });
289
277
 
290
- console.log(`Grok3Mini JSON result:`, result);
278
+ console.log(`Grok 4.3 JSON result:`, result);
291
279
 
292
280
  expect(result).to.be.an('object');
293
281
  expect(result).to.have.property('name');