modelmix 4.7.2 → 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,53 +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
- | `gemini31flashLite()`| Google | gemini-3.1-flash-lite-preview | [\$0.25 / \$1.50][3] |
194
- | `grok45()` | Grok | grok-4.5 | [\$2.00 / \$6.00][6] |
195
- | `grok43()` | Grok | grok-4.3 | [\$1.25 / \$2.50][6] |
196
- | `grok420multiAgent()`| Grok | grok-4.20-multi-agent-0309 | [\$1.25 / \$2.50][6] |
197
- | `grok420()` | Grok | grok-4.20-0309 (†) | [\$1.25 / \$2.50][6] |
198
- | `qwen36plus()` | Fireworks/Together | qwen3p6-plus / Qwen3.6-Plus | [\$0.50 / \$3.00][10] |
199
- | `qwen37plus()` | Fireworks | models/qwen3p7-plus | [\$0.40 / \$1.60][10] |
200
- | `qwen38max()` | OpenRouter | qwen/qwen3.8-max | [\$2.00 / \$6.00][12] |
201
- | `deepseekV4Flash()` | Fireworks | models/deepseek-v4-flash | [\$0.14 / \$0.28][10] |
202
- | `deepseekV4Pro()` | Fireworks | models/deepseek-v4-pro | [\$1.74 / \$3.48][10] |
203
- | `GLM52()` | Together | zai-org/GLM-5.2 | [\$1.40 / \$4.40][7] |
204
- | `GLM51()` | Fireworks | models/glm-5p1 | [\$1.05 / \$3.50][10] |
205
- | `minimaxM3()` | MiniMax | MiniMax-M3 | [\$0.30 / \$1.20][9] |
206
- | `minimaxM27()` | MiniMax | MiniMax-M2.7 | [\$0.30 / \$1.20][9] |
207
- | `sonar()` | Perplexity | sonar | [\$1.00 / \$1.00][4] |
208
- | `sonarPro()` | Perplexity | sonar-pro | [\$3.00 / \$15.00][4] |
209
- | `hermes3()` | Lambda | Hermes-3-Llama-3.1-405B-FP8 | [\$0.80 / \$0.80][8] |
210
- | `kimiK3()` | Moonshot | kimi-k3 | [\$3.00 / \$15.00][11] |
211
- | `kimiK25()` | Together | Kimi-K2.5 | [\$0.50 / \$2.80][7] |
212
- | `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] |
213
214
 
214
215
  [1]: https://platform.openai.com/docs/pricing "Pricing | OpenAI"
215
216
  [2]: https://docs.anthropic.com/en/docs/about-claude/pricing "Pricing - Anthropic"
@@ -225,11 +226,6 @@ Here's a comprehensive list of available methods:
225
226
  [12]: https://openrouter.ai/qwen/qwen3.8-max "OpenRouter Pricing"
226
227
 
227
228
  Each method accepts optional `options`, `config`, and (for multi-provider methods) `mix` parameters to customize behavior.
228
- † `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.
229
- For NVIDIA on DeepSeek V4 Flash/Pro, use `deepseekV4Flash({ mix: { nvidia: true } })` or `deepseekV4Pro({ mix: { nvidia: true } })`.
230
- For Together on Qwen 3.6 Plus, use `qwen36plus({ mix: { fireworks: false, together: true } })`.
231
- For OpenRouter on Qwen 3.7 Plus, use `qwen37plus({ mix: { fireworks: false, openrouter: true } })`.
232
- For OpenRouter instead of Moonshot's native API, use `kimiK3({ mix: { moonshot: false, openrouter: true } })`.
233
229
 
234
230
  ```javascript
235
231
  const result = await ModelMix.new({
@@ -243,7 +239,9 @@ const result = await ModelMix.new({
243
239
 
244
240
  ## 🔄 Templates
245
241
 
246
- 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.
247
245
 
248
246
  ### Core methods
249
247
 
@@ -251,16 +249,16 @@ ModelMix includes a simple but powerful templating system. You can write your sy
251
249
  | --- | --- |
252
250
  | `setSystemFromFile(path)` | Load the system prompt from a file |
253
251
  | `addTextFromFile(path)` | Load a user message from a file |
254
- | `replace({ key: value })` | Replace placeholders in all messages and the system prompt |
255
- | `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 |
256
254
 
257
255
  ### Basic example with `replace`
258
256
 
259
257
  ```javascript
260
258
  const gpt = ModelMix.new().gpt52();
261
259
 
262
- gpt.addText('Write a short story about a {animal} that lives in {place}.');
263
- 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' });
264
262
 
265
263
  console.log(await gpt.message());
266
264
  ```
@@ -271,15 +269,15 @@ Instead of writing long prompts inline, keep them in separate Markdown files. Th
271
269
 
272
270
  **`prompts/system.md`**
273
271
  ```markdown
274
- You are {role}, an expert in {topic}.
275
- Always respond in {language}.
272
+ You are <%- role %>, an expert in <%- topic %>.
273
+ Always respond in <%- language %>.
276
274
  ```
277
275
 
278
276
  **`prompts/task.md`**
279
277
  ```markdown
280
278
  Analyze the following and provide 3 key insights:
281
279
 
282
- {content}
280
+ <%- content %>
283
281
  ```
284
282
 
285
283
  **`app.js`**
