modelmix 4.3.4 → 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
 
@@ -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 `debug: 3` in the configuration:
319
404
 
320
405
  ```javascript
321
406
  const mix = ModelMix.new({
322
407
  config: {
323
- debug: true
408
+ debug: 3
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: 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(', ');
@@ -772,6 +855,11 @@ class ModelMix {
772
855
 
773
856
  const result = await providerInstance.create({ options: currentOptions, config: currentConfig });
774
857
 
858
+ // Calculate cost based on model pricing
859
+ if (result.tokens) {
860
+ result.tokens.cost = ModelMix.calculateCost(currentModelKey, result.tokens);
861
+ }
862
+
775
863
  if (result.toolCalls && result.toolCalls.length > 0) {
776
864
 
777
865
  if (result.message) {
@@ -832,6 +920,7 @@ class ModelMix {
832
920
 
833
921
  if (currentConfig.debug >= 1) console.log('');
834
922
 
923
+ this.lastRaw = result;
835
924
  return result;
836
925
 
837
926
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modelmix",
3
- "version": "4.3.4",
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)