modelmix 4.7.4 → 5.0.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
@@ -163,54 +163,54 @@ Here's a comprehensive list of available methods:
163
163
 
164
164
  | Method | Provider | Model | Price (I/O) per 1 M tokens |
165
165
  | ------------------- | ---------- | ---------------------------- | -------------------------- |
166
- | `gpt56sol()` | OpenAI | gpt-5.6-sol | [\$5.00 / \$30.00][1] |
167
- | `gpt56terra()` | OpenAI | gpt-5.6-terra | [\$2.00 / \$12.00][1] |
168
- | `gpt56luna()` | OpenAI | gpt-5.6-luna | [\$0.20 / \$1.20][1] |
169
- | `gpt55()` | OpenAI | gpt-5.5 | [\$5.00 / \$30.00][1] |
170
- | `gpt54()` | OpenAI | gpt-5.4 | [\$2.50 / \$15.00][1] |
171
- | `gpt54mini()` | OpenAI | gpt-5.4-mini | [\$0.75 / \$4.50][1] |
172
- | `gpt54nano()` | OpenAI | gpt-5.4-nano | [\$0.20 / \$1.25][1] |
173
- | `gpt53codex()` | OpenAI | gpt-5.3-codex | [\$1.25 / \$14.00][1] |
174
- | `gpt52()` | OpenAI | gpt-5.2 | [\$1.75 / \$14.00][1] |
175
- | `gpt51()` | OpenAI | gpt-5.1 | [\$1.25 / \$10.00][1] |
176
- | `gpt5mini()` | OpenAI | gpt-5-mini | [\$0.25 / \$2.00][1] |
177
- | `gpt5nano()` | OpenAI | gpt-5-nano | [\$0.05 / \$0.40][1] |
178
- | `gpt41()` | OpenAI | gpt-4.1 | [\$2.00 / \$8.00][1] |
179
- | `gpt41mini()` | OpenAI | gpt-4.1-mini | [\$0.40 / \$1.60][1] |
180
- | `gpt41nano()` | OpenAI | gpt-4.1-nano | [\$0.10 / \$0.40][1] |
181
- | `gptOss()` | Together | gpt-oss-120B | [\$0.15 / \$0.60][7] |
182
- | `fable5()` | Anthropic | claude-fable-5 | [\$10.00 / \$50.00][2] |
183
- | `opus5()` | Anthropic | claude-opus-5 | [\$5.00 / \$25.00][2] |
184
- | `opus48()` | Anthropic | claude-opus-4-8 | [\$5.00 / \$25.00][2] |
185
- | `opus47()` | Anthropic | claude-opus-4-7 | [\$5.00 / \$25.00][2] |
186
- | `opus46()` | Anthropic | claude-opus-4-6 | [\$5.00 / \$25.00][2] |
187
- | `sonnet5()` | Anthropic | claude-sonnet-5 | [\$3.00 / \$15.00][2] |
188
- | `sonnet46()` | Anthropic | claude-sonnet-4-6 | [\$3.00 / \$15.00][2] |
189
- | `haiku45()` | Anthropic | claude-haiku-4-5-20251001 | [\$1.00 / \$5.00][2] |
190
- | `gemini31pro()` | Google | gemini-3.1-pro-preview | [\$2.00 / \$12.00][3] |
191
- | `gemini36flash()` | Google | gemini-3.6-flash | [\$1.50 / \$7.50][3] |
192
- | `gemini35flash()` | Google | gemini-3.5-flash | [\$0.75 / \$4.50][3] |
193
- | `gemini35flashLite()`| Google | gemini-3.5-flash-lite | [\$0.30 / \$2.50][3] |
194
- | `gemini31flashLite()`| Google | gemini-3.1-flash-lite-preview | [\$0.25 / \$1.50][3] |
195
- | `grok45()` | Grok | grok-4.5 | [\$2.00 / \$6.00][6] |
196
- | `grok43()` | Grok | grok-4.3 | [\$1.25 / \$2.50][6] |
197
- | `grok420multiAgent()`| Grok | grok-4.20-multi-agent-0309 | [\$1.25 / \$2.50][6] |
198
- | `grok420()` | Grok | grok-4.20-0309 (†) | [\$1.25 / \$2.50][6] |
199
- | `qwen36plus()` | Fireworks/Together | qwen3p6-plus / Qwen3.6-Plus | [\$0.50 / \$3.00][10] |
200
- | `qwen37plus()` | Fireworks | models/qwen3p7-plus | [\$0.40 / \$1.60][10] |
201
- | `qwen38max()` | OpenRouter | qwen/qwen3.8-max | [\$2.00 / \$6.00][12] |
202
- | `deepseekV4Flash()` | Fireworks | models/deepseek-v4-flash | [\$0.14 / \$0.28][10] |
203
- | `deepseekV4Pro()` | Fireworks | models/deepseek-v4-pro | [\$1.74 / \$3.48][10] |
204
- | `GLM52()` | Together | zai-org/GLM-5.2 | [\$1.40 / \$4.40][7] |
205
- | `GLM51()` | Fireworks | models/glm-5p1 | [\$1.05 / \$3.50][10] |
206
- | `minimaxM3()` | MiniMax | MiniMax-M3 | [\$0.30 / \$1.20][9] |
207
- | `minimaxM27()` | MiniMax | MiniMax-M2.7 | [\$0.30 / \$1.20][9] |
208
- | `sonar()` | Perplexity | sonar | [\$1.00 / \$1.00][4] |
209
- | `sonarPro()` | Perplexity | sonar-pro | [\$3.00 / \$15.00][4] |
210
- | `hermes3()` | Lambda | Hermes-3-Llama-3.1-405B-FP8 | [\$0.80 / \$0.80][8] |
211
- | `kimiK3()` | Moonshot | kimi-k3 | [\$3.00 / \$15.00][11] |
212
- | `kimiK25()` | Together | Kimi-K2.5 | [\$0.50 / \$2.80][7] |
213
- | `kimiK26()` | Fireworks | models/kimi-k2p6 | [\$0.95 / \$4.00][10] |
166
+ | `gpt56sol()` | OpenAI | gpt-5.6-sol | [\$5.00/\$30.00][1] |
167
+ | `gpt56terra()` | OpenAI | gpt-5.6-terra | [\$2.00/\$12.00][1] |
168
+ | `gpt56luna()` | OpenAI | gpt-5.6-luna | [\$0.20/\$1.20][1] |
169
+ | `gpt55()` | OpenAI | gpt-5.5 | [\$5.00/\$30.00][1] |
170
+ | `gpt54()` | OpenAI | gpt-5.4 | [\$2.50/\$15.00][1] |
171
+ | `gpt54mini()` | OpenAI | gpt-5.4-mini | [\$0.75/\$4.50][1] |
172
+ | `gpt54nano()` | OpenAI | gpt-5.4-nano | [\$0.20/\$1.25][1] |
173
+ | `gpt53codex()` | OpenAI | gpt-5.3-codex | [\$1.25/\$14.00][1] |
174
+ | `gpt52()` | OpenAI | gpt-5.2 | [\$1.75/\$14.00][1] |
175
+ | `gpt51()` | OpenAI | gpt-5.1 | [\$1.25/\$10.00][1] |
176
+ | `gpt5mini()` | OpenAI | gpt-5-mini | [\$0.25/\$2.00][1] |
177
+ | `gpt5nano()` | OpenAI | gpt-5-nano | [\$0.05/\$0.40][1] |
178
+ | `gpt41()` | OpenAI | gpt-4.1 | [\$2.00/\$8.00][1] |
179
+ | `gpt41mini()` | OpenAI | gpt-4.1-mini | [\$0.40/\$1.60][1] |
180
+ | `gpt41nano()` | OpenAI | gpt-4.1-nano | [\$0.10/\$0.40][1] |
181
+ | `gptOss()` | Together | gpt-oss-120B | [\$0.15/\$0.60][7] |
182
+ | `fable5()` | Anthropic | claude-fable-5 | [\$10.00/\$50.00][2] |
183
+ | `opus5()` | Anthropic | claude-opus-5 | [\$5.00/\$25.00][2] |
184
+ | `opus48()` | Anthropic | claude-opus-4-8 | [\$5.00/\$25.00][2] |
185
+ | `opus47()` | Anthropic | claude-opus-4-7 | [\$5.00/\$25.00][2] |
186
+ | `opus46()` | Anthropic | claude-opus-4-6 | [\$5.00/\$25.00][2] |
187
+ | `sonnet5()` | Anthropic | claude-sonnet-5 | [\$3.00/\$15.00][2] |
188
+ | `sonnet46()` | Anthropic | claude-sonnet-4-6 | [\$3.00/\$15.00][2] |
189
+ | `haiku45()` | Anthropic | claude-haiku-4-5-20251001 | [\$1.00/\$5.00][2] |
190
+ | `gemini31pro()` | Google | gemini-3.1-pro-preview | [\$2.00/\$12.00][3] |
191
+ | `gemini36flash()` | Google | gemini-3.6-flash | [\$1.50/\$7.50][3] |
192
+ | `gemini35flash()` | Google | gemini-3.5-flash | [\$0.75/\$4.50][3] |
193
+ | `gemini35flashLite()`| Google | gemini-3.5-flash-lite | [\$0.30/\$2.50][3] |
194
+ | `gemini31flashLite()`| Google | gemini-3.1-flash-lite-preview | [\$0.25/\$1.50][3] |
195
+ | `grok45()` | Grok | grok-4.5 | [\$2.00/\$6.00][6] |
196
+ | `grok43()` | Grok | grok-4.3 | [\$1.25/\$2.50][6] |
197
+ | `grok420multiAgent()`| Grok | grok-4.20-multi-agent-0309 | [\$1.25/\$2.50][6] |
198
+ | `grok420()` | Grok | grok-4.20-0309 (†) | [\$1.25/\$2.50][6] |
199
+ | `qwen36plus()` | Fireworks | qwen3p6-plus | [\$0.50/\$3.00][10] |
200
+ | `qwen37plus()` | Fireworks | models/qwen3p7-plus | [\$0.40/\$1.60][10] |
201
+ | `qwen38max()` | OpenRouter | qwen/qwen3.8-max | [\$2.00/\$6.00][12] |
202
+ | `deepseekV4Flash()` | Fireworks | models/deepseek-v4-flash | [\$0.14/\$0.28][10] |
203
+ | `deepseekV4Pro()` | Fireworks | models/deepseek-v4-pro | [\$1.74/\$3.48][10] |
204
+ | `GLM52()` | Together | zai-org/GLM-5.2 | [\$1.40/\$4.40][7] |
205
+ | `GLM51()` | Fireworks | models/glm-5p1 | [\$1.05/\$3.50][10] |
206
+ | `minimaxM3()` | MiniMax | MiniMax-M3 | [\$0.30/\$1.20][9] |
207
+ | `minimaxM27()` | MiniMax | MiniMax-M2.7 | [\$0.30/\$1.20][9] |
208
+ | `sonar()` | Perplexity | sonar | [\$1.00/\$1.00][4] |
209
+ | `sonarPro()` | Perplexity | sonar-pro | [\$3.00/\$15.00][4] |
210
+ | `hermes3()` | Lambda | Hermes-3-Llama-3.1-405B-FP8 | [\$0.80/\$0.80][8] |
211
+ | `kimiK3()` | Moonshot | kimi-k3 | [\$3.00/\$15.00][11] |
212
+ | `kimiK25()` | Together | Kimi-K2.5 | [\$0.50/\$2.80][7] |
213
+ | `kimiK26()` | Fireworks | models/kimi-k2p6 | [\$0.95/\$4.00][10] |
214
214
 
