modelmix 4.3.2 → 4.3.6

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
@@ -23,6 +23,11 @@ Recommended: install dotenv to manage environment variables
23
23
  npm install modelmix dotenv
24
24
  ```
25
25
 
26
+ > **AI Skill**: You can also add ModelMix as a skill for AI agentic development:
27
+ > ```bash
28
+ > npx skills add https://github.com/clasen/ModelMix --skill modelmix
29
+ > ```
30
+
26
31
  2. **Setup your environment variables (.env file)**:
27
32
  Only the API keys you plan to use are required.
28
33
  ```plaintext
@@ -55,7 +60,7 @@ console.log(await model.json(outputExample));
55
60
  const setup = {
56
61
  config: {
57
62
  system: "You are ALF, if they ask your name, respond with 'ALF'.",
58
- debug: true
63
+ debug: 2
59
64
  }
60
65
  };
61
66
 
@@ -139,6 +144,7 @@ Here's a comprehensive list of available methods:
139
144
  | `gpt41mini()` | OpenAI | gpt-4.1-mini | [\$0.40 / \$1.60][1] |
140
145
  | `gpt41nano()` | OpenAI | gpt-4.1-nano | [\$0.10 / \$0.40][1] |
141
146
  | `gptOss()` | Together | gpt-oss-120B | [\$0.15 / \$0.60][7] |
147
+ | `opus46[think]()` | Anthropic | claude-opus-4-6 | [\$5.00 / \$25.00][2] |
142
148
  | `opus45[think]()` | Anthropic | claude-opus-4-5-20251101 | [\$5.00 / \$25.00][2] |
143
149
  | `opus41[think]()` | Anthropic | claude-opus-4-1-20250805 | [\$15.00 / \$75.00][2] |
144
150
  | `sonnet45[think]()`| Anthropic | claude-sonnet-4-5-20250929 | [\$3.00 / \$15.00][2] |
