modelmix 5.1.10 → 5.1.15

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.
Files changed (45) hide show
  1. package/README.md +42 -10
  2. package/demo/free.js +1 -3
  3. package/demo/short.js +15 -13
  4. package/effort.js +13 -7
  5. package/http-client.js +6 -6
  6. package/index.d.ts +13 -6
  7. package/index.js +204 -144
  8. package/lib/abort-signal.js +57 -0
  9. package/lib/model-chain.js +2 -2
  10. package/lib/provider-api-key.js +9 -0
  11. package/lib/providers/anthropic.js +24 -22
  12. package/lib/providers/base.js +21 -28
  13. package/lib/providers/google.js +12 -8
  14. package/lib/providers/openai-compatible.js +19 -48
  15. package/lib/providers/openai-options.js +16 -0
  16. package/lib/providers/openai.js +48 -47
  17. package/lib/providers.js +5 -2
  18. package/lib/token-usage.js +19 -4
  19. package/mcp-tools.js +5 -2
  20. package/package.json +3 -2
  21. package/plugins/rlm/index.d.ts +1 -0
  22. package/plugins/rlm/lib/budget.js +2 -7
  23. package/plugins/rlm/lib/isolated-vm-sandbox.js +8 -1
  24. package/plugins/rlm/lib/planner-prompt.js +1 -7
  25. package/plugins/rlm/lib/plugin.js +24 -11
  26. package/plugins/rlm/lib/validation.js +14 -0
  27. package/plugins/rlm/lib/variable-descriptors.js +1 -5
  28. package/plugins/rlm/lib/worker-catalog.js +1 -5
  29. package/plugins/rlm/test/isolated-vm-sandbox.test.js +23 -0
  30. package/pnpm-workspace.yaml +10 -5
  31. package/schema.js +0 -28
  32. package/skills/modelmix/SKILL.md +34 -12
  33. package/test/abort.test.js +517 -0
  34. package/test/anthropic.test.js +77 -1
  35. package/test/bottleneck.test.js +2 -2
  36. package/test/effort.test.js +24 -1
  37. package/test/fallback.test.js +101 -1
  38. package/test/glm.test.js +2 -6
  39. package/test/kimi.test.js +2 -6
  40. package/test/live.test.js +10 -5
  41. package/test/muse.test.js +24 -7
  42. package/test/provider-expansion.test.js +17 -0
  43. package/test/public-api.test.js +2 -2
  44. package/test/qwen.test.js +2 -3
  45. package/test/tokens.test.js +23 -8
@@ -1,8 +1,84 @@
1
1
  const { expect } = require('chai');
2
2
  const nock = require('nock');
3
- const { ModelMix, MixAnthropic } = require('../index.js');
3
+ const { ModelMix, MixAnthropic, MixOpenRouter } = require('../index.js');
4
4
 