215
215
  [1]: https://platform.openai.com/docs/pricing "Pricing | OpenAI"
216
216
  [2]: https://docs.anthropic.com/en/docs/about-claude/pricing "Pricing - Anthropic"
@@ -226,11 +226,6 @@ Here's a comprehensive list of available methods:
226
226
  [12]: https://openrouter.ai/qwen/qwen3.8-max "OpenRouter Pricing"
227
227
 
228
228
  Each method accepts optional `options`, `config`, and (for multi-provider methods) `mix` parameters to customize behavior.
229
- † `grok420()` resolves to `grok-4.20-0309-non-reasoning` by default, or `grok-4.20-0309-reasoning` when `.effort(20+)` / `-1` (or native non-`none` `reasoning_effort`) is set.
230
- For NVIDIA on DeepSeek V4 Flash/Pro, use `deepseekV4Flash({ mix: { nvidia: true } })` or `deepseekV4Pro({ mix: { nvidia: true } })`.
231
- For Together on Qwen 3.6 Plus, use `qwen36plus({ mix: { fireworks: false, together: true } })`.
232
- For OpenRouter on Qwen 3.7 Plus, use `qwen37plus({ mix: { fireworks: false, openrouter: true } })`.
233
- For OpenRouter instead of Moonshot's native API, use `kimiK3({ mix: { moonshot: false, openrouter: true } })`.
234
229
 
