modelmix 4.6.0 → 4.6.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.
package/MODELS.md CHANGED
@@ -344,6 +344,15 @@ All providers inherit from `MixCustom` base class which provides common function
344
344
  - **Output Format**: Same as OpenAI
345
345
  - **Special Notes**: Uses OpenAI-compatible format
346
346
 
347
+ ### Moonshot (MixKimi)
348
+ - **Base URL**: `https://api.moonshot.ai/v1/chat/completions`
349
+ - **Input Format**: Same as OpenAI
350
+ - **Output Format**: Same as OpenAI
351
+ - **Special Notes**:
352
+ - Requires `MOONSHOT_API_KEY` environment variable
353
+ - Available model: `kimi-k3`
354
+ - K3 always uses thinking and requires fixed sampling parameters to be omitted
355
+
347
356
  ### MiniMax (MixMiniMax)
348
357
  - **Base URL**: `https://api.minimax.io/v1/chat/completions`
349
358
  - **Input Format**: Same as OpenAI
package/README.md CHANGED
@@ -32,6 +32,7 @@ Only the API keys you plan to use are required.
32
32
  ANTHROPIC_API_KEY="sk-ant-..."
33
33
  OPENAI_API_KEY="sk-proj-..."
34
34
  OPENROUTER_API_KEY="sk-or-..."
35
+ MOONSHOT_API_KEY="your-moonshot-key..."
35
36
  MINIMAX_API_KEY="your-minimax-key..."
36
37
  NVIDIA_API_KEY="nvapi-..."
37
38
  ...
@@ -48,7 +49,7 @@ try { process.loadEnvFile(); } catch {}
48
49
 
49
50
  // Get structured JSON responses
50
51
  const model = ModelMix.new()
51
- .sonnet46() // Anthropic claude-sonnet-4-6
52
+ .opus48() // Anthropic claude-opus-4-8
52
53
  .addText("Name and capital of 3 South American countries.");
53
54
 
54
55
  const outputExample = { countries: [{ name: "", capital: "" }] };
@@ -66,9 +67,9 @@ const setup = {
66
67
  };
67
68
 
68
69
  const model = await ModelMix.new(setup)
69
- .sonnet46() // (main model) Anthropic claude-sonnet-4-5-20250929
70
- .gpt5mini() // (fallback 2) OpenAI gpt-5-mini
71
- .gemini3flash({ config: { temperature: 0 } }) // (fallback 3) Google gemini-3-flash
70
+ .sonnet5() // (main model) Anthropic claude-sonnet-5
71
+ .gpt56luna() // (fallback 2) OpenAI gpt-5.6-luna
72
+ .gemini36flash({ config: { temperature: 0 } }) // (fallback 3) Google gemini-36-flash
72
73
  .grok43() // (fallback 4) Grok grok-4.3
73
74
  .addText("What's your name?");
74
75
 
@@ -113,7 +114,7 @@ BRAVE_API_KEY="BSA0..._fm"
113
114
  ```
114
115
 
115
116
  ```javascript
116
- const mmix = ModelMix.new({ config: { max_history: 10 } }).gpt5nano();
117
+ const mmix = ModelMix.new({ config: { max_history: 10 } }).gpt56sol();
117
118
  mmix.setSystem('You are an assistant and today is ' + new Date().toISOString());
118
119
 
119
120
  // Add web search capability through MCP
@@ -160,7 +161,8 @@ Here's a comprehensive list of available methods:
160
161
  | `sonnet46[think]()` | Anthropic | claude-sonnet-4-6 | [\$3.00 / \$15.00][2] |
161
162
  | `haiku45[think]()` | Anthropic | claude-haiku-4-5-20251001 | [\$1.00 / \$5.00][2] |
162
163
  | `gemini31pro()` | Google | gemini-3.1-pro-preview | [\$2.00 / \$12.00][3] |
