modelmix 4.7.4 → 5.0.1
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/AGENTS.md +43 -0
- package/README.md +283 -92
- package/demo/demo.js +4 -4
- package/demo/prompt.md +2 -2
- package/http-client.js +15 -2
- package/index.d.ts +102 -12
- package/index.js +965 -275
- package/package.json +4 -1
- package/skills/modelmix/SKILL.md +109 -19
- package/test/README.md +10 -9
- package/test/anthropic.test.js +146 -9
- package/test/effort.test.js +2 -2
- package/test/fallback.test.js +192 -2
- package/test/fixtures/account-details.txt +4 -0
- package/test/fixtures/choice-options.txt +6 -0
- package/test/fixtures/choice-template.txt +2 -0
- package/test/fixtures/dynamic-include.txt +2 -0
- package/test/fixtures/system-rules.txt +1 -0
- package/test/fixtures/system-template.txt +2 -0
- package/test/fixtures/template.txt +4 -11
- package/test/fixtures/tree.ejs +6 -0
- package/test/grok.test.js +28 -1
- package/test/history.test.js +2 -2
- package/test/live.mcp.js +20 -20
- package/test/live.test.js +11 -11
- package/test/templates.test.js +621 -280
- package/test/tokens.test.js +409 -16
package/AGENTS.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Repository Guidelines
|
|
2
|
+
|
|
3
|
+
## Project Overview
|
|
4
|
+
|
|
5
|
+
ModelMix is a CommonJS Node.js library for LLM providers, fallback chains, rate limiting, templates, multimodal requests, and MCP tools. Node is not pinned; the root uses pnpm 11.18 and requires no compilation.
|
|
6
|
+
|
|
7
|
+
## Project Structure & Module Organization
|
|
8
|
+
|
|
9
|
+
- `index.js` owns the public API and provider classes; synchronize public contracts with `index.d.ts`.
|
|
10
|
+
- Root helper modules isolate effort mapping, schemas, HTTP, multipart, and MCP behavior.
|
|
11
|
+
- `test/*.test.js` contains Mocha suites; fixtures and setup live under `test/`.
|
|
12
|
+
- `demo/` holds examples and a separate npm manifest; `skills/modelmix/` contains the published skill.
|
|
13
|
+
- `node_modules/` is generated and ignored. Update `pnpm-lock.yaml` only through pnpm.
|
|
14
|
+
|
|
15
|
+
## Build, Test, and Development Commands
|
|
16
|
+
|
|
17
|
+
- `pnpm install` installs locked root dependencies.
|
|
18
|
+
- `pnpm test` runs the complete Mocha suite with shared setup.
|
|
19
|
+
- `pnpm run test:offline` runs the main mocked regressions without intentional live-provider coverage.
|
|
20
|
+
- `pnpm run test:templates`, `pnpm run test:fallback`, and `pnpm run test:watch` support focused development.
|
|
21
|
+
- `pnpm run test:live` and `pnpm run test:live.mcp` require real credentials, may incur costs, and must be reported separately.
|
|
22
|
+
|
|
23
|
+
No build, lint, formatter, or standalone typecheck command is configured. Do not add tooling or dependencies without approval.
|
|
24
|
+
|
|
25
|
+
## Coding Style & Naming Conventions
|
|
26
|
+
|
|
27
|
+
Use four-space indentation, semicolons, single quotes, CommonJS `require`, `camelCase`, and `PascalCase` classes. Keep provider behavior in `Mix*` classes. Public shortcut changes must update implementation, declarations, tests, docs, demos, and the skill together. Add aliases only with approval.
|
|
28
|
+
|
|
29
|
+
## Testing Guidelines
|
|
30
|
+
|
|
31
|
+
Use Mocha, Chai, Sinon, and Nock. Name regression files `*.test.js`; reproduce bugs before fixing them. Offline tests must not depend on order or real keys. Prefer instance seams such as `_choiceRandom()` over global stubs.
|
|
32
|
+
|
|
33
|
+
## Configuration & Security
|
|
34
|
+
|
|
35
|
+
There is no configuration module: callers pass policy through `ModelMix.new({ config, options })`; credentials come from environment variables. Never commit `.env` files, credentials, secret payloads, or unrequested defaults.
|
|
36
|
+
|
|
37
|
+
## Commit & Pull Request Guidelines
|
|
38
|
+
|
|
39
|
+
History favors short imperative subjects, commonly `feat:`, `fix:`, or `chore:`. Keep commits scoped and do not commit unless requested. Pull requests should explain behavior and compatibility impact, link issues, and list exact test commands and results; include screenshots only for visible changes.
|
|
40
|
+
|
|
41
|
+
## Domain & Contributor Conventions
|
|
42
|
+
|
|
43
|
+
A **provider** is an API backend, a **model shortcut** is a fluent method, and a **fallback chain** is ordered. Write code and docs in English; reply in the contributor's language.
|
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
|
|
167
|
-
| `gpt56terra()` | OpenAI | gpt-5.6-terra | [\$2.00
|
|
168
|
-
| `gpt56luna()` | OpenAI | gpt-5.6-luna | [\$0.20
|
|
169
|
-
| `gpt55()` | OpenAI | gpt-5.5 | [\$5.00
|
|
170
|
-
| `gpt54()` | OpenAI | gpt-5.4 | [\$2.50
|
|
171
|
-
| `gpt54mini()` | OpenAI | gpt-5.4-mini | [\$0.75
|
|
172
|
-
| `gpt54nano()` | OpenAI | gpt-5.4-nano | [\$0.20
|
|
173
|
-
| `gpt53codex()` | OpenAI | gpt-5.3-codex | [\$1.25
|
|
174
|
-
| `gpt52()` | OpenAI | gpt-5.2 | [\$1.75
|
|
175
|
-
| `gpt51()` | OpenAI | gpt-5.1 | [\$1.25
|
|
176
|
-
| `gpt5mini()` | OpenAI | gpt-5-mini | [\$0.25
|
|
177
|
-
| `gpt5nano()` | OpenAI | gpt-5-nano | [\$0.05
|
|
178
|
-
| `gpt41()` | OpenAI | gpt-4.1 | [\$2.00
|
|
179
|
-
| `gpt41mini()` | OpenAI | gpt-4.1-mini | [\$0.40
|
|
180
|
-
| `gpt41nano()` | OpenAI | gpt-4.1-nano | [\$0.10
|
|
181
|
-
| `gptOss()` | Together | gpt-oss-120B | [\$0.15
|
|
182
|
-
| `fable5()` | Anthropic | claude-fable-5 | [\$10.00
|
|
183
|
-
| `opus5()` | Anthropic | claude-opus-5 | [\$5.00
|
|
184
|
-
| `opus48()` | Anthropic | claude-opus-4-8 | [\$5.00
|
|
185
|
-
| `opus47()` | Anthropic | claude-opus-4-7 | [\$5.00
|
|
186
|
-
| `opus46()` | Anthropic | claude-opus-4-6 | [\$5.00
|
|
187
|
-
| `sonnet5()` | Anthropic | claude-sonnet-5 | [\$3.00
|
|
188
|
-
| `sonnet46()` | Anthropic | claude-sonnet-4-6 | [\$3.00
|
|
189
|
-
| `haiku45()` | Anthropic | claude-haiku-4-5-20251001 | [\$1.00
|
|
190
|
-
| `gemini31pro()` | Google | gemini-3.1-pro-preview | [\$2.00
|
|
191
|
-
| `gemini36flash()`
|
|
192
|
-
| `gemini35flash()`
|
|
193
|
-
| `gemini35flashLite()`| Google
|
|
194
|
-
| `gemini31flashLite()`| Google | gemini-3.1-flash-lite-preview | [\$0.25
|
|
195
|
-
| `grok45()` | Grok | grok-4.5 | [\$2.00
|
|
196
|
-
| `grok43()` | Grok | grok-4.3 | [\$1.25
|
|
197
|
-
| `grok420multiAgent()`| Grok | grok-4.20-multi-agent-0309 | [\$1.25
|
|
198
|
-
| `grok420()` | Grok | grok-4.20-0309 (†) | [\$1.25
|
|
199
|
-
| `qwen36plus()` | Fireworks
|
|
200
|
-
| `qwen37plus()` | Fireworks | models/qwen3p7-plus | [\$0.40
|
|
201
|
-
| `qwen38max()` | OpenRouter | qwen/qwen3.8-max | [\$2.00
|
|
202
|
-
| `deepseekV4Flash()` | Fireworks | models/deepseek-v4-flash
|
|
203
|
-
| `deepseekV4Pro()` | Fireworks | models/deepseek-v4-pro | [\$1.74
|
|
204
|
-
| `GLM52()` | Together | zai-org/GLM-5.2 | [\$1.40
|
|
205
|
-
| `GLM51()` | Fireworks | models/glm-5p1 | [\$1.05
|
|
206
|
-
| `minimaxM3()` | MiniMax | MiniMax-M3 | [\$0.30
|
|
207
|
-
| `minimaxM27()` | MiniMax | MiniMax-M2.7 | [\$0.30
|
|
208
|
-
| `sonar()` | Perplexity | sonar | [\$1.00
|
|
209
|
-
| `sonarPro()` | Perplexity | sonar-pro | [\$3.00
|
|
210
|
-
| `hermes3()` | Lambda | Hermes-3-Llama-3.1-405B-FP8 | [\$0.80
|
|
211
|
-
| `kimiK3()` | Moonshot | kimi-k3 | [\$3.00
|
|
212
|
-
| `kimiK25()` | Together | Kimi-K2.5 | [\$0.50
|
|
213
|
-
| `kimiK26()` | Fireworks | models/kimi-k2p6 | [\$0.95
|
|
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
|
|
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,35 +249,42 @@ 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
|
-
| `
|
|
256
|
-
| `
|
|
252
|
+
| `assign({ key: value })` | Assign EJS template data |
|
|
253
|
+
| `assignKey(key, value)` | Assign one EJS template-data value |
|
|
254
|
+
| `assignKeyFromFile(key, path)` | Assign an EJS-rendered file to one template-data key |
|
|
257
255
|
|
|
258
|
-
### Basic example with `
|
|
256
|
+
### Basic example with `assign`
|
|
259
257
|
|
|
260
258
|
```javascript
|
|
261
259
|
const gpt = ModelMix.new().gpt52();
|
|
262
260
|
|
|
263
|
-
gpt.addText('Write a short story about a
|
|
264
|
-
gpt.
|
|
261
|
+
gpt.addText('Write a short story about a <%- animal %> that lives in <%- place %>.');
|
|
262
|
+
gpt.assign({ animal: 'cat', place: 'a haunted castle' });
|
|
265
263
|
|
|
266
264
|
console.log(await gpt.message());
|
|
267
265
|
```
|
|
268
266
|
|
|
267
|
+
Use `assignKey()` when assigning a single value:
|
|
268
|
+
|
|
269
|
+
```javascript
|
|
270
|
+
gpt.assignKey('animal', 'cat');
|
|
271
|
+
```
|
|
272
|
+
|
|
269
273
|
### Loading prompts from `.md` files
|
|
270
274
|
|
|
271
275
|
Instead of writing long prompts inline, keep them in separate Markdown files. This makes them easier to read, edit, and version control.
|
|
272
276
|
|
|
273
277
|
**`prompts/system.md`**
|
|
274
278
|
```markdown
|
|
275
|
-
You are
|
|
276
|
-
Always respond in
|
|
279
|
+
You are <%- role %>, an expert in <%- topic %>.
|
|
280
|
+
Always respond in <%- language %>.
|
|
277
281
|
```
|
|
278
282
|
|
|
279
283
|
**`prompts/task.md`**
|
|
280
284
|
```markdown
|
|
281
285
|
Analyze the following and provide 3 key insights:
|
|
282
286
|
|
|
283
|
-
|
|
287
|
+
<%- content %>
|
|
284
288
|
```
|
|
285
289
|
|
|
286
290
|
**`app.js`**
|
|
@@ -290,25 +294,48 @@ const gpt = ModelMix.new().gpt56luna();
|
|
|
290
294
|
gpt.setSystemFromFile('./prompts/system.md');
|
|
291
295
|
gpt.addTextFromFile('./prompts/task.md');
|
|
292
296
|
|
|
293
|
-
gpt.
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
297
|
+
gpt.assign({
|
|
298
|
+
role: 'a senior analyst',
|
|
299
|
+
topic: 'market trends',
|
|
300
|
+
language: 'Spanish',
|
|
301
|
+
content: 'Bitcoin surpassed $100,000 in December 2024...'
|
|
298
302
|
});
|
|
299
303
|
|
|
300
304
|
console.log(await gpt.message());
|
|
301
305
|
```
|
|
302
306
|
|
|
303
|
-
###
|
|
307
|
+
### Simple includes
|
|
308
|
+
|
|
309
|
+
Use EJS `include` to compose a prompt from other files. Include paths are resolved relative to the template containing them.
|
|
310
|
+
|
|
311
|
+
```ejs
|
|
312
|
+
<%- include('shared/rules.md') %>
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
For example:
|
|
316
|
+
|
|
317
|
+
**`prompts/task.md`**
|
|
318
|
+
```markdown
|
|
319
|
+
Analyze the request following these rules:
|
|
320
|
+
|
|
321
|
+
<%- include('shared/rules.md') %>
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
**`prompts/shared/rules.md`**
|
|
325
|
+
```markdown
|
|
326
|
+
- Be concise
|
|
327
|
+
- Explain assumptions
|
|
328
|
+
```
|
|
304
329
|
|
|
305
|
-
|
|
330
|
+
### Dynamic includes
|
|
331
|
+
|
|
332
|
+
When the file changes at runtime, pass its path as template data and call `include` with that variable. This replaces the file-injection use case while keeping composition inside the template.
|
|
306
333
|
|
|
307
334
|
**`prompts/summarize.md`**
|
|
308
335
|
```markdown
|
|
309
336
|
Summarize the following article in 3 bullet points:
|
|
310
337
|
|
|
311
|
-
|
|
338
|
+
<%- include(articleFile) %>
|
|
312
339
|
```
|
|
313
340
|
|
|
314
341
|
**`app.js`**
|
|
@@ -316,28 +343,51 @@ Summarize the following article in 3 bullet points:
|
|
|
316
343
|
const gpt = ModelMix.new().gpt5mini();
|
|
317
344
|
|
|
318
345
|
gpt.addTextFromFile('./prompts/summarize.md');
|
|
319
|
-
gpt.
|
|
346
|
+
gpt.assign({ articleFile: '../data/article.md' });
|
|
347
|
+
|
|
348
|
+
console.log(await gpt.message());
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Static and dynamic include paths are resolved relative to the containing template. Included files are EJS template source, so both the path and file must be controlled by the developer. Pass untrusted runtime content through ordinary `assign()` values instead of using it as an include path.
|
|
352
|
+
|
|
353
|
+
### Assigning a rendered file to a key
|
|
354
|
+
|
|
355
|
+
Use `assignKeyFromFile()` when the outer template needs the rendered contents of a file as one data value:
|
|
356
|
+
|
|
357
|
+
```javascript
|
|
358
|
+
const gpt = ModelMix.new().gpt5mini();
|
|
359
|
+
|
|
360
|
+
gpt.assign({ language: 'Spanish' });
|
|
361
|
+
gpt.assignKeyFromFile('rules', './prompts/rules.md');
|
|
362
|
+
gpt.addText('Follow these rules:\n<%- rules %>');
|
|
320
363
|
|
|
321
364
|
console.log(await gpt.message());
|
|
322
365
|
```
|
|
323
366
|
|
|
367
|
+
`assignKeyFromFile()` uses EJS `include` internally. The assigned file can access ordinary `assign()` data and use includes relative to its own path. It is rendered once per request and reused across the system prompt and messages in that request. The file is template source and must be developer-controlled.
|
|
368
|
+
|
|
324
369
|
### Full template workflow
|
|
325
370
|
|
|
326
371
|
Combine all methods to build reusable, file-based prompt pipelines:
|
|
327
372
|
|
|
328
373
|
**`prompts/system.md`**
|
|
329
374
|
```markdown
|
|
330
|
-
You are
|
|
375
|
+
You are <%- role %>. Follow these rules:
|
|
376
|
+
<%- include('partials/rules.md') %>
|
|
377
|
+
- Respond in <%- language %>
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
**`prompts/partials/rules.md`**
|
|
381
|
+
```markdown
|
|
331
382
|
- Be concise
|
|
332
383
|
- Use examples when possible
|
|
333
|
-
- Respond in {language}
|
|
334
384
|
```
|
|
335
385
|
|
|
336
386
|
**`prompts/review.md`**
|
|
337
387
|
```markdown
|
|
338
388
|
Review the following code and suggest improvements:
|
|
339
389
|
|
|
340
|
-
|
|
390
|
+
<%- include('../src/utils.js') %>
|
|
341
391
|
```
|
|
342
392
|
|
|
343
393
|
**`app.js`**
|
|
@@ -347,12 +397,79 @@ const gpt = ModelMix.new().gpt5mini();
|
|
|
347
397
|
gpt.setSystemFromFile('./prompts/system.md');
|
|
348
398
|
gpt.addTextFromFile('./prompts/review.md');
|
|
349
399
|
|
|
350
|
-
gpt.
|
|
351
|
-
gpt.replaceKeyFromFile('{code}', './src/utils.js');
|
|
400
|
+
gpt.assign({ role: 'a senior code reviewer', language: 'English' });
|
|
352
401
|
|
|
353
402
|
console.log(await gpt.message());
|
|
354
403
|
```
|
|
355
404
|
|
|
405
|
+
### EJS output and control flow
|
|
406
|
+
|
|
407
|
+
Use `<%- value %>` for raw prompt content and `<%= value %>` only when XML escaping is intentional. Missing variables and missing files throw immediately.
|
|
408
|
+
|
|
409
|
+
```ejs
|
|
410
|
+
<% if (user.active) { %>
|
|
411
|
+
Review these roles:
|
|
412
|
+
<% user.roles.forEach(role => { %>
|
|
413
|
+
- <%- role %>
|
|
414
|
+
<% }) %>
|
|
415
|
+
<% } %>
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### Random prompt choices
|
|
419
|
+
|
|
420
|
+
Use a `choice` block to include exactly one prompt variant. When no weights are present, every option has the same probability:
|
|
421
|
+
|
|
422
|
+
```ejs
|
|
423
|
+
<% choice %>
|
|
424
|
+
<% option %>
|
|
425
|
+
Use emojis.
|
|
426
|
+
<% option %>
|
|
427
|
+
Use few emojis.
|
|
428
|
+
<% option %>
|
|
429
|
+
Do not use emojis.
|
|
430
|
+
<% /choice %>
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
Add a positive weight after every `option` when the probabilities should differ:
|
|
434
|
+
|
|
435
|
+
```ejs
|
|
436
|
+
<% choice %>
|
|
437
|
+
<% option 20 %>
|
|
438
|
+
Use emojis.
|
|
439
|
+
<% option 40 %>
|
|
440
|
+
Use few emojis.
|
|
441
|
+
<% option 40 %>
|
|
442
|
+
Do not use emojis.
|
|
443
|
+
<% /choice %>
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
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.
|
|
447
|
+
|
|
448
|
+
### Recursive includes
|
|
449
|
+
|
|
450
|
+
An included template can include itself to render recursive data. Always define a stopping condition:
|
|
451
|
+
|
|
452
|
+
```ejs
|
|
453
|
+
<%- node.text %>
|
|
454
|
+
|
|
455
|
+
<% if (node.children?.length && depth < maxDepth) { %>
|
|
456
|
+
<% for (const child of node.children) { %>
|
|
457
|
+
<%- include('tree.ejs', { node: child, depth: depth + 1, maxDepth }) %>
|
|
458
|
+
<% } %>
|
|
459
|
+
<% } %>
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
```javascript
|
|
463
|
+
const gpt = ModelMix.new().gpt5mini();
|
|
464
|
+
|
|
465
|
+
gpt.addTextFromFile('./prompts/tree.ejs');
|
|
466
|
+
gpt.assign({ node: promptTree, depth: 0, maxDepth: 10 });
|
|
467
|
+
|
|
468
|
+
console.log(await gpt.message());
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
Content supplied through `assign()` remains data. EJS tags inside that content are not executed recursively; use `assignKeyFromFile()` only for developer-controlled EJS files that should be rendered.
|
|
472
|
+
|
|
356
473
|
## 🧩 JSON Structured Output
|
|
357
474
|
|
|
358
475
|
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 +658,29 @@ Every response from `raw()` now includes a `tokens` object with the following st
|
|
|
541
658
|
```javascript
|
|
542
659
|
{
|
|
543
660
|
tokens: {
|
|
544
|
-
input:
|
|
545
|
-
output:
|
|
546
|
-
total:
|
|
547
|
-
cached:
|
|
548
|
-
|
|
549
|
-
|
|
661
|
+
input: 1200, // Total input tokens, including cache reads and writes
|
|
662
|
+
output: 50, // Number of output tokens
|
|
663
|
+
total: 1250, // Total tokens used
|
|
664
|
+
cached: 1024, // Input tokens read from cache
|
|
665
|
+
cacheWrite: 0, // Input tokens written to cache
|
|
666
|
+
cacheWrite5m: 0, // Anthropic writes using the 5-minute TTL
|
|
667
|
+
cacheWrite1h: 0, // Anthropic writes using the 1-hour TTL
|
|
668
|
+
uncachedInput: 176, // max(0, input - cached - cacheWrite)
|
|
669
|
+
cacheHitRate: 0.8533, // cached / input, rounded to 4 decimals
|
|
670
|
+
cacheSavings: 0.00018432, // USD saved by cache reads
|
|
671
|
+
cacheWritePremium: 0, // Extra USD paid to write this cache entry
|
|
672
|
+
breakEvenHits: 0, // Full future hits needed to recover that premium
|
|
673
|
+
cost: 0.00011568, // Total estimated cost in USD
|
|
674
|
+
costBreakdown: {
|
|
675
|
+
uncachedInput: 0.0000352,
|
|
676
|
+
cachedInput: 0.00002048,
|
|
677
|
+
cacheWrite: 0,
|
|
678
|
+
cacheWrite5m: 0,
|
|
679
|
+
cacheWrite1h: 0,
|
|
680
|
+
output: 0.00006,
|
|
681
|
+
total: 0.00011568
|
|
682
|
+
},
|
|
683
|
+
speed: 42 // Output tokens per second (int)
|
|
550
684
|
}
|
|
551
685
|
}
|
|
552
686
|
```
|
|
@@ -558,10 +692,66 @@ After calling `message()` or `json()`, use `lastRaw` to access the complete resp
|
|
|
558
692
|
```javascript
|
|
559
693
|
const text = await model.message();
|
|
560
694
|
console.log(model.lastRaw.tokens);
|
|
561
|
-
//
|
|
695
|
+
// Same normalized token and cost structure returned by raw()
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
`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).
|
|
699
|
+
|
|
700
|
+
### GPT-5.6 prompt caching
|
|
701
|
+
|
|
702
|
+
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.
|
|
703
|
+
|
|
704
|
+
```javascript
|
|
705
|
+
const model = ModelMix.new()
|
|
706
|
+
.gpt56luna({
|
|
707
|
+
options: {
|
|
708
|
+
prompt_cache_key: 'support-agent-v1',
|
|
709
|
+
prompt_cache_options: { mode: 'explicit', ttl: '30m' }
|
|
710
|
+
}
|
|
711
|
+
})
|
|
712
|
+
.addText(longStableInstructions, {
|
|
713
|
+
cache: { breakpoint: true }
|
|
714
|
+
})
|
|
715
|
+
.addText('Answer this variable request.');
|
|
716
|
+
|
|
717
|
+
const result = await model.raw();
|
|
718
|
+
console.log(result.tokens.cached, result.tokens.cacheWrite, result.tokens.cost);
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
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`.
|
|
722
|
+
|
|
723
|
+
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.
|
|
724
|
+
|
|
725
|
+
GPT-5.6 prices per 1M tokens:
|
|
726
|
+
|
|
727
|
+
| Model | Input | Cached input | Cache write | Output |
|
|
728
|
+
| --- | ---: | ---: | ---: | ---: |
|
|
729
|
+
| `gpt-5.6-sol` | $5.00 | $0.50 | $6.25 | $30.00 |
|
|
730
|
+
| `gpt-5.6-terra` | $2.00 | $0.20 | $2.50 | $12.00 |
|
|
731
|
+
| `gpt-5.6-luna` | $0.20 | $0.02 | $0.25 | $1.20 |
|
|
732
|
+
|
|
733
|
+
### Cross-provider cache fallback
|
|
734
|
+
|
|
735
|
+
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:
|
|
736
|
+
|
|
737
|
+
```javascript
|
|
738
|
+
const model = ModelMix.new()
|
|
739
|
+
.gpt56luna({
|
|
740
|
+
options: {
|
|
741
|
+
prompt_cache_key: 'support-agent-v1',
|
|
742
|
+
prompt_cache_options: { mode: 'explicit', ttl: '30m' }
|
|
743
|
+
}
|
|
744
|
+
})
|
|
745
|
+
.haiku45({
|
|
746
|
+
options: {
|
|
747
|
+
cache_control: { type: 'ephemeral', ttl: '1h' }
|
|
748
|
+
}
|
|
749
|
+
})
|
|
750
|
+
.addText(longStableInstructions, { cache: { breakpoint: true } })
|
|
751
|
+
.addText('Answer this variable request.');
|
|
562
752
|
```
|
|
563
753
|
|
|
564
|
-
|
|
754
|
+
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
755
|
|
|
566
756
|
## 🐛 Enabling Debug Mode
|
|
567
757
|
|
|
@@ -675,18 +865,19 @@ new ModelMix(args = { options: {}, config: {} })
|
|
|
675
865
|
|
|
676
866
|
- `setSystem(text)`: Sets the system prompt.
|
|
677
867
|
- `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
|
-
- `
|
|
683
|
-
- `
|
|
868
|
+
- `addText(text, config = { role: "user", cache? })`: Adds a text message.
|
|
869
|
+
- `addTextFromFile(filePath, config = { role: "user", cache? })`: Adds a text message from a file.
|
|
870
|
+
- `addImage(filePath, config = { role: "user", cache? })`: Adds an image message from a file path.
|
|
871
|
+
- `addImageFromUrl(url, config = { role: "user", cache? })`: Adds an image message from URL.
|
|
872
|
+
- `assign(keyValues)`: Assigns EJS data for messages and system prompts.
|
|
873
|
+
- `assignKey(key, value)`: Assigns one EJS data value.
|
|
874
|
+
- `assignKeyFromFile(key, filePath)`: Renders an EJS file through `include` and assigns its output to one key.
|
|
684
875
|
- `message()`: Sends the message and returns the response.
|
|
685
876
|
- `raw()`: Sends the message and returns the complete response data including:
|
|
686
877
|
- `message`: The text response from the model
|
|
687
878
|
- `think`: Reasoning/thinking content (if available)
|
|
688
879
|
- `toolCalls`: Array of tool calls made by the model (if any)
|
|
689
|
-
- `tokens`:
|
|
880
|
+
- `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
881
|
- `response`: The raw API response
|
|
691
882
|
- `stream(callback)`: Sends the message and streams the response, invoking the callback with each streamed part.
|
|
692
883
|
- `json(schemaExample, descriptions = {}, options = {})`: Forces the model to return a response in a specific JSON format.
|
|
@@ -829,4 +1020,4 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
|
829
1020
|
|
|
830
1021
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
831
1022
|
|
|
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.
|
|
1023
|
+
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
|
|
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.
|
|
25
|
+
mmix.assign({ 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
|
|
30
|
-
gpt.
|
|
29
|
+
const gpt = mmix.gpt51(gptArgs).addText("Have you ever eaten a <%- animal %>?");
|
|
30
|
+
gpt.assignKey('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:
|
|
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),
|