modelmix 5.2.2 → 5.3.0

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/index.js CHANGED
@@ -499,6 +499,12 @@ class ModelMix {
499
499
  gpt6astra(args = {}) {
500
500
  return this._attachOpenAIWithOpenRouter('gpt-6-astra', MixOpenAIResponses, args);
501
501
  }
502
+ gpt6sol(args = {}) {
503
+ return this._attachOpenAIWithOpenRouter('gpt-6-sol', MixOpenAIResponses, args);
504
+ }
505
+ gpt6luna(args = {}) {
506
+ return this._attachOpenAIWithOpenRouter('gpt-6-luna', MixOpenAIResponses, args);
507
+ }
502
508
  gpt56sol(args = {}) {
503
509
  return this._attachOpenAIWithOpenRouter('gpt-5.6-sol', MixOpenAIResponses, args);
504
510
  }
@@ -545,6 +551,9 @@ class ModelMix {
545
551
  if (mix.openrouter) this.attach('anthropic/claude-fable-5.1', new MixOpenRouter({ options, config }));
546
552
  return this;
547
553
  }
554
+ opus55({ options = {}, config = {} } = {}) {
555
+ return this.attach('claude-opus-5-5', new MixAnthropic({ options, config }));
556
+ }
548
557
  opus50({ options = {}, config = {} } = {}) {
549
558
  return this.attach('claude-opus-5', new MixAnthropic({ options, config }));
550
559
  }
@@ -769,6 +778,13 @@ class ModelMix {
769
778
  return this;
770
779
  }
771
780
 
781
+ mimo26pro({ options = {}, config = {}, mix = { openrouter: true } } = {}) {
782
+ mix = { ...this.mix, ...mix };
783
+ if (mix.mimo) this.attach('mimo-v2.6-pro', new MixMiMo({ options, config }));
784
+ if (mix.openrouter) this.attach('xiaomi/mimo-v2.6-pro', new MixOpenRouter({ options, config }));
785
+ return this;
786
+ }
787
+
772
788
  deepseekPro({ options = {}, config = {} } = {}) {
773
789
  return this.attach('deepseek/deepseek-v4-pro-0813', new MixOpenRouter({ options, config }));
774
790
  }
@@ -3,9 +3,9 @@ const { normalizeEffort } = require('../effort');
3
3
  const CHAIN_MODEL_SHORTCUTS = new Set([
4
4
  'gpt5', 'gpt5mini', 'gpt5nano',
5
5
  'gpt51', 'gpt52', 'gpt54', 'gpt54mini', 'gpt54nano', 'gpt54pro',
6
- 'gpt6astra', 'gpt55', 'gpt55pro', 'gpt56sol', 'gpt56terra', 'gpt56luna',
6
+ 'gpt6astra', 'gpt6sol', 'gpt6luna', 'gpt55', 'gpt55pro', 'gpt56sol', 'gpt56terra', 'gpt56luna',
7
7
  'gptRealtime', 'gptRealtimeMini', 'gpt53codex', 'gpt53chat', 'gptOss',
8
- 'fable51', 'fable50', 'fable5', 'opus50', 'opus5', 'opus48', 'opus47', 'opus46',
8
+ 'fable51', 'fable50', 'fable5', 'opus55', 'opus50', 'opus5', 'opus48', 'opus47', 'opus46',
9
9
  'sonnet50', 'sonnet5', 'sonnet46', 'sonnet45', 'haiku45',
10
10
  'gemini31pro', 'gemini38flash', 'gemini37flash', 'gemini36flash', 'gemini35flash',
11
11
  'gemini35flashLite', 'gemini31flashLite', 'sonarPro', 'sonar',
@@ -14,7 +14,7 @@ const CHAIN_MODEL_SHORTCUTS = new Set([
14
14
  'qwen35397b', 'qwen36plus', 'qwen37plus', 'qwen38max', 'qwen3827b', 'qwen38flash',
15
15
  'hermes470b', 'hermes4405b', 'hermes3',
16
16
  'kimiK26', 'kimiK27Code', 'kimiK3', 'kimiK25',
17
- 'minimaxM27', 'minimaxM3', 'mimo25', 'mimo25pro',
17
+ 'minimaxM27', 'minimaxM3', 'mimo25', 'mimo25pro', 'mimo26pro',
18
18
  'deepseekV4Pro', 'deepseekPro', 'deepseekV4Flash', 'deepseekV41Flash', 'GLM52', 'GLM53', 'GLM53Flash'
19
19
  ]);
20
20
 
@@ -3,7 +3,7 @@ function normalizeOpenAIOptions(options) {
3
3
  delete options.max_tokens;
4
4
  delete options.temperature;
5
5
  }
6
- if (options.model?.includes('gpt-5') || /^(?:openai\/)?gpt-6-astra(?:-|$)/.test(options.model || '')) {
6
+ if (options.model?.includes('gpt-5') || /^(?:openai\/)?gpt-6(?:-|$)/.test(options.model || '')) {
7
7
  if (options.max_tokens) {
8
8
  options.max_completion_tokens = options.max_tokens;
9
9
  delete options.max_tokens;
@@ -11,6 +11,12 @@ const {
11
11
  validateProviderExecution
12
12
  } = require('../abort-signal');
13
13
 
14
+ /** Explicit prompt cache controls belong to the documented "GPT-5.6 and later" family. */
15
+ function isGPT56OrLater(model) {
16
+ if (typeof model !== 'string') return false;
17
+ return model.startsWith('gpt-5.6') || /^(?:openai\/)?gpt-6(?:-|$)/.test(model);
18
+ }
19
+
14
20
  function createOpenAIProviders({
15
21
  ModelMix,
16
22
  MixCustom,
@@ -38,9 +44,8 @@ function createOpenAIProviders({
38
44
  }
39
45
 
40
46
  static buildResponsesRequest(options = {}, config = {}) {
41
- const isGPT56 = typeof options.model === 'string' && options.model.startsWith('gpt-5.6');
42
47
  const input = MixOpenAIResponses.messagesToResponsesInput(options.messages, {
43
- translateNeutralCache: isGPT56
48
+ translateNeutralCache: isGPT56OrLater(options.model)
44
49
  });
45
50
  if (config.system) {
46
51
  input.unshift({ role: 'developer', content: [{ type: 'input_text', text: config.system }] });
@@ -106,7 +111,7 @@ function createOpenAIProviders({
106
111
  }
107
112
 
108
113
  static validatePromptCaching(options, input) {
109
- const isGPT56 = typeof options.model === 'string' && options.model.startsWith('gpt-5.6');
114
+ const supportsCacheControls = isGPT56OrLater(options.model);
110
115
  const cacheOptions = options.prompt_cache_options;
111
116
  const breakpoints = input.flatMap(message => Array.isArray(message.content)
112
117
  ? message.content
@@ -114,14 +119,14 @@ function createOpenAIProviders({
114
119
  .map(block => block.prompt_cache_breakpoint)
115
120
  : []);
116
121
 
117
- if (isGPT56 && options.prompt_cache_retention !== undefined) {
118
- throw new Error('GPT-5.6 does not support prompt_cache_retention; use prompt_cache_options.ttl instead.');
122
+ if (supportsCacheControls && options.prompt_cache_retention !== undefined) {
123
+ throw new Error('GPT-5.6 and later models do not support prompt_cache_retention; use prompt_cache_options.ttl instead.');
119
124
  }
120
- if (!isGPT56 && cacheOptions !== undefined) {
121
- throw new Error('prompt_cache_options is only supported by GPT-5.6 models.');
125
+ if (!supportsCacheControls && cacheOptions !== undefined) {
126
+ throw new Error('prompt_cache_options is only supported by GPT-5.6 and later models.');
122
127
  }
123
- if (!isGPT56 && breakpoints.length > 0) {
124
- throw new Error('prompt_cache_breakpoint is only supported by GPT-5.6 models.');
128
+ if (!supportsCacheControls && breakpoints.length > 0) {
129
+ throw new Error('prompt_cache_breakpoint is only supported by GPT-5.6 and later models.');
125
130
  }
126
131
  if (cacheOptions !== undefined) {
127
132
  if (!isPlainObject(cacheOptions)) {
@@ -22,9 +22,11 @@ function usesLongContextRates(pricing, inputTokens) {
22
22
  const MODEL_PRICING = {
23
23
  // OpenAI
24
24
  'gpt-6-astra': { input: 10.00, cachedInput: 1.00, cacheWrite: 12.50, output: 50.00, longContext: OPENAI_LONG_CONTEXT_PRICING },
25
+ 'gpt-6-sol': { input: 2.00, cachedInput: 0.20, cacheWrite: 2.50, output: 10.00, longContext: OPENAI_LONG_CONTEXT_PRICING },
26
+ 'gpt-6-luna': { input: 0.10, cachedInput: 0.01, cacheWrite: 0.125, output: 0.50, longContext: OPENAI_LONG_CONTEXT_PRICING },
25
27
  'gpt-realtime-mini': { input: 0.60, cachedInput: 0.06, output: 2.40 },
26
28
  'gpt-realtime': { input: 4.00, cachedInput: 0.40, output: 16.00 },
27
- 'gpt-5.6-sol': { input: 5.00, cachedInput: 0.50, cacheWrite: 6.25, output: 30.00, longContext: OPENAI_LONG_CONTEXT_PRICING },
29
+ 'gpt-5.6-sol': { input: 4.00, cachedInput: 0.40, cacheWrite: 5.00, output: 20.00, longContext: OPENAI_LONG_CONTEXT_PRICING },
28
30
  'gpt-5.6-terra': { input: 2.00, cachedInput: 0.20, cacheWrite: 2.50, output: 12.00, longContext: OPENAI_LONG_CONTEXT_PRICING },
29
31
  'gpt-5.6-luna': { input: 0.20, cachedInput: 0.02, cacheWrite: 0.25, output: 1.20, longContext: OPENAI_LONG_CONTEXT_PRICING },
30
32
  'gpt-5.5-pro': { input: 30.00, output: 180.00 },
@@ -53,6 +55,7 @@ const MODEL_PRICING = {
53
55
  'claude-fable-5-1': { input: 10.00, cachedInput: 0.25, cacheWrite: 12.50, cacheWrite1h: 20.00, output: 50.00 },
54
56
  'anthropic/claude-fable-5.1': { input: 10.00, cachedInput: 0.25, cacheWrite: 12.50, cacheWrite1h: 20.00, output: 50.00 },
55
57
  'claude-fable-5': { input: 10.00, cachedInput: 1.00, cacheWrite: 12.50, cacheWrite1h: 20.00, output: 50.00 },
58
+ 'claude-opus-5-5': { input: 5.00, cachedInput: 0.50, cacheWrite: 6.25, cacheWrite1h: 10.00, output: 25.00 },
56
59
  'claude-opus-5': { input: 5.00, cachedInput: 0.50, cacheWrite: 6.25, cacheWrite1h: 10.00, output: 25.00 },
57
60
  'claude-sonnet-5': { input: 3.00, cachedInput: 0.30, cacheWrite: 3.75, cacheWrite1h: 6.00, output: 15.00 },
58
61
  'claude-opus-4-8': { input: 5.00, cachedInput: 0.50, cacheWrite: 6.25, cacheWrite1h: 10.00, output: 25.00 },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modelmix",
3
- "version": "5.2.2",
3
+ "version": "5.3.0",
4
4
  "description": "🧬 Reliable interface with automatic fallback for AI LLMs.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -82,5 +82,6 @@
82
82
  "test:benchmark": "mocha plugins/benchmark/test/**/*.test.js --timeout 10000 --require test/setup.js",
83
83
  "test:rlm": "mocha plugins/rlm/test/**/*.test.js --timeout 10000 --require test/setup.js",
84
84
  "test:offline": "mocha test/abort.test.js test/json.test.js test/fallback.test.js test/templates.test.js test/images.test.js test/bottleneck.test.js test/tokens.test.js test/history.test.js test/anthropic.test.js test/effort.test.js test/grok.test.js test/google.test.js test/moderation.test.js test/plugins.test.js plugins/skills/test/**/*.test.js plugins/benchmark/test/**/*.test.js plugins/rlm/test/**/*.test.js --timeout 10000 --require test/setup.js"
85
- }
86
- }
85
+ },
86
+ "packageManager": "pnpm@12.4.1"
87
+ }
@@ -164,7 +164,7 @@ ModelMix.new({ config: { effort: 80 } })
164
164
  | DeepSeek V4 | off | `low`↑ | `high`↑ | `high`↑ | `max`↑ | — |
165
165
  | MiniMax M3 | off | adaptive | adaptive | adaptive | adaptive | adaptive |
166
166
 
167
- \* GPT-6 Astra maps 0–39 / 40–59 / 60–79 / 80–99 / 100 to `low` / `medium` / `high` / `xhigh` / `max`. GPT-5.6 maps `100` to `max`; 80–99 remains `xhigh`. Qwen 3.8 27B and Flash map 0–39 / 40–79 / 80–100 to `low` / `medium` / `xhigh`; Qwen 3.8 Flash is the managed production version based on Flash-Next. GLM 5.3 and GLM 5.3 Flash require reasoning and map those bands to `low` / `high` / `max`. Gemini bands: 0–24 / 25–49 / 50–74 / 75–100. Gemini 3.8 Flash and 3.7 Flash support only `low` / `medium` / `high`, so the first two bands clamp to `low`; `-1` keeps their native `medium` default. DeepSeek `↑` = thinking on; `off` = thinking disabled. MiniMax `off`/`adaptive` = `thinking.disabled` / `thinking.type=adaptive`. Gemini 2.5 maps 0–100 to `thinkingBudget`. Anthropic: adaptive + `output_config.effort` on Claude 5 / Fable / Opus 4.6+ / Sonnet 4.6+; Sonnet 4.5 / Haiku 4.5 use `thinking.type=enabled` + `budget_tokens`. Grok 4.6 maps 0–39 / 40–59 / 60–79 / 80–100 to `low` / `medium` / `high` / `xhigh`; without effort it uses native `high`. `-1` = adaptive/dynamic when available, else no-op. Levels clamp per model. Former `*think()` methods are removed — use `.effort(n).<model>()`. Kimi: `kimiK25()` / `kimiK26()`. Grok 4.20: `.grok420()` non-reasoning; `.effort(20+|-1).grok420()` selects reasoning.
167
+ \* GPT-6 Astra maps 0–39 / 40–59 / 60–79 / 80–99 / 100 to `low` / `medium` / `high` / `xhigh` / `max`. GPT-6 Sol, GPT-6 Luna, and GPT-5.6 map `100` to `max` and keep `xhigh` for 80–99, with OpenAI's `none` band at 0–19. Qwen 3.8 27B and Flash map 0–39 / 40–79 / 80–100 to `low` / `medium` / `xhigh`; Qwen 3.8 Flash is the managed production version based on Flash-Next. GLM 5.3 and GLM 5.3 Flash require reasoning and map those bands to `low` / `high` / `max`. Gemini bands: 0–24 / 25–49 / 50–74 / 75–100. Gemini 3.8 Flash and 3.7 Flash support only `low` / `medium` / `high`, so the first two bands clamp to `low`; `-1` keeps their native `medium` default. DeepSeek `↑` = thinking on; `off` = thinking disabled. MiniMax `off`/`adaptive` = `thinking.disabled` / `thinking.type=adaptive`. Gemini 2.5 maps 0–100 to `thinkingBudget`. Anthropic: adaptive + `output_config.effort` on Claude 5 / Fable / Opus 4.6+ / Sonnet 4.6+; Sonnet 4.5 / Haiku 4.5 use `thinking.type=enabled` + `budget_tokens`. Grok 4.6 maps 0–39 / 40–59 / 60–79 / 80–100 to `low` / `medium` / `high` / `xhigh`; without effort it uses native `high`. `-1` = adaptive/dynamic when available, else no-op. Levels clamp per model. Former `*think()` methods are removed — use `.effort(n).<model>()`. Kimi: `kimiK25()` / `kimiK26()`. Grok 4.20: `.grok420()` non-reasoning; `.effort(20+|-1).grok420()` selects reasoning.
168
168
 
169
169
  ## Available Model Shorthands
170
170
 
@@ -172,12 +172,12 @@ ModelMix.new({ config: { effort: 80 } })
172
172
 
173
173
  Use `ModerationMix.new().openai()` with `.raw()` to classify text and images through OpenAI's Moderations endpoint. Read the results from `raw.moderation`. `ModerationMix` accepts moderation providers as ordered fallbacks, rejects generative providers, and does not generate text or support streaming.
174
174
 
175
- `gpt6astra()` `gpt56sol()` `gpt56terra()` `gpt56luna()` `gpt55()` `gpt55pro()` `gpt54()` `gpt54mini()` `gpt54nano()` `gpt54pro()` `gpt53codex()` `gpt53chat()` `gpt52()` `gpt51()` `gpt5()` `gpt5mini()` `gpt5nano()` `gptRealtime()` `gptRealtimeMini()` `gptOss()`
175
+ `gpt6astra()` `gpt6sol()` `gpt6luna()` `gpt56sol()` `gpt56terra()` `gpt56luna()` `gpt55()` `gpt55pro()` `gpt54()` `gpt54mini()` `gpt54nano()` `gpt54pro()` `gpt53codex()` `gpt53chat()` `gpt52()` `gpt51()` `gpt5()` `gpt5mini()` `gpt5nano()` `gptRealtime()` `gptRealtimeMini()` `gptOss()`
176
176
 
177
177
  Every textual GPT-5 and GPT-6 shortcut registers only the official OpenAI model by default. Pass `mix: { openrouter: true }` to `ModelMix.new()` or to an individual shortcut to append its `openai/*` OpenRouter route as a fallback. `gpt53chat()` uses `gpt-5.3-chat-latest` officially and `openai/gpt-5.3-chat` through OpenRouter. Both API keys are required when that fallback is enabled. Realtime shortcuts remain official-only.
178
178
 
179
179
  ### Anthropic
180
- `fable51()` `fable50()` `opus50()` `opus48()` `opus47()` `opus46()` `sonnet5()` `sonnet46()` `sonnet45()` `haiku45()`
180
+ `fable51()` `fable50()` `opus55()` `opus50()` `opus48()` `opus47()` `opus46()` `sonnet5()` `sonnet46()` `sonnet45()` `haiku45()`
181
181
 
182
182
  Use `.effort(n)` (or `config.effort`) to enable Anthropic thinking — e.g. `.effort(100).opus50()`. `fable5()` and `opus5()` remain available as compatibility aliases.
183
183
 
@@ -204,6 +204,9 @@ Use `.effort(n)` (or `config.effort`) to enable Anthropic thinking — e.g. `.ef
204
204
  ### MiniMax
205
205
  `minimaxM27()` `minimaxM3()`
206
206
 
207
+ ### MiMo
208
+ `mimo26pro()` — uses OpenRouter (`xiaomi/mimo-v2.6-pro`) by default; the native API requires `MIMO_API_KEY` and `mix: { mimo: true }`.
209
+
207
210
  ### DeepSeek
208
211
  `deepseekV41Flash({ mix: { deepseek: true, openrouter: false } })` uses the native API at `https://api.deepseek.com/chat/completions` with model `deepseek-flash` (currently V4.1 Flash). Requires `DEEPSEEK_API_KEY`; `MixDeepSeek` supports explicit `.attach()` calls. Unified effort and native cache usage are supported, and assistant reasoning is preserved for tool continuations. Cost estimates use peak rates per 1M tokens: $0.30 input, $0.006 cached input, $1.20 output; off-peak charges are half. Enabling all three providers orders them DeepSeek → Fireworks → OpenRouter.
209
212
 
@@ -408,7 +411,7 @@ console.log(model.lastRaw.think); // reasoning content (if available)
408
411
  console.log(model.lastRaw.response); // raw API response
409
412
  ```
410
413
 
411
- ### GPT-5.6 explicit prompt caching
414
+ ### GPT-5.6 and GPT-6 explicit prompt caching
412
415
 
413
416
  ```javascript
414
417
  const model = ModelMix.new()
@@ -431,9 +434,9 @@ const model = ModelMix.new()
431
434
  const result = await model.raw();
432
435
  ```
433
436
 
434
- `cache: { breakpoint: true }` is provider-neutral: GPT-5.6 receives `prompt_cache_breakpoint`, Anthropic receives `cache_control`, and unsupported providers omit it. Keep native request policies inside each model shorthand so they do not leak across fallbacks. Anthropic usage separates `cacheWrite5m` and `cacheWrite1h`; `cacheWrite` stays as their compatible aggregate.
437
+ `cache: { breakpoint: true }` is provider-neutral: GPT-5.6 and GPT-6 receive `prompt_cache_breakpoint`, Anthropic receives `cache_control`, and unsupported providers omit it. Keep native request policies inside each model shorthand so they do not leak across fallbacks. Anthropic usage separates `cacheWrite5m` and `cacheWrite1h`; `cacheWrite` stays as their compatible aggregate.
435
438
 
436
- GPT-5.6 replaces `prompt_cache_retention` with `prompt_cache_options.ttl`. Explicit breakpoints also work on image methods and Responses-native `input_text`, `input_image`, and `input_file` blocks. Prompts need at least 1,024 tokens to be cached. Requests over 272K input tokens use 2× input and 1.5× output prices for the complete request; ModelMix applies these multipliers to `cost`, `costBreakdown`, and cache economics.
439
+ GPT-5.6 and GPT-6 replace `prompt_cache_retention` with `prompt_cache_options.ttl`. Explicit breakpoints also work on image methods and Responses-native `input_text`, `input_image`, and `input_file` blocks. Prompts need at least 1,024 tokens to be cached. Requests over 272K input tokens use 2× input and 1.5× output prices for the complete request; ModelMix applies these multipliers to `cost`, `costBreakdown`, and cache economics.
437
440
 
438
441
  ### Add images
439
442
 
@@ -122,6 +122,15 @@ describe('Anthropic Model Registration Tests', () => {
122
122
  expect(model.models[0].provider).to.be.instanceOf(MixAnthropic);
123
123
  });
124
124
 
125
+ it('should register Claude Opus 5.5', () => {
126
+ const model = ModelMix.new();
127
+ model.opus55();
128
+
129
+ expect(model.models).to.have.length(1);
130
+ expect(model.models[0].key).to.equal('claude-opus-5-5');
131
+ expect(model.models[0].provider).to.be.instanceOf(MixAnthropic);
132
+ });
133
+
125
134
  it('should keep opus5() as an alias for opus50()', () => {
126
135
  const model = ModelMix.new();
127
136
 
@@ -147,9 +156,21 @@ describe('Anthropic Model Registration Tests', () => {
147
156
  expect(options.thinking).to.deep.equal({ type: 'adaptive', display: 'summarized' });
148
157
  });
149
158
 
159
+ it('should apply max effort thinking via .effort(100).opus55()', () => {
160
+ const model = ModelMix.new().effort(100).opus55();
161
+ const { applyUnifiedEffort } = require('../effort.js');
162
+
163
+ expect(model.config.effort).to.equal(100);
164
+ const options = { model: 'claude-opus-5-5' };
165
+ applyUnifiedEffort(options, model.config, 'anthropic', 'claude-opus-5-5');
166
+ expect(options.output_config).to.deep.equal({ effort: 'max' });
167
+ expect(options.thinking).to.deep.equal({ type: 'adaptive', display: 'summarized' });
168
+ });
169
+
150
170
  describe('Sampling params (temperature/top_p/top_k)', () => {
151
171
  it('should detect models that reject sampling params', () => {
152
172
  expect(MixAnthropic.rejectsSamplingParams('claude-opus-5')).to.equal(true);
173
+ expect(MixAnthropic.rejectsSamplingParams('claude-opus-5-5')).to.equal(true);
153
174
  expect(MixAnthropic.rejectsSamplingParams('claude-opus-4-8')).to.equal(true);
154
175
  expect(MixAnthropic.rejectsSamplingParams('claude-opus-4-7')).to.equal(true);
155
176
  expect(MixAnthropic.rejectsSamplingParams('claude-sonnet-5')).to.equal(true);
@@ -90,6 +90,15 @@ describe('Unified effort scale', () => {
90
90
  }
91
91
  });
92
92
 
93
+ it('maps GPT-6 Sol and Luna effort from none to max', () => {
94
+ for (const model of ['gpt-6-sol', 'gpt-6-luna']) {
95
+ for (const [effort, level] of [[0, 'none'], [20, 'low'], [40, 'medium'], [60, 'high'], [99, 'xhigh'], [100, 'max']]) {
96
+ expect(mapEffort('openai', effort, model)).to.deep.equal({ reasoning_effort: level });
97
+ }
98
+ expect(mapEffort('openai', 100, `openai/${model}`)).to.deep.equal({ reasoning_effort: 'max' });
99
+ }
100
+ });
101
+
93
102
  it('sets OpenAI adaptive only when supported (otherwise no-op)', () => {
94
103
  expect(mapEffort('openai', -1)).to.equal(null);
95
104
  expect(mapEffort('openai', -1, 'gpt-5.2')).to.equal(null);
@@ -197,6 +206,10 @@ describe('Unified effort scale', () => {
197
206
  thinking: { type: 'adaptive', display: 'summarized' },
198
207
  output_config: { effort: 'low' }
199
208
  });
209
+ expect(mapEffort('anthropic', 50, 'claude-opus-5-5')).to.deep.equal({
210
+ thinking: { type: 'adaptive', display: 'summarized' },
211
+ output_config: { effort: 'high' }
212
+ });
200
213
  expect(mapEffort('anthropic', 90, 'claude-fable-5-1')).to.deep.equal({
201
214
  thinking: { type: 'adaptive', display: 'summarized' },
202
215
  output_config: { effort: 'max' }
@@ -115,6 +115,8 @@ 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
+ ['gpt6sol', 'gpt-6-sol', 'openai/gpt-6-sol', MixOpenAIResponses],
119
+ ['gpt6luna', 'gpt-6-luna', 'openai/gpt-6-luna', MixOpenAIResponses],
118
120
  ['gpt5', 'gpt-5', 'openai/gpt-5', MixOpenAI],
119
121
  ['gpt5mini', 'gpt-5-mini', 'openai/gpt-5-mini', MixOpenAI],
120
122
  ['gpt5nano', 'gpt-5-nano', 'openai/gpt-5-nano', MixOpenAI],
@@ -232,6 +234,48 @@ describe('Provider Fallback Chain Tests', () => {
232
234
  expect(openRouterRequest).to.not.have.property('temperature');
233
235
  });
234
236
 
237
+ it('should fallback from the official GPT-6 Sol and Luna endpoints to OpenRouter', async () => {
238
+ const cases = [
239
+ ['gpt6sol', 'gpt-6-sol', 'Hello from GPT-6 Sol through OpenRouter!'],
240
+ ['gpt6luna', 'gpt-6-luna', 'Hello from GPT-6 Luna through OpenRouter!']
241
+ ];
242
+
243
+ for (const [shortcut, officialKey, content] of cases) {
244
+ let openRouterRequest;
245
+ const instance = ModelMix.new().effort(100);
246
+ instance[shortcut]({ mix: { openrouter: true } }).addText('Hello');
247
+
248
+ nock('https://api.openai.com')
249
+ .post('/v1/responses', body => {
250
+ expect(body.model).to.equal(officialKey);
251
+ expect(body.reasoning).to.deep.equal({ effort: 'max' });
252
+ expect(body.max_output_tokens).to.equal(8192);
253
+ expect(body).to.not.have.property('temperature');
254
+ return true;
255
+ })
256
+ .reply(503, { error: 'Service unavailable' });
257
+
258
+ nock('https://openrouter.ai')
259
+ .post('/api/v1/chat/completions', body => {
260
+ openRouterRequest = body;
261
+ return true;
262
+ })
263
+ .reply(200, {
264
+ choices: [{
265
+ message: {
266
+ role: 'assistant',
267
+ content
268
+ }
269
+ }]
270
+ });
271
+
272
+ expect(await instance.message()).to.equal(content);
273
+ expect(openRouterRequest.model).to.equal(`openai/${officialKey}`);
274
+ expect(openRouterRequest.max_completion_tokens).to.equal(8192);
275
+ expect(openRouterRequest).to.not.have.property('temperature');
276
+ }
277
+ });
278
+
235
279
  it('should keep the default fable51 chain on Anthropic', () => {
236
280
  model.chain('fable51@80');
237
281
 
@@ -1,7 +1,9 @@
1
1
  const { expect } = require('chai');
2
+ const nock = require('nock');
2
3
  const {
3
4
  ModelMix,
4
5
  MixFireworks,
6
+ MixMiMo,
5
7
  MixMiniMax,
6
8
  MixNVIDIA,
7
9
  MixOpenRouter,
@@ -97,4 +99,29 @@ describe('Provider expansion regressions', () => {
97
99
  expect(model.models[0].key).to.equal('accounts/fireworks/models/minimax-m3');
98
100
  expect(model.models[0].provider).to.be.instanceOf(MixFireworks);
99
101
  });
102
+
103
+ it('should send MiMo 2.6 Pro requests to the native MiMo API', async () => {
104
+ const api = nock('https://api.xiaomimimo.com')
105
+ .post('/v1/chat/completions', body => {
106
+ expect(body.model).to.equal('mimo-v2.6-pro');
107
+ return true;
108
+ })
109
+ .reply(200, {
110
+ choices: [{ message: { content: 'ok' } }],
111
+ usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }
112
+ });
113
+
114
+ const model = ModelMix.new().mimo26pro({
115
+ mix: { mimo: true, openrouter: false },
116
+ config: { apiKey: 'test-mimo-key' }
117
+ });
118
+
119
+ expect(model.models).to.have.length(1);
120
+ expect(model.models[0].provider).to.be.instanceOf(MixMiMo);
121
+
122
+ const response = await model.addText('Hi').message();
123
+
124
+ expect(response).to.equal('ok');
125
+ api.done();
126
+ });
100
127
  });
@@ -252,29 +252,35 @@ describe('Token Usage Tracking', () => {
252
252
  ]);
253
253
  });
254
254
 
255
- it('should translate neutral cache breakpoints for GPT-5.6 and filter them for older models', async function () {
255
+ it('should translate neutral cache breakpoints for GPT-5.6 and GPT-6 and filter them for older models', async function () {
256
256
  const breakpoint = { mode: 'explicit' };
257
257
  const model = ModelMix.new()
258
258
  .addText('Stable text', { role: 'developer', cache: { breakpoint: true } })
259
259
  .addImageFromUrl('data:image/png;base64,AAAA', { cache: { breakpoint: true } });
260
260
  const messages = await model.prepareMessages();
261
- const gpt56Request = MixOpenAIResponses.buildResponsesRequest({
262
- model: 'gpt-5.6-luna',
263
- messages,
264
- prompt_cache_options: { mode: 'explicit', ttl: '30m' }
265
- });
266
- const olderRequest = MixOpenAIResponses.buildResponsesRequest({
267
- model: 'gpt-5.4',
268
- messages
269
- });
270
261
 
271
262
  expect(messages[0].content[0]).to.deep.include({ cache: { breakpoint: true } });
272
263
  expect(messages[0].content[0]).to.not.have.property('prompt_cache_breakpoint');
273
- expect(gpt56Request.input[0].content[0].prompt_cache_breakpoint).to.deep.equal(breakpoint);
274
- expect(gpt56Request.input[1].content[0]).to.deep.equal({
275
- type: 'input_image',
276
- image_url: 'data:image/png;base64,AAAA',
277
- prompt_cache_breakpoint: breakpoint
264
+
265
+ for (const key of ['gpt-5.6-luna', 'gpt-6-astra', 'gpt-6-sol', 'gpt-6-luna']) {
266
+ const request = MixOpenAIResponses.buildResponsesRequest({
267
+ model: key,
268
+ messages,
269
+ prompt_cache_options: { mode: 'explicit', ttl: '30m' }
270
+ });
271
+
272
+ expect(request.prompt_cache_options).to.deep.equal({ mode: 'explicit', ttl: '30m' });
273
+ expect(request.input[0].content[0].prompt_cache_breakpoint).to.deep.equal(breakpoint);
274
+ expect(request.input[1].content[0]).to.deep.equal({
275
+ type: 'input_image',
276
+ image_url: 'data:image/png;base64,AAAA',
277
+ prompt_cache_breakpoint: breakpoint
278
+ });
279
+ }
280
+
281
+ const olderRequest = MixOpenAIResponses.buildResponsesRequest({
282
+ model: 'gpt-5.4',
283
+ messages
278
284
  });
279
285
  expect(olderRequest.input[0].content[0]).to.not.have.property('cache');
280
286
  expect(olderRequest.input[0].content[0]).to.not.have.property('prompt_cache_breakpoint');
@@ -347,6 +353,12 @@ describe('Token Usage Tracking', () => {
347
353
  prompt_cache_retention: '24h'
348
354
  })).to.throw('prompt_cache_options.ttl');
349
355
 
356
+ expect(() => MixOpenAIResponses.buildResponsesRequest({
357
+ model: 'gpt-6-sol',
358
+ messages: [{ role: 'user', content: 'Hi' }],
359
+ prompt_cache_retention: '24h'
360
+ })).to.throw('prompt_cache_options.ttl');
361
+
350
362
  expect(() => MixOpenAIResponses.buildResponsesRequest({
351
363
  model: 'gpt-5.4',
352
364
  messages: [{ role: 'user', content: 'Hi' }],
@@ -417,6 +429,35 @@ describe('Token Usage Tracking', () => {
417
429
  }
418
430
  });
419
431
 
432
+ it('should register GPT-6 Sol and Luna shortcuts with OpenAI Responses provider', function () {
433
+ const model = ModelMix.new({ mix: { openrouter: true } })
434
+ .gpt6sol()
435
+ .gpt6luna();
436
+
437
+ expect(model.models.map(({ key }) => key)).to.deep.equal([
438
+ 'gpt-6-sol',
439
+ 'openai/gpt-6-sol',
440
+ 'gpt-6-luna',
441
+ 'openai/gpt-6-luna'
442
+ ]);
443
+ expect(model.models[0].provider).to.be.instanceOf(MixOpenAIResponses);
444
+ expect(model.models[1].provider).to.be.instanceOf(MixOpenRouter);
445
+ expect(model.models[2].provider).to.be.instanceOf(MixOpenAIResponses);
446
+ expect(model.models[3].provider).to.be.instanceOf(MixOpenRouter);
447
+ });
448
+
449
+ it('should account for GPT-6 Sol and Luna cache usage and the long-context boundary', function () {
450
+ const tokens = { input: 272_000, cached: 100_000, cacheWrite: 20_000, output: 1_000 };
451
+ for (const key of ['gpt-6-sol', 'openai/gpt-6-sol']) {
452
+ expect(ModelMix.calculateCost(key, tokens)).to.be.closeTo(0.384, 1e-10);
453
+ expect(ModelMix.calculateCost(key, { ...tokens, input: 272_001 })).to.be.closeTo(0.763004, 1e-10);
454
+ }
455
+ for (const key of ['gpt-6-luna', 'openai/gpt-6-luna']) {
456
+ expect(ModelMix.calculateCost(key, tokens)).to.be.closeTo(0.0192, 1e-10);
457
+ expect(ModelMix.calculateCost(key, { ...tokens, input: 272_001 })).to.be.closeTo(0.0381502, 1e-10);
458
+ }
459
+ });
460
+
420
461
  it('should register GPT-5.6 shortcuts with OpenAI Responses provider', function () {
421
462
  const model = ModelMix.new({ mix: { openrouter: true } })
422
463
  .gpt56sol()
@@ -437,8 +478,8 @@ describe('Token Usage Tracking', () => {
437
478
  expect(model.models[3].provider).to.be.instanceOf(MixOpenRouter);
438
479
  expect(model.models[4].provider).to.be.instanceOf(MixOpenAIResponses);
439
480
  expect(model.models[5].provider).to.be.instanceOf(MixOpenRouter);
440
- expect(ModelMix.calculateCost('gpt-5.6-sol', { input: 1_000_000, output: 1_000_000 })).to.equal(55);
441
- expect(ModelMix.calculateCost('openai/gpt-5.6-sol', { input: 1_000_000, output: 1_000_000 })).to.equal(55);
481
+ expect(ModelMix.calculateCost('gpt-5.6-sol', { input: 1_000_000, output: 1_000_000 })).to.equal(38);
482
+ expect(ModelMix.calculateCost('openai/gpt-5.6-sol', { input: 1_000_000, output: 1_000_000 })).to.equal(38);
442
483
  expect(ModelMix.calculateCost('gpt-5.6-terra', { input: 1_000_000, output: 1_000_000 })).to.equal(22);
443
484
  expect(ModelMix.calculateCost('gpt-5.6-luna', { input: 1_000_000, output: 1_000_000 })).to.equal(2.2);
444
485
  expect(ModelMix.calculateCost('openai/gpt-5.3-chat', { input: 1_000_000, output: 1_000_000 })).to.equal(15.75);
@@ -674,6 +715,38 @@ describe('Token Usage Tracking', () => {
674
715
  }
675
716
  });
676
717
 
718
+ it('should register MiMo 2.6 Pro with native and OpenRouter providers', function () {
719
+ const originalMimoApiKey = process.env.MIMO_API_KEY;
720
+ const originalOpenRouterApiKey = process.env.OPENROUTER_API_KEY;
721
+
722
+ process.env.MIMO_API_KEY = 'test-mimo-key';
723
+ process.env.OPENROUTER_API_KEY = 'test-openrouter-key';
724
+
725
+ try {
726
+ const model = ModelMix.new().mimo26pro();
727
+ const withNative = ModelMix.new()
728
+ .mimo26pro({ mix: { mimo: true, openrouter: true } });
729
+
730
+ expect(model.models.map(({ key }) => key)).to.deep.equal(['xiaomi/mimo-v2.6-pro']);
731
+ expect(model.models[0].provider).to.be.instanceOf(MixOpenRouter);
732
+
733
+ expect(withNative.models.map(({ key }) => key)).to.deep.equal([
734
+ 'mimo-v2.6-pro',
735
+ 'xiaomi/mimo-v2.6-pro'
736
+ ]);
737
+ expect(withNative.models[0].provider).to.be.instanceOf(MixMiMo);
738
+ expect(withNative.models[1].provider).to.be.instanceOf(MixOpenRouter);
739
+
740
+ expect(ModelMix.new().chain('mimo26pro').models[0].key).to.equal('xiaomi/mimo-v2.6-pro');
741
+ } finally {
742
+ if (originalMimoApiKey === undefined) delete process.env.MIMO_API_KEY;
743
+ else process.env.MIMO_API_KEY = originalMimoApiKey;
744
+
745
+ if (originalOpenRouterApiKey === undefined) delete process.env.OPENROUTER_API_KEY;
746
+ else process.env.OPENROUTER_API_KEY = originalOpenRouterApiKey;
747
+ }
748
+ });
749
+
677
750
  it('should register GPT-OSS 120B through the current OpenRouter model ID', function () {
678
751
  const originalOpenRouterApiKey = process.env.OPENROUTER_API_KEY;
679
752
  process.env.OPENROUTER_API_KEY = 'test-openrouter-key';