163
- | `gemini35flash()` | Google | gemini-3.5-flash | N/A |
164
+ | `gemini36flash()` | Google | gemini-3.6-flash | [\$1.50 / \$7.50][3] |
165
+ | `gemini35flash()` | Google | gemini-3.5-flash | [\$0.75 / \$4.50][3] |
164
166
  | `gemini31flashLite()`| Google | gemini-3.1-flash-lite-preview | [\$0.25 / \$1.50][3] |
165
167
  | `grok43()` | Grok | grok-4.3 | [\$1.25 / \$2.50][6] |
166
168
  | `grok420multiAgent()`| Grok | grok-4.20-multi-agent-0309 | [\$1.25 / \$2.50][6] |
@@ -175,6 +177,7 @@ Here's a comprehensive list of available methods:
175
177
  | `sonar()` | Perplexity | sonar | [\$1.00 / \$1.00][4] |
176
178
  | `sonarPro()` | Perplexity | sonar-pro | [\$3.00 / \$15.00][4] |
177
179
  | `hermes3()` | Lambda | Hermes-3-Llama-3.1-405B-FP8 | [\$0.80 / \$0.80][8] |
180
+ | `kimiK3()` | Moonshot | kimi-k3 | [\$3.00 / \$15.00][11] |
178
181
  | `kimiK25think()` | Together | Kimi-K2.5 | [\$0.50 / \$2.80][7] |
179
182
  | `kimiK26think()` | Fireworks | models/kimi-k2p6 | [\$0.95 / \$4.00][10] |
180
183
 
@@ -188,10 +191,12 @@ Here's a comprehensive list of available methods:
188
191
  [8]: https://lambda.ai/inference "Lambda Pricing"
189
192
  [9]: https://platform.minimax.io/docs/api-reference/anthropic-api-compatible-cache#supported-models-and-pricing "MiniMax Pricing"
190
193
  [10]: https://fireworks.ai/pricing#serverless-pricing "Fireworks Pricing"
194
+ [11]: https://platform.kimi.ai/docs/guide/kimi-k3-pricing "Kimi K3 Pricing"
191
195
 
192
196
  Each method accepts optional `options`, `config`, and (for multi-provider methods) `mix` parameters to customize behavior.
193
197
  For NVIDIA on DeepSeek V4 Pro, use `deepseekV4Pro({ mix: { nvidia: true } })`.
194
198
  For Together on Qwen 3.6 Plus, use `qwen36plus({ mix: { fireworks: false, together: true } })`.
199
+ For OpenRouter instead of Moonshot's native API, use `kimiK3({ mix: { moonshot: false, openrouter: true } })`.
195
200
 
196
201
  ```javascript
197
202
  const result = await ModelMix.new({
@@ -246,7 +251,7 @@ Analyze the following and provide 3 key insights:
246
251
 
247
252
  **`app.js`**
248
253
  ```javascript
249
- const gpt = ModelMix.new().gpt5mini();
254
+ const gpt = ModelMix.new().gpt56luna();
250
255
 
251
256
  gpt.setSystemFromFile('./prompts/system.md');
252
257
  gpt.addTextFromFile('./prompts/task.md');
@@ -326,7 +331,7 @@ await model.json(schemaExample, schemaDescription, options)
326
331
 