235
230
  ```javascript
236
231
  const result = await ModelMix.new({
@@ -244,7 +239,9 @@ const result = await ModelMix.new({
244
239
 
245
240
  ## 🔄 Templates
246
241
 
247
- 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.
242
+ ModelMix renders system prompts and user messages with [EJS](https://ejs.co/). Templates can be inline or stored in external files, and support variables, conditionals, loops, and relative includes.
243
+
244
+ Templates are executable JavaScript and must be controlled by the developer. Pass untrusted content only as template data, never as the template source.
248
245
 
249
246
  ### Core methods
250
247
 
@@ -252,16 +249,16 @@ ModelMix includes a simple but powerful templating system. You can write your sy
252
249
  | --- | --- |
253
250
  | `setSystemFromFile(path)` | Load the system prompt from a file |
254
251
  | `addTextFromFile(path)` | Load a user message from a file |
255
- | `replace({ key: value })` | Replace placeholders in all messages and the system prompt |
256
- | `replaceKeyFromFile(key, path)` | Replace a placeholder with the contents of a file |
252
+ | `replace({ key: value })` | Add EJS template data |
253
+ | `replaceKeyFromFile(key, path)` | Add a file's raw contents as template data |
257
254
 
258
255
  ### Basic example with `replace`
259
256
 
260
257
  ```javascript
