modelmix 3.8.2 → 3.8.4

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.
@@ -0,0 +1,387 @@
1
+ const { expect } = require('chai');
2
+ const sinon = require('sinon');
3
+ const nock = require('nock');
4
+ const { ModelMix } = require('../index.js');
5
+
6
+ describe('Provider Fallback Chain Tests', () => {
7
+
8
+ afterEach(() => {
9
+ nock.cleanAll();
10
+ sinon.restore();
11
+ });
12
+
13
+ describe('Basic Fallback Chain', () => {
14
+ let model;
15
+
16
+ beforeEach(() => {
17
+ model = ModelMix.new({
18
+ config: { debug: false }
19
+ });
20
+ });
21
+
22
+ it('should use primary provider when available', async () => {
23
+ model.gpt5mini().sonnet4().addText('Hello');
24
+
25
+ // Mock successful OpenAI response
26
+ nock('https://api.openai.com')
27
+ .post('/v1/chat/completions')
28
+ .reply(200, {
29
+ choices: [{
30
+ message: {
31
+ role: 'assistant',
32
+ content: 'Hello from GPT-5 mini!'
33
+ }
34
+ }]
35
+ });
36
+
37
+ const response = await model.message();
38
+
39
+ expect(response).to.include('Hello from GPT-5 mini!');
40
+ });
41
+
42
+ it('should fallback to secondary provider when primary fails', async () => {
43
+ model.gpt5mini().sonnet4().addText('Hello');
44
+
45
+ // Mock failed OpenAI response (GPT-5 mini)
46
+ nock('https://api.openai.com')
47
+ .post('/v1/chat/completions')
48
+ .reply(500, { error: 'Server error' });
49
+
50
+ // Mock successful Anthropic response (Sonnet 4)
51
+ nock('https://api.anthropic.com')
52
+ .post('/v1/messages')
53
+ .reply(200, {
54
+ content: [{
55
+ type: 'text',
56
+ text: 'Hello from Claude Sonnet 4!'
57
+ }]
58
+ });
59
+
60
+ const response = await model.message();
61
+
62
+ expect(response).to.include('Hello from Claude Sonnet 4!');
63
+ });
64
+
65
+ it('should cascade through multiple fallbacks', async () => {
66
+ model.gpt5mini().sonnet4().gemini25flash().addText('Hello');
67
+
68
+ // Mock failed OpenAI response
69
+ nock('https://api.openai.com')
70
+ .post('/v1/chat/completions')
71
+ .reply(429, { error: 'Rate limit exceeded' });
72
+
73
+ // Mock failed Anthropic response
74
+ nock('https://api.anthropic.com')
75
+ .post('/v1/messages')
76
+ .reply(401, { error: 'Unauthorized' });
77
+
78
+ // Mock successful Google response
79
+ nock('https://generativelanguage.googleapis.com')
80
+ .post(/.*generateContent/)
81
+ .reply(200, {
82
+ candidates: [{
83
+ content: {
84
+ parts: [{
85
+ text: 'Hello from Google Gemini 2.5 Flash!'
86
+ }]
87
+ }
88
+ }]
89
+ });
90
+
91
+ const response = await model.message();
92
+
93
+ expect(response).to.include('Hello from Google Gemini 2.5 Flash!');
94
+ });
95
+
96
+ it('should throw error when all providers fail', async () => {
97
+ model.gpt5mini().sonnet4().addText('Hello');
98
+
99
+ // Mock all providers failing
100
+ nock('https://api.openai.com')
101
+ .post('/v1/chat/completions')
102
+ .reply(500, { error: 'All servers down' });
103
+
104
+ nock('https://api.anthropic.com')
105
+ .post('/v1/messages')
106
+ .reply(500, { error: 'All servers down' });
107
+
108
+ try {
109
+ await model.message();
110
+ expect.fail('Should have thrown an error');
111
+ } catch (error) {
112
+ expect(error.message).to.include('500');
113
+ }
114
+ });
115
+ });
116
+
117
+ describe('Cross-Provider Fallback', () => {
118
+ let model;
119
+
120
+ beforeEach(() => {
121
+ model = ModelMix.new({
122
+ config: { debug: false }
123
+ });
124
+ });
125
+
126
+ it('should fallback from OpenAI to Anthropic', async () => {
127
+ model.gpt5mini().sonnet4().addText('Test message');
128
+
129
+ // Mock OpenAI failure
130
+ nock('https://api.openai.com')
131
+ .post('/v1/chat/completions')
132
+ .reply(503, { error: 'Service unavailable' });
133
+
134
+ // Mock Anthropic success
135
+ nock('https://api.anthropic.com')
136
+ .post('/v1/messages')
137
+ .reply(200, {
138
+ content: [{
139
+ type: 'text',
140
+ text: 'Response from Anthropic'
141
+ }]
142
+ });
143
+
144
+ const response = await model.message();
145
+
146
+ expect(response).to.include('Response from Anthropic');
147
+ });
148
+
149
+ it('should fallback from Anthropic to Google', async () => {
150
+ model.sonnet4().gemini25flash().addText('Test message');
151
+
152
+ // Mock Anthropic failure
153
+ nock('https://api.anthropic.com')
154
+ .post('/v1/messages')
155
+ .reply(401, { error: 'Unauthorized' });
156
+
157
+ // Mock Google success
158
+ nock('https://generativelanguage.googleapis.com')
159
+ .post(/.*generateContent/)
160
+ .reply(200, {
161
+ candidates: [{
162
+ content: {
163
+ parts: [{
164
+ text: 'Response from Google Gemini 2.5 Flash'
165
+ }]
166
+ }
167
+ }]
168
+ });
169
+
170
+ const response = await model.message();
171
+
172
+ expect(response).to.include('Response from Google Gemini 2.5 Flash');
173
+ });
174
+
175
+ it('should handle network timeout fallback', async () => {
176
+ model.gpt5mini().sonnet4().addText('Hello');
177
+
178
+ // Mock timeout error on first provider (using 408 Request Timeout)
179
+ nock('https://api.openai.com')
180
+ .post('/v1/chat/completions')
181
+ .reply(408, { error: 'Request timeout' });
182
+
183
+ // Mock successful response on fallback (Anthropic)
184
+ nock('https://api.anthropic.com')
185
+ .post('/v1/messages')
186
+ .reply(200, {
187
+ content: [{
188
+ type: 'text',
189
+ text: 'Quick fallback response from Claude'
190
+ }]
191
+ });
192
+
193
+ const response = await model.message();
194
+
195
+ expect(response).to.include('Quick fallback response from Claude');
196
+ });
197
+ });
198
+
199
+ describe('Fallback with Different Response Formats', () => {
200
+ let model;
201
+
202
+ beforeEach(() => {
203
+ model = ModelMix.new({
204
+ config: { debug: false }
205
+ });
206
+ });
207
+
208
+ it('should handle JSON fallback correctly', async () => {
209
+ const schema = { name: 'Alice', age: 30 };
210
+ model.gpt5mini().sonnet4().addText('Generate user data');
211
+
212
+ // Mock OpenAI failure
213
+ nock('https://api.openai.com')
214
+ .post('/v1/chat/completions')
215
+ .reply(400, { error: 'Bad request' });
216
+
217
+ // Mock Anthropic success with JSON
218
+ nock('https://api.anthropic.com')
219
+ .post('/v1/messages')
220
+ .reply(200, {
221
+ content: [{
222
+ type: 'text',
223
+ text: JSON.stringify({ name: 'Bob', age: 25 })
224
+ }]
225
+ });
226
+
227
+ const result = await model.json(schema);
228
+
229
+ expect(result).to.have.property('name');
230
+ expect(result).to.have.property('age');
231
+ expect(result.name).to.equal('Bob');
232
+ expect(result.age).to.equal(25);
233
+ });
234
+
235
+ it('should preserve message history through fallbacks', async () => {
236
+ model.gpt5mini().sonnet4()
237
+ .addText('First message')
238
+ .addText('Second message');
239
+
240
+ // Mock first provider failure
241
+ nock('https://api.openai.com')
242
+ .post('/v1/chat/completions')
243
+ .reply(500, { error: 'Server error' });
244
+
245
+ // Mock second provider success (Anthropic)
246
+ nock('https://api.anthropic.com')
247
+ .post('/v1/messages')
248
+ .reply(200, {
249
+ content: [{
250
+ type: 'text',
251
+ text: 'Fallback response with context from Claude'
252
+ }]
253
+ });
254
+
255
+ const response = await model.message();
256
+
257
+ expect(response).to.include('Fallback response with context from Claude');
258
+ });
259
+ });
260
+
261
+ describe('Fallback Configuration', () => {
262
+ it('should respect custom provider configurations in fallback', async () => {
263
+ const model = ModelMix.new({
264
+ config: { debug: false },
265
+ options: { temperature: 0.5 }
266
+ });
267
+
268
+ // Configure with custom temperature for fallback
269
+ model.gpt5mini({ options: { temperature: 0.6 } })
270
+ .sonnet4({ options: { temperature: 0.7 } })
271
+ .addText('Creative response');
272
+
273
+ // Mock first provider failure
274
+ nock('https://api.openai.com')
275
+ .post('/v1/chat/completions')
276
+ .reply(503, { error: 'Service unavailable' });
277
+
278
+ // Mock second provider success - verify temperature in request (Anthropic)
279
+ nock('https://api.anthropic.com')
280
+ .post('/v1/messages')
281
+ .reply(function(uri, requestBody) {
282
+ const body = typeof requestBody === 'string' ? JSON.parse(requestBody) : requestBody;
283
+ expect(body.temperature).to.equal(0.7);
284
+ return [200, {
285
+ content: [{
286
+ type: 'text',
287
+ text: 'Creative fallback response from Claude'
288
+ }]
289
+ }];
290
+ });
291
+
292
+ const response = await model.message();
293
+
294
+ expect(response).to.include('Creative fallback response from Claude');
295
+ });
296
+
297
+ it('should handle provider-specific options in fallback chain', async () => {
298
+ const model = ModelMix.new({
299
+ config: { debug: false }
300
+ });
301
+
302
+ model.gpt5mini({ options: { max_tokens: 100 } })
303
+ .sonnet4({ options: { max_tokens: 200 } })
304
+ .addText('Generate text');
305
+
306
+ // Mock OpenAI failure
307
+ nock('https://api.openai.com')
308
+ .post('/v1/chat/completions')
309
+ .reply(429, { error: 'Rate limited' });
310
+
311
+ // Mock Anthropic success - verify max_tokens
312
+ nock('https://api.anthropic.com')
313
+ .post('/v1/messages')
314
+ .reply(function (uri, body) {
315
+ expect(body.max_tokens).to.equal(200);
316
+ return [200, {
317
+ content: [{
318
+ type: 'text',
319
+ text: 'Fallback with correct max_tokens'
320
+ }]
321
+ }];
322
+ });
323
+
324
+ const response = await model.message();
325
+
326
+ expect(response).to.include('Fallback with correct max_tokens');
327
+ });
328
+ });
329
+
330
+ describe('Error Handling in Fallback', () => {
331
+ let model;
332
+
333
+ beforeEach(() => {
334
+ model = ModelMix.new({
335
+ config: { debug: false }
336
+ });
337
+ });
338
+
339
+ it('should provide detailed error information when all fallbacks fail', async () => {
340
+ model.gpt5mini().sonnet4().gemini25flash().addText('Test');
341
+
342
+ // Mock all providers failing with different errors
343
+ nock('https://api.openai.com')
344
+ .post('/v1/chat/completions')
345
+ .reply(500, { error: 'OpenAI server error' });
346
+
347
+ nock('https://api.anthropic.com')
348
+ .post('/v1/messages')
349
+ .reply(401, { error: 'Anthropic auth error' });
350
+
351
+ nock('https://generativelanguage.googleapis.com')
352
+ .post(/.*generateContent/)
353
+ .reply(403, { error: 'Google forbidden' });
354
+
355
+ try {
356
+ await model.message();
357
+ expect.fail('Should have thrown an error');
358
+ } catch (error) {
359
+ // Should contain information about the final error
360
+ expect(error.message).to.be.a('string');
361
+ }
362
+ });
363
+
364
+ it('should handle malformed responses in fallback', async () => {
365
+ model.gpt5mini().sonnet4().addText('Test');
366
+
367
+ // Mock malformed response from first provider
368
+ nock('https://api.openai.com')
369
+ .post('/v1/chat/completions')
370
+ .reply(200, { invalid: 'response' });
371
+
372
+ // Mock valid response from fallback (Anthropic)
373
+ nock('https://api.anthropic.com')
374
+ .post('/v1/messages')
375
+ .reply(200, {
376
+ content: [{
377
+ type: 'text',
378
+ text: 'Valid fallback response from Claude'
379
+ }]
380
+ });
381
+
382
+ const response = await model.message();
383
+
384
+ expect(response).to.include('Valid fallback response from Claude');
385
+ });
386
+ });
387
+ });
@@ -0,0 +1,36 @@
1
+ {
2
+ "users": [
3
+ {
4
+ "id": 1,
5
+ "name": "Alice Smith",
6
+ "email": "alice@example.com",
7
+ "role": "admin",
8
+ "active": true
9
+ },
10
+ {
11
+ "id": 2,
12
+ "name": "Bob Johnson",
13
+ "email": "bob@example.com",
14
+ "role": "user",
15
+ "active": false
16
+ },
17
+ {
18
+ "id": 3,
19
+ "name": "Carol Davis",
20
+ "email": "carol@example.com",
21
+ "role": "moderator",
22
+ "active": true
23
+ }
24
+ ],
25
+ "settings": {
26
+ "theme": "dark",
27
+ "notifications": true,
28
+ "language": "en",
29
+ "timezone": "UTC"
30
+ },
31
+ "metadata": {
32
+ "version": "1.0.0",
33
+ "last_updated": "2023-12-01T10:00:00Z",
34
+ "total_users": 3
35
+ }
36
+ }
Binary file
@@ -0,0 +1,15 @@
1
+ Hello {{name}}, welcome to {{platform}}!
2
+
3
+ Your account details:
4
+ - Username: {{username}}
5
+ - Role: {{role}}
6
+ - Created: {{created_date}}
7
+
8
+ Please visit {{website}} for more information.
9
+
10
+ {{#if premium}}
11
+ Thank you for being a premium member!
12
+ {{/if}}
13
+
14
+ Best regards,
15
+ The {{company}} Team
@@ -0,0 +1,87 @@
1
+ const { expect } = require('chai');
2
+ const sinon = require('sinon');
3
+ const nock = require('nock');
4
+ const { ModelMix } = require('../index.js');
5
+
6
+ describe('Image Processing and Multimodal Support Tests', () => {
7
+
8
+ afterEach(() => {
9
+ nock.cleanAll();
10
+ sinon.restore();
11
+ });
12
+
13
+ describe('Image Data Handling', () => {
14
+ let model;
15
+
16
+ const max_history = 2;
17
+
18
+ beforeEach(() => {
19
+ model = ModelMix.new({
20
+ config: { debug: false },
21
+ config: { max_history }
22
+ });
23
+ });
24
+
25
+ it('should handle base64 image data correctly', async () => {
26
+ const base64Image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAFUlEQVR42mP8z8BQz0AEYBxVSF+FABJADveWkH6oAAAAAElFTkSuQmCC';
27
+
28
+ model.gpt4o()
29
+ .addText('What do you see in this image?')
30
+ .addImageFromUrl(base64Image);
31
+
32
+ nock('https://api.openai.com')
33
+ .post('/v1/chat/completions')
34
+ .reply(function (uri, body) {
35
+ expect(body.messages[1].content).to.be.an('array');
36
+ expect(body.messages[1].content).to.have.length(max_history);
37
+ expect(body.messages[1].content[max_history - 1].image_url.url).to.equal(base64Image);
38
+
39
+ return [200, {
40
+ choices: [{
41
+ message: {
42
+ role: 'assistant',
43
+ content: 'I can see a small test image'
44
+ }
45
+ }]
46
+ }];
47
+ });
48
+
49
+ const response = await model.message();
50
+ expect(response).to.include('I can see a small test image');
51
+ });
52
+
53
+ it('should support multimodal with sonnet4()', async () => {
54
+ const base64Image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAFUlEQVR42mP8z8BQz0AEYBxVSF+FABJADveWkH6oAAAAAElFTkSuQmCC';
55
+
56
+ model.sonnet4()
57
+ .addText('Describe this image')
58
+ .addImageFromUrl(base64Image);
59
+
60
+ // Claude expects images as base64 in the content array
61
+ // We'll check that the message is formatted as expected
62
+ nock('https://api.anthropic.com')
63
+ .post('/v1/messages')
64
+ .reply(function (uri, body) {
65
+ console.log(body.messages);
66
+ // body is already parsed as JSON by nock
67
+ expect(body.messages).to.be.an('array');
68
+ // Find the message with the image
69
+ const userMsg = body.messages.find(m => m.role === 'user');
70
+ expect(userMsg).to.exist;
71
+ const imageContent = userMsg.content.find(c => c.type === 'image');
72
+ expect(imageContent).to.exist;
73
+ expect(imageContent.source.type).to.equal('base64');
74
+ expect(imageContent.source.data).to.equal(base64Image.split(',')[1]);
75
+ expect(imageContent.source.media_type).to.equal('image/png');
76
+ return [200, {
77
+ content: [{ type: "text", text: "This is a small PNG test image." }],
78
+ role: "assistant"
79
+ }];
80
+ });
81
+
82
+ const response = await model.message();
83
+ expect(response).to.include('small PNG test image');
84
+ });
85
+
86
+ });
87
+ });