327
332
  ```javascript
328
333
  const model = ModelMix.new()
329
- .gpt5mini()
334
+ .gpt56luna()
330
335
  .addText('Name and capital of 3 South American countries.');
331
336
 
332
337
  const result = await model.json({ countries: [{ name: "", capital: "" }] });
package/demo/default.env CHANGED
@@ -7,4 +7,5 @@ XAI_API_KEY="xai-..."
7
7
  CEREBRAS_API_KEY="csk-..."
8
8
  GEMINI_API_KEY="AIza..."
9
9
  LAMBDA_API_KEY="secret_..."
10
+ MOONSHOT_API_KEY="your-moonshot-key..."
10
11
  BRAVE_API_KEY="BSA0..._fm"
package/index.js CHANGED
@@ -76,6 +76,7 @@ const MODEL_PRICING = {
76
76
  'gemini-3.1-pro-preview':[2.00, 12.00],
77
77
  'gemini-3-pro-preview': [2.00, 12.00],
78
78
  'gemini-3-flash-preview': [0.50, 3.00],
79
+ 'gemini-3.6-flash': [1.50, 7.50],
79
80
  'gemini-3.5-flash': [0.75, 4.50],
80
81
  'gemini-2.5-pro': [1.25, 10.00],
81
82
  'gemini-2.5-flash': [0.30, 2.50],
@@ -118,6 +119,9 @@ const MODEL_PRICING = {
118
119
  // Kimi K2.5 (Together/Fireworks/OpenRouter)
119
120
  'moonshotai/Kimi-K2.5': [0.50, 2.80],
120
121
  'moonshotai/kimi-k2.5': [0.50, 2.80],
122
+ // Kimi K3
123
+ 'kimi-k3': [3.00, 15.00],
124
+ 'moonshotai/kimi-k3': [3.00, 15.00],
121
125
  // DeepSeek V3.2 (OpenRouter)
122
126
  'deepseek/deepseek-v3.2': [0.56, 1.68],
123
127
  // GLM 4.7 (OpenRouter/Cerebras)
@@ -445,6 +449,9 @@ class ModelMix {
445
449
  gemini3flash({ options = {}, config = {} } = {}) {
446
450
  return this.attach('gemini-3-flash-preview', new MixGoogle({ options, config }));
447
451
  }
452
+ gemini36flash({ options = {}, config = {} } = {}) {
453
+ return this.attach('gemini-3.6-flash', new MixGoogle({ options, config }));
454
+ }
448
455
  gemini35flash({ options = {}, config = {} } = {}) {
449
456
  return this.attach('gemini-3.5-flash', new MixGoogle({ options, config }));
450
457
  }
@@ -523,6 +530,13 @@ class ModelMix {
523
530
  return this;
524
531
  }
525
532
 
533
+ kimiK3({ options = {}, config = {}, mix = { moonshot: true, openrouter: false } } = {}) {
534
+ mix = { ...this.mix, ...mix };
535
+ if (mix.moonshot) this.attach('kimi-k3', new MixKimi({ options, config }));
536
+ if (mix.openrouter) this.attach('moonshotai/kimi-k3', new MixOpenRouter({ options, config }));
537
+ return this;
538
+ }
539
+
526
540
  kimiK25think({ options = {}, config = {}, mix = { together: true } } = {}) {
527
541
  mix = { ...this.mix, ...mix };
528
542
  if (mix.together) this.attach('moonshotai/Kimi-K2.5', new MixTogether({ options, config }));
@@ -1079,8 +1093,9 @@ class ModelMix {
1079
1093
  }
1080
1094
 
1081
1095
  if (result.toolCalls && result.toolCalls.length > 0) {
1082
-
1083
- if (result.message) {
1096
+ if (result.assistantMessage) {
1097
+ this.messages.push(result.assistantMessage);
1098
+ } else if (result.message) {
1084
1099
  if (result.signature) {
1085
1100
  this.messages.push({
1086
1101
  role: "assistant", content: [{
@@ -1094,7 +1109,9 @@ class ModelMix {
1094
1109
  }
1095
1110
  }
1096
1111
 
1097
- this.messages.push({ role: "assistant", content: null, tool_calls: result.toolCalls });
1112
+ if (!result.assistantMessage) {
1113
+ this.messages.push({ role: "assistant", content: null, tool_calls: result.toolCalls });
1114
+ }
1098
1115
 
1099
1116
  const toolResults = await this.processToolCalls(result.toolCalls);
1100
1117
  for (const toolResult of toolResults) {
@@ -1153,7 +1170,9 @@ class ModelMix {
1153
1170
  this.messages = [];
1154
1171
  } else if (result.message) {
1155
1172
  // Persist assistant response for multi-turn conversations
1156
- if (result.signature) {
1173
+ if (result.assistantMessage) {
1174
+ this.messages.push(result.assistantMessage);
1175
+ } else if (result.signature) {
1157
1176
  this.messages.push({
1158
1177
  role: "assistant", content: [{
1159
1178
  type: "thinking",
@@ -1653,7 +1672,12 @@ class MixOpenAI extends MixCustom {
1653
1672
  for (const message of messages) {
1654
1673
 
1655
1674
  if (message.tool_calls) {
1656
- results.push({ role: 'assistant', tool_calls: message.tool_calls })
1675
+ results.push({
1676
+ role: 'assistant',
1677
+ content: message.content ?? null,
1678
+ ...(message.reasoning_content && { reasoning_content: message.reasoning_content }),
1679
+ tool_calls: message.tool_calls
1680
+ })
1657
1681
  continue;
1658
1682
  }
1659
1683
 
@@ -2102,6 +2126,46 @@ class MixOpenRouter extends MixOpenAI {
2102
2126
  }
2103
2127
  }
2104
2128
 
2129
+ class MixKimi extends MixOpenAI {
2130
+ getDefaultConfig(customConfig) {
2131
+ if (!process.env.MOONSHOT_API_KEY) {
2132
+ throw new Error('Moonshot API key not found. Please provide it in config or set MOONSHOT_API_KEY environment variable.');
2133
+ }
2134
+
2135
+ return MixCustom.prototype.getDefaultConfig.call(this, {
2136
+ url: 'https://api.moonshot.ai/v1/chat/completions',
2137
+ apiKey: process.env.MOONSHOT_API_KEY,
2138
+ ...customConfig
2139
+ });
2140
+ }
2141
+
2142
+ async create({ config = {}, options = {} } = {}) {
2143
+ if (Object.hasOwn(options, 'max_tokens')) {
2144
+ options.max_completion_tokens = options.max_tokens;
2145
+ delete options.max_tokens;
2146
+ }
2147
+
2148
+ delete options.temperature;
2149
+ delete options.top_p;
2150
+ delete options.n;
2151
+ delete options.presence_penalty;
2152
+ delete options.frequency_penalty;
2153
+
2154
+ return super.create({ config, options });
2155
+ }
2156
+
2157
+ extractDelta(data) {
2158
+ return data?.choices?.[0]?.delta?.content || '';
2159
+ }
2160
+
2161
+ processResponse(response) {
2162
+ return {
2163
+ ...super.processResponse(response),
2164
+ assistantMessage: response.data?.choices?.[0]?.message
2165
+ };
2166
+ }
2167
+ }
2168
+
2105
2169
  class MixAnthropic extends MixCustom {
2106
2170
 
2107
2171
  static thinkingOptions = {
@@ -2931,4 +2995,4 @@ class MixGoogle extends MixCustom {
2931
2995
  }
2932
2996
  }
2933
2997
 
2934
- module.exports = { MixCustom, ModelMix, MixAnthropic, MixMiniMax, MixMiMo, MixOpenAI, MixOpenAIResponses, MixOpenAIWebSocket, MixOpenRouter, MixPerplexity, MixOllama, MixLMStudio, MixGroq, MixTogether, MixGrok, MixCerebras, MixGoogle, MixFireworks, MixNVIDIA };
2998
+ module.exports = { MixCustom, ModelMix, MixAnthropic, MixKimi, 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.6.0",
3
+ "version": "4.6.4",
4
4
  "description": "🧬 Reliable interface with automatic fallback for AI LLMs.",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: modelmix
3
- description: Instructions for using the ModelMix Node.js library to interact with multiple AI LLM providers through a unified interface. Use when writing code that calls AI models (OpenAI, Anthropic, Google, Groq, Perplexity, Grok, MiniMax, Fireworks, Together, Lambda, Cerebras, OpenRouter, Ollama, LM Studio), chaining models with fallback, getting structured JSON from LLMs, adding MCP tools, streaming responses, managing multi-provider AI workflows, round-robin load balancing, or rate limiting API requests in Node.js. Also use when the user mentions "modelmix", "ModelMix", asks to "call an LLM", "query a model", "add AI to my app", or wants to integrate any supported provider.
3
+ description: Instructions for using the ModelMix Node.js library to interact with multiple AI LLM providers through a unified interface. Use when writing code that calls AI models (OpenAI, Anthropic, Google, Groq, Perplexity, Grok, Moonshot, MiniMax, Fireworks, Together, Lambda, Cerebras, OpenRouter, Ollama, LM Studio), chaining models with fallback, getting structured JSON from LLMs, adding MCP tools, streaming responses, managing multi-provider AI workflows, round-robin load balancing, or rate limiting API requests in Node.js. Also use when the user mentions "modelmix", "ModelMix", asks to "call an LLM", "query a model", "add AI to my app", or wants to integrate any supported provider.
4
4
  metadata:
5
5
  tags: [llm, ai, openai, anthropic, google, groq, perplexity, grok, mcp, streaming, json-output]
6
6
  ---
@@ -101,7 +101,7 @@ If `sonnet46` fails, it automatically tries `gpt52`, then `gemini3flash`.
101
101
  Thinking variants: append `think` — e.g. `fable5think()` `opus48think()` `opus47think()` `opus46think()` `sonnet5think()` `sonnet46think()` `sonnet45think()` `sonnet4think()` `opus41think()` `haiku45think()`
102
102
 
103
103
  ### Google
104
- `gemini3pro()` `gemini3flash()` `gemini35flash()` `gemini25pro()` `gemini25flash()`
104
+ `gemini3pro()` `gemini3flash()` `gemini36flash()` `gemini35flash()` `gemini25pro()` `gemini25flash()`
105
105
 
106
106
  ### Grok
107
107
  `grok43()` `grok420multiAgent()` `grok420()` `grok420think()` `grok41()` `grok41think()`
@@ -112,6 +112,9 @@ Thinking variants: append `think` — e.g. `fable5think()` `opus48think()` `opus
112
112
  ### Together