261
258
  const gpt = ModelMix.new().gpt52();
262
259
 
263
- gpt.addText('Write a short story about a {animal} that lives in {place}.');
264
- gpt.replace({ '{animal}': 'cat', '{place}': 'a haunted castle' });
260
+ gpt.addText('Write a short story about a <%- animal %> that lives in <%- place %>.');
261
+ gpt.replace({ animal: 'cat', place: 'a haunted castle' });
265
262
 
266
263
  console.log(await gpt.message());
267
264
  ```
@@ -272,15 +269,15 @@ Instead of writing long prompts inline, keep them in separate Markdown files. Th
272
269
 
273
270
  **`prompts/system.md`**
274
271
  ```markdown
275
- You are {role}, an expert in {topic}.
276
- Always respond in {language}.
272
+ You are <%- role %>, an expert in <%- topic %>.
273
+ Always respond in <%- language %>.
277
274
  ```
278
275
 
279
276
  **`prompts/task.md`**
280
277
  ```markdown
281
278
  Analyze the following and provide 3 key insights:
282
279
 
283
- {content}
280
+ <%- content %>
284
281
  ```
285
282
 
286
283
  **`app.js`**
@@ -291,16 +288,16 @@ gpt.setSystemFromFile('./prompts/system.md');
291
288
  gpt.addTextFromFile('./prompts/task.md');
292
289
 
293
290
  gpt.replace({
294
- '{role}': 'a senior analyst',
295
- '{topic}': 'market trends',
296
- '{language}': 'Spanish',
297
- '{content}': 'Bitcoin surpassed $100,000 in December 2024...'
291
+ role: 'a senior analyst',
292
+ topic: 'market trends',
293
+ language: 'Spanish',
294
+ content: 'Bitcoin surpassed $100,000 in December 2024...'
298
295
  });
299
296
 
300
297
  console.log(await gpt.message());
301
298
  ```
302
299
 
303
- ### Injecting file contents into a placeholder
300
+ ### Injecting file contents as template data
304
301
 
305
302
  Use `replaceKeyFromFile` when the replacement value itself is a large text stored in a file.
306
303
 