@@ -186,107 +192,175 @@ const result = await ModelMix.new({
186
192
  .message();
187
193
  ```
188
194
 
189
- ## 🔄 Templating Methods
195
+ ## 🔄 Templates
196
+
197
+ ModelMix includes a simple but powerful templating system. You can write your system prompts and user messages in external `.md` files with placeholders, then use `replace` to fill them in at runtime.
190
198
 
191
- ### `replace` Method
199
+ ### Core methods
192
200
 
193
- The `replace` method is used to define key-value pairs for text replacement in the messages and system prompt.
201
+ | Method | Description |
202
+ | --- | --- |
203
+ | `setSystemFromFile(path)` | Load the system prompt from a file |
204
+ | `addTextFromFile(path)` | Load a user message from a file |
205
+ | `replace({ key: value })` | Replace placeholders in all messages and the system prompt |
206
+ | `replaceKeyFromFile(key, path)` | Replace a placeholder with the contents of a file |
207
+
208
+ ### Basic example with `replace`
194
209
 
195
- #### Usage:
196
210
  ```javascript
197
- model.replace({ '{{key1}}': 'value1', '{{key2}}': 'value2' });
211
+ const gpt = ModelMix.new().gpt5mini();
212
+
213
+ gpt.addText('Write a short story about a {animal} that lives in {place}.');
214
+ gpt.replace({ '{animal}': 'cat', '{place}': 'a haunted castle' });
215
+
216
+ console.log(await gpt.message());
198
217
  ```
199
218
 
200
- #### How it works:
201
- 1. It updates the `config.replace` object with the provided key-value pairs.
202
- 2. In the template, placeholders like `{{key1}}` will be replaced with 'value1'.
219
+ ### Loading prompts from `.md` files
203
220
 
204
- #### Example:
205
- ```javascript
206
- model
207
- .replace({ '{{name}}': 'Alice', '{{age}}': '30' })
208
- .addText('Hello {{name}}, are you {{age}} years old?');
221
+ Instead of writing long prompts inline, keep them in separate Markdown files. This makes them easier to read, edit, and version control.
222
+
223
+ **`prompts/system.md`**
224
+ ```markdown
225
+ You are {role}, an expert in {topic}.
226
+ Always respond in {language}.
209
227
  ```
210
- This would result in the message: "Hello Alice, are you 30 years old?"
211
228
 
212
- ### `replaceKeyFromFile` Method
229
+ **`prompts/task.md`**
230
+ ```markdown
231
+ Analyze the following and provide 3 key insights:
213
232
 
214
- The `replaceKeyFromFile` method is similar to `replace`, but it reads the replacement value from a file.
233
+ {content}
234
+ ```
215
235
 
216
- #### Usage:
236
+ **`app.js`**
217
237
  ```javascript
218
- model.replaceKeyFromFile('longText', './path/to/file.txt');
238
+ const gpt = ModelMix.new().gpt5mini();
239
+
240
+ gpt.setSystemFromFile('./prompts/system.md');
241
+ gpt.addTextFromFile('./prompts/task.md');
242
+
243
+ gpt.replace({
244
+ '{role}': 'a senior analyst',
245
+ '{topic}': 'market trends',
246
+ '{language}': 'Spanish',
247
+ '{content}': 'Bitcoin surpassed $100,000 in December 2024...'
248
+ });
249
+
250
+ console.log(await gpt.message());
219
251
  ```
220
252
 
221
- #### How it works:
222
- 1. It reads the content of the specified file synchronously.
223
- 2. It then calls the `replace` method, using the provided key and the file content as the value.
253
+ ### Injecting file contents into a placeholder
254
+
255
+ Use `replaceKeyFromFile` when the replacement value itself is a large text stored in a file.
256
+
257
+ **`prompts/summarize.md`**
258
+ ```markdown
259
+ Summarize the following article in 3 bullet points:
224
260
 
225
- #### Example:
261
+ {article}
262
+ ```
263
+
264
+ **`app.js`**
226
265
  ```javascript
227
- messageHandler
228
- .replaceKeyFromFile('article_file_contents', './article.txt')
229
- .addText('Please summarize this article: article_file_contents');
266
+ const gpt = ModelMix.new().gpt5mini();
267
+
268
+ gpt.addTextFromFile('./prompts/summarize.md');
269
+ gpt.replaceKeyFromFile('{article}', './data/article.md');
270
+
271
+ console.log(await gpt.message());
230
272
  ```
231
- This would replace `article_file_contents` with the entire content of 'article.txt'.
232
273
 
233
- ### When to use each method:
234
- - Use `replace` for short, inline replacements or dynamically generated content.
235
- - Use `replaceKeyFromFile` for longer texts or content that's stored externally.
274
+ ### Full template workflow
275
+
276
+ Combine all methods to build reusable, file-based prompt pipelines:
236
277
 
237
- Both methods allow for flexible content insertion, enabling you to create dynamic and customizable prompts for your AI model interactions.
278
+ **`prompts/system.md`**
279
+ ```markdown
280
+ You are {role}. Follow these rules:
281
+ - Be concise
282
+ - Use examples when possible
283
+ - Respond in {language}
284
+ ```
238
285
 
239
- ## 🧩 JSON Export Options
286
+ **`prompts/review.md`**
287
+ ```markdown
288
+ Review the following code and suggest improvements:
240
289
 
241
- The `json` method signature includes these options:
290
+ {code}
291
+ ```
242
292
 
293
+ **`app.js`**
243
294
  ```javascript
244
- async json(schemaExample = null, schemaDescription = {}, {
245
- type = 'json_object',
246
- addExample = false,
247
- addSchema = true,
248
- addNote = false
249
- } = {})
295
+ const gpt = ModelMix.new().gpt5mini();
296
+
297
+ gpt.setSystemFromFile('./prompts/system.md');
298
+ gpt.addTextFromFile('./prompts/review.md');
299
+
300
+ gpt.replace({ '{role}': 'a senior code reviewer', '{language}': 'English' });
301
+ gpt.replaceKeyFromFile('{code}', './src/utils.js');
302
+
303
+ console.log(await gpt.message());
250
304
  ```
251
305
 
252
- ### Option Details
306
+ ## 🧩 JSON Structured Output
253
307
 
254
- **`addSchema` (default: `true`)**
255
- - When set to `true`, includes the generated JSON schema in the system prompt
308
+ The `json` method forces the model to return a structured JSON response. You define the shape with an example object and optionally describe each field.
256
309
 
257
- **`addExample` (default: `false`)**
258
- - When set to `true`, adds the example JSON structure to the system prompt
310
+ ```javascript
311
+ await model.json(schemaExample, schemaDescription, options)
312
+ ```
259
313
 
260
- **`addNote` (default: `false`)**
261
- - When set to `true`, adds a technical note about JSON formatting requirements
262
- - Specifically adds this instruction to the system prompt:
263
- ```
264
- Output JSON Note: Escape all unescaped double quotes, backslashes, and ASCII control characters inside JSON strings, and ensure the output contains no comments.
265
- ```
266
- - Helps prevent common JSON parsing errors
314
+ ### Basic usage
267
315
 
268
- ### Usage Examples
316
+ ```javascript
317
+ const model = ModelMix.new()
318
+ .gpt5mini()
319
+ .addText('Name and capital of 3 South American countries.');
320
+
321
+ const result = await model.json({ countries: [{ name: "", capital: "" }] });
322
+ console.log(result);
323
+ // { countries: [{ name: "Argentina", capital: "Buenos Aires" }, ...] }
324
+ ```
325
+
326
+ ### Adding field descriptions
327
+
328
+ The second argument lets you describe each field so the model understands exactly what you expect:
269
329
 
270
330
  ```javascript
271
- // Basic usage with example and note
272
- const result = await model.json(
273
- { name: "John", age: 30, skills: ["JavaScript", "Python"] },
274
- { name: "Person's full name", age: "Age in years" },
275
- { addExample: true, addNote: true }
276
- );
331
+ const model = ModelMix.new()
332
+ .gpt5mini()
333
+ .addText('Name and capital of 3 South American countries.');
277
334
 
278
- // Only add the example, skip the technical note
279
335
  const result = await model.json(
280
- { status: "success", data: [] },
281
- {},
282
- { addExample: true, addNote: false }
336
+ { countries: [{ name: "Argentina", capital: "BUENOS AIRES" }] },
337
+ { countries: [{ name: "name of the country", capital: "capital of the country in uppercase" }] },
338
+ { addNote: true }
283
339
  );
340
+ console.log(result);
341
+ // { countries: [
342
+ // { name: "Brazil", capital: "BRASILIA" },
343
+ // { name: "Colombia", capital: "BOGOTA" },
344
+ // { name: "Chile", capital: "SANTIAGO" }
345
+ // ]}
346
+ ```
284
347
 
285
- // Add note for robust JSON parsing
348
+ The example values (like `"Argentina"` and `"BUENOS AIRES"`) show the model the expected format, while the descriptions clarify what each field should contain.
349
+
350
+ ### Options
351
+
352
+ | Option | Default | Description |
353
+ | --- | --- | --- |
354
+ | `addSchema` | `true` | Include the generated JSON schema in the system prompt |
355
+ | `addExample` | `false` | Include the example object in the system prompt |
356
+ | `addNote` | `false` | Add a note about JSON escaping to prevent parsing errors |
357
+
358
+ ```javascript
359
+ // Include the example and the escaping note
286
360
  const result = await model.json(
287
- { message: "Hello \"world\"" },
288
- {},
289
- { addNote: true }
361
+ { name: "John", age: 30, skills: ["JavaScript"] },
362
+ { name: "Full name", age: "Age in years", skills: "List of programming languages" },
363
+ { addExample: true, addNote: true }
290
364
  );
291
365
  ```
292
366
 
@@ -310,16 +384,28 @@ Every response from `raw()` now includes a `tokens` object with the following st
310
384
  }
311
385
  ```
312
386
 
387
+ ### `lastRaw` — Access full response after `message()` or `json()`
388
+
389
+ After calling `message()` or `json()`, use `lastRaw` to access the complete response (tokens, thinking, tool calls, etc.). It has the same structure as `raw()`.
390
+
391
+ ```javascript
392
+ const text = await model.message();
393
+ console.log(model.lastRaw.tokens);
394
+ // { input: 122, output: 86, total: 541, cost: 0.000319 }
395
+ ```
396
+
397
+ The `cost` field is the estimated cost in USD based on the model's pricing per 1M tokens (input/output). If the model is not found in the pricing table, `cost` will be `null`.
398
+
313
399
  ## 🐛 Enabling Debug Mode
314
400
 
315
401
  To activate debug mode in ModelMix and view detailed request information, follow these two steps:
316
402
 
317
- 1. In the ModelMix constructor, include `debug: true` in the configuration:
403
+ 1. In the ModelMix constructor, include `debug: 3` in the configuration:
318
404
 
319
405
  ```javascript
320
406
  const mix = ModelMix.new({
321
407
  config: {
322
- debug: true
408
+ debug: 3
323
409
  // ... other configuration options ...
324
410
  }
325
411
  });
@@ -389,10 +475,14 @@ new ModelMix(args = { options: {}, config: {} })
389
475
  - `new()`: `static` Creates a new `ModelMix`.
390
476
  - `new()`: Creates a new `ModelMix` using instance setup.
391
477
 
478
+ - `setSystem(text)`: Sets the system prompt.
479
+ - `setSystemFromFile(filePath)`: Sets the system prompt from a file.
392
480
  - `addText(text, config = { role: "user" })`: Adds a text message.
393
- - `addTextFromFile(filePath, config = { role: "user" })`: Adds a text message from a file path.
481
+ - `addTextFromFile(filePath, config = { role: "user" })`: Adds a text message from a file.
394
482
  - `addImage(filePath, config = { role: "user" })`: Adds an image message from a file path.
395
483
  - `addImageFromUrl(url, config = { role: "user" })`: Adds an image message from URL.
484
+ - `replace(keyValues)`: Defines placeholder replacements for messages and system prompt.
485
+ - `replaceKeyFromFile(key, filePath)`: Defines a placeholder replacement with file contents as value.
396
486
  - `message()`: Sends the message and returns the response.
397
487
  - `raw()`: Sends the message and returns the complete response data including:
398
488
  - `message`: The text response from the model
package/demo/custom.js CHANGED
@@ -9,7 +9,7 @@ const mmix = new ModelMix({
9
9
  config: {
10
10
  system: 'You are ALF from Melmac.',
11
11
  max_history: 2,
12
- debug: true
12
+ debug: 3
13
13
  }
14
14
  });
15
15
 
package/demo/demo.js CHANGED
@@ -10,7 +10,7 @@ const mmix = new ModelMix({
10
10
  system: 'You are {name} from Melmac.',
11
11
  max_history: 2,
12
12
  bottleneck: { maxConcurrent: 1 },
13
- debug: true,
13
+ debug: 3,
14
14
  }
15
15
  });