113
113
  `qwen36plus()` `GLM52()` `kimiK25think()` `gptOss()`
114
114
 
115
+ ### Moonshot
116
+ `kimiK3()` — requires `MOONSHOT_API_KEY`; use `{ mix: { moonshot: false, openrouter: true } }` for OpenRouter.
117
+
115
118
  ### MiniMax
116
119
  `minimaxM25()` `minimaxM27()` `minimaxM3()`
117
120
 
@@ -471,7 +474,7 @@ const model = ModelMix.new({
471
474
 
472
475
  ## Available Provider Classes
473
476
 
474
- `MixOpenAI` `MixAnthropic` `MixGoogle` `MixPerplexity` `MixGroq` `MixTogether` `MixGrok` `MixOpenRouter` `MixOllama` `MixLMStudio` `MixCustom` `MixCerebras` `MixFireworks` `MixMiniMax` `MixLambda`
477
+ `MixOpenAI` `MixAnthropic` `MixGoogle` `MixPerplexity` `MixGroq` `MixTogether` `MixGrok` `MixOpenRouter` `MixOllama` `MixLMStudio` `MixCustom` `MixCerebras` `MixFireworks` `MixKimi` `MixMiniMax` `MixLambda`
475
478
 
476
479
  ## Troubleshooting
477
480
 
package/test/kimi.test.js CHANGED
@@ -1,5 +1,6 @@
1
1
  const { expect } = require('chai');
2
- const { ModelMix, MixTogether } = require('../index.js');
2
+ const nock = require('nock');
3
+ const { ModelMix, MixKimi, MixOpenRouter, MixTogether } = require('../index.js');
3
4
 
4
5
  describe('Kimi Model Registration Tests', () => {
5
6
  it('should register Together Kimi K2.7 Code by default', () => {
@@ -10,4 +11,146 @@ describe('Kimi Model Registration Tests', () => {
10
11
  expect(model.models[0].key).to.equal('moonshotai/Kimi-K2.7-Code');
11
12
  expect(model.models[0].provider).to.be.instanceOf(MixTogether);
12
13
  });
14
+
15
+ it('should register Kimi K3 with the native Moonshot provider by default', () => {
16
+ const originalMoonshotApiKey = process.env.MOONSHOT_API_KEY;
17
+ process.env.MOONSHOT_API_KEY = 'test-moonshot-key';
18
+
19
+ try {
20
+ const model = ModelMix.new().kimiK3();
21
+
22
+ expect(model.models).to.have.length(1);
23
+ expect(model.models[0].key).to.equal('kimi-k3');
24
+ expect(model.models[0].provider).to.be.instanceOf(MixKimi);
25
+ expect(model.models[0].provider.config.url).to.equal('https://api.moonshot.ai/v1/chat/completions');
26
+ } finally {
27
+ if (originalMoonshotApiKey === undefined) delete process.env.MOONSHOT_API_KEY;
28
+ else process.env.MOONSHOT_API_KEY = originalMoonshotApiKey;
29
+ }
30
+ });
31
+
32
+ it('should support Kimi K3 through OpenRouter when requested', () => {
33
+ const originalOpenRouterApiKey = process.env.OPENROUTER_API_KEY;
34
+ process.env.OPENROUTER_API_KEY = 'test-openrouter-key';
35
+
36
+ try {
37
+ const model = ModelMix.new().kimiK3({ mix: { moonshot: false, openrouter: true } });
38
+
39
+ expect(model.models).to.have.length(1);
40
+ expect(model.models[0].key).to.equal('moonshotai/kimi-k3');
41
+ expect(model.models[0].provider).to.be.instanceOf(MixOpenRouter);
42
+ } finally {
43
+ if (originalOpenRouterApiKey === undefined) delete process.env.OPENROUTER_API_KEY;
44
+ else process.env.OPENROUTER_API_KEY = originalOpenRouterApiKey;
45
+ }
46
+ });
47
+
48
+ it('should adapt Kimi K3 requests to its fixed sampling API', async () => {
49
+ const originalMoonshotApiKey = process.env.MOONSHOT_API_KEY;
50
+ process.env.MOONSHOT_API_KEY = 'test-moonshot-key';
51
+
52
+ try {
53
+ const provider = new MixKimi();
54
+ let requestBody;
55
+ nock('https://api.moonshot.ai')
56
+ .post('/v1/chat/completions', body => {
57
+ requestBody = body;
58
+ return true;
59
+ })
60
+ .reply(200, {
61
+ choices: [{ message: { content: 'Done' } }]
62
+ });
63
+
64
+ await provider.create({
65
+ config: { system: 'You are an assistant.' },
66
+ options: {
67
+ model: 'kimi-k3',
68
+ messages: [{ role: 'user', content: 'Hello' }],
69
+ max_tokens: 1000,
70
+ temperature: 0.5,
71
+ top_p: 0.9,
72
+ n: 2,
73
+ presence_penalty: 0.2,
74
+ frequency_penalty: 0.3
75
+ }
76
+ });
77
+
78
+ expect(requestBody.max_completion_tokens).to.equal(1000);
79
+ expect(requestBody).to.not.have.property('max_tokens');
80
+ expect(requestBody).to.not.have.property('temperature');
81
+ expect(requestBody).to.not.have.property('top_p');
82
+ expect(requestBody).to.not.have.property('n');
83
+ expect(requestBody).to.not.have.property('presence_penalty');
84
+ expect(requestBody).to.not.have.property('frequency_penalty');
85
+ } finally {
86
+ if (originalMoonshotApiKey === undefined) delete process.env.MOONSHOT_API_KEY;
87
+ else process.env.MOONSHOT_API_KEY = originalMoonshotApiKey;
88
+ }
89
+ });
90
+
91
+ it('should preserve Kimi K3 reasoning content in tool-call continuations', async () => {
92
+ const originalMoonshotApiKey = process.env.MOONSHOT_API_KEY;
93
+ process.env.MOONSHOT_API_KEY = 'test-moonshot-key';
94
+
95
+ try {
96
+ let continuationBody;
97
+ nock('https://api.moonshot.ai')
98
+ .post('/v1/chat/completions')
99
+ .reply(200, {
100
+ choices: [{
101
+ message: {
102
+ role: 'assistant',
103
+ content: 'I will calculate that.',
104
+ reasoning_content: 'I need the calculator tool.',
105
+ tool_calls: [{
106
+ id: 'call_calculate',
107
+ type: 'function',
108
+ function: {
109
+ name: 'calculate',
110
+ arguments: '{"expression":"2 + 2"}'
111
+ }
112
+ }]
113
+ }
114
+ }]
115
+ })
116
+ .post('/v1/chat/completions', body => {
117
+ continuationBody = body;
118
+ return true;
119
+ })
120
+ .reply(200, {
121
+ choices: [{ message: { role: 'assistant', content: 'The answer is 4.' } }]
122
+ });
123
+
124
+ const model = ModelMix.new().kimiK3();
125
+ model.addTool({
126
+ name: 'calculate',
127
+ description: 'Evaluates an expression.',
128
+ inputSchema: {
129
+ type: 'object',
130
+ properties: { expression: { type: 'string' } },
131
+ required: ['expression']
132
+ }
133
+ }, ({ expression }) => expression === '2 + 2' ? 4 : null);
134
+ model.addText('What is 2 + 2?');
135
+
136
+ expect(await model.message()).to.equal('The answer is 4.');
137
+ const assistantMessage = continuationBody.messages.find(message => message.role === 'assistant');
138
+ expect(assistantMessage).to.deep.equal({
139
+ role: 'assistant',
140
+ content: 'I will calculate that.',
141
+ reasoning_content: 'I need the calculator tool.',
142
+ tool_calls: [{
143
+ id: 'call_calculate',
144
+ type: 'function',
145
+ function: {
146
+ name: 'calculate',
147
+ arguments: '{"expression":"2 + 2"}'
148
+ }
149
+ }]
150
+ });
151
+ } finally {
152
+ if (originalMoonshotApiKey === undefined) delete process.env.MOONSHOT_API_KEY;
153
+ else process.env.MOONSHOT_API_KEY = originalMoonshotApiKey;
154
+ }
155
+ });
13
156
  });
@@ -105,13 +105,17 @@ describe('Token Usage Tracking', () => {
105
105
  expect(ModelMix.calculateCost('gpt-5.6-luna', { input: 1_000_000, output: 1_000_000 })).to.equal(7);
106
106
  });
107
107
 
108
- it('should register Gemini 3.5 Flash shortcut with Google provider', function () {
108
+ it('should register Gemini Flash shortcuts with Google provider', function () {
109
109
  const model = ModelMix.new()
110
+ .gemini36flash()
110
111
  .gemini35flash();
111
112
 
112
- expect(model.models).to.have.length(1);
113
- expect(model.models[0].key).to.equal('gemini-3.5-flash');
114
- expect(model.models[0].provider).to.be.instanceOf(MixGoogle);
113
+ expect(model.models.map(({ key }) => key)).to.deep.equal([
114
+ 'gemini-3.6-flash',
115
+ 'gemini-3.5-flash'
116
+ ]);
117
+ expect(model.models.every(({ provider }) => provider instanceof MixGoogle)).to.equal(true);
118
+ expect(ModelMix.calculateCost('gemini-3.6-flash', { input: 1_000_000, output: 1_000_000 })).to.equal(9);
115
119
  expect(ModelMix.calculateCost('gemini-3.5-flash', { input: 1_000_000, output: 1_000_000 })).to.equal(5.25);
116
120
  });
117
121