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/README.md +44 -16
- package/demo/json.js +13 -11
- package/index.js +138 -25
- package/package.json +2 -2
- package/schema.js +49 -5
- package/skills/modelmix/SKILL.md +35 -5
- package/test/bottleneck.test.js +14 -10
- package/test/fallback.test.js +13 -13
- package/test/history.test.js +572 -0
- package/test/images.test.js +3 -3
- package/test/json.test.js +188 -3
- package/test/live.mcp.js +7 -7
- package/test/live.test.js +8 -8
- package/test/templates.test.js +15 -15
- package/test/tokens.test.js +19 -4
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.
|
|
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.
|
|
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.
|
|
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-
|
|
35
|
-
const model = ModelMix.new(setup).
|
|
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).
|
|
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-
|
|
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).
|
|
532
|
-
{ name: 'Sonnet 4.
|
|
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-
|
|
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
|
|
51
|
-
const model = ModelMix.new(setup).
|
|
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).
|
|
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.
|
|
103
|
-
const model = ModelMix.new(setup).
|
|
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).
|
|
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
|
-
.
|
|
155
|
+
.sonnet46();
|
|
156
156
|
|
|
157
157
|
model.addText('Say "fallback test successful" and nothing else.');
|
|
158
158
|
|
package/test/templates.test.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
450
|
+
model.gpt51().replace(null);
|
|
451
451
|
}).to.not.throw();
|
|
452
452
|
|
|
453
453
|
expect(() => {
|
|
454
|
-
model.
|
|
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.
|
|
459
|
+
model.gpt51()
|
|
460
460
|
.replaceKeyFromFile('{{bad_file}}', '/path/that/does/not/exist.txt')
|
|
461
461
|
.addText('Content: {{bad_file}}');
|
|
462
462
|
|
package/test/tokens.test.js
CHANGED
|
@@ -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
|
-
.
|
|
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
|
-
.
|
|
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.
|
|
114
|
-
{ name: 'Google', create: (m) => m.
|
|
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) {
|