16
16
 
@@ -33,7 +33,7 @@ gpt.replace({ '{animal}': 'cat' });
33
33
  await gpt.json({ time: '24:00:00', message: 'Hello' }, { time: 'Time in format HH:MM:SS' });
34
34
 
35
35
  console.log("\n" + '--------| sonnet45() |--------');
36
- const claude = mmix.new({ config: { debug: true } }).sonnet45();
36
+ const claude = mmix.new({ config: { debug: 2 } }).sonnet45();
37
37
  claude.addImageFromUrl('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAFUlEQVR42mP8z8BQz0AEYBxVSF+FABJADveWkH6oAAAAAElFTkSuQmCC');
38
38
  claude.addText('in one word, which is the main color of the image?');
39
39
  const imageDescription = await claude.message();
package/demo/free.js CHANGED
@@ -1,7 +1,7 @@
1
1
  process.loadEnvFile();
2
2
  import { ModelMix } from '../index.js';
3
3
 
4
- const ai = ModelMix.new({ config: { debug: true } })
4
+ const ai = ModelMix.new({ config: { debug: 2 } })
5
5
  .gptOss()
6
6
  .kimiK2()
7
7
  .deepseekR1()
package/demo/gpt51.js CHANGED
@@ -3,7 +3,7 @@ import { ModelMix } from '../index.js';
3
3
 
4
4
  const mmix = new ModelMix({
5
5
  config: {
6
- debug: true,
6
+ debug: 2,
7
7
  }
8
8
  });
9
9
 
package/demo/grok.js CHANGED
@@ -9,7 +9,7 @@ const mmix = new ModelMix({
9
9
  config: {
10
10
  system: 'You are ALF from Melmac.',
11
11
  max_history: 2,
12
- debug: true
12
+ debug: 2
13
13
  }
14
14
  });
15
15
 
package/demo/images.js CHANGED
@@ -1,7 +1,7 @@
1
1
  process.loadEnvFile();
2
2
  import { ModelMix } from '../index.js';
3
3
 
4
- const model = ModelMix.new({ config: { max_history: 2, debug: true } }).maverick()
4
+ const model = ModelMix.new({ config: { max_history: 2, debug: 2 } }).maverick()
5
5
  // model.addImageFromUrl('https://pbs.twimg.com/media/F6-GsjraAAADDGy?format=jpg');