5
5
  describe('Anthropic Model Registration Tests', () => {
6
+ it('should register Claude Fable 5.1 through Anthropic by default', () => {
7
+ const model = ModelMix.new().fable51();
8
+
9
+ expect(model.models.map(({ key }) => key)).to.deep.equal(['claude-fable-5-1']);
10
+ expect(model.models[0].provider).to.be.instanceOf(MixAnthropic);
11
+ });
12
+
13
+ it('should allow enabling or selecting the OpenRouter Claude Fable 5.1 route', () => {
14
+ const both = ModelMix.new().fable51({ mix: { openrouter: true } });
15
+ const routed = ModelMix.new().fable51({
16
+ mix: { anthropic: false, openrouter: true }
17
+ });
18
+
19
+ expect(both.models.map(({ key }) => key)).to.deep.equal([
20
+ 'claude-fable-5-1',
21
+ 'anthropic/claude-fable-5.1'
22
+ ]);
23
+ expect(both.models[1].provider).to.be.instanceOf(MixOpenRouter);
24
+ expect(routed.models.map(({ key }) => key)).to.deep.equal(['anthropic/claude-fable-5.1']);
25
+ expect(routed.models[0].provider).to.be.instanceOf(MixOpenRouter);
26
+ });
27
+
28
+ it('should strip unsupported sampling params from OpenRouter Claude Fable 5.1 requests', async () => {
29
+ const provider = new MixOpenRouter();
30
+ let requestBody;
31
+ nock('https://openrouter.ai')
32
+ .post('/api/v1/chat/completions', body => {
33
+ requestBody = body;
34
+ return true;
35
+ })
36
+ .reply(200, {
37
+ choices: [{ message: { content: 'Done' } }],
38
+ usage: { prompt_tokens: 1, completion_tokens: 1 }
39
+ });
40
+
41
+ await provider.create({
42
+ config: { system: 'You are an assistant.' },
43
+ options: {
44
+ model: 'anthropic/claude-fable-5.1',
45
+ messages: [{ role: 'user', content: 'Hello' }],
46
+ max_tokens: 100,
47
+ temperature: 1,
48
+ top_p: 0.9,
49
+ top_k: 40
50
+ }
51
+ });
52
+
53
+ expect(requestBody).to.not.have.property('temperature');
54
+ expect(requestBody).to.not.have.property('top_p');
55
+ expect(requestBody).to.not.have.property('top_k');
56
+ expect(requestBody.model).to.equal('anthropic/claude-fable-5.1');
57
+ });
58
+
59
+ it('should price Claude Fable 5.1 cache usage equally across providers', () => {
60
+ const tokens = {
61
+ input: 3_000_000,
62
+ uncachedInput: 1_000_000,
63
+ cached: 1_000_000,
64
+ cacheWrite: 1_000_000,
65
+ cacheWrite5m: 1_000_000,
66
+ output: 1_000_000
67
+ };
68
+ const expected = {
69
+ uncachedInput: 10,
70
+ cachedInput: 0.25,
71
+ cacheWrite: 12.5,
72
+ cacheWrite5m: 12.5,
73
+ cacheWrite1h: 0,
74
+ output: 50,
75
+ total: 72.75
76
+ };
77
+
78
+ expect(ModelMix.calculateCostBreakdown('claude-fable-5-1', tokens)).to.deep.equal(expected);
79
+ expect(ModelMix.calculateCostBreakdown('anthropic/claude-fable-5.1', tokens)).to.deep.equal(expected);
80
+ });
81
+
6
82
  it('should register Claude Fable 5', () => {
7
83
  const model = ModelMix.new();
8
84
  model.fable50();
@@ -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();
250
+ model.gpt51({ mix: { openrouter: false } });
251
251
 
252
252
  nock('https://api.openai.com')
253
253
  .post('/v1/responses')
@@ -455,4 +455,4 @@ describe('Rate Limiting with Bottleneck Tests', () => {
455
455
  model.addText('Event test').message();
456
456
  });
457
457
  });
458
- });
458
+ });
@@ -77,6 +77,7 @@ describe('Unified effort scale', () => {
77
77
  expect(mapEffort('openai', 99, 'gpt-5.6-luna')).to.deep.equal({ reasoning_effort: 'xhigh' });
78
78
  for (const model of ['gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.6-luna']) {
79
79
  expect(mapEffort('openai', 100, model)).to.deep.equal({ reasoning_effort: 'max' });
80
+ expect(mapEffort('openai', 100, `openai/${model}`)).to.deep.equal({ reasoning_effort: 'max' });
80
81
  }
81
82
  });
82
83
 
@@ -87,6 +88,8 @@ describe('Unified effort scale', () => {
87
88
 
88
89
  it('clamps OpenAI to model-supported levels', () => {
89
90
  expect(mapEffort('openai', 10, 'gpt-5.3-codex')).to.deep.equal({ reasoning_effort: 'low' });
91
+ expect(mapEffort('openai', 10, 'openai/gpt-5.3-codex')).to.deep.equal({ reasoning_effort: 'low' });
92
+ expect(mapEffort('openai', 0, 'openai/gpt-5')).to.deep.equal({ reasoning_effort: 'minimal' });
90
93
  expect(mapEffort('openai', 10, 'gpt-oss-120b')).to.deep.equal({ reasoning_effort: 'low' });
91
94
  });
92
95
 
@@ -132,6 +135,16 @@ describe('Unified effort scale', () => {
132
135
  });
133
136
  });
