modelmix 5.2.3 → 5.3.1

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.
@@ -1,7 +1,7 @@
1
1
  const { expect } = require('chai');
2
2
  const sinon = require('sinon');
3
3
 
4
- const { MixOpenAI, ModelMix } = require('../../..');
4
+ const { MixOpenAI, MixOpenAIResponses, ModelMix } = require('../../..');
5
5
  const { benchmark } = require('..');
6
6
 
7
7
  const criteria = {
@@ -53,18 +53,18 @@ function directContext(invoke, overrides = {}) {
53
53
 
54
54
  describe('Empty benchmark responses', () => {
55
55
  it('records a generation failure and never evaluates an empty candidate', async () => {
56
- const plugin = benchmark({ criteriaModel: 'gpt5nano', models: ['gpt5', 'gpt5mini'] });
56
+ const plugin = benchmark({ criteriaModel: 'gpt5nano', models: ['gpt52', 'gpt5mini'] });
57
57
  const result = await plugin.execute(directContext(async input => {
58
58
  if (input.system.includes('define evaluation criteria')) return metricsResult(JSON.stringify(criteria));
59
59
  if (input.system.includes('evaluate one candidate response')) {
60
60
  expect(input.messages[0].content).to.include('usable response');
61
61
  return metricsResult(validEvaluation());
62
62
  }
63
- return metricsResult(modelKey(input) === 'gpt-5' ? ' ' : 'usable response');
63
+ return metricsResult(modelKey(input) === 'gpt-5.2' ? ' ' : 'usable response');
64
64
  }));
65
65
  expect(result.benchmark.results[0]).to.include({ response: null, score: null });
66
66
  expect(result.benchmark.results[0].evaluationCount).to.deep.equal({ expected: 0, valid: 0 });
67
- expect(result.benchmark.errors[0]).to.include({ stage: 'response', participant: 'gpt5' });
67
+ expect(result.benchmark.errors[0]).to.include({ stage: 'response', participant: 'gpt52' });
68
68
  expect(result.benchmark.errors[0].error.message).to.include('no text response');
69
69
  });
70
70
  });
@@ -91,7 +91,7 @@ describe('benchmark plugin', () => {
91
91
  try {
92
92
  const plugin = benchmark({
93
93
  criteriaModel: 'deepseekV41Flash@20',
94
- models: ['deepseekV41Flash@60', 'gpt5'],
94
+ models: ['deepseekV41Flash@60', 'gpt52'],
95
95
  mix: { deepseek: true, openrouter: false }
96
96
  });
97
97
  const raw = await plugin.execute(directContext(async input => {
@@ -122,7 +122,7 @@ describe('benchmark plugin', () => {
122
122
 
123
123
  it('accepts a closing Markdown delimiter on criteria and evaluations without changing the response', async () => {
124
124
  const response = 'Candidate with ```text\ncontent\n```';
125
- const plugin = benchmark({ criteriaModel: 'gpt5nano', models: ['gpt5', 'sonnet5'] });
125
+ const plugin = benchmark({ criteriaModel: 'gpt5nano', models: ['gpt52', 'sonnet5'] });
126
126
  const raw = await plugin.execute(directContext(async input => {
127
127
  if (input.system.includes('define evaluation criteria')) {
128
128
  return metricsResult(JSON.stringify(criteria) + '```');
@@ -138,7 +138,7 @@ describe('benchmark plugin', () => {
138
138
  });
139
139
 
140
140
  it('still rejects invalid or incomplete evaluations with closing Markdown delimiters', async () => {
141
- const plugin = benchmark({ criteriaModel: 'gpt5nano', models: ['gpt5', 'sonnet5'] });
141
+ const plugin = benchmark({ criteriaModel: 'gpt5nano', models: ['gpt52', 'sonnet5'] });
142
142
  let evaluation = 0;
143
143
  const invalid = [validEvaluation() + ' explanation```', '{"scores":[]}```'];
144
144
  const raw = await plugin.execute(directContext(async input => {
@@ -209,7 +209,7 @@ describe('benchmark plugin', () => {
209
209
  }
210
210
 
211
211
  it('preserves Markdown fences inside valid JSON evaluations and the final JSON report', async () => {
212
- sinon.stub(MixOpenAI.prototype, 'create').callsFake(async ({ config }) => {
212
+ const respond = async ({ config }) => {
213
213
  if (config.system.includes('define evaluation criteria')) {
214
214
  return metricsResult(JSON.stringify(criteria));
215
215
  }
@@ -219,9 +219,11 @@ describe('benchmark plugin', () => {
219
219
  return metricsResult(JSON.stringify(value));
220
220
  }
221
221
  return metricsResult('```text\nA candidate answer\n```');
222
- });
222
+ };
223
+ sinon.stub(MixOpenAI.prototype, 'create').callsFake(respond);
224
+ sinon.stub(MixOpenAIResponses.prototype, 'create').callsFake(respond);
223
225
  const report = await ModelMix.new().use(benchmark({
224
- criteriaModel: 'gpt5nano', models: ['gpt5', 'gpt5mini']
226
+ criteriaModel: 'gpt5nano', models: ['gpt52', 'gpt5mini']
225
227
  })).addText('Evaluate the answer.').json();
226
228
  expect(report.errors).to.deep.equal([]);
227
229
  expect(report.results[0].response).to.equal('```text\nA candidate answer\n```');
@@ -229,7 +231,7 @@ describe('benchmark plugin', () => {
229
231
  });
230
232
 
231
233
  it('rejects truncated output with its finish reason and preserves the text and metrics', async () => {
232
- const plugin = benchmark({ criteriaModel: 'gpt5nano', models: ['gpt5', 'sonnet5'] });
234
+ const plugin = benchmark({ criteriaModel: 'gpt5nano', models: ['gpt52', 'sonnet5'] });
233
235
  const log = sinon.stub(console, 'log');
234
236
  const raw = await plugin.execute(directContext(async input => {
235
237
  if (input.system.includes('define evaluation criteria')) {
@@ -258,7 +260,7 @@ describe('benchmark plugin', () => {
258
260
  const calls = [];
259
261
  const plugin = benchmark({
260
262
  criteriaModel: 'gpt5nano@10',
261
- models: ['gpt5@0', 'gpt5mini@25', 'sonnet5@50']
263
+ models: ['gpt52@0', 'gpt5mini@25', 'sonnet5@50']
262
264
  });
263
265
  const context = directContext(async input => {
264
266
  calls.push(input);
@@ -279,7 +281,7 @@ describe('benchmark plugin', () => {
279
281
  expect(report.criteria.model).to.include({ id: 'gpt5nano@10', effort: 10 });
280
282
  expect(report.results).to.have.length(3);
281
283
  expect(report.results.map(result => result.response)).to.deep.equal([
282
- 'response:gpt-5',
284
+ 'response:gpt-5.2',
283
285
  'response:gpt-5-mini',
284
286
  'response:claude-sonnet-5'
285
287
  ]);
@@ -312,7 +314,7 @@ describe('benchmark plugin', () => {
312
314
  it('treats aliases as one model for duplicates and self-evaluation exclusion', async () => {
313
315
  const plugin = benchmark({
314
316
  criteriaModel: 'gpt5nano',
315
- models: ['sonnet5@10', 'sonnet50@20', 'gpt5@30']
317
+ models: ['sonnet5@10', 'sonnet50@20', 'gpt52@30']
316
318
  });
317
319
  const raw = await plugin.execute(directContext(async input => {
318
320
  if (input.system.includes('define evaluation criteria')) {
@@ -329,8 +331,8 @@ describe('benchmark plugin', () => {
329
331
  { expected: 1, valid: 1 },
330
332
  { expected: 2, valid: 2 }
331
333
  ]);
332
- expect(raw.benchmark.results[0].evaluations[0].judge.canonicalModel).to.equal('gpt-5');
333
- expect(raw.benchmark.results[1].evaluations[0].judge.canonicalModel).to.equal('gpt-5');
334
+ expect(raw.benchmark.results[0].evaluations[0].judge.canonicalModel).to.equal('gpt-5.2');
335
+ expect(raw.benchmark.results[1].evaluations[0].judge.canonicalModel).to.equal('gpt-5.2');
334
336
  expect(raw.benchmark.results[2].evaluations.map(item => item.judge.id)).to.deep.equal([
335
337
  'sonnet5@10',
336
338
  'sonnet50@20'
@@ -338,7 +340,7 @@ describe('benchmark plugin', () => {
338
340
 
339
341
  const duplicate = benchmark({
340
342
  criteriaModel: 'gpt5nano',
341
- models: ['sonnet5@100', 'sonnet50@100', 'gpt5']
343
+ models: ['sonnet5@100', 'sonnet50@100', 'gpt52']
342
344
  });
343
345
  await expectRejection(duplicate.execute(directContext(async () => {
344
346
  throw new Error('should not be called');
@@ -346,7 +348,7 @@ describe('benchmark plugin', () => {
346
348
 
347
349
  const inheritedDuplicate = benchmark({
348
350
  criteriaModel: 'gpt5nano',
349
- models: ['sonnet5', 'sonnet50', 'gpt5']
351
+ models: ['sonnet5', 'sonnet50', 'gpt52']
350
352
  });
351
353
  await expectRejection(inheritedDuplicate.execute(directContext(async () => {
352
354
  throw new Error('should not be called');
@@ -367,7 +369,7 @@ describe('benchmark plugin', () => {
367
369
  const evaluationCalls = [];
368
370
  const plugin = benchmark({
369
371
  criteriaModel: 'gpt5nano',
370
- models: ['gpt5', 'gpt5mini', 'sonnet5']
372
+ models: ['gpt52', 'gpt5mini', 'sonnet5']
371
373
  });
372
374
  const raw = await plugin.execute(directContext(async input => {
373
375
  if (input.system.includes('define evaluation criteria')) {
@@ -382,7 +384,7 @@ describe('benchmark plugin', () => {
382
384
  }
383
385
  return metricsResult(validEvaluation(9, 7));
384
386
  }
385
- if (modelKey(input) === 'gpt-5') throw new Error('participant unavailable');
387
+ if (modelKey(input) === 'gpt-5.2') throw new Error('participant unavailable');
386
388
  return metricsResult(`response:${modelKey(input)}`);
387
389
  }));
388
390
 
@@ -395,11 +397,11 @@ describe('benchmark plugin', () => {
395
397
  expect(sonnet.evaluationCount).to.deep.equal({ expected: 2, valid: 2 });
396
398
  expect(sonnet.score).to.equal(8);
397
399
  expect(evaluationCalls).to.deep.include({
398
- judge: 'gpt-5',
400
+ judge: 'gpt-5.2',
399
401
  response: 'response:gpt-5-mini'
400
402
  });
401
403
  expect(evaluationCalls).to.deep.include({
402
- judge: 'gpt-5',
404
+ judge: 'gpt-5.2',
403
405
  response: 'response:claude-sonnet-5'
404
406
  });
405
407
  expect(raw.benchmark.errors.map(error => error.stage)).to.deep.equal([
@@ -414,7 +416,7 @@ describe('benchmark plugin', () => {
414
416
  it('rejects incomplete evaluations without using any of their scores', async () => {
415
417
  const plugin = benchmark({
416
418
  criteriaModel: 'gpt5nano',
417
- models: ['gpt5', 'sonnet5']
419
+ models: ['gpt52', 'sonnet5']
418
420
  });
419
421
  const raw = await plugin.execute(directContext(async input => {
420
422
  if (input.system.includes('define evaluation criteria')) {
@@ -446,7 +448,7 @@ describe('benchmark plugin', () => {
446
448
  it('aborts when criteria generation fails and propagates cancellation', async () => {
447
449
  const invalidCriteria = benchmark({
448
450
  criteriaModel: 'gpt5nano',
449
- models: ['gpt5', 'sonnet5']
451
+ models: ['gpt52', 'sonnet5']
450
452
  });
451
453
  await expectRejection(invalidCriteria.execute(directContext(async () => (
452
454
  metricsResult(JSON.stringify({ criteria: [] }))
@@ -465,7 +467,7 @@ describe('benchmark plugin', () => {
465
467
  it('logs intermediate progress when ModelMix debug is enabled', async () => {
466
468
  const plugin = benchmark({
467
469
  criteriaModel: 'gpt5nano',
468
- models: ['gpt5', 'sonnet5']
470
+ models: ['gpt52', 'sonnet5']
469
471
  });
470
472
  const log = sinon.stub(console, 'log');
471
473
  try {
@@ -486,13 +488,13 @@ describe('benchmark plugin', () => {
486
488
 
487
489
  const output = log.args.flat().join('\n');
488
490
  expect(output).to.include('[benchmark] Generating criteria with gpt5nano.');
489
- expect(output).to.include('[benchmark] Running response 1/2: gpt5.');
491
+ expect(output).to.include('[benchmark] Running response 1/2: gpt52.');
490
492
  expect(output).to.include('[benchmark] Running evaluation 2/2:');
491
493
  expect(output).to.include('[benchmark] Completed benchmark');
492
494
  });
493
495
 
494
496
  it('integrates with ModelMix json() and exposes the same report through lastRaw', async () => {
495
- sinon.stub(MixOpenAI.prototype, 'create').callsFake(async ({ config, options }) => {
497
+ const respond = async ({ config, options }) => {
496
498
  if (config.system.includes('define evaluation criteria')) {
497
499
  return metricsResult(JSON.stringify(criteria));
498
500
  }
@@ -501,11 +503,13 @@ describe('benchmark plugin', () => {
501
503
  }
502
504
  expect(options).to.not.have.property('response_format');
503
505
  return metricsResult(`response:${options.model}`);
504
- });
506
+ };
507
+ sinon.stub(MixOpenAI.prototype, 'create').callsFake(respond);
508
+ sinon.stub(MixOpenAIResponses.prototype, 'create').callsFake(respond);
505
509
  const model = ModelMix.new()
506
510
  .use(benchmark({
507
511
  criteriaModel: 'gpt5nano',
508
- models: ['gpt5', 'gpt5mini']
512
+ models: ['gpt52', 'gpt5mini']
509
513
  }))
510
514
  .addText('Complete the task.');
511
515
 
@@ -84,13 +84,13 @@ Chain shorthand methods to attach providers. First model is primary; others are
84
84
 
85
85
  ```javascript
86
86
  const model = ModelMix.new()
87
- .sonnet46() // primary
87
+ .sonnet5() // primary
88
88
  .gpt52() // fallback 1
89
89
  .gemini38flash() // fallback 2
90
90
  .addText("Hello!")
91
91
  ```
92
92
 
93
- If `sonnet46` fails, it automatically tries `gpt52`, then `gemini38flash`.
93
+ If `sonnet5` fails, it automatically tries `gpt52`, then `gemini38flash`.
94
94
 
95
95
  The equivalent `chain()` form accepts public shortcut names directly in the
96
96
  same order. Append `@effort` for a per-model unified effort override (`-1` or
@@ -99,7 +99,7 @@ provider default when no chain effort is configured:
99
99
 
100
100
  ```javascript
101
101
  const model = ModelMix.new()
102
- .chain('sonnet46', 'gpt52@20', 'gemini38flash@-1')
102
+ .chain('sonnet5', 'gpt52@20', 'gemini38flash@-1')
103
103
  .addText('Hello!');
104
104
  ```
105
105
 
@@ -147,7 +147,7 @@ With plugin tools, native `options.tools` entries are combined with registered a
147
147
  Provider-agnostic reasoning intensity. **Not** an `options` field — use `config.effort` or `.effort(n)`.
148
148
 
149
149
  ```javascript
150
- ModelMix.new({ config: { effort: 40 } }).sonnet46().addText('Plan this refactor').message();
150
+ ModelMix.new({ config: { effort: 40 } }).sonnet5().addText('Plan this refactor').message();
151
151
  ModelMix.new().deepseekV4Flash({ config: { effort: 100 } }).addText('...').message();
152
152
  ModelMix.new().effort(-1).minimaxM3().addText('Quick question').message();
153
153
 
@@ -164,7 +164,7 @@ ModelMix.new({ config: { effort: 80 } })
164
164
  | DeepSeek V4 | off | `low`↑ | `high`↑ | `high`↑ | `max`↑ | — |
165
165
  | MiniMax M3 | off | adaptive | adaptive | adaptive | adaptive | adaptive |
166
166
 
167
- \* GPT-6 Astra maps 0–39 / 40–59 / 60–79 / 80–99 / 100 to `low` / `medium` / `high` / `xhigh` / `max`. GPT-5.6 maps `100` to `max`; 80–99 remains `xhigh`. Qwen 3.8 27B and Flash map 0–39 / 40–79 / 80–100 to `low` / `medium` / `xhigh`; Qwen 3.8 Flash is the managed production version based on Flash-Next. GLM 5.3 and GLM 5.3 Flash require reasoning and map those bands to `low` / `high` / `max`. Gemini bands: 0–24 / 25–49 / 50–74 / 75–100. Gemini 3.8 Flash and 3.7 Flash support only `low` / `medium` / `high`, so the first two bands clamp to `low`; `-1` keeps their native `medium` default. 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`. Grok 4.6 maps 0–39 / 40–59 / 60–79 / 80–100 to `low` / `medium` / `high` / `xhigh`; without effort it uses native `high`. `-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.
167
+ \* GPT-6 Astra maps 0–39 / 40–59 / 60–79 / 80–99 / 100 to `low` / `medium` / `high` / `xhigh` / `max`. GPT-6 Sol, GPT-6 Luna, and GPT-5.6 map `100` to `max` and keep `xhigh` for 80–99, with OpenAI's `none` band at 0–19. Qwen 3.8 27B and Flash map 0–39 / 40–79 / 80–100 to `low` / `medium` / `xhigh`; Qwen 3.8 Flash is the managed production version based on Flash-Next. GLM 5.3 and GLM 5.3 Flash require reasoning and map those bands to `low` / `high` / `max`. Gemini bands: 0–24 / 25–49 / 50–74 / 75–100. Gemini 3.8 Flash and 3.7 Flash support only `low` / `medium` / `high`, so the first two bands clamp to `low`; `-1` keeps their native `medium` default. 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`. Grok 4.6 maps 0–39 / 40–59 / 60–79 / 80–100 to `low` / `medium` / `high` / `xhigh`; without effort it uses native `high`. `-1` = adaptive/dynamic when available, else no-op. Levels clamp per model. Former `*think()` methods are removed — use `.effort(n).<model>()`. Kimi: `kimiK26()`.
168
168
 
169
169
  ## Available Model Shorthands
170
170
 
@@ -172,31 +172,31 @@ ModelMix.new({ config: { effort: 80 } })
172
172
 
173
173
  Use `ModerationMix.new().openai()` with `.raw()` to classify text and images through OpenAI's Moderations endpoint. Read the results from `raw.moderation`. `ModerationMix` accepts moderation providers as ordered fallbacks, rejects generative providers, and does not generate text or support streaming.
174
174
 
175
- `gpt6astra()` `gpt56sol()` `gpt56terra()` `gpt56luna()` `gpt55()` `gpt55pro()` `gpt54()` `gpt54mini()` `gpt54nano()` `gpt54pro()` `gpt53codex()` `gpt53chat()` `gpt52()` `gpt51()` `gpt5()` `gpt5mini()` `gpt5nano()` `gptRealtime()` `gptRealtimeMini()` `gptOss()`
175
+ `gpt6astra()` `gpt6sol()` `gpt6luna()` `gpt56sol()` `gpt56terra()` `gpt56luna()` `gpt55()` `gpt55pro()` `gpt54()` `gpt54mini()` `gpt54nano()` `gpt54pro()` `gpt52()` `gpt5mini()` `gpt5nano()` `gptRealtime()` `gptRealtimeMini()`
176
176
 
177
- Every textual GPT-5 and GPT-6 shortcut registers only the official OpenAI model by default. Pass `mix: { openrouter: true }` to `ModelMix.new()` or to an individual shortcut to append its `openai/*` OpenRouter route as a fallback. `gpt53chat()` uses `gpt-5.3-chat-latest` officially and `openai/gpt-5.3-chat` through OpenRouter. Both API keys are required when that fallback is enabled. Realtime shortcuts remain official-only.
177
+ Every textual GPT-5 and GPT-6 shortcut registers only the official OpenAI model by default. Pass `mix: { openrouter: true }` to `ModelMix.new()` or to an individual shortcut to append its `openai/*` OpenRouter route as a fallback. Both API keys are required when that fallback is enabled. Realtime shortcuts remain official-only.
178
178
 
179
179
  ### Anthropic
180
- `fable51()` `fable50()` `opus55()` `opus50()` `opus48()` `opus47()` `opus46()` `sonnet5()` `sonnet46()` `sonnet45()` `haiku45()`
180
+ `fable51()` `fable50()` `opus55()` `opus50()` `opus48()` `opus47()` `opus46()` `sonnet5()` `sonnet45()` `haiku45()`
181
181
 
182
182
  Use `.effort(n)` (or `config.effort`) to enable Anthropic thinking — e.g. `.effort(100).opus50()`. `fable5()` and `opus5()` remain available as compatibility aliases.
183
183
 
184
184
  `fable51()` registers `claude-fable-5-1` through Anthropic by default. Pass `mix: { openrouter: true }` to append `anthropic/claude-fable-5.1` as its fallback.
185
185
 
186
186
  ### Google
187
- `gemini31pro()` `gemini38flash()` `gemini37flash()` `gemini36flash()` `gemini35flash()` `gemini35flashLite()` `gemini31flashLite()`
187
+ `gemini38flash()` `gemini37flash()` `gemini36flash()` `gemini35flash()` `gemini35flashLite()` `gemini31flashLite()`
188
188
 
189
189
  ### Grok
190
- `grok47()` `grok46()` `grok45()` `grok43()` `grok420multiAgent()` `grok420()`
190
+ `grok47()` `grok46()` `grok43()`
191
191
 
192
192
  ### Perplexity
193
193
  `sonar()` `sonarPro()`
194
194
 
195
195
  ### Together
196
- `museGlimmer30b()` `qwen36plus()` `qwen37plus()` `GLM52()` `kimiK25()` `kimiK26()` `kimiK27Code()` `kimiK3()` `minimaxM27()` `minimaxM3()` `gptOss()`
196
+ `museGlimmer30b()` `qwen36plus()` `qwen37plus()` `GLM52()` `kimiK26()` `kimiK27Code()` `kimiK3()` `minimaxM27()` `minimaxM3()`
197
197
 
198
198
  ### NVIDIA
199
- `museGlimmer30b()` `gptOss()` `minimaxM27()`
199
+ `museGlimmer30b()` `minimaxM27()`
200
200
 
201
201
  ### Moonshot
202
202
  `kimiK3()` — requires `MOONSHOT_API_KEY`; Fireworks, OpenRouter, and Together are available through `mix`.
@@ -208,12 +208,10 @@ Use `.effort(n)` (or `config.effort`) to enable Anthropic thinking — e.g. `.ef
208
208
  `mimo26pro()` — uses OpenRouter (`xiaomi/mimo-v2.6-pro`) by default; the native API requires `MIMO_API_KEY` and `mix: { mimo: true }`.
209
209
 
210
210
  ### DeepSeek
211
- `deepseekV41Flash({ mix: { deepseek: true, openrouter: false } })` uses the native API at `https://api.deepseek.com/chat/completions` with model `deepseek-flash` (currently V4.1 Flash). Requires `DEEPSEEK_API_KEY`; `MixDeepSeek` supports explicit `.attach()` calls. Unified effort and native cache usage are supported, and assistant reasoning is preserved for tool continuations. Cost estimates use peak rates per 1M tokens: $0.30 input, $0.006 cached input, $1.20 output; off-peak charges are half. Enabling all three providers orders them DeepSeek → Fireworks → OpenRouter.
211
+ `deepseekV41Flash({ mix: { deepseek: true, openrouter: false } })` uses the native API at `https://api.deepseek.com/chat/completions` with model `deepseek-flash` (currently V4.1 Flash). Requires `DEEPSEEK_API_KEY`; `MixDeepSeek` supports explicit `.attach()` calls. Unified effort and native cache usage are supported, and assistant reasoning is preserved for tool continuations. Cost estimates use peak rates per 1M tokens: $0.30 input, $0.006 cached input, $1.20 output; off-peak charges are half. Optional routes: `mix: { fireworks: true }` selects `accounts/fireworks/models/deepseek-v4p1-flash` with `FIREWORKS_API_KEY`, and `mix: { openrouter: true }` selects `deepseek/deepseek-v4.1-flash` (text and image input) with `OPENROUTER_API_KEY`; enabling all three routes orders them DeepSeek → Fireworks → OpenRouter.
212
212
 
213
213
  ### Fireworks
214
- `museGlimmer30b()` `gptOss()` `qwen36plus()` (private/on-demand only) `qwen37plus()` `qwen38max()` `deepseekV4Flash()` `deepseekV4Pro()` `kimiK26()` `kimiK27Code()` `kimiK3()` `minimaxM27()` `minimaxM3()` `GLM52()`
215
-
216
- `deepseekV41Flash({ mix: { fireworks: true, openrouter: false } })` selects `accounts/fireworks/models/deepseek-v4p1-flash` and requires `FIREWORKS_API_KEY`. Per 1M tokens: $0.22 input, $0.007 cached input, $0.66 output. Set both providers to `true` for Fireworks followed by OpenRouter fallback; no arguments selects OpenRouter.
214
+ `museGlimmer30b()` `qwen36plus()` (private/on-demand only) `qwen37plus()` `qwen38max()` `deepseekV4Flash()` `deepseekV4Pro()` `kimiK26()` `kimiK27Code()` `kimiK3()` `minimaxM27()` `minimaxM3()` `GLM52()`
217
215
 
218
216
  ### Cerebras
219
217
  `GLM46()`
@@ -221,14 +219,12 @@ Use `.effort(n)` (or `config.effort`) to enable Anthropic thinking — e.g. `.ef
221
219
  ### OpenRouter
222
220
  `deepseekPro()` uses `deepseek/deepseek-v4-pro-0813` through OpenRouter. Requires `OPENROUTER_API_KEY`; supports the DeepSeek effort mapping and `chain('deepseekPro@100')`. Base cost estimates per 1M tokens: $0.5808 input, $0.05808 cached input, $1.7424 output. Actual rates may change, including provider and time-based pricing.
223
221
 
224
- `museGlimmer30b()` `museSpark12()` `museSpark12c()` `museSpark13()` `museSpark13c()` `gptOss()` `qwen35397b()` `qwen36plus()` `qwen37plus()` `qwen3827b()` `qwen38flash()` `hermes470b()` `hermes4405b()` `qwen38max()` `kimiK27Code()` `kimiK3()` `minimaxM27()` `minimaxM3()` `GLM45()` `GLM52()` `GLM53()` `GLM53Flash()` `deepseekV41Flash()`
225
-
226
- `deepseekV41Flash()` selects `deepseek/deepseek-v4.1-flash` (text and image input) and requires `OPENROUTER_API_KEY`. It supports the DeepSeek effort mapping above, including `chain('deepseekV41Flash@100')`. Base cost estimates per 1M tokens: $0.15 input, $0.015 cached input, $0.60 output; actual OpenRouter pricing varies by provider and time.
222
+ `museGlimmer30b()` `museSpark12()` `museSpark12c()` `museSpark13()` `museSpark13c()` `qwen35397b()` `qwen36plus()` `qwen37plus()` `qwen3827b()` `qwen38flash()` `hermes470b()` `hermes4405b()` `qwen38max()` `kimiK27Code()` `kimiK3()` `minimaxM27()` `minimaxM3()` `GLM45()` `GLM52()` `GLM53()` `GLM53Flash()`
227
223
 
228
224
  Muse Spark: the `c` suffix selects Contributor, where prompts and outputs may be used to improve Meta products. Without `c`, the standard tier is selected. Use `museSpark12c()` for the former `museSpark12()` Contributor behavior.
229
225
 
230
226
  ### Multi-provider (auto-fallback across free/paid tiers)
231
- `hermes3()` `kimiK25()`
227
+ `hermes3()` `kimiK26()`
232
228
 
233
229
  ### Local
234
230
  `lmstudio()` — for LM Studio local models
@@ -411,7 +407,7 @@ console.log(model.lastRaw.think); // reasoning content (if available)
411
407
  console.log(model.lastRaw.response); // raw API response
412
408
  ```
413
409
 
414
- ### GPT-5.6 explicit prompt caching
410
+ ### GPT-5.6 and GPT-6 explicit prompt caching
415
411
 
416
412
  ```javascript
417
413
  const model = ModelMix.new()
@@ -434,9 +430,9 @@ const model = ModelMix.new()
434
430
  const result = await model.raw();
435
431
  ```
436
432
 
437
- `cache: { breakpoint: true }` is provider-neutral: GPT-5.6 receives `prompt_cache_breakpoint`, Anthropic receives `cache_control`, and unsupported providers omit it. Keep native request policies inside each model shorthand so they do not leak across fallbacks. Anthropic usage separates `cacheWrite5m` and `cacheWrite1h`; `cacheWrite` stays as their compatible aggregate.
433
+ `cache: { breakpoint: true }` is provider-neutral: GPT-5.6 and GPT-6 receive `prompt_cache_breakpoint`, Anthropic receives `cache_control`, and unsupported providers omit it. Keep native request policies inside each model shorthand so they do not leak across fallbacks. Anthropic usage separates `cacheWrite5m` and `cacheWrite1h`; `cacheWrite` stays as their compatible aggregate.
438
434
 
439
- GPT-5.6 replaces `prompt_cache_retention` with `prompt_cache_options.ttl`. Explicit breakpoints also work on image methods and Responses-native `input_text`, `input_image`, and `input_file` blocks. Prompts need at least 1,024 tokens to be cached. Requests over 272K input tokens use 2× input and 1.5× output prices for the complete request; ModelMix applies these multipliers to `cost`, `costBreakdown`, and cache economics.
435
+ GPT-5.6 and GPT-6 replace `prompt_cache_retention` with `prompt_cache_options.ttl`. Explicit breakpoints also work on image methods and Responses-native `input_text`, `input_image`, and `input_file` blocks. Prompts need at least 1,024 tokens to be cached. Requests over 272K input tokens use 2× input and 1.5× output prices for the complete request; ModelMix applies these multipliers to `cost`, `costBreakdown`, and cache economics.
440
436
 
441
437
  ### Add images
442
438
 
@@ -609,13 +605,13 @@ For full debug output, also set: `DEBUG=ModelMix* node script.js`
609
605
 
610
606
  ```javascript
611
607
  const model = ModelMix.new()
612
- .gptOss()
613
- .kimiK25()
608
+ .kimiK26()
609
+ .GLM52()
614
610
  .addText("What is the capital of France?");
615
611
  console.log(await model.message());
616
612
  ```
617
613
 
618
- These use providers with free quotas (Groq, Cerebras, and Together). OpenRouter is disabled because its GPT-OSS 120B route is no longer free. If one runs out of quota, ModelMix falls back to the next.
614
+ Each shortcut registers its default provider and accepts additional providers through `mix`. If one model fails, ModelMix falls back to the next.
619
615
 
620
616
  ### Multi-provider routing
621
617
 
@@ -624,16 +620,11 @@ Some model shorthands register the same model across multiple providers for maxi
624
620
  ```javascript
625
621
  const model = ModelMix.new({
626
622
  mix: {
627
- anthropic: true, // fable51() default: true
628
- openrouter: false, // fallback default: false
629
- cerebras: true, // default: true
630
- groq: true, // default: true
631
- together: false, // default: false
632
- lambda: false, // default: false
633
- minimax: false, // default: false
634
- fireworks: false // default: false
623
+ fireworks: true, // kimiK26() default: true
624
+ openrouter: false, // default: false
625
+ together: false // default: false
635
626
  }
636
- }).kimiK25();
627
+ }).kimiK26();
637
628
  ```
638
629
 
639
630
  ## Agent Usage Rules
@@ -443,7 +443,7 @@ describe('AbortSignal execution contract', () => {
443
443
  });
444
444
  });
445
445
  const model = ModelMix.new({ config: { bottleneck: { maxConcurrent: 1, minTime: 0 } } })
446
- .gpt51()
446
+ .gpt52()
447
447
  .addText('test');
448
448
 
449
449
  const execution = model.raw(controller.signal);
@@ -72,9 +72,9 @@ describe('Rate Limiting with Bottleneck Tests', () => {
72
72
  it('should enforce minimum time between requests', async () => {
73
73
  const startTimes = [];
74
74
 
75
- model.gpt51();
75
+ model.gpt52();
76
76
 
77
- // Mock API responses (gpt51 uses /v1/responses)
77
+ // Mock API responses (gpt52 uses /v1/responses)
78
78
  nock('https://api.openai.com')
79
79
  .post('/v1/responses')
80
80
  .times(3)
@@ -116,9 +116,9 @@ describe('Rate Limiting with Bottleneck Tests', () => {
116
116
  }
117
117
  });
118
118
 
119
- model.gpt51();
119
+ model.gpt52();
120
120
 
121
- // Mock API with delay to simulate concurrent requests (gpt51 uses /v1/responses)
121
+ // Mock API with delay to simulate concurrent requests (gpt52 uses /v1/responses)
122
122
  nock('https://api.openai.com')
123
123
  .post('/v1/responses')
124
124
  .times(5)
@@ -171,7 +171,7 @@ describe('Rate Limiting with Bottleneck Tests', () => {
171
171
  it('should apply rate limiting to OpenAI requests', async () => {
172
172
  const requestTimes = [];
173
173
 
174
- model.gpt51();
174
+ model.gpt52();
175
175
 
176
176
  nock('https://api.openai.com')
177
177
  .post('/v1/responses')
@@ -195,7 +195,7 @@ describe('Rate Limiting with Bottleneck Tests', () => {
195
195
  it('should apply rate limiting to Anthropic requests', async () => {
196
196
  const requestTimes = [];
197
197
 
198
- model.sonnet46();
198
+ model.sonnet5();
199
199
 
200
200
  nock('https://api.anthropic.com')
201
201
  .post('/v1/messages')
@@ -247,7 +247,7 @@ describe('Rate Limiting with Bottleneck Tests', () => {
247
247
  });
248
248
 
249
249
  it('should handle rate limiting with API errors', async () => {
250
- model.gpt51({ mix: { openrouter: false } });
250
+ model.gpt52({ mix: { openrouter: false } });
251
251
 
252
252
  nock('https://api.openai.com')
253
253
  .post('/v1/responses')
@@ -269,7 +269,7 @@ describe('Rate Limiting with Bottleneck Tests', () => {
269
269
  it('should continue rate limiting after errors', async () => {
270
270
  const requestTimes = [];
271
271
 
272
- model.gpt51();
272
+ model.gpt52();
273
273
 
274
274
  // First request fails
275
275
  nock('https://api.openai.com')
@@ -319,7 +319,7 @@ describe('Rate Limiting with Bottleneck Tests', () => {
319
319
  }
320
320
  });
321
321
 
322
- model.gpt51();
322
+ model.gpt52();
323
323
 
324
324
  let requestCount = 0;
325
325
 
@@ -360,7 +360,7 @@ describe('Rate Limiting with Bottleneck Tests', () => {
360
360
  }
361
361
  });
362
362
 
363
- model.gpt51();
363
+ model.gpt52();
364
364
 
365
365
  const results = [];
366
366
 
@@ -403,7 +403,7 @@ describe('Rate Limiting with Bottleneck Tests', () => {
403
403
  }
404
404
  });
405
405
 
406
- model.gpt51();
406
+ model.gpt52();
407
407
 
408
408
  nock('https://api.openai.com')
409
409
  .post('/v1/responses')
@@ -445,7 +445,7 @@ describe('Rate Limiting with Bottleneck Tests', () => {
445
445
  done();
446
446
  });
447
447
 
448
- model.gpt51();
448
+ model.gpt52();
449
449
 
450
450
  nock('https://api.openai.com')
451
451
  .post('/v1/responses')
@@ -90,6 +90,15 @@ describe('Unified effort scale', () => {
90
90
  }
91
91
  });
92
92
 
93
+ it('maps GPT-6 Sol and Luna effort from none to max', () => {
94
+ for (const model of ['gpt-6-sol', 'gpt-6-luna']) {
95
+ for (const [effort, level] of [[0, 'none'], [20, 'low'], [40, 'medium'], [60, 'high'], [99, 'xhigh'], [100, 'max']]) {
96
+ expect(mapEffort('openai', effort, model)).to.deep.equal({ reasoning_effort: level });
97
+ }
98
+ expect(mapEffort('openai', 100, `openai/${model}`)).to.deep.equal({ reasoning_effort: 'max' });
99
+ }
100
+ });
101
+
93
102
  it('sets OpenAI adaptive only when supported (otherwise no-op)', () => {
94
103
  expect(mapEffort('openai', -1)).to.equal(null);
95
104
  expect(mapEffort('openai', -1, 'gpt-5.2')).to.equal(null);