modelmix 4.6.0 → 4.6.2

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
  ...
@@ -175,6 +176,7 @@ Here's a comprehensive list of available methods:
175
176
  | `sonar()` | Perplexity | sonar | [\$1.00 / \$1.00][4] |
176
177
  | `sonarPro()` | Perplexity | sonar-pro | [\$3.00 / \$15.00][4] |
177
178
  | `hermes3()` | Lambda | Hermes-3-Llama-3.1-405B-FP8 | [\$0.80 / \$0.80][8] |
179
+ | `kimiK3()` | Moonshot | kimi-k3 | [\$3.00 / \$15.00][11] |
178
180
  | `kimiK25think()` | Together | Kimi-K2.5 | [\$0.50 / \$2.80][7] |
179
181
  | `kimiK26think()` | Fireworks | models/kimi-k2p6 | [\$0.95 / \$4.00][10] |
180
182
 
@@ -188,10 +190,12 @@ Here's a comprehensive list of available methods:
188
190
  [8]: https://lambda.ai/inference "Lambda Pricing"
189
191
  [9]: https://platform.minimax.io/docs/api-reference/anthropic-api-compatible-cache#supported-models-and-pricing "MiniMax Pricing"
190
192
  [10]: https://fireworks.ai/pricing#serverless-pricing "Fireworks Pricing"
193
+ [11]: https://platform.kimi.ai/docs/guide/kimi-k3-pricing "Kimi K3 Pricing"
191
194
 
192
195
  Each method accepts optional `options`, `config`, and (for multi-provider methods) `mix` parameters to customize behavior.
193
196
  For NVIDIA on DeepSeek V4 Pro, use `deepseekV4Pro({ mix: { nvidia: true } })`.
194
197
  For Together on Qwen 3.6 Plus, use `qwen36plus({ mix: { fireworks: false, together: true } })`.
198
+ For OpenRouter instead of Moonshot's native API, use `kimiK3({ mix: { moonshot: false, openrouter: true } })`.
195
199
 