@@ -290,16 +288,16 @@ gpt.setSystemFromFile('./prompts/system.md');
290
288
  gpt.addTextFromFile('./prompts/task.md');
291
289
 
292
290
  gpt.replace({
293
- '{role}': 'a senior analyst',
294
- '{topic}': 'market trends',
295
- '{language}': 'Spanish',
296
- '{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...'
297
295
  });
298
296
 
299
297
  console.log(await gpt.message());
300
298
  ```
301
299
 
302
- ### Injecting file contents into a placeholder
300
+ ### Injecting file contents as template data
303
301
 
304
302
  Use `replaceKeyFromFile` when the replacement value itself is a large text stored in a file.
305
303
 
@@ -307,7 +305,7 @@ Use `replaceKeyFromFile` when the replacement value itself is a large text store
307
305
  ```markdown
308
306
  Summarize the following article in 3 bullet points:
309
307
 
310
- {article}
308
+ <%- article %>
311
309
  ```
312
310
 
313
311
  **`app.js`**
@@ -315,7 +313,7 @@ Summarize the following article in 3 bullet points:
315
313
  const gpt = ModelMix.new().gpt5mini();
316
314
 
317
315
  gpt.addTextFromFile('./prompts/summarize.md');
318
- gpt.replaceKeyFromFile('{article}', './data/article.md');
316
+ gpt.replaceKeyFromFile('article', './data/article.md');
319
317
 
320
318
  console.log(await gpt.message());
321
319
  ```
@@ -326,17 +324,17 @@ Combine all methods to build reusable, file-based prompt pipelines:
326
324
 
327
325
  **`prompts/system.md`**
328
326
  ```markdown
329
- You are {role}. Follow these rules:
327
+ You are <%- role %>. Follow these rules:
330
328
  - Be concise
331
329
  - Use examples when possible
332
- - Respond in {language}
330
+ - Respond in <%- language %>
333
331
  ```
334
332
 
335
333
  **`prompts/review.md`**
336
334
  ```markdown
337
335
  Review the following code and suggest improvements:
338
336
 
339
- {code}
337
+ <%- code %>
340
338
  ```
341
339
 
342
340
  **`app.js`**
@@ -346,12 +344,63 @@ const gpt = ModelMix.new().gpt5mini();
346
344
  gpt.setSystemFromFile('./prompts/system.md');
347
345
  gpt.addTextFromFile('./prompts/review.md');
348
346
 
349
- gpt.replace({ '{role}': 'a senior code reviewer', '{language}': 'English' });
350
- gpt.replaceKeyFromFile('{code}', './src/utils.js');
347
+ gpt.replace({ role: 'a senior code reviewer', language: 'English' });
348
+ gpt.replaceKeyFromFile('code', './src/utils.js');
351
349
 
352
350
  console.log(await gpt.message());
353
351
  ```
354
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
+
355
404
  ## 🧩 JSON Structured Output
356
405
 
357
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.
@@ -540,12 +589,29 @@ Every response from `raw()` now includes a `tokens` object with the following st
540
589
  ```javascript
541
590
  {
542
591
  tokens: {
543
- input: 150, // Number of tokens in the prompt/input
544
- output: 75, // Number of tokens in the completion/output
545
- total: 225, // Total tokens used (input + output)
546
- cached: 100, // Cached input tokens reported by the provider (0 when absent)
547
- cost: 0.0012, // Estimated cost in USD (null if model not in pricing table)
548
- 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)
549
615
  }
550
616
  }
551
617
  ```
@@ -557,10 +623,66 @@ After calling `message()` or `json()`, use `lastRaw` to access the complete resp
557
623
  ```javascript
558
624
  const text = await model.message();
559
625
  console.log(model.lastRaw.tokens);
560
- // { 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.');
561
683
  ```
562
684
 
563
- 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.
564
686
 
565
687
  ## 🐛 Enabling Debug Mode
566
688
 
@@ -674,18 +796,18 @@ new ModelMix(args = { options: {}, config: {} })
674
796
 
675
797
  - `setSystem(text)`: Sets the system prompt.
676
798
  - `setSystemFromFile(filePath)`: Sets the system prompt from a file.
677
- - `addText(text, config = { role: "user" })`: Adds a text message.
678
- - `addTextFromFile(filePath, config = { role: "user" })`: Adds a text message from a file.
679
- - `addImage(filePath, config = { role: "user" })`: Adds an image message from a file path.
680
- - `addImageFromUrl(url, config = { role: "user" })`: Adds an image message from URL.
681
- - `replace(keyValues)`: Defines placeholder replacements for messages and system prompt.
682
- - `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.
683
805
  - `message()`: Sends the message and returns the response.
684
806
  - `raw()`: Sends the message and returns the complete response data including:
685
807
  - `message`: The text response from the model
686
808
  - `think`: Reasoning/thinking content (if available)
687
809
  - `toolCalls`: Array of tool calls made by the model (if any)
688
- - `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)
689
811
  - `response`: The raw API response
690
812
  - `stream(callback)`: Sends the message and streams the response, invoking the callback with each streamed part.
691
813
  - `json(schemaExample, descriptions = {}, options = {})`: Forces the model to return a response in a specific JSON format.
@@ -828,4 +950,4 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
828
950
 
829
951
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
830
952
 
831
- 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),