modelmix 4.3.4 → 4.4.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/README.md CHANGED
@@ -18,10 +18,13 @@ Ever found yourself wanting to integrate AI models into your projects but worrie
18
18
  ## 🛠️ Usage
19
19
 
20
20
  1. **Install the ModelMix package:**
21
- Recommended: install dotenv to manage environment variables
22
21
  ```bash
23
- npm install modelmix dotenv
22
+ npm install modelmix
24
23
  ```
24
+ > **AI Skill**: You can also add ModelMix as a skill for AI agentic development:
25
+ > ```bash
26
+ > npx skills add https://github.com/clasen/ModelMix --skill modelmix
27
+ > ```
25
28
 
26
29
  2. **Setup your environment variables (.env file)**:
27
30
  Only the API keys you plan to use are required.
@@ -34,6 +37,8 @@ MINIMAX_API_KEY="your-minimax-key..."
34
37
  GEMINI_API_KEY="AIza..."
35
38
  ```
36
39
 
40
+ For environment variables, use `dotenv` or Node's built-in `process.loadEnvFile()`.
41
+
37
42
  3. **Create and configure your models**:
38
43
 
39
44
  ```javascript
@@ -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
 
@@ -187,107 +192,175 @@ const result = await ModelMix.new({
187
192
  .message();
188
193
  ```
189
194
 
190
- ## 🔄 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.
191
198
 
192
- ### `replace` Method
199
+ ### Core methods
193
200
 
194
- 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`
195
209
 
196
- #### Usage:
197
210
  ```javascript
198
- 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());
199
217
  ```
200
218
 
201
- #### How it works:
202
- 1. It updates the `config.replace` object with the provided key-value pairs.
203
- 2. In the template, placeholders like `{{key1}}` will be replaced with 'value1'.
219
+ ### Loading prompts from `.md` files
204
220
 
205
- #### Example:
206
- ```javascript
207
- model
208
- .replace({ '{{name}}': 'Alice', '{{age}}': '30' })
209
- .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}.
210
227
  ```
211
- This would result in the message: "Hello Alice, are you 30 years old?"
212
228
 
213
- ### `replaceKeyFromFile` Method
229
+ **`prompts/task.md`**
230
+ ```markdown
231
+ Analyze the following and provide 3 key insights:
214
232
 
215
- The `replaceKeyFromFile` method is similar to `replace`, but it reads the replacement value from a file.
233
+ {content}
234
+ ```
216
235
 
217
- #### Usage:
236
+ **`app.js`**
218
237
  ```javascript
219
- 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());
220
251
  ```
221
252
 
222
- #### How it works:
223
- 1. It reads the content of the specified file synchronously.
224
- 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:
225
260
 
226
- #### Example:
261
+ {article}
262
+ ```
263
+
264
+ **`app.js`**
227
265
  ```javascript
228
- messageHandler
229
- .replaceKeyFromFile('article_file_contents', './article.txt')
230
- .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());
231
272
  ```
232
- This would replace `article_file_contents` with the entire content of 'article.txt'.
233
273
 
234
- ### When to use each method:
235
- - Use `replace` for short, inline replacements or dynamically generated content.
236
- - 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:
237
277
 
238
- 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
+ ```
239
285
 
240
- ## 🧩 JSON Export Options
286
+ **`prompts/review.md`**
287
+ ```markdown
288
+ Review the following code and suggest improvements:
241
289
 
242
- The `json` method signature includes these options:
290
+ {code}
291
+ ```
243
292
 
293
+ **`app.js`**
244
294
  ```javascript
245
- async json(schemaExample = null, schemaDescription = {}, {
246
- type = 'json_object',
247
- addExample = false,
248
- addSchema = true,
249
- addNote = false
250
- } = {})
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());
251
304
  ```
252
305
 
253
- ### Option Details
306
+ ## 🧩 JSON Structured Output
254
307
 
255
- **`addSchema` (default: `true`)**
256
- - 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.
257
309
 
258
- **`addExample` (default: `false`)**
259
- - When set to `true`, adds the example JSON structure to the system prompt
310
+ ```javascript
311
+ await model.json(schemaExample, schemaDescription, options)
312
+ ```
260
313
 
261
- **`addNote` (default: `false`)**
262
- - When set to `true`, adds a technical note about JSON formatting requirements
263
- - Specifically adds this instruction to the system prompt:
264
- ```
265
- Output JSON Note: Escape all unescaped double quotes, backslashes, and ASCII control characters inside JSON strings, and ensure the output contains no comments.
266
- ```
267
- - Helps prevent common JSON parsing errors
314
+ ### Basic usage
268
315
 
269
- ### 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:
270
329
 
271
330
  ```javascript
272
- // Basic usage with example and note
273
- const result = await model.json(
274
- { name: "John", age: 30, skills: ["JavaScript", "Python"] },
275
- { name: "Person's full name", age: "Age in years" },
276
- { addExample: true, addNote: true }
277
- );
331
+ const model = ModelMix.new()
332
+ .gpt5mini()
333
+ .addText('Name and capital of 3 South American countries.');
278
334
 
279
- // Only add the example, skip the technical note
280
335
  const result = await model.json(
281
- { status: "success", data: [] },
282
- {},
283
- { 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 }
284
339
  );
340
+ console.log(result);
341
+ // { countries: [
342
+ // { name: "Brazil", capital: "BRASILIA" },
343
+ // { name: "Colombia", capital: "BOGOTA" },
344
+ // { name: "Chile", capital: "SANTIAGO" }
345
+ // ]}
346
+ ```
285
347
 
286
- // 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
287
360
  const result = await model.json(
288
- { message: "Hello \"world\"" },
289
- {},
290
- { 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 }
291
364
  );
292
365
  ```
293
366
 
@@ -311,16 +384,28 @@ Every response from `raw()` now includes a `tokens` object with the following st
311
384
  }
312
385
  ```
313
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
+
314
399
  ## 🐛 Enabling Debug Mode
315
400
 
316
401
  To activate debug mode in ModelMix and view detailed request information, follow these two steps:
317
402
 
318
- 1. In the ModelMix constructor, include `debug: true` in the configuration:
403
+ 1. In the ModelMix constructor, include a `debug` level in the configuration:
319
404
 
320
405
  ```javascript
321
406
  const mix = ModelMix.new({
322
407
  config: {
323
- debug: true
408
+ debug: 4 // 0=silent, 1=minimal, 2=summary, 3=full (no truncate), 4=verbose (raw details)
324
409
  // ... other configuration options ...
325
410
  }
326
411
  });
@@ -390,10 +475,14 @@ new ModelMix(args = { options: {}, config: {} })
390
475
  - `new()`: `static` Creates a new `ModelMix`.
391
476
  - `new()`: Creates a new `ModelMix` using instance setup.
392
477
 
478
+ - `setSystem(text)`: Sets the system prompt.
479
+ - `setSystemFromFile(filePath)`: Sets the system prompt from a file.
393
480
  - `addText(text, config = { role: "user" })`: Adds a text message.
394
- - `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.
395
482
  - `addImage(filePath, config = { role: "user" })`: Adds an image message from a file path.
396
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.
397
486
  - `message()`: Sends the message and returns the response.
398
487
  - `raw()`: Sends the message and returns the complete response data including:
399
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: 3,
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.
@@ -34,7 +110,7 @@ class ModelMix {
34
110
  this.config = {
35
111
  system: 'You are an assistant.',
36
112
  max_history: 1, // Default max history
37
- debug: 0, // 0=silent, 1=minimal, 2=readable summary, 3=full details
113
+ debug: 0, // 0=silent, 1=minimal, 2=readable summary, 3=full (no truncate), 4=verbose (raw details)
38
114
  bottleneck: defaultBottleneckConfig,
39
115
  roundRobin: false, // false=fallback mode, true=round robin rotation
40
116
  ...config
@@ -82,12 +158,19 @@ 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
 
90
- static formatInputSummary(messages, system) {
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
+
173
+ static formatInputSummary(messages, system, debug = 2) {
91
174
  const lastMessage = messages[messages.length - 1];
92
175
  let inputText = '';
93
176
 
@@ -98,38 +181,39 @@ 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 noTruncate = debug >= 3;
185
+ const systemStr = noTruncate ? (system || '') : ModelMix.truncate(system, 500);
186
+ const inputStr = noTruncate ? inputText : ModelMix.truncate(inputText, 1200);
103
187
  const msgCount = `(${messages.length} msg${messages.length !== 1 ? 's' : ''})`;
104
188
 
105
- return `${systemStr} \n| ${inputStr} ${msgCount}`;
189
+ return `| SYSTEM\n${systemStr}\n| INPUT ${msgCount}\n${inputStr}`;
106
190
  }
107
191
 
108
192
  static formatOutputSummary(result, debug) {
109
193
  const parts = [];
194
+ const noTruncate = debug >= 3;
110
195
  if (result.message) {
111
196
  // Try to parse as JSON for better formatting
112
197
  try {
113
198
  const parsed = JSON.parse(result.message.trim());
114
199
  // If it's valid JSON and debug >= 2, show it formatted
115
200
  if (debug >= 2) {
116
- parts.push(`Output (JSON):\n${ModelMix.formatJSON(parsed)}`);
201
+ parts.push(`| OUTPUT (JSON)\n${ModelMix.formatJSON(parsed)}`);
117
202
  } else {
118
- parts.push(`Output: ${ModelMix.truncate(result.message, 150)}`);
203
+ parts.push(`| OUTPUT\n${ModelMix.truncate(result.message, 1500)}`);
119
204
  }
120
205
  } catch (e) {
121
- // Not JSON, show truncated as before
122
- parts.push(`Output: ${ModelMix.truncate(result.message, 150)}`);
206
+ parts.push(`| OUTPUT\n${noTruncate ? result.message : ModelMix.truncate(result.message, 1500)}`);
123
207
  }
124
208
  }
125
209
  if (result.think) {
126
- parts.push(`Think: ${ModelMix.truncate(result.think, 80)}`);
210
+ parts.push(`| THINK\n${noTruncate ? result.think : ModelMix.truncate(result.think, 800)}`);
127
211
  }
128
212
  if (result.toolCalls && result.toolCalls.length > 0) {
129
213
  const toolNames = result.toolCalls.map(t => t.function?.name || t.name).join(', ');
130
- parts.push(`Tools: ${toolNames}`);
214
+ parts.push(`| TOOLS\n${toolNames}`);
131
215
  }
132
- return parts.join(' | ');
216
+ return parts.join('\n');
133
217
  }
134
218
 
135
219
  attach(key, provider) {
@@ -759,7 +843,7 @@ class ModelMix {
759
843
  const header = `\n${prefix} [${providerName}:${currentModelKey}] #${originalIndex + 1}${suffix}`;
760
844
 
761
845
  if (currentConfig.debug >= 2) {
762
- console.log(`${header} | ${ModelMix.formatInputSummary(this.messages, currentConfig.system)}`);
846
+ console.log(`${header}\n${ModelMix.formatInputSummary(this.messages, currentConfig.system, currentConfig.debug)}`);
763
847
  } else {
764
848
  console.log(header);
765
849
  }
@@ -772,6 +856,11 @@ class ModelMix {
772
856
 
773
857
  const result = await providerInstance.create({ options: currentOptions, config: currentConfig });
774
858
 
859
+ // Calculate cost based on model pricing
860
+ if (result.tokens) {
861
+ result.tokens.cost = ModelMix.calculateCost(currentModelKey, result.tokens);
862
+ }
863
+
775
864
  if (result.toolCalls && result.toolCalls.length > 0) {
776
865
 
777
866
  if (result.message) {
@@ -809,11 +898,14 @@ class ModelMix {
809
898
 
810
899
  // debug level 2: Readable summary of output
811
900
  if (currentConfig.debug >= 2) {
812
- console.log(`✓ ${ModelMix.formatOutputSummary(result, currentConfig.debug).trim()}`);
901
+ const tokenInfo = result.tokens
902
+ ? ` ${result.tokens.input}→${result.tokens.output} tok` + (result.tokens.cost != null ? ` $${result.tokens.cost.toFixed(4)}` : '')
903
+ : '';
904
+ console.log(`✓${tokenInfo}\n${ModelMix.formatOutputSummary(result, currentConfig.debug).trim()}`);
813
905
  }
814
906
 
815
- // debug level 3 (debug): Full response details
816
- if (currentConfig.debug >= 3) {
907
+ // debug level 4 (verbose): Full response details
908
+ if (currentConfig.debug >= 4) {
817
909
  if (result.response) {
818
910
  console.log('\n[RAW RESPONSE]');
819
911
  console.log(ModelMix.formatJSON(result.response));
@@ -832,6 +924,7 @@ class ModelMix {
832
924
 
833
925
  if (currentConfig.debug >= 1) console.log('');
834
926
 
927
+ this.lastRaw = result;
835
928
  return result;
836
929
 
837
930
  } catch (error) {
@@ -1059,8 +1152,8 @@ class MixCustom {
1059
1152
 
1060
1153
  options.messages = this.convertMessages(options.messages, config);
1061
1154
 
1062
- // debug level 3 (debug): Full request details
1063
- if (config.debug >= 3) {
1155
+ // debug level 4 (verbose): Full request details
1156
+ if (config.debug >= 4) {
1064
1157
  console.log('\n[REQUEST DETAILS]');
1065
1158
 
1066
1159
  console.log('\n[CONFIG]');
@@ -1943,8 +2036,8 @@ class MixGoogle extends MixCustom {
1943
2036
  };
1944
2037
 
1945
2038
  try {
1946
- // debug level 3 (debug): Full request details
1947
- if (config.debug >= 3) {
2039
+ // debug level 4 (verbose): Full request details
2040
+ if (config.debug >= 4) {
1948
2041
  console.log('\n[REQUEST DETAILS - GOOGLE]');
1949
2042
 
1950
2043
  console.log('\n[CONFIG]');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modelmix",
3
- "version": "4.3.4",
3
+ "version": "4.4.0",
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,320 @@
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
+ ## Common Tasks
24
+
25
+ - [Get a text response](#get-a-text-response)
26
+ - [Get structured JSON](#get-structured-json)
27
+ - [Stream a response](#stream-a-response)
28
+ - [Get raw response (tokens, thinking, tool calls)](#get-raw-response-tokens-thinking-tool-calls)
29
+ - [Access full response after `message()` or `json()` with `lastRaw`](#access-full-response-after-message-or-json-with-lastraw)
30
+ - [Add images](#add-images)
31
+ - [Use templates with placeholders](#use-templates-with-placeholders)
32
+ - [Round-robin load balancing](#round-robin-load-balancing)
33
+ - [MCP integration (external tools)](#mcp-integration-external-tools)
34
+ - [Custom local tools (addTool)](#custom-local-tools-addtool)
35
+ - [Rate limiting (Bottleneck)](#rate-limiting-bottleneck)
36
+ - [Debug mode](#debug-mode)
37
+ - [Use free-tier models](#use-free-tier-models)
38
+ - [Conversation history](#conversation-history)
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ npm install modelmix
44
+ ```
45
+
46
+ ## Core Concepts
47
+
48
+ ### Import
49
+
50
+ ```javascript
51
+ import { ModelMix } from 'modelmix';
52
+ ```
53
+
54
+ ### Creating an Instance
55
+
56
+ ```javascript
57
+ // Static factory (preferred)
58
+ const model = ModelMix.new();
59
+
60
+ // With global options
61
+ const model = ModelMix.new({
62
+ options: { max_tokens: 4096, temperature: 0.7 },
63
+ config: {
64
+ system: "You are a helpful assistant.",
65
+ max_history: 5,
66
+ debug: 0, // 0=silent, 1=minimal, 2=summary, 3=full (no truncate), 4=verbose
67
+ roundRobin: false // false=fallback, true=rotate models
68
+ }
69
+ });
70
+ ```
71
+
72
+ ### Attaching Models (Fluent Chain)
73
+
74
+ Chain shorthand methods to attach providers. First model is primary; others are fallbacks:
75
+
76
+ ```javascript
77
+ const model = ModelMix.new()
78
+ .sonnet45() // primary
79
+ .gpt5mini() // fallback 1
80
+ .gemini3flash() // fallback 2
81
+ .addText("Hello!")
82
+ ```
83
+
84
+ If `sonnet45` fails, it automatically tries `gpt5mini`, then `gemini3flash`.
85
+
86
+ ## Available Model Shorthands
87
+
88
+ - **OpenAI**: `gpt52` `gpt51` `gpt5` `gpt5mini` `gpt5nano` `gpt41` `gpt41mini` `gpt41nano`
89
+ - **Anthropic**: `opus46` `opus45` `sonnet45` `sonnet4` `haiku45` `haiku35` (thinking variants: add `think` suffix)
90
+ - **Google**: `gemini3pro` `gemini3flash` `gemini25pro` `gemini25flash`
91
+ - **Grok**: `grok4` `grok41` (thinking variant available)
92
+ - **Perplexity**: `sonar` `sonarPro`
93
+ - **Groq**: `scout` `maverick`
94
+ - **Together**: `qwen3` `kimiK2`
95
+ - **Multi-provider**: `deepseekR1` `gptOss`
96
+ - **MiniMax**: `minimaxM21`
97
+ - **Fireworks**: `deepseekV32` `GLM47`
98
+
99
+ Each method is called as `mix.methodName()` and accepts optional `{ options, config }` to override per-model settings.
100
+
101
+ ## Common Tasks
102
+
103
+ ### Get a text response
104
+
105
+ ```javascript
106
+ const answer = await ModelMix.new()
107
+ .gpt5mini()
108
+ .addText("What is the capital of France?")
109
+ .message();
110
+ ```
111
+
112
+ ### Get structured JSON
113
+
114
+ ```javascript
115
+ const result = await ModelMix.new()
116
+ .gpt5mini()
117
+ .addText("Name and capital of 3 South American countries.")
118
+ .json(
119
+ { countries: [{ name: "", capital: "" }] }, // schema example
120
+ { countries: [{ name: "country name", capital: "in uppercase" }] }, // descriptions
121
+ { addNote: true } // options
122
+ );
123
+ // result.countries → [{ name: "Brazil", capital: "BRASILIA" }, ...]
124
+ ```
125
+
126
+ `json()` signature: `json(schemaExample, schemaDescription?, { addSchema, addExample, addNote }?)`
127
+
128
+ ### Stream a response
129
+
130
+ ```javascript
131
+ await ModelMix.new()
132
+ .gpt5mini()
133
+ .addText("Tell me a story.")
134
+ .stream(({ delta, message }) => {
135
+ process.stdout.write(delta);
136
+ });
137
+ ```
138
+
139
+ ### Get raw response (tokens, thinking, tool calls)
140
+
141
+ ```javascript
142
+ const raw = await ModelMix.new()
143
+ .sonnet45think()
144
+ .addText("Solve this step by step: 2+2*3")
145
+ .raw();
146
+ // raw.message, raw.think, raw.tokens, raw.toolCalls, raw.response
147
+ ```
148
+
149
+ ### Access full response after `message()` or `json()` with `lastRaw`
150
+
151
+ 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()`.
152
+
153
+ ```javascript
154
+ const model = ModelMix.new().gpt5mini().addText("Hello!");
155
+ const text = await model.message();
156
+ console.log(model.lastRaw.tokens);
157
+ // { input: 122, output: 86, total: 541, cost: 0.000319 }
158
+ console.log(model.lastRaw.think); // reasoning content (if available)
159
+ console.log(model.lastRaw.response); // raw API response
160
+ ```
161
+
162
+ ### Add images
163
+
164
+ ```javascript
165
+ const model = ModelMix.new().sonnet45();
166
+ model.addImage('./photo.jpg'); // from file
167
+ model.addImageFromUrl('https://example.com/img.png'); // from URL
168
+ model.addText('Describe this image.');
169
+ const description = await model.message();
170
+ ```
171
+
172
+ ### Use templates with placeholders
173
+
174
+ ```javascript
175
+ const model = ModelMix.new().gpt5mini();
176
+ model.setSystemFromFile('./prompts/system.md');
177
+ model.addTextFromFile('./prompts/task.md');
178
+ model.replace({
179
+ '{role}': 'data analyst',
180
+ '{language}': 'Spanish'
181
+ });
182
+ model.replaceKeyFromFile('{code}', './src/utils.js');
183
+ console.log(await model.message());
184
+ ```
185
+
186
+ ### Round-robin load balancing
187
+
188
+ ```javascript
189
+ const pool = ModelMix.new({ config: { roundRobin: true } })
190
+ .gpt5mini()
191
+ .sonnet45()
192
+ .gemini3flash();
193
+
194
+ // Each call rotates to the next model
195
+ const r1 = await pool.new().addText("Request 1").message();
196
+ const r2 = await pool.new().addText("Request 2").message();
197
+ ```
198
+
199
+ ### MCP integration (external tools)
200
+
201
+ ```javascript
202
+ const model = ModelMix.new({ config: { max_history: 10 } }).gpt5nano();
203
+ model.setSystem('You are an assistant. Today is ' + new Date().toISOString());
204
+ await model.addMCP('@modelcontextprotocol/server-brave-search');
205
+ model.addText('Use Internet: What is the latest news about AI?');
206
+ console.log(await model.message());
207
+ ```
208
+
209
+ Requires `BRAVE_API_KEY` in `.env` for Brave Search MCP.
210
+
211
+ ### Custom local tools (addTool)
212
+
213
+ ```javascript
214
+ const model = ModelMix.new({ config: { max_history: 10 } }).gpt5mini();
215
+
216
+ model.addTool({
217
+ name: "get_weather",
218
+ description: "Get weather for a city",
219
+ inputSchema: {
220
+ type: "object",
221
+ properties: { city: { type: "string" } },
222
+ required: ["city"]
223
+ }
224
+ }, async ({ city }) => {
225
+ return `The weather in ${city} is sunny, 25C`;
226
+ });
227
+
228
+ model.addText("What's the weather in Tokyo?");
229
+ console.log(await model.message());
230
+ ```
231
+
232
+ ### Rate limiting (Bottleneck)
233
+
234
+ ```javascript
235
+ const model = ModelMix.new({
236
+ config: {
237
+ bottleneck: {
238
+ maxConcurrent: 4,
239
+ minTime: 1000
240
+ }
241
+ }
242
+ }).gpt5mini();
243
+ ```
244
+
245
+ ### Debug mode
246
+
247
+ ```javascript
248
+ const model = ModelMix.new({
249
+ config: { debug: 2 } // 0=silent, 1=minimal, 2=summary, 3=full (no truncate), 4=verbose
250
+ }).gpt5mini();
251
+ ```
252
+
253
+ For full debug output, also set the env: `DEBUG=ModelMix* node script.js`
254
+
255
+ ### Use free-tier models
256
+
257
+ ```javascript
258
+ // These use providers with free quotas (OpenRouter, Groq, Cerebras)
259
+ const model = ModelMix.new()
260
+ .gptOss()
261
+ .kimiK2()
262
+ .deepseekR1()
263
+ .hermes3()
264
+ .addText("What is the capital of France?");
265
+ console.log(await model.message());
266
+ ```
267
+
268
+ ### Conversation history
269
+
270
+ ```javascript
271
+ const chat = ModelMix.new({ config: { max_history: 10 } }).gpt5mini();
272
+ chat.addText("My name is Martin.");
273
+ await chat.message();
274
+ chat.addText("What's my name?");
275
+ const reply = await chat.message(); // "Martin"
276
+ ```
277
+
278
+ ## Agent Usage Rules
279
+
280
+ - Always check `package.json` for `modelmix` before running `npm install`.
281
+ - Use `ModelMix.new()` static factory to create instances (not `new ModelMix()`).
282
+ - Store API keys in `.env` and load with `dotenv/config` or `process.loadEnvFile()`. Never hardcode keys.
283
+ - Chain models for resilience: primary model first, fallbacks after.
284
+ - When using MCP tools or `addTool()`, set `max_history` to at least 3.
285
+ - Use `.json()` for structured output instead of parsing text manually.
286
+ - Use `.message()` for simple text, `.raw()` when you need tokens/thinking/toolCalls.
287
+ - For thinking models, append `think` to the method name (e.g. `sonnet45think()`).
288
+ - Template placeholders use `{key}` syntax in both system prompts and user messages.
289
+ - The library uses CommonJS internally (`require`) but supports ESM import via `{ ModelMix }`.
290
+ - Available provider Mix classes for custom setups: `MixOpenAI`, `MixAnthropic`, `MixGoogle`, `MixPerplexity`, `MixGroq`, `MixTogether`, `MixGrok`, `MixOpenRouter`, `MixOllama`, `MixLMStudio`, `MixCustom`, `MixCerebras`, `MixFireworks`, `MixMiniMax`.
291
+
292
+ ## API Quick Reference
293
+
294
+ | Method | Returns | Description |
295
+ | --- | --- | --- |
296
+ | `.addText(text)` | `this` | Add user message |
297
+ | `.addTextFromFile(path)` | `this` | Add user message from file |
298
+ | `.setSystem(text)` | `this` | Set system prompt |
299
+ | `.setSystemFromFile(path)` | `this` | Set system prompt from file |
300
+ | `.addImage(path)` | `this` | Add image from file |
301
+ | `.addImageFromUrl(url)` | `this` | Add image from URL or data URI |
302
+ | `.replace({})` | `this` | Set placeholder replacements |
303
+ | `.replaceKeyFromFile(key, path)` | `this` | Replace placeholder with file content |
304
+ | `.message()` | `Promise<string>` | Get text response |
305
+ | `.json(example, desc?, opts?)` | `Promise<object>` | Get structured JSON |
306
+ | `.raw()` | `Promise<{message, think, toolCalls, tokens, response}>` | Full response |
307
+ | `.lastRaw` | `object \| null` | Full response from last `message()`/`json()`/`block()`/`stream()` call |
308
+ | `.stream(callback)` | `Promise` | Stream response |
309
+ | `.block()` | `Promise<string>` | Extract code block from response |
310
+ | `.addMCP(package)` | `Promise` | Add MCP server tools |
311
+ | `.addTool(def, callback)` | `this` | Register custom local tool |
312
+ | `.addTools([{tool, callback}])` | `this` | Register multiple tools |
313
+ | `.removeTool(name)` | `this` | Remove a tool |
314
+ | `.listTools()` | `{local, mcp}` | List registered tools |
315
+ | `.new()` | `ModelMix` | Clone instance sharing models |
316
+ | `.attach(key, provider)` | `this` | Attach custom provider |
317
+
318
+ ## References
319
+
320
+ - [GitHub Repository](https://github.com/clasen/ModelMix)