196
200
  ```javascript
197
201
  const result = await ModelMix.new({
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
@@ -118,6 +118,9 @@ const MODEL_PRICING = {
118
118
  // Kimi K2.5 (Together/Fireworks/OpenRouter)
119
119
  'moonshotai/Kimi-K2.5': [0.50, 2.80],
120
120
  'moonshotai/kimi-k2.5': [0.50, 2.80],
121
+ // Kimi K3
122
+ 'kimi-k3': [3.00, 15.00],
123
+ 'moonshotai/kimi-k3': [3.00, 15.00],
121
124
  // DeepSeek V3.2 (OpenRouter)
122
125
  'deepseek/deepseek-v3.2': [0.56, 1.68],
123
126
  // GLM 4.7 (OpenRouter/Cerebras)
@@ -523,6 +526,13 @@ class ModelMix {
523
526
  return this;
524
527
  }
525
528
 
529
+ kimiK3({ options = {}, config = {}, mix = { moonshot: true, openrouter: false } } = {}) {
530
+ mix = { ...this.mix, ...mix };
531
+ if (mix.moonshot) this.attach('kimi-k3', new MixKimi({ options, config }));
532
+ if (mix.openrouter) this.attach('moonshotai/kimi-k3', new MixOpenRouter({ options, config }));
533
+ return this;
534
+ }
535
+
526
536
  kimiK25think({ options = {}, config = {}, mix = { together: true } } = {}) {
527
537
  mix = { ...this.mix, ...mix };
528
538
  if (mix.together) this.attach('moonshotai/Kimi-K2.5', new MixTogether({ options, config }));
@@ -1079,8 +1089,9 @@ class ModelMix {
1079
1089
  }
1080
1090
 
1081
1091
  if (result.toolCalls && result.toolCalls.length > 0) {
1082
-
1083
- if (result.message) {
1092
+ if (result.assistantMessage) {
1093
+ this.messages.push(result.assistantMessage);
1094
+ } else if (result.message) {
1084
1095
  if (result.signature) {
1085
1096
  this.messages.push({
1086
1097
  role: "assistant", content: [{
@@ -1094,7 +1105,9 @@ class ModelMix {
1094
1105
  }
1095
1106
  }
1096
1107
 
1097
- this.messages.push({ role: "assistant", content: null, tool_calls: result.toolCalls });
1108
+ if (!result.assistantMessage) {
1109
+ this.messages.push({ role: "assistant", content: null, tool_calls: result.toolCalls });
1110
+ }
1098
1111
 
1099
1112
  const toolResults = await this.processToolCalls(result.toolCalls);
1100
1113
  for (const toolResult of toolResults) {
@@ -1153,7 +1166,9 @@ class ModelMix {
1153
1166
  this.messages = [];
1154
1167
  } else if (result.message) {
1155
1168
  // Persist assistant response for multi-turn conversations
1156
- if (result.signature) {
1169
+ if (result.assistantMessage) {
1170
+ this.messages.push(result.assistantMessage);
1171
+ } else if (result.signature) {
1157
1172
  this.messages.push({
1158
1173
  role: "assistant", content: [{
1159
1174
  type: "thinking",
@@ -1653,7 +1668,12 @@ class MixOpenAI extends MixCustom {
1653
1668
  for (const message of messages) {
1654
1669
 
1655
1670
  if (message.tool_calls) {
1656
- results.push({ role: 'assistant', tool_calls: message.tool_calls })
1671
+ results.push({
1672
+ role: 'assistant',
1673
+ content: message.content ?? null,
1674
+ ...(message.reasoning_content && { reasoning_content: message.reasoning_content }),
1675
+ tool_calls: message.tool_calls
1676
+ })
1657
1677
  continue;
1658
1678
  }
1659
1679
 
@@ -2102,6 +2122,46 @@ class MixOpenRouter extends MixOpenAI {
2102
2122
  }
2103
2123
  }
2104
2124
 
2125
+ class MixKimi extends MixOpenAI {
2126
+ getDefaultConfig(customConfig) {
2127
+ if (!process.env.MOONSHOT_API_KEY) {
2128
+ throw new Error('Moonshot API key not found. Please provide it in config or set MOONSHOT_API_KEY environment variable.');
2129
+ }
2130
+
2131
+ return MixCustom.prototype.getDefaultConfig.call(this, {
2132
+ url: 'https://api.moonshot.ai/v1/chat/completions',
2133
+ apiKey: process.env.MOONSHOT_API_KEY,
2134
+ ...customConfig
2135
+ });
2136
+ }
2137
+
2138
+ async create({ config = {}, options = {} } = {}) {
2139
+ if (Object.hasOwn(options, 'max_tokens')) {
2140
+ options.max_completion_tokens = options.max_tokens;
2141
+ delete options.max_tokens;
2142
+ }
2143
+
2144
+ delete options.temperature;
2145
+ delete options.top_p;
2146
+ delete options.n;
2147
+ delete options.presence_penalty;
2148
+ delete options.frequency_penalty;
2149
+
2150
+ return super.create({ config, options });
2151
+ }
2152
+
2153
+ extractDelta(data) {
2154
+ return data?.choices?.[0]?.delta?.content || '';
2155
+ }
2156
+
2157
+ processResponse(response) {
2158
+ return {
2159
+ ...super.processResponse(response),
2160
+ assistantMessage: response.data?.choices?.[0]?.message
2161
+ };
2162
+ }
2163
+ }
2164
+
2105
2165
  class MixAnthropic extends MixCustom {
2106
2166
 
2107
2167
  static thinkingOptions = {
@@ -2931,4 +2991,4 @@ class MixGoogle extends MixCustom {
2931
2991
  }
2932
2992
  }
2933
2993
 
2934
- module.exports = { MixCustom, ModelMix, MixAnthropic, MixMiniMax, MixMiMo, MixOpenAI, MixOpenAIResponses, MixOpenAIWebSocket, MixOpenRouter, MixPerplexity, MixOllama, MixLMStudio, MixGroq, MixTogether, MixGrok, MixCerebras, MixGoogle, MixFireworks, MixNVIDIA };
2994
+ 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.2",
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
  ---
@@ -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
  });