@@ -308,7 +305,7 @@ Use `replaceKeyFromFile` when the replacement value itself is a large text store
308
305
  ```markdown
309
306
  Summarize the following article in 3 bullet points:
310
307
 
311
- {article}
308
+ <%- article %>
312
309
  ```
313
310
 
314
311
  **`app.js`**
@@ -316,7 +313,7 @@ Summarize the following article in 3 bullet points:
316
313
  const gpt = ModelMix.new().gpt5mini();
317
314
 
318
315
  gpt.addTextFromFile('./prompts/summarize.md');
319
- gpt.replaceKeyFromFile('{article}', './data/article.md');
316
+ gpt.replaceKeyFromFile('article', './data/article.md');
320
317
 
321
318
  console.log(await gpt.message());
322
319
  ```
@@ -327,17 +324,17 @@ Combine all methods to build reusable, file-based prompt pipelines:
327
324
 
328
325
  **`prompts/system.md`**
329
326
  ```markdown
330
- You are {role}. Follow these rules:
327
+ You are <%- role %>. Follow these rules:
331
328
  - Be concise
332
329
  - Use examples when possible
333
- - Respond in {language}
330
+ - Respond in <%- language %>
334
331
  ```
335
332
 
336
333
  **`prompts/review.md`**
337
334
  ```markdown
338
335
  Review the following code and suggest improvements:
339
336
 
340
- {code}
337
+ <%- code %>
341
338
  ```
342
339
 
343
340
  **`app.js`**
@@ -347,12 +344,63 @@ const gpt = ModelMix.new().gpt5mini();
347
344
  gpt.setSystemFromFile('./prompts/system.md');
348
345
  gpt.addTextFromFile('./prompts/review.md');
349
346
 
350
- gpt.replace({ '{role}': 'a senior code reviewer', '{language}': 'English' });
351
- gpt.replaceKeyFromFile('{code}', './src/utils.js');
347
+ gpt.replace({ role: 'a senior code reviewer', language: 'English' });
348
+ gpt.replaceKeyFromFile('code', './src/utils.js');
352
349
 
353
350
  console.log(await gpt.message());
354
351
  ```
355
352
 
353
+ ### EJS output and control flow
354
+
355
+ Use `<%- value %>` for raw prompt content and `<%= value %>` only when XML escaping is intentional. Missing variables and missing files throw immediately.
356
+
357
+ ```ejs
358
+ <% if (user.active) { %>
359
+ Review these roles:
360
+ <% user.roles.forEach(role => { %>
361
+ - <%- role %>
362
+ <% }) %>
363
+ <% } %>
364
+ ```
365
+
366
+ ### Random prompt choices
367
+
368
+ Use a `choice` block to include exactly one prompt variant. When no weights are present, every option has the same probability:
369
+
370
+ ```ejs
371
+ <% choice %>
372
+ <% option %>
373
+ Use emojis.
374
+ <% option %>
375
+ Use few emojis.
376
+ <% option %>
377
+ Do not use emojis.
378
+ <% /choice %>
379
+ ```
380
+
381
+ Add a positive weight after every `option` when the probabilities should differ:
382
+
383
+ ```ejs
384
+ <% choice %>
385
+ <% option 20 %>
386
+ Use emojis.
387
+ <% option 40 %>
388
+ Use few emojis.
389
+ <% option 40 %>
390
+ Do not use emojis.
391
+ <% /choice %>
392
+ ```
393
+
394
+ Weights are relative and do not need to total 100. A block must either give every option a weight or omit all weights. Directives must be on their own lines; choices can be nested and can also appear inside relative includes. Each new request makes a new selection, while retries, provider fallbacks, and tool continuations keep the original selection.
395
+
396
+ File templates can include files relative to their own path:
397
+
398
+ ```ejs
399
+ <%- include('shared/rules.md') %>
400
+ ```
401
+
402
+ Content supplied through `replace()` or `replaceKeyFromFile()` is rendered once as data. EJS tags inside that content are not executed recursively.
403
+
356
404
  ## 🧩 JSON Structured Output
357
405
 
358
406
  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.
