modelmix 5.3.0 → 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.
- package/.gitignore +138 -0
- package/README.md +10 -24
- package/demo/demo.js +2 -2
- package/demo/fallback.js +1 -1
- package/demo/free.js +3 -3
- package/demo/gemini.js +3 -3
- package/demo/json.js +0 -1
- package/demo/round-robin.js +2 -2
- package/demo/short.js +1 -1
- package/effort.js +1 -1
- package/index.d.ts +0 -12
- package/index.js +1 -50
- package/lib/model-chain.js +7 -7
- package/package.json +3 -4
- package/plugins/benchmark/test/benchmark.test.js +33 -29
- package/skills/modelmix/SKILL.md +23 -32
- package/test/abort.test.js +1 -1
- package/test/bottleneck.test.js +12 -12
- package/test/fallback.test.js +19 -23
- package/test/grok.test.js +1 -4
- package/test/history.test.js +2 -2
- package/test/images.test.js +3 -3
- package/test/json.test.js +2 -2
- package/test/live.test.js +6 -8
- package/test/provider-expansion.test.js +10 -17
- package/test/templates.test.js +32 -32
- package/test/tokens.test.js +5 -6
- package/demo/package-lock.json +0 -516
|
@@ -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: ['
|
|
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: '
|
|
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', '
|
|
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: ['
|
|
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: ['
|
|
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
|
-
|
|
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: ['
|
|
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: ['
|
|
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: ['
|
|
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', '
|
|
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', '
|
|
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', '
|
|
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: ['
|
|
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: ['
|
|
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: ['
|
|
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: ['
|
|
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:
|
|
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
|
-
|
|
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: ['
|
|
512
|
+
models: ['gpt52', 'gpt5mini']
|
|
509
513
|
}))
|
|
510
514
|
.addText('Complete the task.');
|
|
511
515
|
|
package/skills/modelmix/SKILL.md
CHANGED
|
@@ -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
|
-
.
|
|
87
|
+
.sonnet5() // primary
|
|
88
88
|
.gpt52() // fallback 1
|
|
89
89
|
.gemini38flash() // fallback 2
|
|
90
90
|
.addText("Hello!")
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
If `
|
|
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('
|
|
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 } }).
|
|
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-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: `
|
|
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()` `gpt6sol()` `gpt6luna()` `gpt56sol()` `gpt56terra()` `gpt56luna()` `gpt55()` `gpt55pro()` `gpt54()` `gpt54mini()` `gpt54nano()` `gpt54pro()` `
|
|
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.
|
|
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()` `
|
|
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
|
-
`
|
|
187
|
+
`gemini38flash()` `gemini37flash()` `gemini36flash()` `gemini35flash()` `gemini35flashLite()` `gemini31flashLite()`
|
|
188
188
|
|
|
189
189
|
### Grok
|
|
190
|
-
`grok47()` `grok46()` `
|
|
190
|
+
`grok47()` `grok46()` `grok43()`
|
|
191
191
|
|
|
192
192
|
### Perplexity
|
|
193
193
|
`sonar()` `sonarPro()`
|
|
194
194
|
|
|
195
195
|
### Together
|
|
196
|
-
`museGlimmer30b()` `qwen36plus()` `qwen37plus()` `GLM52()` `
|
|
196
|
+
`museGlimmer30b()` `qwen36plus()` `qwen37plus()` `GLM52()` `kimiK26()` `kimiK27Code()` `kimiK3()` `minimaxM27()` `minimaxM3()`
|
|
197
197
|
|
|
198
198
|
### NVIDIA
|
|
199
|
-
`museGlimmer30b()` `
|
|
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.
|
|
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()` `
|
|
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()` `
|
|
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()` `
|
|
227
|
+
`hermes3()` `kimiK26()`
|
|
232
228
|
|
|
233
229
|
### Local
|
|
234
230
|
`lmstudio()` — for LM Studio local models
|
|
@@ -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
|
-
.
|
|
613
|
-
.
|
|
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
|
-
|
|
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
|
-
|
|
628
|
-
openrouter: false,
|
|
629
|
-
|
|
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
|
-
}).
|
|
627
|
+
}).kimiK26();
|
|
637
628
|
```
|
|
638
629
|
|
|
639
630
|
## Agent Usage Rules
|
package/test/abort.test.js
CHANGED
|
@@ -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
|
-
.
|
|
446
|
+
.gpt52()
|
|
447
447
|
.addText('test');
|
|
448
448
|
|
|
449
449
|
const execution = model.raw(controller.signal);
|
package/test/bottleneck.test.js
CHANGED
|
@@ -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.
|
|
75
|
+
model.gpt52();
|
|
76
76
|
|
|
77
|
-
// Mock API 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.
|
|
119
|
+
model.gpt52();
|
|
120
120
|
|
|
121
|
-
// Mock API with delay to simulate concurrent requests (
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
448
|
+
model.gpt52();
|
|
449
449
|
|
|
450
450
|
nock('https://api.openai.com')
|
|
451
451
|
.post('/v1/responses')
|