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.
- package/README.md +22 -31
- package/demo/benchmark.js +4 -7
- 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 +3 -1
- package/index.d.ts +2 -12
- package/index.js +7 -50
- package/lib/model-chain.js +8 -8
- package/lib/providers/openai-options.js +1 -1
- package/lib/providers/openai.js +14 -9
- package/lib/token-usage.js +3 -1
- package/package.json +1 -1
- package/plugins/benchmark/test/benchmark.test.js +33 -29
- package/skills/modelmix/SKILL.md +26 -35
- package/test/abort.test.js +1 -1
- package/test/bottleneck.test.js +12 -12
- package/test/effort.test.js +9 -0
- package/test/fallback.test.js +63 -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 +63 -23
package/test/fallback.test.js
CHANGED
|
@@ -115,13 +115,11 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
115
115
|
expect(ModelMix.new().mix.openrouter).to.equal(false);
|
|
116
116
|
const shortcuts = [
|
|
117
117
|
['gpt6astra', 'gpt-6-astra', 'openai/gpt-6-astra', MixOpenAIResponses],
|
|
118
|
-
['
|
|
118
|
+
['gpt6sol', 'gpt-6-sol', 'openai/gpt-6-sol', MixOpenAIResponses],
|
|
119
|
+
['gpt6luna', 'gpt-6-luna', 'openai/gpt-6-luna', MixOpenAIResponses],
|
|
119
120
|
['gpt5mini', 'gpt-5-mini', 'openai/gpt-5-mini', MixOpenAI],
|
|
120
121
|
['gpt5nano', 'gpt-5-nano', 'openai/gpt-5-nano', MixOpenAI],
|
|
121
|
-
['gpt51', 'gpt-5.1', 'openai/gpt-5.1', MixOpenAIResponses],
|
|
122
122
|
['gpt52', 'gpt-5.2', 'openai/gpt-5.2', MixOpenAIResponses],
|
|
123
|
-
['gpt53codex', 'gpt-5.3-codex', 'openai/gpt-5.3-codex', MixOpenAIResponses],
|
|
124
|
-
['gpt53chat', 'gpt-5.3-chat-latest', 'openai/gpt-5.3-chat', MixOpenAIResponses],
|
|
125
123
|
['gpt54', 'gpt-5.4', 'openai/gpt-5.4', MixOpenAIResponses],
|
|
126
124
|
['gpt54mini', 'gpt-5.4-mini', 'openai/gpt-5.4-mini', MixOpenAIResponses],
|
|
127
125
|
['gpt54nano', 'gpt-5.4-nano', 'openai/gpt-5.4-nano', MixOpenAIResponses],
|
|
@@ -151,10 +149,10 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
151
149
|
.gpt56sol({ mix: { openrouter: false } });
|
|
152
150
|
expect(locallyDisabled.models.map(({ key }) => key)).to.deep.equal(['gpt-5.6-sol']);
|
|
153
151
|
|
|
154
|
-
const inherited = ModelMix.new({ mix: { openrouter: true } }).new().
|
|
152
|
+
const inherited = ModelMix.new({ mix: { openrouter: true } }).new().gpt5mini();
|
|
155
153
|
expect(inherited.models.map(({ key }) => key)).to.deep.equal([
|
|
156
|
-
'gpt-5',
|
|
157
|
-
'openai/gpt-5'
|
|
154
|
+
'gpt-5-mini',
|
|
155
|
+
'openai/gpt-5-mini'
|
|
158
156
|
]);
|
|
159
157
|
});
|
|
160
158
|
|
|
@@ -232,6 +230,48 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
232
230
|
expect(openRouterRequest).to.not.have.property('temperature');
|
|
233
231
|
});
|
|
234
232
|
|
|
233
|
+
it('should fallback from the official GPT-6 Sol and Luna endpoints to OpenRouter', async () => {
|
|
234
|
+
const cases = [
|
|
235
|
+
['gpt6sol', 'gpt-6-sol', 'Hello from GPT-6 Sol through OpenRouter!'],
|
|
236
|
+
['gpt6luna', 'gpt-6-luna', 'Hello from GPT-6 Luna through OpenRouter!']
|
|
237
|
+
];
|
|
238
|
+
|
|
239
|
+
for (const [shortcut, officialKey, content] of cases) {
|
|
240
|
+
let openRouterRequest;
|
|
241
|
+
const instance = ModelMix.new().effort(100);
|
|
242
|
+
instance[shortcut]({ mix: { openrouter: true } }).addText('Hello');
|
|
243
|
+
|
|
244
|
+
nock('https://api.openai.com')
|
|
245
|
+
.post('/v1/responses', body => {
|
|
246
|
+
expect(body.model).to.equal(officialKey);
|
|
247
|
+
expect(body.reasoning).to.deep.equal({ effort: 'max' });
|
|
248
|
+
expect(body.max_output_tokens).to.equal(8192);
|
|
249
|
+
expect(body).to.not.have.property('temperature');
|
|
250
|
+
return true;
|
|
251
|
+
})
|
|
252
|
+
.reply(503, { error: 'Service unavailable' });
|
|
253
|
+
|
|
254
|
+
nock('https://openrouter.ai')
|
|
255
|
+
.post('/api/v1/chat/completions', body => {
|
|
256
|
+
openRouterRequest = body;
|
|
257
|
+
return true;
|
|
258
|
+
})
|
|
259
|
+
.reply(200, {
|
|
260
|
+
choices: [{
|
|
261
|
+
message: {
|
|
262
|
+
role: 'assistant',
|
|
263
|
+
content
|
|
264
|
+
}
|
|
265
|
+
}]
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
expect(await instance.message()).to.equal(content);
|
|
269
|
+
expect(openRouterRequest.model).to.equal(`openai/${officialKey}`);
|
|
270
|
+
expect(openRouterRequest.max_completion_tokens).to.equal(8192);
|
|
271
|
+
expect(openRouterRequest).to.not.have.property('temperature');
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
235
275
|
it('should keep the default fable51 chain on Anthropic', () => {
|
|
236
276
|
model.chain('fable51@80');
|
|
237
277
|
|
|
@@ -265,7 +305,7 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
265
305
|
});
|
|
266
306
|
|
|
267
307
|
it('should use primary provider when available', async () => {
|
|
268
|
-
model.gpt5mini().
|
|
308
|
+
model.gpt5mini().sonnet5().addText('Hello');
|
|
269
309
|
|
|
270
310
|
// Mock successful OpenAI response
|
|
271
311
|
nock('https://api.openai.com')
|
|
@@ -285,7 +325,7 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
285
325
|
});
|
|
286
326
|
|
|
287
327
|
it('should fallback to secondary provider when primary fails', async () => {
|
|
288
|
-
model.gpt5mini().
|
|
328
|
+
model.gpt5mini().sonnet5().addText('Hello');
|
|
289
329
|
|
|
290
330
|
// Mock failed OpenAI response (GPT-5 mini)
|
|
291
331
|
nock('https://api.openai.com')
|
|
@@ -399,7 +439,7 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
399
439
|
});
|
|
400
440
|
|
|
401
441
|
it('should cascade through multiple fallbacks', async () => {
|
|
402
|
-
model.gpt5mini().
|
|
442
|
+
model.gpt5mini().sonnet5().gemini37flash().addText('Hello');
|
|
403
443
|
|
|
404
444
|
// Mock failed OpenAI response
|
|
405
445
|
nock('https://api.openai.com')
|
|
@@ -430,7 +470,7 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
430
470
|
});
|
|
431
471
|
|
|
432
472
|
it('should throw error when all providers fail', async () => {
|
|
433
|
-
model.gpt5mini().
|
|
473
|
+
model.gpt5mini().sonnet5().addText('Hello');
|
|
434
474
|
|
|
435
475
|
// Mock all providers failing
|
|
436
476
|
nock('https://api.openai.com')
|
|
@@ -460,7 +500,7 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
460
500
|
});
|
|
461
501
|
|
|
462
502
|
it('should fallback from OpenAI to Anthropic', async () => {
|
|
463
|
-
model.gpt5mini().
|
|
503
|
+
model.gpt5mini().sonnet5().addText('Test message');
|
|
464
504
|
|
|
465
505
|
// Mock OpenAI failure
|
|
466
506
|
nock('https://api.openai.com')
|
|
@@ -483,7 +523,7 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
483
523
|
});
|
|
484
524
|
|
|
485
525
|
it('should fallback from Anthropic to Google', async () => {
|
|
486
|
-
model.
|
|
526
|
+
model.sonnet5().gemini37flash().addText('Test message');
|
|
487
527
|
|
|
488
528
|
// Mock Anthropic failure
|
|
489
529
|
nock('https://api.anthropic.com')
|
|
@@ -509,7 +549,7 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
509
549
|
});
|
|
510
550
|
|
|
511
551
|
it('should handle network timeout fallback', async () => {
|
|
512
|
-
model.gpt5mini().
|
|
552
|
+
model.gpt5mini().sonnet5().addText('Hello');
|
|
513
553
|
|
|
514
554
|
// Mock timeout error on first provider (using 408 Request Timeout)
|
|
515
555
|
nock('https://api.openai.com')
|
|
@@ -543,7 +583,7 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
543
583
|
|
|
544
584
|
it('should handle JSON fallback correctly', async () => {
|
|
545
585
|
const schema = { name: 'Alice', age: 30 };
|
|
546
|
-
model.gpt5mini().
|
|
586
|
+
model.gpt5mini().sonnet5().addText('Generate user data');
|
|
547
587
|
|
|
548
588
|
// Mock OpenAI failure
|
|
549
589
|
nock('https://api.openai.com')
|
|
@@ -569,7 +609,7 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
569
609
|
});
|
|
570
610
|
|
|
571
611
|
it('should preserve message history through fallbacks', async () => {
|
|
572
|
-
model.gpt5mini().
|
|
612
|
+
model.gpt5mini().sonnet5()
|
|
573
613
|
.addText('First message')
|
|
574
614
|
.addText('Second message');
|
|
575
615
|
|
|
@@ -600,7 +640,7 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
600
640
|
config: { debug: false, retry: { enabled: false } }
|
|
601
641
|
});
|
|
602
642
|
|
|
603
|
-
model.gpt5mini().
|
|
643
|
+
model.gpt5mini().sonnet5().addText('Hello');
|
|
604
644
|
|
|
605
645
|
nock('https://api.openai.com')
|
|
606
646
|
.post('/v1/chat/completions')
|
|
@@ -648,7 +688,7 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
648
688
|
config: { debug: false, retry: { enabled: true, retries: 2, baseDelayMs: 0, maxDelayMs: 0 } }
|
|
649
689
|
});
|
|
650
690
|
|
|
651
|
-
model.gpt5mini().
|
|
691
|
+
model.gpt5mini().sonnet5().addText('Hello');
|
|
652
692
|
|
|
653
693
|
nock('https://api.openai.com')
|
|
654
694
|
.post('/v1/chat/completions')
|
|
@@ -672,7 +712,7 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
672
712
|
config: { debug: false, retry: { enabled: true, retries: 1, baseDelayMs: 0, maxDelayMs: 0 } }
|
|
673
713
|
});
|
|
674
714
|
|
|
675
|
-
model.gpt5mini().
|
|
715
|
+
model.gpt5mini().sonnet5().addText('Hello');
|
|
676
716
|
|
|
677
717
|
nock('https://api.openai.com')
|
|
678
718
|
.post('/v1/chat/completions')
|
|
@@ -702,7 +742,7 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
702
742
|
|
|
703
743
|
// Configure with custom temperature for fallback
|
|
704
744
|
model.gpt5mini({ options: { temperature: 0.6 } })
|
|
705
|
-
.
|
|
745
|
+
.sonnet45({ options: { temperature: 0.7 } })
|
|
706
746
|
.addText('Creative response');
|
|
707
747
|
|
|
708
748
|
// Mock first provider failure
|
|
@@ -735,7 +775,7 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
735
775
|
});
|
|
736
776
|
|
|
737
777
|
model.gpt5mini({ options: { max_tokens: 100 } })
|
|
738
|
-
.
|
|
778
|
+
.sonnet5({ options: { max_tokens: 200 } })
|
|
739
779
|
.addText('Generate text');
|
|
740
780
|
|
|
741
781
|
// Mock OpenAI failure
|
|
@@ -869,7 +909,7 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
869
909
|
});
|
|
870
910
|
|
|
871
911
|
it('should provide detailed error information when all fallbacks fail', async () => {
|
|
872
|
-
model.gpt5mini().
|
|
912
|
+
model.gpt5mini().sonnet5().gemini37flash().addText('Test');
|
|
873
913
|
|
|
874
914
|
// Mock all providers failing with different errors
|
|
875
915
|
nock('https://api.openai.com')
|
|
@@ -894,7 +934,7 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
894
934
|
});
|
|
895
935
|
|
|
896
936
|
it('should handle malformed responses in fallback', async () => {
|
|
897
|
-
model.gpt5mini().
|
|
937
|
+
model.gpt5mini().sonnet5().addText('Test');
|
|
898
938
|
|
|
899
939
|
// Mock malformed response from first provider
|
|
900
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.
|