@@ -541,12 +589,29 @@ Every response from `raw()` now includes a `tokens` object with the following st
541
589
  ```javascript
542
590
  {
543
591
  tokens: {
544
- input: 150, // Number of tokens in the prompt/input
545
- output: 75, // Number of tokens in the completion/output
546
- total: 225, // Total tokens used (input + output)
547
- cached: 100, // Cached input tokens reported by the provider (0 when absent)
548
- cost: 0.0012, // Estimated cost in USD (null if model not in pricing table)
549
- speed: 42 // Output tokens per second (int)
592
+ input: 1200, // Total input tokens, including cache reads and writes
593
+ output: 50, // Number of output tokens
594
+ total: 1250, // Total tokens used
595
+ cached: 1024, // Input tokens read from cache
596
+ cacheWrite: 0, // Input tokens written to cache
597
+ cacheWrite5m: 0, // Anthropic writes using the 5-minute TTL
598
+ cacheWrite1h: 0, // Anthropic writes using the 1-hour TTL
599
+ uncachedInput: 176, // max(0, input - cached - cacheWrite)
600
+ cacheHitRate: 0.8533, // cached / input, rounded to 4 decimals
601
+ cacheSavings: 0.00018432, // USD saved by cache reads
602
+ cacheWritePremium: 0, // Extra USD paid to write this cache entry
603
+ breakEvenHits: 0, // Full future hits needed to recover that premium
604
+ cost: 0.00011568, // Total estimated cost in USD
605
+ costBreakdown: {
606
+ uncachedInput: 0.0000352,
607
+ cachedInput: 0.00002048,
608
+ cacheWrite: 0,
609
+ cacheWrite5m: 0,
610
+ cacheWrite1h: 0,
611
+ output: 0.00006,
612
+ total: 0.00011568
613
+ },
614
+ speed: 42 // Output tokens per second (int)
550
615
  }
551
616
  }
552
617
  ```
@@ -558,10 +623,66 @@ After calling `message()` or `json()`, use `lastRaw` to access the complete resp
558
623
  ```javascript
559
624
  const text = await model.message();
560
625
  console.log(model.lastRaw.tokens);
561
- // { input: 122, output: 86, total: 208, cached: 41, cost: 0.000319, speed: 38 }
626
+ // Same normalized token and cost structure returned by raw()
627
+ ```
628
+
629
+ `cached` aggregates cache reads reported by the provider, while `cacheWrite` aggregates cache writes. Anthropic additionally exposes `cacheWrite5m` and `cacheWrite1h` because those writes cost 1.25× and 2× the normal input rate, respectively. `cacheSavings` compares cache reads with the normal input rate, `cacheWritePremium` compares writes with that rate, and `breakEvenHits` estimates how many complete future hits recover the current write premium. For Anthropic, `input` is normalized to include uncached input, cache reads, and cache writes. Missing usage or pricing categories return `0`. The `speed` field is the generation speed measured in output tokens per second (integer).
630
+
631
+ ### GPT-5.6 prompt caching
632
+
633
+ GPT-5.6 supports implicit or explicit caching through `prompt_cache_options`. Put the explicit breakpoint at the end of the stable prefix; the provider only caches prompts with at least 1,024 tokens.
634
+
635
+ ```javascript
636
+ const model = ModelMix.new()
637
+ .gpt56luna({
638
+ options: {
639
+ prompt_cache_key: 'support-agent-v1',
640
+ prompt_cache_options: { mode: 'explicit', ttl: '30m' }
641
+ }
642
+ })
643
+ .addText(longStableInstructions, {
644
+ cache: { breakpoint: true }
645
+ })
646
+ .addText('Answer this variable request.');
647
+
648
+ const result = await model.raw();
649
+ console.log(result.tokens.cached, result.tokens.cacheWrite, result.tokens.cost);
650
+ ```
651
+
652
+ The provider-neutral `cache: { breakpoint: true }` option is accepted by `addTextFromFile()`, `addImage()`, `addImageFromUrl()`, and `addImageFromBuffer()`. Responses-native `input_text`, `input_image`, and `input_file` blocks preserve the native `prompt_cache_breakpoint` field when supplied directly through `options.messages`.
653
+
654
+ GPT-5.6 uses `prompt_cache_options.ttl`; `prompt_cache_retention` remains available for earlier OpenAI models. ModelMix rejects the incompatible control instead of silently dropping it. For GPT-5.6 requests over 272K input tokens, the cost calculation applies the documented 2× input and 1.5× output multipliers to the complete request, including cache reads and writes.
655
+
656
+ GPT-5.6 prices per 1M tokens:
657
+
658
+ | Model | Input | Cached input | Cache write | Output |
659
+ | --- | ---: | ---: | ---: | ---: |
660
+ | `gpt-5.6-sol` | $5.00 | $0.50 | $6.25 | $30.00 |
661
+ | `gpt-5.6-terra` | $2.00 | $0.20 | $2.50 | $12.00 |
662
+ | `gpt-5.6-luna` | $0.20 | $0.02 | $0.25 | $1.20 |
663
+
664
+ ### Cross-provider cache fallback
665
+
666
+ Neutral breakpoints are translated at the last moment by each provider adapter. Native request policies remain scoped to their model, so they cannot leak into a fallback request:
667
+
668
+ ```javascript
669
+ const model = ModelMix.new()
670
+ .gpt56luna({
671
+ options: {
672
+ prompt_cache_key: 'support-agent-v1',
673
+ prompt_cache_options: { mode: 'explicit', ttl: '30m' }
674
+ }
675
+ })
676
+ .haiku45({
677
+ options: {
678
+ cache_control: { type: 'ephemeral', ttl: '1h' }
679
+ }
680
+ })
681
+ .addText(longStableInstructions, { cache: { breakpoint: true } })
682
+ .addText('Answer this variable request.');
562
683
  ```
