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
package/test/fallback.test.js
CHANGED
|
@@ -117,13 +117,9 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
117
117
|
['gpt6astra', 'gpt-6-astra', 'openai/gpt-6-astra', MixOpenAIResponses],
|
|
118
118
|
['gpt6sol', 'gpt-6-sol', 'openai/gpt-6-sol', MixOpenAIResponses],
|
|
119
119
|
['gpt6luna', 'gpt-6-luna', 'openai/gpt-6-luna', MixOpenAIResponses],
|
|
120
|
-
['gpt5', 'gpt-5', 'openai/gpt-5', MixOpenAI],
|
|
121
120
|
['gpt5mini', 'gpt-5-mini', 'openai/gpt-5-mini', MixOpenAI],
|
|
122
121
|
['gpt5nano', 'gpt-5-nano', 'openai/gpt-5-nano', MixOpenAI],
|
|
123
|
-
['gpt51', 'gpt-5.1', 'openai/gpt-5.1', MixOpenAIResponses],
|
|
124
122
|
['gpt52', 'gpt-5.2', 'openai/gpt-5.2', MixOpenAIResponses],
|
|
125
|
-
['gpt53codex', 'gpt-5.3-codex', 'openai/gpt-5.3-codex', MixOpenAIResponses],
|
|
126
|
-
['gpt53chat', 'gpt-5.3-chat-latest', 'openai/gpt-5.3-chat', MixOpenAIResponses],
|
|
127
123
|
['gpt54', 'gpt-5.4', 'openai/gpt-5.4', MixOpenAIResponses],
|
|
128
124
|
['gpt54mini', 'gpt-5.4-mini', 'openai/gpt-5.4-mini', MixOpenAIResponses],
|
|
129
125
|
['gpt54nano', 'gpt-5.4-nano', 'openai/gpt-5.4-nano', MixOpenAIResponses],
|
|
@@ -153,10 +149,10 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
153
149
|
.gpt56sol({ mix: { openrouter: false } });
|
|
154
150
|
expect(locallyDisabled.models.map(({ key }) => key)).to.deep.equal(['gpt-5.6-sol']);
|
|
155
151
|
|
|
156
|
-
const inherited = ModelMix.new({ mix: { openrouter: true } }).new().
|
|
152
|
+
const inherited = ModelMix.new({ mix: { openrouter: true } }).new().gpt5mini();
|
|
157
153
|
expect(inherited.models.map(({ key }) => key)).to.deep.equal([
|
|
158
|
-
'gpt-5',
|
|
159
|
-
'openai/gpt-5'
|
|
154
|
+
'gpt-5-mini',
|
|
155
|
+
'openai/gpt-5-mini'
|
|
160
156
|
]);
|
|
161
157
|
});
|
|
162
158
|
|
|
@@ -309,7 +305,7 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
309
305
|
});
|
|
310
306
|
|
|
311
307
|
it('should use primary provider when available', async () => {
|
|
312
|
-
model.gpt5mini().
|
|
308
|
+
model.gpt5mini().sonnet5().addText('Hello');
|
|
313
309
|
|
|
314
310
|
// Mock successful OpenAI response
|
|
315
311
|
nock('https://api.openai.com')
|
|
@@ -329,7 +325,7 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
329
325
|
});
|
|
330
326
|
|
|
331
327
|
it('should fallback to secondary provider when primary fails', async () => {
|
|
332
|
-
model.gpt5mini().
|
|
328
|
+
model.gpt5mini().sonnet5().addText('Hello');
|
|
333
329
|
|
|
334
330
|
// Mock failed OpenAI response (GPT-5 mini)
|
|
335
331
|
nock('https://api.openai.com')
|
|
@@ -443,7 +439,7 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
443
439
|
});
|
|
444
440
|
|
|
445
441
|
it('should cascade through multiple fallbacks', async () => {
|
|
446
|
-
model.gpt5mini().
|
|
442
|
+
model.gpt5mini().sonnet5().gemini37flash().addText('Hello');
|
|
447
443
|
|
|
448
444
|
// Mock failed OpenAI response
|
|
449
445
|
nock('https://api.openai.com')
|
|
@@ -474,7 +470,7 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
474
470
|
});
|
|
475
471
|
|
|
476
472
|
it('should throw error when all providers fail', async () => {
|
|
477
|
-
model.gpt5mini().
|
|
473
|
+
model.gpt5mini().sonnet5().addText('Hello');
|
|
478
474
|
|
|
479
475
|
// Mock all providers failing
|
|
480
476
|
nock('https://api.openai.com')
|
|
@@ -504,7 +500,7 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
504
500
|
});
|
|
505
501
|
|
|
506
502
|
it('should fallback from OpenAI to Anthropic', async () => {
|
|
507
|
-
model.gpt5mini().
|
|
503
|
+
model.gpt5mini().sonnet5().addText('Test message');
|
|
508
504
|
|
|
509
505
|
// Mock OpenAI failure
|
|
510
506
|
nock('https://api.openai.com')
|
|
@@ -527,7 +523,7 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
527
523
|
});
|
|
528
524
|
|
|
529
525
|
it('should fallback from Anthropic to Google', async () => {
|
|
530
|
-
model.
|
|
526
|
+
model.sonnet5().gemini37flash().addText('Test message');
|
|
531
527
|
|
|
532
528
|
// Mock Anthropic failure
|
|
533
529
|
nock('https://api.anthropic.com')
|
|
@@ -553,7 +549,7 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
553
549
|
});
|
|
554
550
|
|
|
555
551
|
it('should handle network timeout fallback', async () => {
|
|
556
|
-
model.gpt5mini().
|
|
552
|
+
model.gpt5mini().sonnet5().addText('Hello');
|
|
557
553
|
|
|
558
554
|
// Mock timeout error on first provider (using 408 Request Timeout)
|
|
559
555
|
nock('https://api.openai.com')
|
|
@@ -587,7 +583,7 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
587
583
|
|
|
588
584
|
it('should handle JSON fallback correctly', async () => {
|
|
589
585
|
const schema = { name: 'Alice', age: 30 };
|
|
590
|
-
model.gpt5mini().
|
|
586
|
+
model.gpt5mini().sonnet5().addText('Generate user data');
|
|
591
587
|
|
|
592
588
|
// Mock OpenAI failure
|
|
593
589
|
nock('https://api.openai.com')
|
|
@@ -613,7 +609,7 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
613
609
|
});
|
|
614
610
|
|
|
615
611
|
it('should preserve message history through fallbacks', async () => {
|
|
616
|
-
model.gpt5mini().
|
|
612
|
+
model.gpt5mini().sonnet5()
|
|
617
613
|
.addText('First message')
|
|
618
614
|
.addText('Second message');
|
|
619
615
|
|
|
@@ -644,7 +640,7 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
644
640
|
config: { debug: false, retry: { enabled: false } }
|
|
645
641
|
});
|
|
646
642
|
|
|
647
|
-
model.gpt5mini().
|
|
643
|
+
model.gpt5mini().sonnet5().addText('Hello');
|
|
648
644
|
|
|
649
645
|
nock('https://api.openai.com')
|
|
650
646
|
.post('/v1/chat/completions')
|
|
@@ -692,7 +688,7 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
692
688
|
config: { debug: false, retry: { enabled: true, retries: 2, baseDelayMs: 0, maxDelayMs: 0 } }
|
|
693
689
|
});
|
|
694
690
|
|
|
695
|
-
model.gpt5mini().
|
|
691
|
+
model.gpt5mini().sonnet5().addText('Hello');
|
|
696
692
|
|
|
697
693
|
nock('https://api.openai.com')
|
|
698
694
|
.post('/v1/chat/completions')
|
|
@@ -716,7 +712,7 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
716
712
|
config: { debug: false, retry: { enabled: true, retries: 1, baseDelayMs: 0, maxDelayMs: 0 } }
|
|
717
713
|
});
|
|
718
714
|
|
|
719
|
-
model.gpt5mini().
|
|
715
|
+
model.gpt5mini().sonnet5().addText('Hello');
|
|
720
716
|
|
|
721
717
|
nock('https://api.openai.com')
|
|
722
718
|
.post('/v1/chat/completions')
|
|
@@ -746,7 +742,7 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
746
742
|
|
|
747
743
|
// Configure with custom temperature for fallback
|
|
748
744
|
model.gpt5mini({ options: { temperature: 0.6 } })
|
|
749
|
-
.
|
|
745
|
+
.sonnet45({ options: { temperature: 0.7 } })
|
|
750
746
|
.addText('Creative response');
|
|
751
747
|
|
|
752
748
|
// Mock first provider failure
|
|
@@ -779,7 +775,7 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
779
775
|
});
|
|
780
776
|
|
|
781
777
|
model.gpt5mini({ options: { max_tokens: 100 } })
|
|
782
|
-
.
|
|
778
|
+
.sonnet5({ options: { max_tokens: 200 } })
|
|
783
779
|
.addText('Generate text');
|
|
784
780
|
|
|
785
781
|
// Mock OpenAI failure
|
|
@@ -913,7 +909,7 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
913
909
|
});
|
|
914
910
|
|
|
915
911
|
it('should provide detailed error information when all fallbacks fail', async () => {
|
|
916
|
-
model.gpt5mini().
|
|
912
|
+
model.gpt5mini().sonnet5().gemini37flash().addText('Test');
|
|
917
913
|
|
|
918
914
|
// Mock all providers failing with different errors
|
|
919
915
|
nock('https://api.openai.com')
|
|
@@ -938,7 +934,7 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
938
934
|
});
|
|
939
935
|
|
|
940
936
|
it('should handle malformed responses in fallback', async () => {
|
|
941
|
-
model.gpt5mini().
|
|
937
|
+
model.gpt5mini().sonnet5().addText('Test');
|
|
942
938
|
|
|
943
939
|
// Mock malformed response from first provider
|
|
944
940
|
nock('https://api.openai.com')
|
package/test/grok.test.js
CHANGED
|
@@ -13,10 +13,7 @@ describe('Grok Model Registration Tests', () => {
|
|
|
13
13
|
const grokModels = [
|
|
14
14
|
{ method: 'grok47', key: 'grok-4.7' },
|
|
15
15
|
{ method: 'grok46', key: 'grok-4.6' },
|
|
16
|
-
{ method: '
|
|
17
|
-
{ method: 'grok43', key: 'grok-4.3' },
|
|
18
|
-
{ method: 'grok420multiAgent', key: 'grok-4.20-multi-agent-0309' },
|
|
19
|
-
{ method: 'grok420', key: GROK420_ALIAS }
|
|
16
|
+
{ method: 'grok43', key: 'grok-4.3' }
|
|
20
17
|
];
|
|
21
18
|
|
|
22
19
|
for (const grokModel of grokModels) {
|
package/test/history.test.js
CHANGED
|
@@ -47,7 +47,7 @@ describe('Conversation History Tests', () => {
|
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
it('should add assistant response to message history after raw()', async () => {
|
|
50
|
-
model.
|
|
50
|
+
model.sonnet5().addText('Hello');
|
|
51
51
|
|
|
52
52
|
nock('https://api.anthropic.com')
|
|
53
53
|
.post('/v1/messages')
|
|
@@ -128,7 +128,7 @@ describe('Conversation History Tests', () => {
|
|
|
128
128
|
const model = ModelMix.new({
|
|
129
129
|
config: { debug: false, max_history: 10 }
|
|
130
130
|
});
|
|
131
|
-
model.
|
|
131
|
+
model.sonnet5();
|
|
132
132
|
|
|
133
133
|
// First turn
|
|
134
134
|
model.addText('Capital of France?');
|
package/test/images.test.js
CHANGED
|
@@ -51,10 +51,10 @@ describe('Image Processing and Multimodal Support Tests', () => {
|
|
|
51
51
|
expect(response).to.include('I can see a small test image');
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
-
it('should support multimodal with
|
|
54
|
+
it('should support multimodal with sonnet5()', async () => {
|
|
55
55
|
const base64Image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAFUlEQVR42mP8z8BQz0AEYBxVSF+FABJADveWkH6oAAAAAElFTkSuQmCC';
|
|
56
56
|
|
|
57
|
-
model.
|
|
57
|
+
model.sonnet5()
|
|
58
58
|
.addText('Describe this image')
|
|
59
59
|
.addImageFromUrl(base64Image);
|
|
60
60
|
|
|
@@ -87,7 +87,7 @@ describe('Image Processing and Multimodal Support Tests', () => {
|
|
|
87
87
|
const pngBase64 = 'iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAFUlEQVR42mP8z8BQz0AEYBxVSF+FABJADveWkH6oAAAAAElFTkSuQmCC';
|
|
88
88
|
const pngBuffer = Buffer.from(pngBase64, 'base64');
|
|
89
89
|
|
|
90
|
-
model.
|
|
90
|
+
model.sonnet5()
|
|
91
91
|
.addText('Describe this image')
|
|
92
92
|
.addImageFromUrl(imageUrl);
|
|
93
93
|
|
package/test/json.test.js
CHANGED
|
@@ -412,7 +412,7 @@ describe('JSON Schema and Structured Output Tests', () => {
|
|
|
412
412
|
}]
|
|
413
413
|
};
|
|
414
414
|
|
|
415
|
-
model.
|
|
415
|
+
model.gpt52().addText('Name and capital of 3 South American countries.');
|
|
416
416
|
|
|
417
417
|
let capturedBody;
|
|
418
418
|
nock('https://api.openai.com')
|
|
@@ -462,7 +462,7 @@ describe('JSON Schema and Structured Output Tests', () => {
|
|
|
462
462
|
}
|
|
463
463
|
};
|
|
464
464
|
|
|
465
|
-
model.
|
|
465
|
+
model.sonnet5().addText('Generate user data');
|
|
466
466
|
|
|
467
467
|
// Mock the API response
|
|
468
468
|
nock('https://api.anthropic.com')
|
package/test/live.test.js
CHANGED
|
@@ -180,24 +180,22 @@ describe('Live Integration Tests', function () {
|
|
|
180
180
|
|
|
181
181
|
describe('Additional Model Tests', function () {
|
|
182
182
|
|
|
183
|
-
it('should work with
|
|
184
|
-
const model = ModelMix.new(setup).
|
|
183
|
+
it('should work with Kimi K2.6 model', async function () {
|
|
184
|
+
const model = ModelMix.new(setup).kimiK26({
|
|
185
185
|
options: { max_tokens: 128 }
|
|
186
186
|
});
|
|
187
187
|
|
|
188
|
-
model.addText('Say "
|
|
188
|
+
model.addText('Say "Kimik26 test successful" and nothing else.');
|
|
189
189
|
|
|
190
190
|
const response = await model.message();
|
|
191
|
-
console.log(`
|
|
191
|
+
console.log(`Kimi K2.6 response: ${response}`);
|
|
192
192
|
|
|
193
193
|
expect(response).to.be.a('string');
|
|
194
|
-
expect(response.toLowerCase()).to.include('
|
|
194
|
+
expect(response.toLowerCase()).to.include('kimik26 test successful');
|
|
195
195
|
});
|
|
196
196
|
|
|
197
197
|
const grokSeriesTests = [
|
|
198
|
-
{ name: 'Grok 4.6', factory: (m) => m.grok46(), token: 'grok46' }
|
|
199
|
-
{ name: 'Grok 4.20 reasoning', factory: (m) => m.effort(50).grok420(), token: 'grok420' },
|
|
200
|
-
{ name: 'Grok 4.20 non-reasoning', factory: (m) => m.grok420(), token: 'grok420nr' }
|
|
198
|
+
{ name: 'Grok 4.6', factory: (m) => m.grok46(), token: 'grok46' }
|
|
201
199
|
];
|
|
202
200
|
|
|
203
201
|
for (const grokModel of grokSeriesTests) {
|
|
@@ -16,10 +16,10 @@ describe('Provider expansion regressions', () => {
|
|
|
16
16
|
process.env.MINIMAX_API_KEY = 'test-minimax-key';
|
|
17
17
|
|
|
18
18
|
try {
|
|
19
|
-
const
|
|
19
|
+
const kimi = ModelMix.new().kimiK26();
|
|
20
20
|
const minimax = ModelMix.new().minimaxM27();
|
|
21
21
|
|
|
22
|
-
expect(
|
|
22
|
+
expect(kimi.models.some(({ provider }) => provider instanceof MixOpenRouter)).to.equal(false);
|
|
23
23
|
expect(minimax.models.map(({ key }) => key)).to.deep.equal(['MiniMax-M2.7']);
|
|
24
24
|
expect(minimax.models[0].provider).to.be.instanceOf(MixMiniMax);
|
|
25
25
|
} finally {
|
|
@@ -28,33 +28,26 @@ describe('Provider expansion regressions', () => {
|
|
|
28
28
|
}
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
-
it('should retain every enabled
|
|
32
|
-
const model = ModelMix.new().
|
|
31
|
+
it('should retain every enabled Muse Glimmer 30B provider, including shared model keys', () => {
|
|
32
|
+
const model = ModelMix.new().museGlimmer30b({
|
|
33
33
|
mix: {
|
|
34
34
|
nvidia: true,
|
|
35
35
|
fireworks: true,
|
|
36
36
|
together: true,
|
|
37
|
-
cerebras: true,
|
|
38
|
-
groq: true,
|
|
39
37
|
openrouter: true
|
|
40
38
|
}
|
|
41
39
|
});
|
|
42
40
|
|
|
43
41
|
expect(model.models.map(({ key }) => key)).to.deep.equal([
|
|
44
|
-
'
|
|
45
|
-
'accounts/fireworks/models/
|
|
46
|
-
'
|
|
47
|
-
'
|
|
48
|
-
'openai/gpt-oss-120b',
|
|
49
|
-
'openai/gpt-oss-120b'
|
|
42
|
+
'meta/muse-glimmer-30b',
|
|
43
|
+
'accounts/fireworks/models/muse-glimmer-30b',
|
|
44
|
+
'meta/muse-glimmer-30b',
|
|
45
|
+
'meta-models/Muse-Glimmer-30B'
|
|
50
46
|
]);
|
|
51
47
|
expect(model.models[0].provider).to.be.instanceOf(MixNVIDIA);
|
|
52
48
|
expect(model.models[1].provider).to.be.instanceOf(MixFireworks);
|
|
53
|
-
expect(
|
|
54
|
-
|
|
55
|
-
cached: 500_000,
|
|
56
|
-
output: 1_000_000
|
|
57
|
-
})).to.equal(0.682);
|
|
49
|
+
expect(model.models[2].provider).to.be.instanceOf(MixOpenRouter);
|
|
50
|
+
expect(model.models[3].provider).to.be.instanceOf(MixTogether);
|
|
58
51
|
});
|
|
59
52
|
|
|
60
53
|
it('should retain every enabled MiniMax M2.7 provider in fallback order', () => {
|
package/test/templates.test.js
CHANGED
|
@@ -38,7 +38,7 @@ describe('EJS Template and File Operations Tests', () => {
|
|
|
38
38
|
describe('EJS rendering', () => {
|
|
39
39
|
it('renders inline variables with plain data keys', async () => {
|
|
40
40
|
const model = ModelMix.new()
|
|
41
|
-
.
|
|
41
|
+
.gpt52()
|
|
42
42
|
.assign({ name: 'Alice', age: 30, city: 'New York' })
|
|
43
43
|
.addText('Hello <%- name %>, you are <%- age %> years old and live in <%- city %>.');
|
|
44
44
|
|
|
@@ -53,7 +53,7 @@ describe('EJS Template and File Operations Tests', () => {
|
|
|
53
53
|
|
|
54
54
|
it('assigns one template data key', async () => {
|
|
55
55
|
const model = ModelMix.new()
|
|
56
|
-
.
|
|
56
|
+
.gpt52()
|
|
57
57
|
.assignKey('name', 'Martin')
|
|
58
58
|
.addText('Hello <%- name %>.');
|
|
59
59
|
|
|
@@ -66,7 +66,7 @@ describe('EJS Template and File Operations Tests', () => {
|
|
|
66
66
|
|
|
67
67
|
it('supports nested data, conditionals, and loops', async () => {
|
|
68
68
|
const model = ModelMix.new()
|
|
69
|
-
.
|
|
69
|
+
.gpt52()
|
|
70
70
|
.assign({
|
|
71
71
|
user: {
|
|
72
72
|
name: 'Charlie',
|
|
@@ -86,7 +86,7 @@ describe('EJS Template and File Operations Tests', () => {
|
|
|
86
86
|
it('keeps raw and XML-escaped output distinct', async () => {
|
|
87
87
|
const value = 'Hello & "World" <test>';
|
|
88
88
|
const model = ModelMix.new()
|
|
89
|
-
.
|
|
89
|
+
.gpt52()
|
|
90
90
|
.assign({ value })
|
|
91
91
|
.addText('Escaped: <%= value %>\nRaw: <%- value %>');
|
|
92
92
|
|
|
@@ -101,7 +101,7 @@ describe('EJS Template and File Operations Tests', () => {
|
|
|
101
101
|
|
|
102
102
|
it('does not execute EJS received through template data', async () => {
|
|
103
103
|
const model = ModelMix.new()
|
|
104
|
-
.
|
|
104
|
+
.gpt52()
|
|
105
105
|
.assign({ payload: '<%- secret %>', secret: 'must-not-render' })
|
|
106
106
|
.addText('Payload: <%- payload %>');
|
|
107
107
|
|
|
@@ -114,7 +114,7 @@ describe('EJS Template and File Operations Tests', () => {
|
|
|
114
114
|
|
|
115
115
|
it('selects uniformly when choice options omit weights', async () => {
|
|
116
116
|
const model = ModelMix.new()
|
|
117
|
-
.
|
|
117
|
+
.gpt52()
|
|
118
118
|
.addText(`<% choice %>
|
|
119
119
|
<% option %>
|
|
120
120
|
Use emojis.
|
|
@@ -134,7 +134,7 @@ Do not use emojis.
|
|
|
134
134
|
|
|
135
135
|
it('selects weighted options using relative weights', async () => {
|
|
136
136
|
const model = ModelMix.new()
|
|
137
|
-
.
|
|
137
|
+
.gpt52()
|
|
138
138
|
.assign({ language: 'Spanish' })
|
|
139
139
|
.addText(`<% choice %>
|
|
140
140
|
<% option 20 %>
|
|
@@ -155,7 +155,7 @@ Do not use emojis in <%- language %>.
|
|
|
155
155
|
|
|
156
156
|
it('supports nested choices', async () => {
|
|
157
157
|
const model = ModelMix.new()
|
|
158
|
-
.
|
|
158
|
+
.gpt52()
|
|
159
159
|
.addText(`<% choice %>
|
|
160
160
|
<% option %>
|
|
161
161
|
Tone:
|
|
@@ -187,7 +187,7 @@ first
|
|
|
187
187
|
<% option %>
|
|
188
188
|
second
|
|
189
189
|
<% /choice %>`;
|
|
190
|
-
const model = ModelMix.new().
|
|
190
|
+
const model = ModelMix.new().gpt52().addText(template);
|
|
191
191
|
const random = sinon.stub(model, '_choiceRandom');
|
|
192
192
|
random.onFirstCall().returns(0.1);
|
|
193
193
|
random.onSecondCall().returns(0.9);
|
|
@@ -208,7 +208,7 @@ second
|
|
|
208
208
|
|
|
209
209
|
it('fails before the request when a variable is missing', async () => {
|
|
210
210
|
const model = ModelMix.new()
|
|
211
|
-
.
|
|
211
|
+
.gpt52()
|
|
212
212
|
.assign({ name: 'David' })
|
|
213
213
|
.addText('Hello <%- name %>, status: <%- status %>');
|
|
214
214
|
|
|
@@ -225,7 +225,7 @@ second
|
|
|
225
225
|
});
|
|
226
226
|
|
|
227
227
|
it('rejects invalid template data immediately', () => {
|
|
228
|
-
const model = ModelMix.new().
|
|
228
|
+
const model = ModelMix.new().gpt52();
|
|
229
229
|
|
|
230
230
|
expect(() => model.assign(null)).to.throw(TypeError, 'Template data must be a plain non-null object.');
|
|
231
231
|
expect(() => model.assign(undefined)).to.throw(TypeError, 'Template data must be a plain non-null object.');
|
|
@@ -278,7 +278,7 @@ second
|
|
|
278
278
|
];
|
|
279
279
|
|
|
280
280
|
for (const testCase of cases) {
|
|
281
|
-
const model = ModelMix.new().
|
|
281
|
+
const model = ModelMix.new().gpt52().addText(testCase.source);
|
|
282
282
|
let error;
|
|
283
283
|
try {
|
|
284
284
|
await model.message();
|
|
@@ -293,7 +293,7 @@ second
|
|
|
293
293
|
|
|
294
294
|
it('rerolls earlier choices after a later template fails to render', async () => {
|
|
295
295
|
const model = ModelMix.new()
|
|
296
|
-
.
|
|
296
|
+
.gpt52()
|
|
297
297
|
.addText(`<% choice %>
|
|
298
298
|
<% option %>
|
|
299
299
|
A
|
|
@@ -333,7 +333,7 @@ A
|
|
|
333
333
|
<% option %>
|
|
334
334
|
B
|
|
335
335
|
<% /choice %>`;
|
|
336
|
-
const model = ModelMix.new().
|
|
336
|
+
const model = ModelMix.new().gpt52().addText(template);
|
|
337
337
|
const random = sinon.stub(model, '_choiceRandom');
|
|
338
338
|
random.onFirstCall().returns(0.1);
|
|
339
339
|
random.onSecondCall().returns(0.9);
|
|
@@ -370,7 +370,7 @@ B
|
|
|
370
370
|
max_history: 10,
|
|
371
371
|
bottleneck: { maxConcurrent: 2, minTime: 0 }
|
|
372
372
|
}
|
|
373
|
-
}).
|
|
373
|
+
}).gpt52().addText(template);
|
|
374
374
|
const content = model.messages[0].content[0];
|
|
375
375
|
const random = sinon.stub(model, '_choiceRandom');
|
|
376
376
|
random.onFirstCall().returns(0.1);
|
|
@@ -396,7 +396,7 @@ B
|
|
|
396
396
|
describe('File templates and data', () => {
|
|
397
397
|
it('renders a file template with a relative include', async () => {
|
|
398
398
|
const model = ModelMix.new()
|
|
399
|
-
.
|
|
399
|
+
.gpt52()
|
|
400
400
|
.assign({
|
|
401
401
|
name: 'Eve',
|
|
402
402
|
platform: 'ModelMix',
|
|
@@ -423,7 +423,7 @@ B
|
|
|
423
423
|
|
|
424
424
|
it('resolves a dynamic include path relative to its template', async () => {
|
|
425
425
|
const model = ModelMix.new()
|
|
426
|
-
.
|
|
426
|
+
.gpt52()
|
|
427
427
|
.assign({ rulesFile: 'system-rules.txt', language: 'Spanish' })
|
|
428
428
|
.addTextFromFile(path.join(fixturesPath, 'dynamic-include.txt'));
|
|
429
429
|
|
|
@@ -436,7 +436,7 @@ B
|
|
|
436
436
|
|
|
437
437
|
it('processes choice directives inside relative includes', async () => {
|
|
438
438
|
const model = ModelMix.new()
|
|
439
|
-
.
|
|
439
|
+
.gpt52()
|
|
440
440
|
.addTextFromFile(path.join(fixturesPath, 'choice-template.txt'));
|
|
441
441
|
sinon.stub(model, '_choiceRandom').returns(0.75);
|
|
442
442
|
|
|
@@ -449,7 +449,7 @@ B
|
|
|
449
449
|
|
|
450
450
|
it('supports recursive includes with an explicit depth limit', async () => {
|
|
451
451
|
const model = ModelMix.new()
|
|
452
|
-
.
|
|
452
|
+
.gpt52()
|
|
453
453
|
.assign({
|
|
454
454
|
node: {
|
|
455
455
|
text: 'Root',
|
|
@@ -481,7 +481,7 @@ B
|
|
|
481
481
|
const base = ModelMix.new()
|
|
482
482
|
.setSystemFromFile(path.join(fixturesPath, 'system-template.txt'))
|
|
483
483
|
.assign({ role: 'data analyst', language: 'Spanish' });
|
|
484
|
-
const model = base.new().
|
|
484
|
+
const model = base.new().gpt52().addText('Analyze this.');
|
|
485
485
|
|
|
486
486
|
mockOpenAI(body => {
|
|
487
487
|
const system = body.input.find(message => message.role === 'developer');
|
|
@@ -494,7 +494,7 @@ B
|
|
|
494
494
|
|
|
495
495
|
it('renders assigned files through EJS includes, including their relative includes', async () => {
|
|
496
496
|
const model = ModelMix.new()
|
|
497
|
-
.
|
|
497
|
+
.gpt52()
|
|
498
498
|
.assign({
|
|
499
499
|
name: 'Eve',
|
|
500
500
|
platform: 'ModelMix',
|
|
@@ -523,7 +523,7 @@ B
|
|
|
523
523
|
const base = ModelMix.new()
|
|
524
524
|
.assign({ language: 'Spanish' })
|
|
525
525
|
.assignKeyFromFile('rules', path.join(fixturesPath, 'system-rules.txt'));
|
|
526
|
-
const model = base.new().
|
|
526
|
+
const model = base.new().gpt52().addText('Rules:\n<%- rules %>');
|
|
527
527
|
|
|
528
528
|
mockOpenAI(body => {
|
|
529
529
|
expect(userTexts(body)[0].trim()).to.equal('Rules:\nAlways respond in Spanish.');
|
|
@@ -534,7 +534,7 @@ B
|
|
|
534
534
|
|
|
535
535
|
it('uses the latest assignment when a plain value and a file share a key', async () => {
|
|
536
536
|
const plainValue = ModelMix.new()
|
|
537
|
-
.
|
|
537
|
+
.gpt52()
|
|
538
538
|
.assign({ language: 'Spanish' })
|
|
539
539
|
.assignKeyFromFile('rules', path.join(fixturesPath, 'system-rules.txt'))
|
|
540
540
|
.assignKey('rules', 'Use the plain value.')
|
|
@@ -546,7 +546,7 @@ B
|
|
|
546
546
|
await plainValue.message();
|
|
547
547
|
|
|
548
548
|
const fileValue = ModelMix.new()
|
|
549
|
-
.
|
|
549
|
+
.gpt52()
|
|
550
550
|
.assign({ language: 'Spanish', rules: 'Ignore this value.' })
|
|
551
551
|
.assignKeyFromFile('rules', path.join(fixturesPath, 'system-rules.txt'))
|
|
552
552
|
.addText('<%- rules %>');
|
|
@@ -559,7 +559,7 @@ B
|
|
|
559
559
|
|
|
560
560
|
it('injects JSON file contents without XML escaping', async () => {
|
|
561
561
|
const model = ModelMix.new()
|
|
562
|
-
.
|
|
562
|
+
.gpt52()
|
|
563
563
|
.assignKeyFromFile('data', path.join(fixturesPath, 'data.json'))
|
|
564
564
|
.addText('Process this data:\n<%- data %>');
|
|
565
565
|
|
|
@@ -580,7 +580,7 @@ B
|
|
|
580
580
|
try {
|
|
581
581
|
fs.writeFileSync(assignedFile, 'Version one for <%- name %>.');
|
|
582
582
|
const model = ModelMix.new()
|
|
583
|
-
.
|
|
583
|
+
.gpt52()
|
|
584
584
|
.assign({ name: 'Eve' })
|
|
585
585
|
.assignKeyFromFile('content', assignedFile)
|
|
586
586
|
.addText('<%- content %>');
|
|
@@ -602,7 +602,7 @@ B
|
|
|
602
602
|
});
|
|
603
603
|
|
|
604
604
|
it('throws immediately when a template or data file is missing', () => {
|
|
605
|
-
const model = ModelMix.new().
|
|
605
|
+
const model = ModelMix.new().gpt52();
|
|
606
606
|
const missingPath = path.join(fixturesPath, 'nonexistent.txt');
|
|
607
607
|
|
|
608
608
|
expect(() => model.addTextFromFile(missingPath)).to.throw(`File not found: ${missingPath}`);
|
|
@@ -622,7 +622,7 @@ B
|
|
|
622
622
|
it('renders system and message templates for JSON output', async () => {
|
|
623
623
|
const schema = { summary: 'Analysis summary', userCount: 0 };
|
|
624
624
|
const model = ModelMix.new()
|
|
625
|
-
.
|
|
625
|
+
.gpt52()
|
|
626
626
|
.setSystem('You are a <%- role %>.')
|
|
627
627
|
.assign({ role: 'data analyst', instruction: 'Count active users' })
|
|
628
628
|
.assignKeyFromFile('data', path.join(fixturesPath, 'data.json'))
|
|
@@ -653,7 +653,7 @@ B
|
|
|
653
653
|
|
|
654
654
|
it('renders the system before adding block instructions', async () => {
|
|
655
655
|
const model = ModelMix.new()
|
|
656
|
-
.
|
|
656
|
+
.gpt52()
|
|
657
657
|
.setSystem('Act as <%- role %>.')
|
|
658
658
|
.assign({ role: 'reviewer' })
|
|
659
659
|
.addText('Review this.');
|
|
@@ -670,7 +670,7 @@ B
|
|
|
670
670
|
|
|
671
671
|
it('keeps rendered history snapshots when template data changes', async () => {
|
|
672
672
|
const model = ModelMix.new({ config: { max_history: 10 } })
|
|
673
|
-
.
|
|
673
|
+
.gpt52()
|
|
674
674
|
.assign({ name: 'Alice' })
|
|
675
675
|
.addText('Hello <%- name %>.');
|
|
676
676
|
|
|
@@ -689,8 +689,8 @@ B
|
|
|
689
689
|
it('keeps a system choice stable across provider fallback', async () => {
|
|
690
690
|
const systems = [];
|
|
691
691
|
const model = ModelMix.new()
|
|
692
|
-
.
|
|
693
|
-
.
|
|
692
|
+
.gpt52()
|
|
693
|
+
.sonnet5()
|
|
694
694
|
.setSystem(`<% choice %>
|
|
695
695
|
<% option %>
|
|
696
696
|
First system.
|
package/test/tokens.test.js
CHANGED
|
@@ -747,20 +747,19 @@ describe('Token Usage Tracking', () => {
|
|
|
747
747
|
}
|
|
748
748
|
});
|
|
749
749
|
|
|
750
|
-
it('should register
|
|
750
|
+
it('should register Kimi K2.6 through the current OpenRouter model ID', function () {
|
|
751
751
|
const originalOpenRouterApiKey = process.env.OPENROUTER_API_KEY;
|
|
752
752
|
process.env.OPENROUTER_API_KEY = 'test-openrouter-key';
|
|
753
753
|
|
|
754
754
|
try {
|
|
755
|
-
const model = ModelMix.new().
|
|
756
|
-
mix: {
|
|
755
|
+
const model = ModelMix.new().kimiK26({
|
|
756
|
+
mix: { fireworks: false, together: false, openrouter: true }
|
|
757
757
|
});
|
|
758
758
|
|
|
759
759
|
expect(model.models.map(({ key }) => key)).to.deep.equal([
|
|
760
|
-
'
|
|
761
|
-
'openai/gpt-oss-120b'
|
|
760
|
+
'moonshotai/kimi-k2.6'
|
|
762
761
|
]);
|
|
763
|
-
expect(model.models[
|
|
762
|
+
expect(model.models[0].provider).to.be.instanceOf(MixOpenRouter);
|
|
764
763
|
} finally {
|
|
765
764
|
if (originalOpenRouterApiKey === undefined) delete process.env.OPENROUTER_API_KEY;
|
|
766
765
|
else process.env.OPENROUTER_API_KEY = originalOpenRouterApiKey;
|