modelmix 4.5.16 → 4.5.20

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 CHANGED
@@ -343,7 +343,7 @@ const result = await model.json(
343
343
 
344
344
  ### Enhanced descriptors
345
345
 
346
- Descriptions support **descriptor objects** with `description`, `required`, `enum`, and `default`:
346
+ Descriptions support **descriptor objects** with `description`, `required`, `enum`, `default`, and `nullable`:
347
347
 
348
348
  ```javascript
349
349
  const result = await model.json(
@@ -359,11 +359,97 @@ const result = await model.json(
359
359
  | Property | Type | Default | Description |
360
360
  | --- | --- | --- | --- |
361
361
  | `description` | `string` | — | Field description for the model |
362
- | `required` | `boolean` | `true` | If `false`, field is removed from `required` and type becomes nullable |
363
- | `enum` | `array` | — | Allowed values. If includes `null`, type auto-becomes nullable |
364
- | `default` | `any` | — | Default value for the field |
362
+ | `required` | `boolean` | `true` | If `false`, field is removed from `required` and its type becomes nullable |
363
+ | `enum` | `array` | — | Restricts the field to specific values. Including `null` in the array auto-makes the type nullable |
364
+ | `default` | `any` | — | Default value hint for the model |
365
+ | `nullable` | `boolean` | `false` | If `true`, makes the type nullable without removing from `required` |
365
366
 
366
- You can mix strings and descriptor objects freely in the same descriptions parameter.
367
+ You can mix plain strings and descriptor objects freely in the same descriptions parameter:
368
+
369
+ ```javascript
370
+ const result = await model.json(
371
+ { name: 'Martin', age: 22, status: 'active' },
372
+ {
373
+ name: 'Full name', // plain string
374
+ age: { description: 'Age in years', required: false }, // optional field
375
+ status: { description: 'Account status', enum: ['active', 'inactive', 'banned'], default: 'active' }
376
+ }
377
+ );
378
+ ```
379
+
380
+ ### Nested object descriptions
381
+
382
+ Pass a nested object as the description value to describe fields inside a nested object:
383
+
384
+ ```javascript
385
+ const result = await model.json(
386
+ { user: { name: 'Alice', age: 30 } },
387
+ {
388
+ user: { name: 'Full name of the user', age: 'Age in years' }
389
+ }
390
+ );
391
+ ```
392
+
393
+ To describe the object field itself (e.g. mark it optional) **and** its nested fields, use the `description` / `required` descriptor for the parent key, which applies only to the parent, while still passing nested descriptions as its own separate key:
394
+
395
+ ```javascript
396
+ // Mark the parent optional but don't describe its children
397
+ const result = await model.json(
398
+ { user: { name: 'Alice', age: 30 } },
399
+ { user: { description: 'User details', required: false } }
400
+ );
401
+ ```
402
+
403
+ ### Array item descriptions
404
+
405
+ Pass descriptions for the items of an array by wrapping the descriptions in an array:
406
+
407
+ ```javascript
408
+ const result = await model.json(
409
+ { countries: [{ name: 'France', capital: 'Paris' }] },
410
+ { countries: [{ name: 'Country name', capital: 'Capital city in uppercase' }] }
411
+ );
412
+ ```
413
+
414
+ To mark the array field itself optional while keeping item descriptions, use a descriptor on the key:
415
+
416
+ ```javascript
417
+ const result = await model.json(
418
+ { tags: ['admin'] },
419
+ { tags: { description: 'List of user roles', required: false } }
420
+ );
421
+ ```
422
+
423
+ ### Automatic type and format detection
424
+
425
+ `generateJsonSchema` infers types and formats automatically from the example values:
426
+
427
+ | Example value | Inferred schema |
428
+ | --- | --- |
429
+ | `42` | `{ type: 'integer' }` |
430
+ | `19.99` | `{ type: 'number' }` |
431
+ | `true` / `false` | `{ type: 'boolean' }` |
432
+ | `null` | `{ type: 'null' }` |
433
+ | `'hello'` | `{ type: 'string' }` |
434
+ | `'user@example.com'` | `{ type: 'string', format: 'email' }` |
435
+ | `'1990-01-01'` | `{ type: 'string', format: 'date', description: 'Date in format YYYY-MM-DD' }` |
436
+ | `'14:30'` | `{ type: 'string', format: 'time', description: 'Time in format HH:MM' }` |
437
+ | `'09:15:45'` | `{ type: 'string', format: 'time', description: 'Time in format HH:MM:SS' }` |
438
+ | `[{ … }]` | `{ type: 'array', items: { … } }` — schema inferred from the first element |
439
+ | `{ … }` | `{ type: 'object', properties: { … }, required: […] }` |
440
+
441
+ When a field carries an `enum` that includes `null`, or has `required: false` or `nullable: true`, its type is widened to `[type, 'null']`. For example:
442
+
443
+ ```javascript
444
+ // enum with null → type becomes ['string', 'null']
445
+ { description: 'Gender', enum: ['m', 'f', null] }
446
+
447
+ // required: false → removes from required[] and type becomes ['string', 'null']
448
+ { description: 'Nickname', required: false }
449
+
450
+ // nullable: true → type becomes ['string', 'null'] but stays in required[]
451
+ { description: 'Middle name', nullable: true }
452
+ ```
367
453
 
368
454
  ### Array auto-wrap
369
455
 
package/index.js CHANGED
@@ -37,6 +37,8 @@ const MODEL_PRICING = {
37
37
  // OpenAI
38
38
  'gpt-realtime-mini': [0.60, 2.40],
39
39
  'gpt-realtime': [4.00, 16.00],
40
+ 'gpt-5.5-pro': [30.00, 180.00],
41
+ 'gpt-5.5': [5.00, 30.00],
40
42
  'gpt-5.4': [2.50, 15.00],
41
43
  'gpt-5.4-pro': [30, 180.00],
42
44
  'gpt-5.4-mini': [0.75, 4.50],
@@ -319,7 +321,13 @@ class ModelMix {
319
321
  }
320
322
  gpt54pro({ options = {}, config = {} } = {}) {
321
323
  return this.attach('gpt-5.4-pro', new MixOpenAIResponses({ options, config }));
322
- }
324
+ }
325
+ gpt55({ options = {}, config = {} } = {}) {
326
+ return this.attach('gpt-5.5', new MixOpenAIResponses({ options, config }));
327
+ }
328
+ gpt55pro({ options = {}, config = {} } = {}) {
329
+ return this.attach('gpt-5.5-pro', new MixOpenAIResponses({ options, config }));
330
+ }
323
331
  gptRealtime({ options = {}, config = {} } = {}) {
324
332
  return this.attach('gpt-realtime', new MixOpenAIWebSocket({ options, config }));
325
333
  }
@@ -522,7 +530,21 @@ class ModelMix {
522
530
  if (mix.minimax) return this.attach('MiniMax-M2.7', new MixMiniMax({ options, config }));
523
531
  if (mix.together) return this.attach('MiniMaxAI/MiniMax-M2.7', new MixTogether({ options, config }));
524
532
  return this;
525
- }
533
+ }
534
+
535
+ mimo25({ options = {}, config = {}, mix = { openrouter: true } } = {}) {
536
+ mix = { ...this.mix, ...mix };
537
+ if (mix.openrouter) this.attach('xiaomi/mimo-v2.5', new MixOpenRouter({ options, config }));
538
+ if (mix.mimo) this.attach('mimo-v2.5', new MixMiMo({ options, config }));
539
+ return this;
540
+ }
541
+
542
+ mimo25pro({ options = {}, config = {}, mix = { openrouter: true } } = {}) {
543
+ mix = { ...this.mix, ...mix };
544
+ if (mix.openrouter) this.attach('xiaomi/mimo-v2.5-pro', new MixOpenRouter({ options, config }));
545
+ if (mix.mimo) this.attach('mimo-v2.5-pro', new MixMiMo({ options, config }));
546
+ return this;
547
+ }
526
548
 
527
549
  deepseekV4Pro({ options = {}, config = {}, mix = { fireworks: true } } = {}) {
528
550
  mix = { ...this.mix, ...mix };
@@ -2308,6 +2330,29 @@ class MixMiniMax extends MixOpenAI {
2308
2330
  }
2309
2331
  }
2310
2332
 
2333
+ class MixMiMo extends MixOpenAI {
2334
+ getDefaultConfig(customConfig) {
2335
+ if (!process.env.MIMO_API_KEY) {
2336
+ throw new Error('MiMo API key not found. Please provide it in config or set MIMO_API_KEY environment variable.');
2337
+ }
2338
+
2339
+ return MixCustom.prototype.getDefaultConfig.call(this, {
2340
+ url: 'https://api.xiaomimimo.com/v1/chat/completions',
2341
+ apiKey: process.env.MIMO_API_KEY,
2342
+ ...customConfig
2343
+ });
2344
+ }
2345
+
2346
+ getDefaultHeaders(customHeaders) {
2347
+ return {
2348
+ 'accept': 'application/json',
2349
+ 'content-type': 'application/json',
2350
+ 'api-key': this.config.apiKey,
2351
+ ...customHeaders
2352
+ };
2353
+ }
2354
+ }
2355
+
2311
2356
  class MixPerplexity extends MixCustom {
2312
2357
  getDefaultConfig(customConfig) {
2313
2358
 
@@ -2846,4 +2891,4 @@ class MixGoogle extends MixCustom {
2846
2891
  }
2847
2892
  }
2848
2893
 
2849
- module.exports = { MixCustom, ModelMix, MixAnthropic, MixMiniMax, MixOpenAI, MixOpenAIResponses, MixOpenAIWebSocket, MixOpenRouter, MixPerplexity, MixOllama, MixLMStudio, MixGroq, MixTogether, MixGrok, MixCerebras, MixGoogle, MixFireworks, MixNVIDIA };
2894
+ module.exports = { MixCustom, ModelMix, MixAnthropic, MixMiniMax, MixMiMo, MixOpenAI, MixOpenAIResponses, MixOpenAIWebSocket, MixOpenRouter, MixPerplexity, MixOllama, MixLMStudio, MixGroq, MixTogether, MixGrok, MixCerebras, MixGoogle, MixFireworks, MixNVIDIA };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modelmix",
3
- "version": "4.5.16",
3
+ "version": "4.5.20",
4
4
  "description": "🧬 Reliable interface with automatic fallback for AI LLMs.",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -32,7 +32,7 @@
32
32
  "lmstudio",
33
33
  "deepseek",
34
34
  "oss",
35
- "k2",
35
+ "k26",
36
36
  "reasoning",
37
37
  "minimax",
38
38
  "thinking",
@@ -176,7 +176,53 @@ const result = await model.json(
176
176
  );
177
177
  ```
178
178
 
179
- Descriptor properties: `description` (string), `required` (boolean, default true — if false, field becomes nullable), `enum` (array — if includes null, type auto-becomes nullable), `default` (any).
179
+ Descriptor properties:
180
+
181
+ | Property | Type | Notes |
182
+ | --- | --- | --- |
183
+ | `description` | string | Field description for the model |
184
+ | `required` | boolean (default `true`) | `false` → removes from `required[]` **and** makes type nullable |
185
+ | `enum` | array | Restricts allowed values. Including `null` auto-makes the type nullable |
186
+ | `default` | any | Default value hint |
187
+ | `nullable` | boolean (default `false`) | `true` → makes type nullable but keeps field in `required[]` |
188
+
189
+ #### Nested object descriptions
190
+
191
+ Pass a plain object as the description value to annotate fields inside a nested object:
192
+
193
+ ```javascript
194
+ model.json(
195
+ { user: { name: 'Alice', age: 30 } },
196
+ { user: { name: 'Full name', age: 'Age in years' } }
197
+ );
198
+ ```
199
+
200
+ To mark the object itself as optional, use a descriptor (only `description`/`required`/`nullable` keys) — it applies to the parent, not the children:
201
+
202
+ ```javascript
203
+ model.json(
204
+ { user: { name: 'Alice', age: 30 } },
205
+ { user: { description: 'User details', required: false } }
206
+ );
207
+ ```
208
+
209
+ #### Array item descriptions
210
+
211
+ Wrap descriptions in an array to annotate items of an array field:
212
+
213
+ ```javascript
214
+ model.json(
215
+ { countries: [{ name: 'France', capital: 'Paris' }] },
216
+ { countries: [{ name: 'Country name', capital: 'Capital in uppercase' }] }
217
+ );
218
+ ```
219
+
220
+ #### Automatic type and format detection
221
+
222
+ Schema types are inferred from example values: `integer` (whole numbers), `number` (floats), `boolean`, `null`, `string`, and special formats:
223
+ - `'user@example.com'` → `{ type: 'string', format: 'email' }`
224
+ - `'1990-01-01'` → `{ type: 'string', format: 'date' }`
225
+ - `'14:30'` / `'09:15:45'` → `{ type: 'string', format: 'time' }`
180
226
 
181
227
  #### Array auto-wrap
182
228
 
@@ -390,7 +436,7 @@ const model = ModelMix.new({
390
436
  - Store API keys in `.env` and load with `dotenv/config` or `process.loadEnvFile()`. Never hardcode keys.
391
437
  - Chain models for resilience: primary model first, fallbacks after.
392
438
  - When using MCP tools or `addTool()`, set `max_history` to at least 3 — tool call/response pairs consume history slots.
393
- - Use `.json()` for structured output instead of parsing text manually. Use descriptor objects `{ description, required, enum, default }` for richer schema control.
439
+ - Use `.json()` for structured output instead of parsing text manually. Use descriptor objects `{ description, required, enum, default, nullable }` for richer schema control.
394
440
  - Use `.message()` for simple text, `.raw()` when you need tokens/thinking/toolCalls.
395
441
  - For thinking models, append `think` to the method name (e.g. `sonnet45think()`).
396
442
  - Template placeholders use `{key}` syntax in both system prompts and user messages.
@@ -1,5 +1,5 @@
1
1
  import { expect } from 'chai';
2
- import { ModelMix, MixAnthropic, MixCustom, MixGoogle, MixOpenAIResponses } from '../index.js';
2
+ import { ModelMix, MixAnthropic, MixCustom, MixGoogle, MixMiMo, MixOpenAIResponses, MixOpenRouter } from '../index.js';
3
3
  import { createRequire } from 'module';
4
4
 
5
5
  const require = createRequire(import.meta.url);
@@ -76,6 +76,74 @@ describe('Token Usage Tracking', () => {
76
76
  expect(request.prompt_cache_retention).to.equal('24h');
77
77
  });
78
78
 
79
+ it('should register GPT-5.5 shortcuts with OpenAI Responses provider', function () {
80
+ const model = ModelMix.new()
81
+ .gpt55()
82
+ .gpt55pro();
83
+
84
+ expect(model.models).to.have.length(2);
85
+ expect(model.models[0].key).to.equal('gpt-5.5');
86
+ expect(model.models[1].key).to.equal('gpt-5.5-pro');
87
+ expect(model.models[0].provider).to.be.instanceOf(MixOpenAIResponses);
88
+ expect(model.models[1].provider).to.be.instanceOf(MixOpenAIResponses);
89
+ });
90
+
91
+ it('should register MiMo shortcuts with native and OpenRouter providers', function () {
92
+ const originalMimoApiKey = process.env.MIMO_API_KEY;
93
+ const originalOpenRouterApiKey = process.env.OPENROUTER_API_KEY;
94
+
95
+ process.env.MIMO_API_KEY = 'test-mimo-key';
96
+ process.env.OPENROUTER_API_KEY = 'test-openrouter-key';
97
+
98
+ try {
99
+ const model = ModelMix.new()
100
+ .mimo25()
101
+ .mimo25pro({ mix: { mimo: true, openrouter: true } });
102
+
103
+ expect(model.models).to.have.length(3);
104
+ expect(model.models[0].key).to.equal('xiaomi/mimo-v2.5');
105
+ expect(model.models[1].key).to.equal('mimo-v2.5-pro');
106
+ expect(model.models[2].key).to.equal('xiaomi/mimo-v2.5-pro');
107
+
108
+ expect(model.models[0].provider).to.be.instanceOf(MixOpenRouter);
109
+ expect(model.models[1].provider).to.be.instanceOf(MixMiMo);
110
+ expect(model.models[2].provider).to.be.instanceOf(MixOpenRouter);
111
+ } finally {
112
+ if (originalMimoApiKey === undefined) delete process.env.MIMO_API_KEY;
113
+ else process.env.MIMO_API_KEY = originalMimoApiKey;
114
+
115
+ if (originalOpenRouterApiKey === undefined) delete process.env.OPENROUTER_API_KEY;
116
+ else process.env.OPENROUTER_API_KEY = originalOpenRouterApiKey;
117
+ }
118
+ });
119
+
120
+ it('should use api-key header for MiMo provider', function () {
121
+ const originalMimoApiKey = process.env.MIMO_API_KEY;
122
+ process.env.MIMO_API_KEY = 'test-mimo-key';
123
+
124
+ try {
125
+ const provider = new MixMiMo();
126
+ expect(provider.headers['api-key']).to.equal('test-mimo-key');
127
+ expect(provider.headers.authorization).to.equal(undefined);
128
+ expect(provider.config.url).to.equal('https://api.xiaomimimo.com/v1/chat/completions');
129
+ } finally {
130
+ if (originalMimoApiKey === undefined) delete process.env.MIMO_API_KEY;
131
+ else process.env.MIMO_API_KEY = originalMimoApiKey;
132
+ }
133
+ });
134
+
135
+ it('should throw a clear error when MIMO_API_KEY is missing', function () {
136
+ const originalMimoApiKey = process.env.MIMO_API_KEY;
137
+ delete process.env.MIMO_API_KEY;
138
+
139
+ try {
140
+ expect(() => new MixMiMo()).to.throw('MIMO_API_KEY');
141
+ } finally {
142
+ if (originalMimoApiKey === undefined) delete process.env.MIMO_API_KEY;
143
+ else process.env.MIMO_API_KEY = originalMimoApiKey;
144
+ }
145
+ });
146
+
79
147
  it('should track tokens in OpenAI response', async function () {
80
148
  this.timeout(30000);
81
149