563
684
 
564
- The `cached` field is a single aggregated count of cached input tokens reported by the provider. 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`. The `speed` field is the generation speed measured in output tokens per second (integer).
685
+ GPT-5.6 receives `prompt_cache_breakpoint`; Anthropic receives `cache_control`; older OpenAI models and providers without an equivalent omit the marker. When a neutral explicit breakpoint is present for Anthropic, its model-scoped `cache_control` becomes that block's policy instead of adding an automatic breakpoint after the variable suffix.
565
686
 
566
687
  ## 🐛 Enabling Debug Mode
567
688
 
@@ -675,18 +796,18 @@ new ModelMix(args = { options: {}, config: {} })
675
796
 
676
797
  - `setSystem(text)`: Sets the system prompt.
677
798
  - `setSystemFromFile(filePath)`: Sets the system prompt from a file.
678
- - `addText(text, config = { role: "user" })`: Adds a text message.
679
- - `addTextFromFile(filePath, config = { role: "user" })`: Adds a text message from a file.
680
- - `addImage(filePath, config = { role: "user" })`: Adds an image message from a file path.
681
- - `addImageFromUrl(url, config = { role: "user" })`: Adds an image message from URL.
682
- - `replace(keyValues)`: Defines placeholder replacements for messages and system prompt.
683
- - `replaceKeyFromFile(key, filePath)`: Defines a placeholder replacement with file contents as value.
799
+ - `addText(text, config = { role: "user", cache? })`: Adds a text message.
800
+ - `addTextFromFile(filePath, config = { role: "user", cache? })`: Adds a text message from a file.
801
+ - `addImage(filePath, config = { role: "user", cache? })`: Adds an image message from a file path.
802
+ - `addImageFromUrl(url, config = { role: "user", cache? })`: Adds an image message from URL.
803
+ - `replace(keyValues)`: Adds EJS data for messages and system prompts.
804
+ - `replaceKeyFromFile(key, filePath)`: Adds raw file contents as an EJS data value.
684
805
  - `message()`: Sends the message and returns the response.
685
806
  - `raw()`: Sends the message and returns the complete response data including:
686
807
  - `message`: The text response from the model
687
808
  - `think`: Reasoning/thinking content (if available)
688
809
  - `toolCalls`: Array of tool calls made by the model (if any)
689
- - `tokens`: Object with `input`, `output`, `total`, and `cached` token counts, plus `cost` (USD) and `speed` (output tokens/sec)
810
+ - `tokens`: Normalized token counts (`input`, `output`, `total`, `cached`, `cacheWrite`, `cacheWrite5m`, `cacheWrite1h`, `uncachedInput`, `cacheHitRate`), cache economics (`cacheSavings`, `cacheWritePremium`, `breakEvenHits`), plus `cost`, `costBreakdown` (USD), and `speed` (output tokens/sec)
690
811
  - `response`: The raw API response
691
812
  - `stream(callback)`: Sends the message and streams the response, invoking the callback with each streamed part.
692
813
  - `json(schemaExample, descriptions = {}, options = {})`: Forces the model to return a response in a specific JSON format.
@@ -829,4 +950,4 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
829
950
 
830
951
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
831
952
 
832
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
953
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/demo/demo.js CHANGED
@@ -6,7 +6,7 @@ const mmix = new ModelMix({
6
6
  temperature: 0.5,
7
7
  },
8
8
  config: {
9
- system: 'You are {name} from Melmac.',
9
+ system: 'You are <%- name %> from Melmac.',
10
10
  max_history: 2,
11
11
  bottleneck: { maxConcurrent: 1 },
12
12
  debug: 3,
@@ -22,12 +22,12 @@ const pplxSettings = {
22
22
  };
23
23
 
24
24
 
25
- mmix.replace({ '{name}': 'ALF' });
25
+ mmix.replace({ name: 'ALF' });
26
26
 
27
27
  console.log("\n" + '--------| gpt51() |--------');
28
28
  const gptArgs = { options: { reasoning_effort: "none", verbosity: "low" } };
29
- const gpt = mmix.gpt51(gptArgs).addText("Have you ever eaten a {animal}?");
30
- gpt.replace({ '{animal}': 'cat' });
29
+ const gpt = mmix.gpt51(gptArgs).addText("Have you ever eaten a <%- animal %>?");
30
+ gpt.replace({ animal: 'cat' });
31
31
  await gpt.json({ time: '24:00:00', message: 'Hello' }, { time: 'Time in format HH:MM:SS' });
32
32
 
33
33
  console.log("\n" + '--------| sonnet45() |--------');
package/demo/prompt.md CHANGED
@@ -1,4 +1,4 @@
1
- # Writing Prompt: {story_title}
1
+ # Writing Prompt: <%- storyTitle %>
2
2
 
3
3
  ## Setting
4
4
  A small, quiet town where nothing unusual ever happens.
@@ -22,4 +22,4 @@ Write a short story (500-1000 words) that explores:
22
22
  ### Additional Elements to Consider
23
23
  - The reactions of other townspeople
24
24
  - Any changes in the town's atmosphere after the package's arrival
25
- - Your character's internal conflict between curiosity and duty
25
+ - Your character's internal conflict between curiosity and duty
package/http-client.js CHANGED
@@ -4,6 +4,19 @@ function headersToObject(headers) {
4
4
  return Object.fromEntries(headers.entries());
5
5
  }
6
6
 
7
+ function sanitizeUrl(url) {
8
+ try {
9
+ const parsed = new URL(url);
10
+ parsed.username = '';
11
+ parsed.password = '';
12
+ parsed.search = '';
13
+ parsed.hash = '';
14
+ return parsed.toString();
15
+ } catch {
16
+ return String(url).replace(/[?#].*$/, '');
17
+ }
18
+ }
19
+
7
20
  async function parseResponseBody(response) {
8
21
  try {
9
22
  return await response.json();
@@ -24,7 +37,7 @@ async function parseJsonBody(response) {
24
37
 
25
38
  async function buildHttpError(url, response) {
26
39
  const details = await parseResponseBody(response);
27
- const error = new Error(`Request to ${url} failed with status code ${response.status}`);
40
+ const error = new Error(`Request to ${sanitizeUrl(url)} failed with status code ${response.status}`);
28
41
  error.isHttpError = true;
29
42
  error.statusCode = response.status;
30
43
  error.details = details;
@@ -64,7 +77,7 @@ async function fetchStreamResponse(url, { method = 'POST', headers = {}, body }
64
77
  throw await buildHttpError(url, response);
65
78
  }
66
79
  if (!response.body) {
67
- throw new Error(`Request to ${url} did not return a readable stream`);
80
+ throw new Error(`Request to ${sanitizeUrl(url)} did not return a readable stream`);
68
81
  }
69
82
  return {
70
83
  data: Readable.fromWeb(response.body),