modelmix 4.4.4 → 4.4.7

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/test/json.test.js CHANGED
@@ -181,6 +181,163 @@ describe('JSON Schema and Structured Output Tests', () => {
181
181
  });
182
182
  });
183
183
 
184
+ describe('Enhanced Descriptor Descriptions', () => {
185
+ it('should support required: false (not in required + nullable)', () => {
186
+ const example = { name: 'Alice', nickname: 'Ali' };
187
+ const descriptions = {
188
+ name: 'Full name',
189
+ nickname: { description: 'Optional nickname', required: false }
190
+ };
191
+ const schema = generateJsonSchema(example, descriptions);
192
+
193
+ expect(schema.required).to.deep.equal(['name']);
194
+ expect(schema.properties.nickname).to.deep.equal({
195
+ type: ['string', 'null'],
196
+ description: 'Optional nickname'
197
+ });
198
+ expect(schema.properties.name).to.deep.equal({
199
+ type: 'string',
200
+ description: 'Full name'
201
+ });
202
+ });
203
+
204
+ it('should support enum', () => {
205
+ const example = { status: 'active' };
206
+ const descriptions = {
207
+ status: { description: 'Account status', enum: ['active', 'inactive', 'banned'] }
208
+ };
209
+ const schema = generateJsonSchema(example, descriptions);
210
+
211
+ expect(schema.properties.status).to.deep.equal({
212
+ type: 'string',
213
+ description: 'Account status',
214
+ enum: ['active', 'inactive', 'banned']
215
+ });
216
+ expect(schema.required).to.deep.equal(['status']);
217
+ });
218
+
219
+ it('should make type nullable when enum includes null', () => {
220
+ const example = { sex: 'm' };
221
+ const descriptions = {
222
+ sex: { description: 'Gender', enum: ['m', 'f', null] }
223
+ };
224
+ const schema = generateJsonSchema(example, descriptions);
225
+
226
+ expect(schema.properties.sex).to.deep.equal({
227
+ type: ['string', 'null'],
228
+ description: 'Gender',
229
+ enum: ['m', 'f', null]
230
+ });
231
+ });
232
+
233
+ it('should support default value', () => {
234
+ const example = { theme: 'light' };
235
+ const descriptions = {
236
+ theme: { description: 'UI theme', default: 'light', enum: ['light', 'dark'] }
237
+ };
238
+ const schema = generateJsonSchema(example, descriptions);
239
+
240
+ expect(schema.properties.theme).to.deep.equal({
241
+ type: 'string',
242
+ description: 'UI theme',
243
+ default: 'light',
244
+ enum: ['light', 'dark']
245
+ });
246
+ });
247
+
248
+ it('should mix string and descriptor descriptions', () => {
249
+ const example = { name: 'martin', age: 22, sex: 'm' };
250
+ const descriptions = {
251
+ name: { description: 'Name of the actor', required: false },
252
+ age: 'Age of the actor',
253
+ sex: { description: 'Gender', enum: ['m', 'f', null], default: 'm' }
254
+ };
255
+ const schema = generateJsonSchema(example, descriptions);
256
+
257
+ expect(schema.required).to.deep.equal(['age', 'sex']);
258
+ expect(schema.properties.name).to.deep.equal({
259
+ type: ['string', 'null'],
260
+ description: 'Name of the actor'
261
+ });
262
+ expect(schema.properties.age).to.deep.equal({
263
+ type: 'integer',
264
+ description: 'Age of the actor'
265
+ });
266
+ expect(schema.properties.sex).to.deep.equal({
267
+ type: ['string', 'null'],
268
+ description: 'Gender',
269
+ enum: ['m', 'f', null],
270
+ default: 'm'
271
+ });
272
+ });
273
+
274
+ it('should not apply descriptor as nested descriptions for objects', () => {
275
+ const example = { user: { name: 'Alice', age: 30 } };
276
+ const descriptions = {
277
+ user: { description: 'User details', required: false }
278
+ };
279
+ const schema = generateJsonSchema(example, descriptions);
280
+
281
+ expect(schema.required).to.deep.equal([]);
282
+ expect(schema.properties.user.description).to.equal('User details');
283
+ expect(schema.properties.user.properties.name).to.deep.equal({ type: 'string' });
284
+ expect(schema.properties.user.properties.age).to.deep.equal({ type: 'integer' });
285
+ });
286
+
287
+ it('should pass nested descriptions correctly for objects', () => {
288
+ const example = { user: { name: 'Alice', age: 30 } };
289
+ const descriptions = {
290
+ user: { name: 'User name', age: 'User age' }
291
+ };
292
+ const schema = generateJsonSchema(example, descriptions);
293
+
294
+ expect(schema.properties.user.properties.name).to.deep.equal({
295
+ type: 'string',
296
+ description: 'User name'
297
+ });
298
+ expect(schema.properties.user.properties.age).to.deep.equal({
299
+ type: 'integer',
300
+ description: 'User age'
301
+ });
302
+ });
303
+
304
+ it('should handle array descriptions in array format', () => {
305
+ const example = {
306
+ countries: [{ name: 'France', capital: 'Paris' }]
307
+ };
308
+ const descriptions = {
309
+ countries: [{ name: 'Country name', capital: 'Capital city' }]
310
+ };
311
+ const schema = generateJsonSchema(example, descriptions);
312
+
313
+ expect(schema.properties.countries.type).to.equal('array');
314
+ expect(schema.properties.countries.items.properties.name.description).to.equal('Country name');
315
+ expect(schema.properties.countries.items.properties.capital.description).to.equal('Capital city');
316
+ });
317
+
318
+ it('should handle descriptor for array field itself', () => {
319
+ const example = { tags: ['admin'] };
320
+ const descriptions = {
321
+ tags: { description: 'User tags', required: false }
322
+ };
323
+ const schema = generateJsonSchema(example, descriptions);
324
+
325
+ expect(schema.properties.tags.description).to.equal('User tags');
326
+ expect(schema.required).to.deep.equal([]);
327
+ });
328
+
329
+ it('should not double-add null to type', () => {
330
+ const example = { status: 'active' };
331
+ const descriptions = {
332
+ status: { required: false, enum: ['active', null] }
333
+ };
334
+ const schema = generateJsonSchema(example, descriptions);
335
+
336
+ const nullCount = schema.properties.status.type.filter(t => t === 'null').length;
337
+ expect(nullCount).to.equal(1);
338
+ });
339
+ });
340
+
184
341
  describe('ModelMix JSON Output', () => {
185
342
  let model;
186
343
 
@@ -198,7 +355,7 @@ describe('JSON Schema and Structured Output Tests', () => {
198
355
  }]
199
356
  };
200
357
 
201
- model.gpt41().addText('List 3 countries');
358
+ model.gpt52().addText('List 3 countries');
202
359
 
203
360
  // Mock the API response
204
361
  nock('https://api.openai.com')
@@ -249,7 +406,7 @@ describe('JSON Schema and Structured Output Tests', () => {
249
406
  }
250
407
  };
251
408
 
252
- model.sonnet4().addText('Generate user data');
409
+ model.sonnet46().addText('Generate user data');
253
410
 
254
411
  // Mock the API response
255
412
  nock('https://api.anthropic.com')
@@ -270,7 +427,7 @@ describe('JSON Schema and Structured Output Tests', () => {
270
427
  });
271
428
 
272
429
  it('should handle JSON parsing errors gracefully', async () => {
273
- model.gpt41().addText('Generate invalid JSON');
430
+ model.gpt52().addText('Generate invalid JSON');
274
431
 
275
432
  // Mock invalid JSON response
276
433
  nock('https://api.openai.com')
@@ -291,5 +448,33 @@ describe('JSON Schema and Structured Output Tests', () => {
291
448
  expect(error.message).to.include('JSON');
292
449
  }
293
450
  });
451
+
452
+ it('should auto-wrap top-level array and unwrap on return', async () => {
453
+ model.gpt52().addText('List 3 countries');
454
+
455
+ nock('https://api.openai.com')
456
+ .post('/v1/chat/completions')
457
+ .reply(200, {
458
+ choices: [{
459
+ message: {
460
+ role: 'assistant',
461
+ content: JSON.stringify({
462
+ out: [
463
+ { name: 'France' },
464
+ { name: 'Germany' },
465
+ { name: 'Spain' }
466
+ ]
467
+ })
468
+ }
469
+ }]
470
+ });
471
+
472
+ const result = await model.json([{ name: 'France' }]);
473
+
474
+ expect(result).to.be.an('array');
475
+ expect(result).to.have.length(3);
476
+ expect(result[0]).to.have.property('name', 'France');
477
+ expect(result[2]).to.have.property('name', 'Spain');
478
+ });
294
479
  });