6
6
  model.addImage('./img.png');
7
7
  model.addText('in one word, which is the main color of the image?');
package/demo/json.js CHANGED
@@ -1,8 +1,8 @@
1
1
  process.loadEnvFile();
2
2
  import { ModelMix } from '../index.js';
3
3
 
4
- const model = await ModelMix.new({ options: { max_tokens: 10000 }, config: { debug: true } })
5
- .gemini3pro()
4
+ const model = await ModelMix.new({ options: { max_tokens: 10000 }, config: { debug: 3 } })
5
+ .gemini3flash()
6
6
  // .gptOss()
7
7
  // .scout({ config: { temperature: 0 } })
8
8
  // .o4mini()
@@ -11,5 +11,17 @@ const model = await ModelMix.new({ options: { max_tokens: 10000 }, config: { deb
11
11
  // .gemini25flash()
12
12
  .addText("Name and capital of 3 South American countries.")
13
13
 
14
- const jsonResult = await model.json({ countries: [{ name: "", capital: "" }] }, {}, { addNote: true });
15
- console.log(jsonResult);
14
+ const jsonResult = await model.json({
15
+ countries: [{
16
+ name: "Argentina",
17
+ capital: "BUENOS AIRES"
18
+ }]
19
+ }, {
20
+ countries: [{
21
+ name: "name of the country",
22
+ capital: "capital of the country in uppercase"
23
+ }]
24
+ }, { addNote: true });
25
+
26
+ console.log(jsonResult);
27
+ console.log(model.lastRaw.tokens);
@@ -92,7 +92,7 @@ async function simpleCalculator() {
92
92
  async function contentGenerator() {
93
93
  console.log('\n=== Content Generator ===');
94
94
 
95
- const mmix = ModelMix.new({ config: { debug: true, max_history: 1 } })
95
+ const mmix = ModelMix.new({ config: { debug: 2, max_history: 1 } })
96
96
  .gemini3flash()
97
97
  .setSystem('You are a creative assistant that can generate different types of content.');
98
98
 
package/demo/minimax.js CHANGED
@@ -6,7 +6,7 @@ process.loadEnvFile();
6
6
  const main = async () => {
7
7
 
8
8
  const bot = ModelMix
9
- .new({ config: { debug: true } })
9
+ .new({ config: { debug: 3 } })
10
10
  .minimaxM21()
11
11
  .setSystem('You are a helpful assistant.');
12
12
 
package/demo/parallel.js CHANGED
@@ -10,7 +10,7 @@ const mix = new ModelMix({
10
10
  bottleneck: {
11
11
  maxConcurrent: 1, // Maximum number of concurrent requests
12
12
  },
13
- debug: true,
13
+ debug: 3,
14
14
  }
15
15
  })
16
16
 
@@ -11,7 +11,7 @@ const isolate = new ivm.Isolate({ memoryLimit: 128 }); // 128MB máximo
11
11
  async function replPowersExample() {
12
12
  console.log('\n=== JavaScript REPL - Potencias de 2 ===\n');
13
13
  const gptArgs = { options: { reasoning_effort: "none", verbosity: null } };
14
- const mmix = ModelMix.new({ config: { debug: true, max_history: 10 } })
14
+ const mmix = ModelMix.new({ config: { debug: 2, max_history: 10 } })
15
15
  .gpt41nano()
16
16
  .gpt52(gptArgs)
17
17
  .gemini3flash()
package/demo/short.js CHANGED
@@ -5,7 +5,7 @@ import { ModelMix } from '../index.js';
5
5
  const setup = {
6
6
  config: {
7
7
  system: "You are ALF, if they ask your name, answer 'ALF'.",
8
- debug: true
8
+ debug: 2
9
9
  }
10
10
  };
11
11
 
package/index.js CHANGED
@@ -10,6 +10,81 @@ const { Client } = require("@modelcontextprotocol/sdk/client/index.js");
10
10
  const { StdioClientTransport } = require("@modelcontextprotocol/sdk/client/stdio.js");
11
11
  const { MCPToolsManager } = require('./mcp-tools');
12
12
 
13
+ // Pricing per 1M tokens: [input, output] in USD
14
+ // Based on provider pricing pages linked in README
15
+ const MODEL_PRICING = {
16
+ // OpenAI
17
+ 'gpt-5.2': [1.75, 14.00],
18
+ 'gpt-5.2-chat-latest': [1.75, 14.00],
19
+ 'gpt-5.1': [1.25, 10.00],
20
+ 'gpt-5': [1.25, 10.00],
21
+ 'gpt-5-mini': [0.25, 2.00],
22
+ 'gpt-5-nano': [0.05, 0.40],
23
+ 'gpt-4.1': [2.00, 8.00],
24
+ 'gpt-4.1-mini': [0.40, 1.60],
25
+ 'gpt-4.1-nano': [0.10, 0.40],
26
+ // gptOss (Together/Groq/Cerebras/OpenRouter)
27
+ 'openai/gpt-oss-120b': [0.15, 0.60],
28
+ 'gpt-oss-120b': [0.15, 0.60],
29
+ 'openai/gpt-oss-120b:free': [0, 0],
30
+ // Anthropic
31
+ 'claude-opus-4-6': [5.00, 25.00],
32
+ 'claude-opus-4-5-20251101': [5.00, 25.00],
33
+ 'claude-opus-4-1-20250805': [15.00, 75.00],
34
+ 'claude-sonnet-4-5-20250929': [3.00, 15.00],
35
+ 'claude-sonnet-4-20250514': [3.00, 15.00],
36
+ 'claude-3-5-haiku-20241022': [0.80, 4.00],
37
+ 'claude-haiku-4-5-20251001': [1.00, 5.00],
38
+ // Google
39
+ 'gemini-3-pro-preview': [2.00, 12.00],
40
+ 'gemini-3-flash-preview': [0.50, 3.00],
41
+ 'gemini-2.5-pro': [1.25, 10.00],
42
+ 'gemini-2.5-flash': [0.30, 2.50],
43
+ // Grok
44
+ 'grok-4-0709': [3.00, 15.00],
45
+ 'grok-4-1-fast-reasoning': [0.20, 0.50],
46
+ 'grok-4-1-fast-non-reasoning': [0.20, 0.50],
47
+ // Fireworks
48
+ 'accounts/fireworks/models/deepseek-v3p2': [0.56, 1.68],
49
+ 'accounts/fireworks/models/glm-4p7': [0.55, 2.19],
50
+ 'accounts/fireworks/models/kimi-k2p5': [0.50, 2.80],
51
+ // MiniMax
52
+ 'MiniMax-M2.1': [0.30, 1.20],
53
+ // Perplexity
54
+ 'sonar': [1.00, 1.00],
55
+ 'sonar-pro': [3.00, 15.00],
56
+ // Scout (Groq/Together/Cerebras)
57
+ 'meta-llama/llama-4-scout-17b-16e-instruct': [0.11, 0.34],
58
+ 'meta-llama/Llama-4-Scout-17B-16E-Instruct': [0.11, 0.34],
59
+ 'llama-4-scout-17b-16e-instruct': [0.11, 0.34],
60
+ // Maverick (Groq/Together/Lambda)
61
+ 'meta-llama/llama-4-maverick-17b-128e-instruct': [0.20, 0.60],
62
+ 'meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8': [0.20, 0.60],
63
+ 'llama-4-maverick-17b-128e-instruct-fp8': [0.20, 0.60],
64
+ // Hermes3 (Lambda/OpenRouter)
65
+ 'Hermes-3-Llama-3.1-405B-FP8': [0.80, 0.80],
66
+ 'nousresearch/hermes-3-llama-3.1-405b:free': [0, 0],
67
+ // Qwen3 (Together/Cerebras)
68
+ 'Qwen/Qwen3-235B-A22B-fp8-tput': [0.20, 0.60],
69
+ 'qwen-3-32b': [0.20, 0.60],
70
+ // Kimi K2 (Together/Groq/OpenRouter)
71
+ 'moonshotai/Kimi-K2-Instruct-0905': [1.00, 3.00],
72
+ 'moonshotai/kimi-k2-instruct-0905': [1.00, 3.00],
73
+ 'moonshotai/kimi-k2:free': [0, 0],
74
+ 'moonshotai/Kimi-K2-Thinking': [1.00, 3.00],
75
+ 'moonshotai/kimi-k2-thinking': [1.00, 3.00],
76
+ // Kimi K2.5 (Together/Fireworks/OpenRouter)
77
+ 'moonshotai/Kimi-K2.5': [0.50, 2.80],
78
+ 'moonshotai/kimi-k2.5': [0.50, 2.80],
79
+ // DeepSeek V3.2 (OpenRouter)
80
+ 'deepseek/deepseek-v3.2': [0.56, 1.68],
81
+ // GLM 4.7 (OpenRouter/Cerebras)
82
+ 'z-ai/glm-4.7': [0.55, 2.19],
83
+ 'zai-glm-4.7': [0.55, 2.19],
84
+ // DeepSeek R1 (OpenRouter free)
85
+ 'deepseek/deepseek-r1-0528:free': [0, 0],
86
+ };
87
+
13
88
  class ModelMix {
14
89
 
15
90
  constructor({ options = {}, config = {}, mix = {} } = {}) {
@@ -19,6 +94,7 @@ class ModelMix {
19
94
  this.toolClient = {};
20
95
  this.mcp = {};
21
96
  this.mcpToolsManager = new MCPToolsManager();
97
+ this.lastRaw = null;
22
98
  this.options = {
23
99
  max_tokens: 8192,
24
100
  temperature: 1, // 1 --> More creative, 0 --> More deterministic.
@@ -82,11 +158,18 @@ class ModelMix {
82
158
  }
83
159
 
84
160
  // debug logging helpers
85
- static truncate(str, maxLen = 100) {
161
+ static truncate(str, maxLen = 1000) {
86
162
  if (!str || typeof str !== 'string') return str;
87
163
  return str.length > maxLen ? str.substring(0, maxLen) + '...' : str;
88
164
  }
89
165
 
166
+ static calculateCost(modelKey, tokens) {
167
+ const pricing = MODEL_PRICING[modelKey];
168
+ if (!pricing) return null;
169
+ const [inputPerMillion, outputPerMillion] = pricing;
170
+ return (tokens.input * inputPerMillion / 1_000_000) + (tokens.output * outputPerMillion / 1_000_000);
171
+ }
172
+
90
173
  static formatInputSummary(messages, system) {
91
174
  const lastMessage = messages[messages.length - 1];
92
175
  let inputText = '';
@@ -98,8 +181,8 @@ class ModelMix {
98
181
  inputText = lastMessage.content;
99
182
  }
100
183
 
101
- const systemStr = `System: ${ModelMix.truncate(system, 50)}`;
102
- const inputStr = `Input: ${ModelMix.truncate(inputText, 120)}`;
184
+ const systemStr = `System: ${ModelMix.truncate(system, 500)}`;
185
+ const inputStr = `Input: ${ModelMix.truncate(inputText, 1200)}`;
103
186
  const msgCount = `(${messages.length} msg${messages.length !== 1 ? 's' : ''})`;
104
187
 
105
188
  return `${systemStr} \n| ${inputStr} ${msgCount}`;
@@ -115,15 +198,15 @@ class ModelMix {
115
198
  if (debug >= 2) {
116
199
  parts.push(`Output (JSON):\n${ModelMix.formatJSON(parsed)}`);
117
200
  } else {
118
- parts.push(`Output: ${ModelMix.truncate(result.message, 150)}`);
201
+ parts.push(`Output: ${ModelMix.truncate(result.message, 1500)}`);
119
202
  }
120
203
  } catch (e) {
121
204
  // Not JSON, show truncated as before
122
- parts.push(`Output: ${ModelMix.truncate(result.message, 150)}`);
205
+ parts.push(`Output: ${ModelMix.truncate(result.message, 1500)}`);
123
206
  }
124
207
  }
125
208
  if (result.think) {
126
- parts.push(`Think: ${ModelMix.truncate(result.think, 80)}`);
209
+ parts.push(`Think: ${ModelMix.truncate(result.think, 800)}`);
127
210
  }
128
211
  if (result.toolCalls && result.toolCalls.length > 0) {
129
212
  const toolNames = result.toolCalls.map(t => t.function?.name || t.name).join(', ');
@@ -190,6 +273,17 @@ class ModelMix {
190
273
  if (mix.openrouter) this.attach('openai/gpt-oss-120b:free', new MixOpenRouter({ options, config }));
191
274
  return this;
192
275
  }
276
+ opus46think({ options = {}, config = {} } = {}) {
277
+ options = { ...MixAnthropic.thinkingOptions, ...options };
278
+ return this.attach('claude-opus-4-6', new MixAnthropic({ options, config }));
279
+ }
280
+ opus45think({ options = {}, config = {} } = {}) {
281
+ options = { ...MixAnthropic.thinkingOptions, ...options };
282
+ return this.attach('claude-opus-4-5-20251101', new MixAnthropic({ options, config }));
283
+ }
284
+ opus46({ options = {}, config = {} } = {}) {
285
+ return this.attach('claude-opus-4-6', new MixAnthropic({ options, config }));
286
+ }
193
287
  opus45({ options = {}, config = {} } = {}) {
194
288
  return this.attach('claude-opus-4-5-20251101', new MixAnthropic({ options, config }));
195
289
  }
@@ -761,6 +855,11 @@ class ModelMix {
761
855
 
762
856
  const result = await providerInstance.create({ options: currentOptions, config: currentConfig });
763
857
 
858
+ // Calculate cost based on model pricing
859
+ if (result.tokens) {
860
+ result.tokens.cost = ModelMix.calculateCost(currentModelKey, result.tokens);
861
+ }
862
+
764
863
  if (result.toolCalls && result.toolCalls.length > 0) {
765
864
 
766
865
  if (result.message) {
@@ -821,6 +920,7 @@ class ModelMix {
821
920
 
822
921
  if (currentConfig.debug >= 1) console.log('');
823
922
 
923
+ this.lastRaw = result;
824
924
  return result;
825
925
 
826
926
  } catch (error) {
@@ -1349,7 +1449,7 @@ class MixAnthropic extends MixCustom {
1349
1449
  static thinkingOptions = {
1350
1450
  thinking: {
1351
1451
  "type": "enabled",
1352
- "budget_tokens": 1024
1452
+ "budget_tokens": 1638
1353
1453
  },
1354
1454
  temperature: 1
1355
1455
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modelmix",
3
- "version": "4.3.2",
3
+ "version": "4.3.6",
4
4
  "description": "🧬 Reliable interface with automatic fallback for AI LLMs.",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -9,6 +9,7 @@
9
9
  },
10
10
  "keywords": [
11
11
  "mcp",
12
+ "skill",
12
13
  "llm",
13
14
  "ai",
14
15
  "model",
@@ -72,4 +73,4 @@
72
73
  "test:live.mcp": "mocha test/live.mcp.js --timeout 60000 --require dotenv/config --require test/setup.js",
73
74
  "test:tokens": "mocha test/tokens.test.js --timeout 10000 --require dotenv/config --require test/setup.js"
74
75
  }
75
- }
76
+ }
@@ -0,0 +1,303 @@
1
+ ---
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 integrating AI models (OpenAI, Anthropic, Google, Groq, Perplexity, Grok, etc.), chaining models with fallback, getting structured JSON from LLMs, adding MCP tools, streaming responses, or managing multi-provider AI workflows in Node.js.
4
+ ---
5
+
6
+ # ModelMix Library Skill
7
+
8
+ ## Overview
9
+
10
+ ModelMix is a Node.js library that provides a unified fluent API to interact with multiple AI LLM providers. It handles automatic fallback between models, round-robin load balancing, structured JSON output, streaming, MCP tool integration, rate limiting, and token tracking.
11
+
12
+ Use this skill when:
13
+ - Integrating one or more AI models into a Node.js project
14
+ - Chaining models with automatic fallback
15
+ - Extracting structured JSON from LLMs
16
+ - Adding MCP tools or custom tools to models
17
+ - Working with templates and file-based prompts
18
+
19
+ Do NOT use this skill for:
20
+ - Python or non-Node.js projects
21
+ - Direct HTTP calls to LLM APIs (use ModelMix instead)
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ npm install modelmix
27
+ ```
28
+
29
+ ## Core Concepts
30
+
31
+ ### Import
32
+
33
+ ```javascript
34
+ import { ModelMix } from 'modelmix';
35
+ ```
36
+
37
+ ### Creating an Instance
38
+
39
+ ```javascript
40
+ // Static factory (preferred)
41
+ const model = ModelMix.new();
42
+
43
+ // With global options
44
+ const model = ModelMix.new({
45
+ options: { max_tokens: 4096, temperature: 0.7 },
46
+ config: {
47
+ system: "You are a helpful assistant.",
48
+ max_history: 5,
49
+ debug: 0, // 0=silent, 1=minimal, 2=summary, 3=full
50
+ roundRobin: false // false=fallback, true=rotate models
51
+ }
52
+ });
53
+ ```
54
+
55
+ ### Attaching Models (Fluent Chain)
56
+
57
+ Chain shorthand methods to attach providers. First model is primary; others are fallbacks:
58
+
59
+ ```javascript
60
+ const model = ModelMix.new()
61
+ .sonnet45() // primary
62
+ .gpt5mini() // fallback 1
63
+ .gemini3flash() // fallback 2
64
+ .addText("Hello!")
65
+ ```
66
+
67
+ If `sonnet45` fails, it automatically tries `gpt5mini`, then `gemini3flash`.
68
+
69
+ ## Available Model Shorthands
70
+
71
+ - **OpenAI**: `gpt52` `gpt51` `gpt5` `gpt5mini` `gpt5nano` `gpt41` `gpt41mini` `gpt41nano`
72
+ - **Anthropic**: `opus46` `opus45` `sonnet45` `sonnet4` `haiku45` `haiku35` (thinking variants: add `think` suffix)
73
+ - **Google**: `gemini3pro` `gemini3flash` `gemini25pro` `gemini25flash`
74
+ - **Grok**: `grok4` `grok41` (thinking variant available)
75
+ - **Perplexity**: `sonar` `sonarPro`
76
+ - **Groq**: `scout` `maverick`
77
+ - **Together**: `qwen3` `kimiK2`
78
+ - **Multi-provider**: `deepseekR1` `gptOss`
79
+ - **MiniMax**: `minimaxM21`
80
+ - **Fireworks**: `deepseekV32` `GLM47`
81
+
82
+ Each method is called as `mix.methodName()` and accepts optional `{ options, config }` to override per-model settings.
83
+
84
+ ## Common Tasks
85
+
86
+ ### Get a text response
87
+
88
+ ```javascript
89
+ const answer = await ModelMix.new()
90
+ .gpt5mini()
91
+ .addText("What is the capital of France?")
92
+ .message();
93
+ ```
94
+
95
+ ### Get structured JSON
96
+
97
+ ```javascript
98
+ const result = await ModelMix.new()
99
+ .gpt5mini()
100
+ .addText("Name and capital of 3 South American countries.")
101
+ .json(
102
+ { countries: [{ name: "", capital: "" }] }, // schema example
103
+ { countries: [{ name: "country name", capital: "in uppercase" }] }, // descriptions
104
+ { addNote: true } // options
105
+ );
106
+ // result.countries → [{ name: "Brazil", capital: "BRASILIA" }, ...]
107
+ ```
108
+
109
+ `json()` signature: `json(schemaExample, schemaDescription?, { addSchema, addExample, addNote }?)`
110
+
111
+ ### Stream a response
112
+
113
+ ```javascript
114
+ await ModelMix.new()
115
+ .gpt5mini()
116
+ .addText("Tell me a story.")
117
+ .stream(({ delta, message }) => {
118
+ process.stdout.write(delta);
119
+ });
120
+ ```
121
+
122
+ ### Get raw response (tokens, thinking, tool calls)
123
+
124
+ ```javascript
125
+ const raw = await ModelMix.new()
126
+ .sonnet45think()
127
+ .addText("Solve this step by step: 2+2*3")
128
+ .raw();
129
+ // raw.message, raw.think, raw.tokens, raw.toolCalls, raw.response
130
+ ```
131
+
132
+ ### Access full response after `message()` or `json()` with `lastRaw`
133
+
134
+ After calling `message()`, `json()`, `block()`, or `stream()`, use `lastRaw` to access the complete response (tokens, thinking, tool calls, etc.). It has the same structure as `raw()`.
135
+
136
+ ```javascript
137
+ const model = ModelMix.new().gpt5mini().addText("Hello!");
138
+ const text = await model.message();
139
+ console.log(model.lastRaw.tokens);
140
+ // { input: 122, output: 86, total: 541, cost: 0.000319 }
141
+ console.log(model.lastRaw.think); // reasoning content (if available)
142
+ console.log(model.lastRaw.response); // raw API response
143
+ ```
144
+
145
+ ### Add images
146
+
147
+ ```javascript
148
+ const model = ModelMix.new().sonnet45();
149
+ model.addImage('./photo.jpg'); // from file
150
+ model.addImageFromUrl('https://example.com/img.png'); // from URL
151
+ model.addText('Describe this image.');
152
+ const description = await model.message();
153
+ ```
154
+
155
+ ### Use templates with placeholders
156
+
157
+ ```javascript
158
+ const model = ModelMix.new().gpt5mini();
159
+ model.setSystemFromFile('./prompts/system.md');
160
+ model.addTextFromFile('./prompts/task.md');
161
+ model.replace({
162
+ '{role}': 'data analyst',
163
+ '{language}': 'Spanish'
164
+ });
165
+ model.replaceKeyFromFile('{code}', './src/utils.js');
166
+ console.log(await model.message());
167
+ ```
168
+
169
+ ### Round-robin load balancing
170
+
171
+ ```javascript
172
+ const pool = ModelMix.new({ config: { roundRobin: true } })
173
+ .gpt5mini()
174
+ .sonnet45()
175
+ .gemini3flash();
176
+
177
+ // Each call rotates to the next model
178
+ const r1 = await pool.new().addText("Request 1").message();
179
+ const r2 = await pool.new().addText("Request 2").message();
180
+ ```
181
+
182
+ ### MCP integration (external tools)
183
+
184
+ ```javascript
185
+ const model = ModelMix.new({ config: { max_history: 10 } }).gpt5nano();
186
+ model.setSystem('You are an assistant. Today is ' + new Date().toISOString());
187
+ await model.addMCP('@modelcontextprotocol/server-brave-search');
188
+ model.addText('Use Internet: What is the latest news about AI?');
189
+ console.log(await model.message());
190
+ ```
191
+
192
+ Requires `BRAVE_API_KEY` in `.env` for Brave Search MCP.
193
+
194
+ ### Custom local tools (addTool)
195
+
196
+ ```javascript
197
+ const model = ModelMix.new({ config: { max_history: 10 } }).gpt5mini();
198
+
199
+ model.addTool({
200
+ name: "get_weather",
201
+ description: "Get weather for a city",
202
+ inputSchema: {
203
+ type: "object",
204
+ properties: { city: { type: "string" } },
205
+ required: ["city"]
206
+ }
207
+ }, async ({ city }) => {
208
+ return `The weather in ${city} is sunny, 25C`;
209
+ });
210
+
211
+ model.addText("What's the weather in Tokyo?");
212
+ console.log(await model.message());
213
+ ```
214
+
215
+ ### Rate limiting (Bottleneck)
216
+
217
+ ```javascript
218
+ const model = ModelMix.new({
219
+ config: {
220
+ bottleneck: {
221
+ maxConcurrent: 4,
222
+ minTime: 1000
223
+ }
224
+ }
225
+ }).gpt5mini();
226
+ ```
227
+
228
+ ### Debug mode
229
+
230
+ ```javascript
231
+ const model = ModelMix.new({
232
+ config: { debug: 2 } // 0=silent, 1=minimal, 2=summary, 3=full
233
+ }).gpt5mini();
234
+ ```
235
+
236
+ For full debug output, also set the env: `DEBUG=ModelMix* node script.js`
237
+
238
+ ### Use free-tier models
239
+
240
+ ```javascript
241
+ // These use providers with free quotas (OpenRouter, Groq, Cerebras)
242
+ const model = ModelMix.new()
243
+ .gptOss()
244
+ .kimiK2()
245
+ .deepseekR1()
246
+ .hermes3()
247
+ .addText("What is the capital of France?");
248
+ console.log(await model.message());
249
+ ```
250
+
251
+ ### Conversation history
252
+
253
+ ```javascript
254
+ const chat = ModelMix.new({ config: { max_history: 10 } }).gpt5mini();
255
+ chat.addText("My name is Martin.");
256
+ await chat.message();
257
+ chat.addText("What's my name?");
258
+ const reply = await chat.message(); // "Martin"
259
+ ```
260
+
261
+ ## Agent Usage Rules
262
+
263
+ - Always check `package.json` for `modelmix` before running `npm install`.
264
+ - Use `ModelMix.new()` static factory to create instances (not `new ModelMix()`).
265
+ - Store API keys in `.env` and load with `dotenv/config` or `process.loadEnvFile()`. Never hardcode keys.
266
+ - Chain models for resilience: primary model first, fallbacks after.
267
+ - When using MCP tools or `addTool()`, set `max_history` to at least 3.
268
+ - Use `.json()` for structured output instead of parsing text manually.
269
+ - Use `.message()` for simple text, `.raw()` when you need tokens/thinking/toolCalls.
270
+ - For thinking models, append `think` to the method name (e.g. `sonnet45think()`).
271
+ - Template placeholders use `{key}` syntax in both system prompts and user messages.
272
+ - The library uses CommonJS internally (`require`) but supports ESM import via `{ ModelMix }`.
273
+ - Available provider Mix classes for custom setups: `MixOpenAI`, `MixAnthropic`, `MixGoogle`, `MixPerplexity`, `MixGroq`, `MixTogether`, `MixGrok`, `MixOpenRouter`, `MixOllama`, `MixLMStudio`, `MixCustom`, `MixCerebras`, `MixFireworks`, `MixMiniMax`.
274
+
275
+ ## API Quick Reference
276
+
277
+ | Method | Returns | Description |
278
+ | --- | --- | --- |
279
+ | `.addText(text)` | `this` | Add user message |
280
+ | `.addTextFromFile(path)` | `this` | Add user message from file |
281
+ | `.setSystem(text)` | `this` | Set system prompt |
282
+ | `.setSystemFromFile(path)` | `this` | Set system prompt from file |
283
+ | `.addImage(path)` | `this` | Add image from file |
284
+ | `.addImageFromUrl(url)` | `this` | Add image from URL or data URI |
285
+ | `.replace({})` | `this` | Set placeholder replacements |
286
+ | `.replaceKeyFromFile(key, path)` | `this` | Replace placeholder with file content |
287
+ | `.message()` | `Promise<string>` | Get text response |
288
+ | `.json(example, desc?, opts?)` | `Promise<object>` | Get structured JSON |
289
+ | `.raw()` | `Promise<{message, think, toolCalls, tokens, response}>` | Full response |
290
+ | `.lastRaw` | `object \| null` | Full response from last `message()`/`json()`/`block()`/`stream()` call |
291
+ | `.stream(callback)` | `Promise` | Stream response |
292
+ | `.block()` | `Promise<string>` | Extract code block from response |
293
+ | `.addMCP(package)` | `Promise` | Add MCP server tools |
294
+ | `.addTool(def, callback)` | `this` | Register custom local tool |
295
+ | `.addTools([{tool, callback}])` | `this` | Register multiple tools |
296
+ | `.removeTool(name)` | `this` | Remove a tool |
297
+ | `.listTools()` | `{local, mcp}` | List registered tools |
298
+ | `.new()` | `ModelMix` | Clone instance sharing models |
299
+ | `.attach(key, provider)` | `this` | Attach custom provider |
300
+
301
+ ## References
302
+
303
+ - [GitHub Repository](https://github.com/clasen/ModelMix)