134
137
 
138
+ it('maps Muse Spark 1.2 Contributor to every supported reasoning level', () => {
139
+ const key = 'meta/muse-spark-1.2-contributor';
140
+ expect(mapEffort('openai', 0, key)).to.deep.equal({ reasoning_effort: 'minimal' });
141
+ expect(mapEffort('openai', 20, key)).to.deep.equal({ reasoning_effort: 'low' });
142
+ expect(mapEffort('openai', 40, key)).to.deep.equal({ reasoning_effort: 'medium' });
143
+ expect(mapEffort('openai', 60, key)).to.deep.equal({ reasoning_effort: 'high' });
144
+ expect(mapEffort('openai', 80, key)).to.deep.equal({ reasoning_effort: 'xhigh' });
145
+ expect(mapEffort('openai', -1, key)).to.equal(null);
146
+ });
147
+
135
148
  it('maps Qwen 3.8 27B to its supported reasoning levels', () => {
136
149
  const key = 'qwen/qwen3.8-27b';
137
150
  expect(mapEffort('openai', 0, key)).to.deep.equal({ reasoning_effort: 'low' });
@@ -171,7 +184,7 @@ describe('Unified effort scale', () => {
171
184
  thinking: { type: 'adaptive', display: 'summarized' },
172
185
  output_config: { effort: 'low' }
173
186
  });
174
- expect(mapEffort('anthropic', 90, 'claude-fable-5')).to.deep.equal({
187
+ expect(mapEffort('anthropic', 90, 'claude-fable-5-1')).to.deep.equal({
175
188
  thinking: { type: 'adaptive', display: 'summarized' },
176
189
  output_config: { effort: 'max' }
177
190
  });
@@ -181,6 +194,16 @@ describe('Unified effort scale', () => {
181
194
  });
182
195
  });
183
196
 
197
+ it('maps OpenRouter Claude Fable 5.1 to Anthropic effort levels', () => {
198
+ const key = 'anthropic/claude-fable-5.1';
199
+ expect(mapEffort('openai', 0, key)).to.deep.equal({ reasoning_effort: 'low' });
200
+ expect(mapEffort('openai', 20, key)).to.deep.equal({ reasoning_effort: 'medium' });
201
+ expect(mapEffort('openai', 40, key)).to.deep.equal({ reasoning_effort: 'high' });
202
+ expect(mapEffort('openai', 60, key)).to.deep.equal({ reasoning_effort: 'xhigh' });
203
+ expect(mapEffort('openai', 80, key)).to.deep.equal({ reasoning_effort: 'max' });
204
+ expect(mapEffort('openai', -1, key)).to.equal(null);
205
+ });
206
+
184
207
  it('maps Anthropic manual models to thinking.type=enabled + budget_tokens', () => {
185
208
  expect(mapEffort('anthropic', 50, 'claude-sonnet-4-5-20250929')).to.deep.equal({
186
209
  thinking: { type: 'enabled', budget_tokens: 8192 }
@@ -3,7 +3,14 @@ const sinon = require('sinon');
3
3
  const nock = require('nock');
4
4
  const { EventEmitter } = require('events');
5
5
  const Module = require('module');
6
- const { MixCustom, MixGoogle, ModelMix } = require('../index.js');
6
+ const {
7
+ MixCustom,
8
+ MixGoogle,
9
+ MixOpenAI,
10
+ MixOpenAIResponses,
11
+ MixOpenRouter,
12
+ ModelMix
13
+ } = require('../index.js');
7
14
 
8
15
  describe('Provider Fallback Chain Tests', () => {
9
16
 
@@ -47,6 +54,99 @@ describe('Provider Fallback Chain Tests', () => {
47
54
  expect(model.models[1].provider.config.effort).to.equal(20);
48
55
  });
49
56
 
57
+ it('should register every supported OpenAI text shortcut with its OpenRouter fallback', () => {
58
+ expect(ModelMix.new().mix.openrouter).to.equal(false);
59
+ const shortcuts = [
60
+ ['gpt5', 'gpt-5', 'openai/gpt-5', MixOpenAI],
61
+ ['gpt5mini', 'gpt-5-mini', 'openai/gpt-5-mini', MixOpenAI],
62
+ ['gpt5nano', 'gpt-5-nano', 'openai/gpt-5-nano', MixOpenAI],
63
+ ['gpt51', 'gpt-5.1', 'openai/gpt-5.1', MixOpenAIResponses],
64
+ ['gpt52', 'gpt-5.2', 'openai/gpt-5.2', MixOpenAIResponses],
65
+ ['gpt53codex', 'gpt-5.3-codex', 'openai/gpt-5.3-codex', MixOpenAIResponses],
66
+ ['gpt53chat', 'gpt-5.3-chat-latest', 'openai/gpt-5.3-chat', MixOpenAIResponses],
67
+ ['gpt54', 'gpt-5.4', 'openai/gpt-5.4', MixOpenAIResponses],
68
+ ['gpt54mini', 'gpt-5.4-mini', 'openai/gpt-5.4-mini', MixOpenAIResponses],
69
+ ['gpt54nano', 'gpt-5.4-nano', 'openai/gpt-5.4-nano', MixOpenAIResponses],
70
+ ['gpt54pro', 'gpt-5.4-pro', 'openai/gpt-5.4-pro', MixOpenAIResponses],
71
+ ['gpt55', 'gpt-5.5', 'openai/gpt-5.5', MixOpenAIResponses],
72
+ ['gpt55pro', 'gpt-5.5-pro', 'openai/gpt-5.5-pro', MixOpenAIResponses],
73
+ ['gpt56sol', 'gpt-5.6-sol', 'openai/gpt-5.6-sol', MixOpenAIResponses],
74
+ ['gpt56terra', 'gpt-5.6-terra', 'openai/gpt-5.6-terra', MixOpenAIResponses],
75
+ ['gpt56luna', 'gpt-5.6-luna', 'openai/gpt-5.6-luna', MixOpenAIResponses]
76
+ ];
77
+
78
+ for (const [shortcut, officialKey, openRouterKey, OfficialProvider] of shortcuts) {
79
+ const official = ModelMix.new()[shortcut]();
80
+ expect(official.models.map(({ key }) => key)).to.deep.equal([officialKey]);
81
+ expect(official.models[0].provider).to.be.instanceOf(OfficialProvider);
82
+
83
+ const routed = ModelMix.new()[shortcut]({ mix: { openrouter: true } });
84
+ expect(routed.models.map(({ key }) => key)).to.deep.equal([officialKey, openRouterKey]);
85
+ expect(routed.models[0].provider).to.be.instanceOf(OfficialProvider);
86
+ expect(routed.models[1].provider).to.be.instanceOf(MixOpenRouter);
87
+
88
+ const globallyRouted = ModelMix.new({ mix: { openrouter: true } })[shortcut]();
89
+ expect(globallyRouted.models.map(({ key }) => key)).to.deep.equal([officialKey, openRouterKey]);
90
+ }
91
+
92
+ const locallyDisabled = ModelMix.new({ mix: { openrouter: true } })
93
+ .gpt56sol({ mix: { openrouter: false } });
94
+ expect(locallyDisabled.models.map(({ key }) => key)).to.deep.equal(['gpt-5.6-sol']);
95
+
96
+ const inherited = ModelMix.new({ mix: { openrouter: true } }).new().gpt5();
97
+ expect(inherited.models.map(({ key }) => key)).to.deep.equal([
98
+ 'gpt-5',
99
+ 'openai/gpt-5'
100
+ ]);
101
+ });
102
+
103
+ it('should append OpenRouter GPT fallbacks in chain() only when enabled globally', () => {
104
+ const official = ModelMix.new().chain('gpt56sol@100');
105
+ expect(official.models.map(({ key }) => key)).to.deep.equal(['gpt-5.6-sol']);
106
+
107
+ const routed = ModelMix.new({ mix: { openrouter: true } }).chain('gpt56sol@100');
108
+ expect(routed.models.map(({ key }) => key)).to.deep.equal([
109
+ 'gpt-5.6-sol',
110
+ 'openai/gpt-5.6-sol'
111
+ ]);
112
+ expect(routed.models.map(({ provider }) => provider.config.effort)).to.deep.equal([100, 100]);
113
+ });
114
+
115
+ it('should fallback from the official GPT-5.6 Sol endpoint to OpenRouter', async () => {
116
+ let openRouterRequest;
117
+ model.gpt56sol({ mix: { openrouter: true } }).addText('Hello');
118
+
119
+ nock('https://api.openai.com')
120
+ .post('/v1/responses')
121
+ .reply(503, { error: 'Service unavailable' });
122
+
123
+ nock('https://openrouter.ai')
124
+ .post('/api/v1/chat/completions', body => {
125
+ openRouterRequest = body;
126
+ return true;
127
+ })
128
+ .reply(200, {
129
+ choices: [{
130
+ message: {
131
+ role: 'assistant',
132
+ content: 'Hello from GPT-5.6 Sol through OpenRouter!'
133
+ }
134
+ }]
135
+ });
136
+
137
+ expect(await model.message()).to.equal('Hello from GPT-5.6 Sol through OpenRouter!');
138
+ expect(openRouterRequest.model).to.equal('openai/gpt-5.6-sol');
139
+ expect(openRouterRequest.max_completion_tokens).to.equal(8192);
140
+ expect(openRouterRequest).to.not.have.property('temperature');
141
+ });
142
+
143
+ it('should keep the default fable51 chain on Anthropic', () => {
144
+ model.chain('fable51@80');
145
+
146
+ expect(model.models.map(({ key }) => key)).to.deep.equal(['claude-fable-5-1']);
147
+ expect(model.models[0].provider.config.effort).to.equal(80);
148
+ });
149
+
50
150
  it('should reject invalid chains before attaching any model', () => {
51
151
  expect(() => model.chain('gpt56luna', 'unknownModel'))
52
152
  .to.throw('Unknown model shortcut "unknownModel" in chain().');
package/test/glm.test.js CHANGED
@@ -2,16 +2,12 @@ const { expect } = require('chai');
2
2
  const { ModelMix, MixFireworks, MixOpenRouter, MixTogether } = require('../index.js');
3
3
 
4
4
  describe('GLM Model Registration Tests', () => {
5
- it('should register Together GLM 5.2 before the OpenRouter fallback by default', () => {
5
+ it('should register only Together GLM 5.2 by default', () => {
6
6
  const model = ModelMix.new();
7
7
  model.GLM52();
8
8
 
9
- expect(model.models.map(({ key }) => key)).to.deep.equal([
10
- 'zai-org/GLM-5.2',
11
- 'z-ai/glm-5.2'
12
- ]);
9
+ expect(model.models.map(({ key }) => key)).to.deep.equal(['zai-org/GLM-5.2']);
13
10
  expect(model.models[0].provider).to.be.instanceOf(MixTogether);
14
- expect(model.models[1].provider).to.be.instanceOf(MixOpenRouter);
15
11
  });
16
12
 
17
13
  it('should register every requested GLM 5.2 provider', () => {
package/test/kimi.test.js CHANGED
@@ -3,16 +3,12 @@ const nock = require('nock');
3
3
  const { ModelMix, MixFireworks, MixKimi, MixOpenRouter, MixTogether } = require('../index.js');
4
4
 
5
5
  describe('Kimi Model Registration Tests', () => {
6
- it('should register Together Kimi K2.7 Code before the OpenRouter fallback by default', () => {
6
+ it('should register only Together Kimi K2.7 Code by default', () => {
7
7
  const model = ModelMix.new();
8
8
  model.kimiK27Code();
9
9
 
10
- expect(model.models.map(({ key }) => key)).to.deep.equal([
11
- 'moonshotai/Kimi-K2.7-Code',
12
- 'moonshotai/kimi-k2.7-code'
13
- ]);
10
+ expect(model.models.map(({ key }) => key)).to.deep.equal(['moonshotai/Kimi-K2.7-Code']);
14
11
  expect(model.models[0].provider).to.be.instanceOf(MixTogether);
15
- expect(model.models[1].provider).to.be.instanceOf(MixOpenRouter);
16
12
  });
17
13
 
18
14
  it('should register every requested Kimi K2.7 Code provider', () => {
package/test/live.test.js CHANGED
@@ -1,5 +1,5 @@
1
1
  const { expect } = require('chai');
2
- const { ModelMix, MixOpenAI, MixAnthropic, MixGoogle } = require('../index.js');
2
+ const { ModelMix, MixOpenAI, MixAnthropic } = require('../index.js');
3
3
  const nock = require('nock');
4
4
 
5
5
  const blueSquareBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAMAAABHPGVmAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDkuMS1jMDAzIDc5Ljk2OTBhODdmYywgMjAyNS8wMy8wNi0yMDo1MDoxNiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDI2LjkgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6REM2QzQ3NEQ2Q0I5MTFGMDlBRTVGQzcwQjMyMkY4MDciIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6REM2QzQ3NEU2Q0I5MTFGMDlBRTVGQzcwQjMyMkY4MDciPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpEQzZDNDc0QjZDQjkxMUYwOUFFNUZDNzBCMzIyRjgwNyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpEQzZDNDc0QzZDQjkxMUYwOUFFNUZDNzBCMzIyRjgwNyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PvArxh0AAAAGUExURQAA/wAAAHtivz4AAAAjSURBVHja7MGBAAAAAMOg+VNf4QBVAQAAAAAAAAAAAI8JMAAndAABi7SX2gAAAABJRU5ErkJggg==';
@@ -181,7 +181,9 @@ describe('Live Integration Tests', function () {
181
181
  describe('Additional Model Tests', function () {
182
182
 
183
183
  it('should work with GPT-OSS model', async function () {
184
- const model = ModelMix.new(setup).gptOss();
184
+ const model = ModelMix.new(setup).gptOss({
185
+ options: { max_tokens: 128 }
186
+ });
185
187
 
186
188
  model.addText('Say "gptoss test successful" and nothing else.');
187
189
 
@@ -241,8 +243,11 @@ describe('Live Integration Tests', function () {
241
243
 
242
244
  describe('JSON Structured Output for New Models', function () {
243
245
 
244
- it('should return structured JSON with KimiK25 Thinking', async function () {
245
- const model = ModelMix.new(setup).kimiK25();
246
+ it('should return structured JSON with Kimi K3', async function () {
247
+ const model = ModelMix.new(setup).kimiK3({
248
+ options: { max_tokens: 512 },
249
+ mix: { moonshot: false, together: true }
250
+ });
246
251
 
247
252
  model.addText('Generate information about a fictional vehicle.');
248
253
 
@@ -253,7 +258,7 @@ describe('Live Integration Tests', function () {
253
258
  manufacturer: "Future Motors"
254
259
  });
255
260
 
256
- console.log(`KimiK25 JSON result:`, result);
261
+ console.log(`Kimi K3 JSON result:`, result);
257
262
 
258
263
  expect(result).to.be.an('object');
259
264
  expect(result).to.have.property('name');
package/test/muse.test.js CHANGED
@@ -7,16 +7,14 @@ const {
7
7
  MixTogether
8
8
  } = require('../index.js');
9
9
 
10
- describe('Muse Glimmer Model Registration Tests', () => {
11
- it('registers Fireworks before the OpenRouter fallback by default', () => {
10
+ describe('Muse Model Registration Tests', () => {
11
+ it('registers only Fireworks by default', () => {
12
12
  const model = ModelMix.new().museGlimmer30b();
13
13
 
14
14
  expect(model.models.map(({ key }) => key)).to.deep.equal([
15
- 'accounts/fireworks/models/muse-glimmer-30b',
16
- 'meta/muse-glimmer-30b'
15
+ 'accounts/fireworks/models/muse-glimmer-30b'
17
16
  ]);
18
17
  expect(model.models[0].provider).to.be.instanceOf(MixFireworks);
19
- expect(model.models[1].provider).to.be.instanceOf(MixOpenRouter);
20
18
  });
21
19
 
22
20
  it('registers every supported provider in fallback order', () => {
@@ -54,8 +52,27 @@ describe('Muse Glimmer Model Registration Tests', () => {
54
52
  const model = ModelMix.new().chain('museGlimmer30b');
55
53
 
56
54
  expect(model.models.map(({ key }) => key)).to.deep.equal([
57
- 'accounts/fireworks/models/muse-glimmer-30b',
58
- 'meta/muse-glimmer-30b'
55
+ 'accounts/fireworks/models/muse-glimmer-30b'
59
56
  ]);
60
57
  });
58
+
59
+ it('registers Muse Spark 1.2 Contributor through OpenRouter', () => {
60
+ const model = ModelMix.new().museSpark12Contributor();
61
+
62
+ expect(model.models).to.have.length(1);
63
+ expect(model.models[0].key).to.equal('meta/muse-spark-1.2-contributor');
64
+ expect(model.models[0].provider).to.be.instanceOf(MixOpenRouter);
65
+ expect(ModelMix.calculateCost('meta/muse-spark-1.2-contributor', {
66
+ input: 1_000_000,
67
+ cached: 250_000,
68
+ output: 1_000_000
69
+ })).to.equal(0.2755);
70
+ });
71
+
72
+ it('supports Muse Spark 1.2 Contributor in chain()', () => {
73
+ const model = ModelMix.new().chain('museSpark12Contributor');
74
+
75
+ expect(model.models).to.have.length(1);
76
+ expect(model.models[0].key).to.equal('meta/muse-spark-1.2-contributor');
77
+ });
61
78
  });
@@ -9,6 +9,23 @@ const {
9
9
  } = require('../index.js');
10
10
 
11
11
  describe('Provider expansion regressions', () => {
12
+ it('should keep OpenRouter fallbacks disabled by default', () => {
13
+ const originalMiniMaxApiKey = process.env.MINIMAX_API_KEY;
14
+ process.env.MINIMAX_API_KEY = 'test-minimax-key';
15
+
16
+ try {
17
+ const gptOss = ModelMix.new().gptOss();
18
+ const minimax = ModelMix.new().minimaxM27();
19
+
20
+ expect(gptOss.models.some(({ provider }) => provider instanceof MixOpenRouter)).to.equal(false);
21
+ expect(minimax.models.map(({ key }) => key)).to.deep.equal(['MiniMax-M2.7']);
22
+ expect(minimax.models[0].provider).to.be.instanceOf(MixMiniMax);
23
+ } finally {
24
+ if (originalMiniMaxApiKey === undefined) delete process.env.MINIMAX_API_KEY;
25
+ else process.env.MINIMAX_API_KEY = originalMiniMaxApiKey;
26
+ }
27
+ });
28
+
12
29
  it('should retain every enabled GPT OSS provider, including shared model keys', () => {
13
30
  const model = ModelMix.new().gptOss({
14
31
  mix: {
@@ -14,6 +14,7 @@ describe('public module boundary', () => {
14
14
  'MixGroq',
15
15
  'MixKimi',
16
16
  'MixLMStudio',
17
+ 'MixLambda',
17
18
  'MixMiMo',
18
19
  'MixMiniMax',
19
20
  'MixModeration',
@@ -57,8 +58,7 @@ describe('public module boundary', () => {
57
58
  ['MiMo', config => new api.MixMiMo({ config })],
58
59
  ['Perplexity', config => new api.MixPerplexity({ config })],
59
60
  ['Grok', config => new api.MixGrok({ config })],
60
- ['Lambda', config => api.ModelMix.new({ mix: { openrouter: false, lambda: true } })
61
- .hermes3({ config }).models[0].provider],
61
+ ['Lambda', config => new api.MixLambda({ config })],
62
62
  ['Groq', config => new api.MixGroq({ config })],
63
63
  ['Together', config => new api.MixTogether({ config })],
64
64
  ['Cerebras', config => new api.MixCerebras({ config })],
package/test/qwen.test.js CHANGED
@@ -67,13 +67,12 @@ describe('Qwen Model Registration Tests', () => {
67
67
  })).to.equal(4.08576);
68
68
  });
69
69
 
70
- it('should register Fireworks Qwen 3.8 Max before the OpenRouter fallback by default', () => {
70
+ it('should register only Fireworks Qwen 3.8 Max by default', () => {
71
71
  const model = ModelMix.new();
72
72
  model.qwen38max();
73
73
 
74
74
  expect(model.models.map(({ key }) => key)).to.deep.equal([
75
- 'accounts/fireworks/models/qwen3p8-2p4t-a95b',
76
- 'qwen/qwen3.8-max'
75
+ 'accounts/fireworks/models/qwen3p8-2p4t-a95b'
77
76
  ]);
78
77
  expect(ModelMix.calculateCost('accounts/fireworks/models/qwen3p8-2p4t-a95b', {
79
78
  input: 1_000_000,
@@ -321,32 +321,47 @@ describe('Token Usage Tracking', () => {
321
321
  });
322
322
 
323
323
  it('should register GPT-5.5 shortcuts with OpenAI Responses provider', function () {
324
- const model = ModelMix.new()
324
+ const model = ModelMix.new({ mix: { openrouter: true } })
325
325
  .gpt55()
326
326
  .gpt55pro();
327
327
 
328
- expect(model.models).to.have.length(2);
329
- expect(model.models[0].key).to.equal('gpt-5.5');
330
- expect(model.models[1].key).to.equal('gpt-5.5-pro');
328
+ expect(model.models.map(({ key }) => key)).to.deep.equal([
329
+ 'gpt-5.5',
330
+ 'openai/gpt-5.5',
331
+ 'gpt-5.5-pro',
332
+ 'openai/gpt-5.5-pro'
333
+ ]);
331
334
  expect(model.models[0].provider).to.be.instanceOf(MixOpenAIResponses);
332
- expect(model.models[1].provider).to.be.instanceOf(MixOpenAIResponses);
335
+ expect(model.models[1].provider).to.be.instanceOf(MixOpenRouter);
336
+ expect(model.models[2].provider).to.be.instanceOf(MixOpenAIResponses);
337
+ expect(model.models[3].provider).to.be.instanceOf(MixOpenRouter);
333
338
  });
334
339
 
335
340
  it('should register GPT-5.6 shortcuts with OpenAI Responses provider', function () {
336
- const model = ModelMix.new()
341
+ const model = ModelMix.new({ mix: { openrouter: true } })
337
342
  .gpt56sol()
338
343
  .gpt56terra()
339
344
  .gpt56luna();
340
345
 
341
346
  expect(model.models.map(({ key }) => key)).to.deep.equal([
342
347
  'gpt-5.6-sol',
348
+ 'openai/gpt-5.6-sol',
343
349
  'gpt-5.6-terra',
344
- 'gpt-5.6-luna'
350
+ 'openai/gpt-5.6-terra',
351
+ 'gpt-5.6-luna',
352
+ 'openai/gpt-5.6-luna'
345
353
  ]);
346
- expect(model.models.every(({ provider }) => provider instanceof MixOpenAIResponses)).to.equal(true);
354
+ expect(model.models[0].provider).to.be.instanceOf(MixOpenAIResponses);
355
+ expect(model.models[1].provider).to.be.instanceOf(MixOpenRouter);
356
+ expect(model.models[2].provider).to.be.instanceOf(MixOpenAIResponses);
357
+ expect(model.models[3].provider).to.be.instanceOf(MixOpenRouter);
358
+ expect(model.models[4].provider).to.be.instanceOf(MixOpenAIResponses);
359
+ expect(model.models[5].provider).to.be.instanceOf(MixOpenRouter);
347
360
  expect(ModelMix.calculateCost('gpt-5.6-sol', { input: 1_000_000, output: 1_000_000 })).to.equal(55);
361
+ expect(ModelMix.calculateCost('openai/gpt-5.6-sol', { input: 1_000_000, output: 1_000_000 })).to.equal(55);
348
362
  expect(ModelMix.calculateCost('gpt-5.6-terra', { input: 1_000_000, output: 1_000_000 })).to.equal(22);
349
363
  expect(ModelMix.calculateCost('gpt-5.6-luna', { input: 1_000_000, output: 1_000_000 })).to.equal(2.2);
364
+ expect(ModelMix.calculateCost('openai/gpt-5.3-chat', { input: 1_000_000, output: 1_000_000 })).to.equal(15.75);
350
365
  });
351
366
 
352
367
  it('should calculate GPT-5.6 cache reads and writes at their actual rates', function () {