295
480
  });
package/test/live.mcp.js CHANGED
@@ -31,8 +31,8 @@ describe('Live MCP Integration Tests', function () {
31
31
 
32
32
  describe('Basic MCP Tool Integration', function () {
33
33
 
34
- it('should use custom MCP tools with GPT-4.1', async function () {
35
- const model = ModelMix.new(setup).gpt41();
34
+ it('should use custom MCP tools with GPT-5.2', async function () {
35
+ const model = ModelMix.new(setup).gpt52();
36
36
 
37
37
  // Add custom calculator tool
38
38
  model.addTool({
@@ -68,8 +68,8 @@ describe('Live MCP Integration Tests', function () {
68
68
  expect(response).to.include('345');
69
69
  });
70
70
 
71
- it('should use custom MCP tools with Claude Sonnet 4', async function () {
72
- const model = ModelMix.new(setup).sonnet4();
71
+ it('should use custom MCP tools with Claude Sonnet 4.6', async function () {
72
+ const model = ModelMix.new(setup).sonnet46();
73
73
 
74
74
  // Add time tool
75
75
  model.addTool({
@@ -505,7 +505,7 @@ describe('Live MCP Integration Tests', function () {
505
505
  const models = [
506
506
  { name: 'GPT-5 Mini', model: ModelMix.new(setup).gpt5mini() },
507
507
  { name: 'GPT-5 Nano', model: ModelMix.new(setup).gpt5nano() },
508
- { name: 'GPT-4.1', model: ModelMix.new(setup).gpt41() }
508
+ { name: 'GPT-5.2', model: ModelMix.new(setup).gpt52() }
509
509
  ];
510
510
 
511
511
  const results = [];
@@ -528,8 +528,8 @@ describe('Live MCP Integration Tests', function () {
528
528
 
529
529
  it('should work with same MCP tools across different Anthropic models', async function () {
530
530
  const models = [
531
- { name: 'Sonnet 4', model: ModelMix.new(setup).sonnet4() },
532
- { name: 'Sonnet 4.5', model: ModelMix.new(setup).sonnet45() },
531
+ { name: 'Sonnet 4', model: ModelMix.new(setup).sonnet46() },
532
+ { name: 'Sonnet 4.6', model: ModelMix.new(setup).sonnet46() },
533
533
  { name: 'Haiku 4.5', model: ModelMix.new(setup).haiku45() }
534
534
  ];
535
535
 
package/test/live.test.js CHANGED
@@ -41,14 +41,14 @@ describe('Live Integration Tests', function () {
41
41
 
42
42
  const response = await model.message();
43
43
 
44
- console.log(`OpenAI GPT-4o response: ${response}`);
44
+ console.log(`OpenAI GPT-5.2 response: ${response}`);
45
45
 
46
46
  expect(response).to.be.a('string');
47
47
  expect(response.toLowerCase()).to.include('blue');
48
48
  });
49
49
 
50
- it('should process images with Anthropic Claude', async function () {
51
- const model = ModelMix.new(setup).sonnet45();
50
+ it('should process images with Anthropic Sonnet 4.6', async function () {
51
+ const model = ModelMix.new(setup).sonnet46();
52
52
 
53
53
  model.addImageFromUrl(blueSquareBase64)
54
54
  .addText('What color is this image? Answer in one word only.');
@@ -61,7 +61,7 @@ describe('Live Integration Tests', function () {
61
61
  });
62
62
 
63
63
  it('should process images with Google Gemini', async function () {
64
- const model = ModelMix.new(setup).gemini25flash();
64
+ const model = ModelMix.new(setup).gemini3flash();
65
65
 
66
66
  model.addImageFromUrl(blueSquareBase64)
67
67
  .addText('What color is this image? Answer in one word only.');
@@ -99,8 +99,8 @@ describe('Live Integration Tests', function () {
99
99
  expect(result.skills).to.be.an('array');
100
100
  });
101
101
 
102
- it('should return structured JSON with Sonnet 4.5 thinking', async function () {
103
- const model = ModelMix.new(setup).sonnet45think();
102
+ it('should return structured JSON with Sonnet 4.6 thinking', async function () {
103
+ const model = ModelMix.new(setup).sonnet46think();
104
104
 
105
105
  model.addText('Generate information about a fictional city.');
106
106
 
@@ -122,7 +122,7 @@ describe('Live Integration Tests', function () {
122
122
  });
123
123
 
124
124
  it('should return structured JSON with Google Gemini', async function () {
125
- const model = ModelMix.new(setup).gemini25flash();
125
+ const model = ModelMix.new(setup).gemini3flash();
126
126
 
127
127
  model.addText('Generate information about a fictional city.');
128
128
 
@@ -152,7 +152,7 @@ describe('Live Integration Tests', function () {
152
152
  // Create a model chain: non-existent model -> Claude
153
153
  const model = ModelMix.new(setup)
154
154
  .attach('non-existent-model', new MixOpenAI())
155
- .sonnet4();
155
+ .sonnet46();
156
156
 
157
157
  model.addText('Say "fallback test successful" and nothing else.');
158
158
 
@@ -27,7 +27,7 @@ describe('Template and File Operations Tests', () => {
27
27
  });
28
28
 
29
29
  it('should replace simple template variables', async () => {
30
- model.gpt41()
30
+ model.gpt51()
31
31
  .replace({
32
32
  '{{name}}': 'Alice',
33
33
  '{{age}}': '30',
@@ -56,7 +56,7 @@ describe('Template and File Operations Tests', () => {
56
56
  });
57
57
 
58
58
  it('should handle multiple template replacements', async () => {
59
- model.gpt41()
59
+ model.gpt51()
60
60
  .replace({ '{{greeting}}': 'Hello' })
61
61
  .replace({ '{{name}}': 'Bob' })
62
62
  .replace({ '{{action}}': 'welcome' })
@@ -82,7 +82,7 @@ describe('Template and File Operations Tests', () => {
82
82
  });
83
83
 
84
84
  it('should handle nested template objects', async () => {
85
- model.gpt41()
85
+ model.gpt51()
86
86
  .replace({
87
87
  '{{user_name}}': 'Charlie',
88
88
  '{{user_role}}': 'admin',
@@ -111,7 +111,7 @@ describe('Template and File Operations Tests', () => {
111
111
  });
112
112
 
113
113
  it('should preserve unreplaced templates', async () => {
114
- model.gpt41()
114
+ model.gpt51()
115
115
  .replace({ '{{name}}': 'David' })
116
116
  .addText('Hello {{name}}, your ID is {{user_id}} and status is {{status}}');
117
117
 
@@ -135,7 +135,7 @@ describe('Template and File Operations Tests', () => {
135
135
  });
136
136
 
137
137
  it('should handle empty and special character replacements', async () => {
138
- model.gpt41()
138
+ model.gpt51()
139
139
  .replace({
140
140
  '{{empty}}': '',
141
141
  '{{special}}': 'Hello & "World" <test>',
@@ -175,7 +175,7 @@ describe('Template and File Operations Tests', () => {
175
175
  });
176
176
 
177
177
  it('should load and replace from template file', async () => {
178
- model.gpt41()
178
+ model.gpt51()
179
179
  .replaceKeyFromFile('{{template}}', path.join(fixturesPath, 'template.txt'))
180
180
  .replace({
181
181
  '{{name}}': 'Eve',
@@ -214,7 +214,7 @@ describe('Template and File Operations Tests', () => {
214
214
  });
215
215
 
216
216
  it('should load and process JSON data file', async () => {
217
- model.gpt41()
217
+ model.gpt51()
218
218
  .replaceKeyFromFile('{{data}}', path.join(fixturesPath, 'data.json'))
219
219
  .addText('Process this data: {{data}}');
220
220
 
@@ -246,7 +246,7 @@ describe('Template and File Operations Tests', () => {
246
246
  });
247
247
 
248
248
  it('should handle file loading errors gracefully', async () => {
249
- model.gpt41()
249
+ model.gpt51()
250
250
  .replaceKeyFromFile('{{missing}}', path.join(fixturesPath, 'nonexistent.txt'))
251
251
  .addText('This should contain: {{missing}}');
252
252
 
@@ -271,7 +271,7 @@ describe('Template and File Operations Tests', () => {
271
271
  });
272
272
 
273
273
  it('should handle multiple file replacements', async () => {
274
- model.gpt41()
274
+ model.gpt51()
275
275
  .replaceKeyFromFile('{{template}}', path.join(fixturesPath, 'template.txt'))
276
276
  .replaceKeyFromFile('{{data}}', path.join(fixturesPath, 'data.json'))
277
277
  .replace({
@@ -315,7 +315,7 @@ describe('Template and File Operations Tests', () => {
315
315
  it('should handle relative and absolute paths', async () => {
316
316
  const absolutePath = path.resolve(fixturesPath, 'template.txt');
317
317
 
318
- model.gpt41()
318
+ model.gpt51()
319
319
  .replaceKeyFromFile('{{absolute}}', absolutePath)
320
320
  .replace({
321
321
  '{{name}}': 'Grace',
@@ -362,7 +362,7 @@ describe('Template and File Operations Tests', () => {
362
362
  });
363
363
 
364
364
  it('should combine file loading with template replacement in complex scenarios', async () => {
365
- model.gpt41()
365
+ model.gpt51()
366
366
  .replaceKeyFromFile('{{user_data}}', path.join(fixturesPath, 'data.json'))
367
367
  .replace({
368
368
  '{{action}}': 'analyze',
@@ -402,7 +402,7 @@ describe('Template and File Operations Tests', () => {
402
402
  roles: ['admin', 'user']
403
403
  };
404
404
 
405
- model.gpt41()
405
+ model.gpt51()
406
406
  .replaceKeyFromFile('{{data}}', path.join(fixturesPath, 'data.json'))
407
407
  .replace({ '{{instruction}}': 'Count active users by role' })
408
408
  .addText('{{instruction}} from this data: {{data}}');
@@ -447,16 +447,16 @@ describe('Template and File Operations Tests', () => {
447
447
 
448
448
  it('should handle template replacement errors gracefully', () => {
449
449
  expect(() => {
450
- model.gpt41().replace(null);
450
+ model.gpt51().replace(null);
451
451
  }).to.not.throw();
452
452
 
453
453
  expect(() => {
454
- model.gpt41().replace(undefined);
454
+ model.gpt51().replace(undefined);
455
455
  }).to.not.throw();
456
456
  });
457
457
 
458
458
  it('should handle file reading errors without crashing', async () => {
459
- model.gpt41()
459
+ model.gpt51()
460
460
  .replaceKeyFromFile('{{bad_file}}', '/path/that/does/not/exist.txt')
461
461
  .addText('Content: {{bad_file}}');
462
462
 
@@ -1,8 +1,23 @@
1
1
  import { expect } from 'chai';
2
2
  import { ModelMix } from '../index.js';
3
+ import { createRequire } from 'module';
4
+
5
+ const require = createRequire(import.meta.url);
6
+ const nock = require('nock');
3
7
 
4
8
  describe('Token Usage Tracking', () => {
5
9
 
10
+ // Ensure nock doesn't interfere with live requests via MockHttpSocket
11
+ before(function() {
12
+ nock.cleanAll();
13
+ nock.restore();
14
+ });
15
+
16
+ after(function() {
17
+ // Re-activate nock for any subsequent test suites
18
+ nock.activate();
19
+ });
20
+
6
21
  it('should track tokens in OpenAI response', async function () {
7
22
  this.timeout(30000);
8
23
 
@@ -30,7 +45,7 @@ describe('Token Usage Tracking', () => {
30
45
  this.timeout(30000);
31
46
 
32
47
  const model = ModelMix.new()
33
- .haiku35()
48
+ .haiku45()
34
49
  .addText('Say hi');
35
50
 
36
51
  const result = await model.raw();
@@ -49,7 +64,7 @@ describe('Token Usage Tracking', () => {
49
64
  this.timeout(30000);
50
65
 
51
66
  const model = ModelMix.new()
52
- .gemini25flash()
67
+ .gemini3flash()
53
68
  .addText('Say hi');
54
69
 
55
70
  const result = await model.raw();
@@ -110,8 +125,8 @@ describe('Token Usage Tracking', () => {
110
125
 
111
126
  const providers = [
112
127
  { name: 'OpenAI', create: (m) => m.gpt5nano() },
113
- { name: 'Anthropic', create: (m) => m.haiku35() },
114
- { name: 'Google', create: (m) => m.gemini25flash() }
128
+ { name: 'Anthropic', create: (m) => m.haiku45() },
129
+ { name: 'Google', create: (m) => m.gemini3flash() }
115
130
  ];
116
131
 
117
132
  for (const provider of providers) {