modelmix 5.0.4 → 5.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +226 -156
- package/demo/short.js +3 -0
- package/effort.js +1 -0
- package/index.d.ts +5 -0
- package/index.js +79 -3
- package/package.json +1 -1
- package/skills/modelmix/SKILL.md +13 -2
- package/test/deepseek.test.js +6 -1
- package/test/effort.test.js +10 -0
- package/test/fallback.test.js +34 -0
- package/test/hermes.test.js +37 -0
- package/test/qwen.test.js +34 -4
package/README.md
CHANGED
|
@@ -4,6 +4,25 @@
|
|
|
4
4
|
|
|
5
5
|
Ever found yourself wanting to integrate AI models into your projects but worried about reliability? ModelMix helps you build resilient AI applications by chaining multiple models together. If one model fails, it automatically switches to the next one, ensuring your application keeps running smoothly.
|
|
6
6
|
|
|
7
|
+
## 📑 Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Features](#-features)
|
|
10
|
+
- [Usage](#-usage)
|
|
11
|
+
- [Shorthand Methods](#-shorthand-methods)
|
|
12
|
+
- [Unified Effort Scale](#-unified-effort-scale)
|
|
13
|
+
- [Templates](#-templates)
|
|
14
|
+
- [JSON Structured Output](#-json-structured-output)
|
|
15
|
+
- [Token Usage Tracking](#-token-usage-tracking)
|
|
16
|
+
- [Prompt Caching](#-prompt-caching)
|
|
17
|
+
- [Model Context Protocol (MCP) Integration](#-model-context-protocol-mcp-integration)
|
|
18
|
+
- [Enabling Debug Mode](#-enabling-debug-mode)
|
|
19
|
+
- [Bottleneck Integration](#-bottleneck-integration)
|
|
20
|
+
- [Retry (Opt-In)](#-retry-optin)
|
|
21
|
+
- [Instance Plugins](#-instance-plugins)
|
|
22
|
+
- [ModelMix Class Overview](#-modelmix-class-overview)
|
|
23
|
+
- [Contributing](#-contributing)
|
|
24
|
+
- [License](#-license)
|
|
25
|
+
|
|
7
26
|
## ✨ Features
|
|
8
27
|
|
|
9
28
|
- **Unified Interface**: Interact with multiple AI models through a single, coherent API.
|
|
@@ -76,6 +95,19 @@ const model = await ModelMix.new(setup)
|
|
|
76
95
|
console.log(await model.message());
|
|
77
96
|
```
|
|
78
97
|
|
|
98
|
+
The same ordered chain can be attached by passing model shortcuts directly to
|
|
99
|
+
`chain()`. Add `@effort` to override unified effort for one model; entries
|
|
100
|
+
without it inherit the chain effort, or use the provider default when the chain
|
|
101
|
+
has no configured effort:
|
|
102
|
+
|
|
103
|
+
```javascript
|
|
104
|
+
const model = ModelMix.new(setup)
|
|
105
|
+
.chain('sonnet5', 'gpt56luna@20', 'gemini37flash@-1')
|
|
106
|
+
.addText("What's your name?");
|
|
107
|
+
|
|
108
|
+
console.log(await model.message());
|
|
109
|
+
```
|
|
110
|
+
|
|
79
111
|
**Use Perplexity to get the price of ETH**
|
|
80
112
|
```javascript
|
|
81
113
|
const ETH = ModelMix.new()
|
|
@@ -101,159 +133,6 @@ This pattern allows you to:
|
|
|
101
133
|
- Track token usage across all providers
|
|
102
134
|
- Keep your code clean and maintainable
|
|
103
135
|
|
|
104
|
-
## 🔌 Instance Plugins
|
|
105
|
-
|
|
106
|
-
Plugins wrap one ModelMix instance without changing global behavior. They run in registration order after templates are rendered and before provider-specific request conversion:
|
|
107
|
-
|
|
108
|
-
```javascript
|
|
109
|
-
const metrics = {
|
|
110
|
-
name: 'metrics',
|
|
111
|
-
async execute(context, next) {
|
|
112
|
-
const startedAt = Date.now();
|
|
113
|
-
const result = await next();
|
|
114
|
-
return { ...result, elapsedMs: Date.now() - startedAt };
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
const model = ModelMix.new()
|
|
119
|
-
.gpt56luna()
|
|
120
|
-
.use(metrics)
|
|
121
|
-
.addText('Summarize this request.');
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
A plugin may edit `context.request`, call `next()`, or return a complete ModelMix result itself. It can also create history-free child executions with `context.invoke()` and choose plugin inheritance:
|
|
125
|
-
|
|
126
|
-
```javascript
|
|
127
|
-
const child = await context.invoke({
|
|
128
|
-
systemFile: './prompts/extract-entities.md',
|
|
129
|
-
assign: { outputLanguage: 'Spanish' },
|
|
130
|
-
messages: [{ role: 'user', content: section }],
|
|
131
|
-
plugins: { exclude: ['recursive-plugin'] },
|
|
132
|
-
history: false
|
|
133
|
-
});
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
Supported policies are `'inherit'`, `'none'`, `{ include: [...] }`, and `{ exclude: [...] }`. Child metadata exposes `executionId`, `parentExecutionId`, and `depth` to middleware. `.new()` inherits registered plugins but not message history.
|
|
137
|
-
|
|
138
|
-
Child `systemFile` templates use the same EJS engine, `assign()` data contract, and relative Markdown includes as ordinary ModelMix templates. Use either `system` or `systemFile`, not both.
|
|
139
|
-
|
|
140
|
-
### Recursive Language Model plugin
|
|
141
|
-
|
|
142
|
-
The separately publishable `@modelmix/rlm` workspace package keeps document parsing, planner prompts, and `isolated-vm` out of the core `modelmix` dependency tree. It requires Node.js 22 or newer.
|
|
143
|
-
|
|
144
|
-
```javascript
|
|
145
|
-
const { ModelMix } = require('modelmix');
|
|
146
|
-
const { rlm } = require('@modelmix/rlm');
|
|
147
|
-
|
|
148
|
-
const fast = ModelMix.new().gpt41mini();
|
|
149
|
-
|
|
150
|
-
const result = await ModelMix.new()
|
|
151
|
-
.gpt56luna()
|
|
152
|
-
.use(rlm({
|
|
153
|
-
maxDepth: 2,
|
|
154
|
-
documents: {
|
|
155
|
-
book: {
|
|
156
|
-
format: 'markdown',
|
|
157
|
-
content: markdownBook
|
|
158
|
-
}
|
|
159
|
-
},
|
|
160
|
-
workers: {
|
|
161
|
-
fast: {
|
|
162
|
-
model: fast,
|
|
163
|
-
intelligence: 2,
|
|
164
|
-
cost: 1,
|
|
165
|
-
speed: 4,
|
|
166
|
-
description: 'Translation, extraction, and simple transformations'
|
|
167
|
-
}
|
|
168
|
-
},
|
|
169
|
-
limits: {
|
|
170
|
-
maxQueryBytes: 64 * 1024,
|
|
171
|
-
sandboxMemoryBytes: 64 * 1024 * 1024,
|
|
172
|
-
maxConcurrentQueries: 4,
|
|
173
|
-
maxCalls: 100,
|
|
174
|
-
maxOutputBytes: 8 * 1024 * 1024,
|
|
175
|
-
maxGeneratedTokens: 100000,
|
|
176
|
-
maxWallTimeMs: 120000
|
|
177
|
-
}
|
|
178
|
-
}))
|
|
179
|
-
.addText('Translate this book to neutral Latin American Spanish.')
|
|
180
|
-
.message();
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
Markdown headings become stable nested sections, lists expose item arrays, and the original source order remains reconstructable. The planner receives only a content-free variable manifest: paths, types, array item counts, serialized UTF-8 byte estimates, string lengths, line and paragraph counts, structural summaries, and partition hints. The document values enter only the isolated sandbox, where generated JavaScript can inspect `variables` and call registered workers through `query()`.
|
|
184
|
-
|
|
185
|
-
A worker normally supplies `model: anotherModelMixInstance`. To offer the current parent chain under a name, register it with `useParent: true` instead; defining both is rejected.
|
|
186
|
-
|
|
187
|
-
Planner instructions live in Markdown templates under `plugins/rlm/prompts/`. The plugin supplies manifests and limits through ModelMix `assign()` and loads the system prompt through `systemFile`, so relative includes work and runtime values are rendered exactly once.
|
|
188
|
-
|
|
189
|
-
## 🎛️ Unified Effort Scale
|
|
190
|
-
|
|
191
|
-
Control reasoning depth with one ModelMix policy value (`-1` adaptive, or `0`–`100`). It lives **outside** native `options` and is mapped to each provider’s effort API at request time.
|
|
192
|
-
|
|
193
|
-
```javascript
|
|
194
|
-
// In config (ModelMix.new or per-model shorthand)
|
|
195
|
-
ModelMix.new({ config: { effort: 50 } }).opus5().addText('...').message();
|
|
196
|
-
ModelMix.new().deepseekV4Flash({ config: { effort: 100 } }).addText('...').message();
|
|
197
|
-
|
|
198
|
-
// Fluent
|
|
199
|
-
ModelMix.new().effort(-1).minimaxM3().addText('...').message();
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
**Native wins:** if you already set a provider-native field (`reasoning_effort`, `output_config.effort`, `thinkingConfig`, etc.), unified `effort` is ignored for that request.
|
|
203
|
-
|
|
204
|
-
| | 0–19 | 20–39 | 40–59 | 60–79 | 80–100 | `-1` |
|
|
205
|
-
|--|------|-------|-------|-------|--------|------|
|
|
206
|
-
| OpenAI | `none` | `low` | `medium` | `high` | `xhigh` | — |
|
|
207
|
-
| Anthropic | `low` | `medium` | `high` | `xhigh` | `max` | adaptive |
|
|
208
|
-
| Gemini 3+ | `minimal` | `low` | `medium` | `high` | — | dynamic |
|
|
209
|
-
| DeepSeek V4 | off | `low`↑ | `high`↑ | `high`↑ | `max`↑ | — |
|
|
210
|
-
| MiniMax M3 | off | adaptive | adaptive | adaptive | adaptive | adaptive |
|
|
211
|
-
|
|
212
|
-
### Provider-specific behavior
|
|
213
|
-
|
|
214
|
-
- **Gemini:** Gemini 3+ uses bands 0–24 / 25–49 / 50–74 / 75–100. Gemini 3.7 Flash clamps these bands to `low` / `low` / `medium` / `high`; `-1` leaves its native `medium` default unchanged. Gemini 2.5 maps 0–100 to `thinkingBudget`.
|
|
215
|
-
- **DeepSeek:** `↑` means thinking is enabled; `off` means it is disabled.
|
|
216
|
-
- **MiniMax:** `off` maps to `thinking.disabled`; `adaptive` maps to `thinking.type=adaptive`.
|
|
217
|
-
- **Anthropic:** Claude 5, Fable, Opus 4.6+, and Sonnet 4.6+ use adaptive thinking with `output_config.effort`. Sonnet 4.5 and Haiku 4.5 use `thinking.type=enabled` with `budget_tokens`.
|
|
218
|
-
- **Grok 4.6:** 0–39 / 40–59 / 60–79 / 80–100 map to `low` / `medium` / `high` / `xhigh`. Without effort, Grok uses its native `high` default.
|
|
219
|
-
|
|
220
|
-
`-1` uses the provider's adaptive or dynamic mode when available; otherwise it is a no-op. Effort levels are clamped to each model's supported range.
|
|
221
|
-
|
|
222
|
-
### Migrating from thinking shorthands
|
|
223
|
-
|
|
224
|
-
The former `*think()` methods were removed. Use `.effort(n).<model>()` with `0`–`100` or `-1` instead.
|
|
225
|
-
|
|
226
|
-
- **Kimi:** use `kimiK25()` or `kimiK26()`.
|
|
227
|
-
- **Grok 4.20:** `.grok420()` selects the non-reasoning model. Use `.effort(20+).grok420()` or `.effort(-1).grok420()` to select the reasoning model.
|
|
228
|
-
|
|
229
|
-
## 🔧 Model Context Protocol (MCP) Integration
|
|
230
|
-
|
|
231
|
-
ModelMix makes it incredibly easy to enhance your AI models with powerful capabilities through the Model Context Protocol. With just a few lines of code, you can add features like web search, code execution, or any custom functionality to your models.
|
|
232
|
-
|
|
233
|
-
### Example: Adding Web Search Capability
|
|
234
|
-
|
|
235
|
-
Include the API key for Brave Search in your .env file.
|
|
236
|
-
```
|
|
237
|
-
BRAVE_API_KEY="BSA0..._fm"
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
```javascript
|
|
241
|
-
const mmix = ModelMix.new({ config: { max_history: 10 } }).gpt56sol();
|
|
242
|
-
mmix.setSystem('You are an assistant and today is ' + new Date().toISOString());
|
|
243
|
-
|
|
244
|
-
// Add web search capability through MCP
|
|
245
|
-
await mmix.addMCP('@modelcontextprotocol/server-brave-search');
|
|
246
|
-
mmix.addText('Use Internet: When did the last Christian pope die?');
|
|
247
|
-
console.log(await mmix.message());
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
This simple integration allows your model to:
|
|
251
|
-
- Search the web in real-time
|
|
252
|
-
- Access up-to-date information
|
|
253
|
-
- Combine AI reasoning with external data
|
|
254
|
-
|
|
255
|
-
The Model Context Protocol makes it easy to add any capability to your models, from web search to code execution, database queries, or custom functions. All with just a few lines of code!
|
|
256
|
-
|
|
257
136
|
## ⚡️ Shorthand Methods
|
|
258
137
|
|
|
259
138
|
ModelMix provides convenient shorthand methods for quickly accessing different AI models.
|
|
@@ -296,17 +175,20 @@ Here's a comprehensive list of available methods:
|
|
|
296
175
|
| `grok43()` | Grok | grok-4.3 | [\$1.25/\$2.50][6] |
|
|
297
176
|
| `grok420multiAgent()`| Grok | grok-4.20-multi-agent-0309 | [\$1.25/\$2.50][6] |
|
|
298
177
|
| `grok420()` | Grok | grok-4.20-0309 (†) | [\$1.25/\$2.50][6] |
|
|
178
|
+
| `qwen35397b()` | OpenRouter | qwen/qwen3.5-397b-a17b | [\$0.385/\$2.45][14] |
|
|
299
179
|
| `qwen36plus()` | Fireworks | qwen3p6-plus | [\$0.50/\$3.00][10] |
|
|
300
180
|
| `qwen37plus()` | Fireworks | models/qwen3p7-plus | [\$0.40/\$1.60][10] |
|
|
301
|
-
| `qwen38max()` |
|
|
181
|
+
| `qwen38max()` | Fireworks | qwen3p8-2p4t-a95b | [\$2.00/\$6.00][10] |
|
|
302
182
|
| `deepseekV4Flash()` | Fireworks | models/deepseek-v4-flash | [\$0.14/\$0.28][10] |
|
|
303
|
-
| `deepseekV4Pro()` | Fireworks | models/deepseek-v4-pro
|
|
183
|
+
| `deepseekV4Pro()` | Fireworks | models/deepseek-v4-pro-0813 | [\$1.32/\$3.96][12] |
|
|
304
184
|
| `GLM52()` | Together | zai-org/GLM-5.2 | [\$1.40/\$4.40][7] |
|
|
305
185
|
| `GLM51()` | Fireworks | models/glm-5p1 | [\$1.05/\$3.50][10] |
|
|
306
186
|
| `minimaxM3()` | MiniMax | MiniMax-M3 | [\$0.30/\$1.20][9] |
|
|
307
187
|
| `minimaxM27()` | MiniMax | MiniMax-M2.7 | [\$0.30/\$1.20][9] |
|
|
308
188
|
| `sonar()` | Perplexity | sonar | [\$1.00/\$1.00][4] |
|
|
309
189
|
| `sonarPro()` | Perplexity | sonar-pro | [\$3.00/\$15.00][4] |
|
|
190
|
+
| `hermes470b()` | OpenRouter | nousresearch/hermes-4-70b | [\$0.13/\$0.40][13] |
|
|
191
|
+
| `hermes4405b()` | OpenRouter | nousresearch/hermes-4-405b | [\$1.00/\$3.00][13] |
|
|
310
192
|
| `hermes3()` | Lambda | Hermes-3-Llama-3.1-405B-FP8 | [\$0.80/\$0.80][8] |
|
|
311
193
|
| `kimiK3()` | Moonshot | kimi-k3 | [\$3.00/\$15.00][11] |
|
|
312
194
|
| `kimiK25()` | Together | Kimi-K2.5 | [\$0.50/\$2.80][7] |
|
|
@@ -325,7 +207,9 @@ Gemini 3.7 Flash and 3.6 Flash use Google's introductory standard pricing throug
|
|
|
325
207
|
[9]: https://platform.minimax.io/docs/api-reference/anthropic-api-compatible-cache#supported-models-and-pricing "MiniMax Pricing"
|
|
326
208
|
[10]: https://fireworks.ai/pricing#serverless-pricing "Fireworks Pricing"
|
|
327
209
|
[11]: https://platform.kimi.ai/docs/guide/kimi-k3-pricing "Kimi K3 Pricing"
|
|
328
|
-
[12]: https://
|
|
210
|
+
[12]: https://fireworks.ai/models/deepseek-ai/deepseek-v4-pro-0813 "DeepSeek V4 Pro 0813 Pricing"
|
|
211
|
+
[13]: https://openrouter.ai/nousresearch "Nous Research Models on OpenRouter"
|
|
212
|
+
[14]: https://openrouter.ai/qwen/qwen3.5-397b-a17b "Qwen3.5 397B A17B on OpenRouter"
|
|
329
213
|
|
|
330
214
|
Each method accepts optional `options`, `config`, and (for multi-provider methods) `mix` parameters to customize behavior.
|
|
331
215
|
|
|
@@ -339,6 +223,46 @@ const result = await ModelMix.new({
|
|
|
339
223
|
.message();
|
|
340
224
|
```
|
|
341
225
|
|
|
226
|
+
## 🎛️ Unified Effort Scale
|
|
227
|
+
|
|
228
|
+
Control reasoning depth with one ModelMix policy value (`-1` adaptive, or `0`–`100`). It lives **outside** native `options` and is mapped to each provider’s effort API at request time.
|
|
229
|
+
|
|
230
|
+
```javascript
|
|
231
|
+
// In config (ModelMix.new or per-model shorthand)
|
|
232
|
+
ModelMix.new({ config: { effort: 50 } }).opus5().addText('...').message();
|
|
233
|
+
ModelMix.new().deepseekV4Flash({ config: { effort: 100 } }).addText('...').message();
|
|
234
|
+
|
|
235
|
+
// Fluent
|
|
236
|
+
ModelMix.new().effort(-1).minimaxM3().addText('...').message();
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
**Native wins:** if you already set a provider-native field (`reasoning_effort`, `output_config.effort`, `thinkingConfig`, etc.), unified `effort` is ignored for that request.
|
|
240
|
+
|
|
241
|
+
| | 0–19 | 20–39 | 40–59 | 60–79 | 80–100 | `-1` |
|
|
242
|
+
|--|------|-------|-------|-------|--------|------|
|
|
243
|
+
| OpenAI | `none` | `low` | `medium` | `high` | `xhigh` | — |
|
|
244
|
+
| Anthropic | `low` | `medium` | `high` | `xhigh` | `max` | adaptive |
|
|
245
|
+
| Gemini 3+ | `minimal` | `low` | `medium` | `high` | — | dynamic |
|
|
246
|
+
| DeepSeek V4 | off | `low`↑ | `high`↑ | `high`↑ | `max`↑ | — |
|
|
247
|
+
| MiniMax M3 | off | adaptive | adaptive | adaptive | adaptive | adaptive |
|
|
248
|
+
|
|
249
|
+
### Provider-specific behavior
|
|
250
|
+
|
|
251
|
+
- **Gemini:** Gemini 3+ uses bands 0–24 / 25–49 / 50–74 / 75–100. Gemini 3.7 Flash clamps these bands to `low` / `low` / `medium` / `high`; `-1` leaves its native `medium` default unchanged. Gemini 2.5 maps 0–100 to `thinkingBudget`.
|
|
252
|
+
- **DeepSeek:** `↑` means thinking is enabled; `off` means it is disabled.
|
|
253
|
+
- **MiniMax:** `off` maps to `thinking.disabled`; `adaptive` maps to `thinking.type=adaptive`.
|
|
254
|
+
- **Anthropic:** Claude 5, Fable, Opus 4.6+, and Sonnet 4.6+ use adaptive thinking with `output_config.effort`. Sonnet 4.5 and Haiku 4.5 use `thinking.type=enabled` with `budget_tokens`.
|
|
255
|
+
- **Grok 4.6:** 0–39 / 40–59 / 60–79 / 80–100 map to `low` / `medium` / `high` / `xhigh`. Without effort, Grok uses its native `high` default.
|
|
256
|
+
|
|
257
|
+
`-1` uses the provider's adaptive or dynamic mode when available; otherwise it is a no-op. Effort levels are clamped to each model's supported range.
|
|
258
|
+
|
|
259
|
+
### Migrating from thinking shorthands
|
|
260
|
+
|
|
261
|
+
The former `*think()` methods were removed. Use `.effort(n).<model>()` with `0`–`100` or `-1` instead.
|
|
262
|
+
|
|
263
|
+
- **Kimi:** use `kimiK25()` or `kimiK26()`.
|
|
264
|
+
- **Grok 4.20:** `.grok420()` selects the non-reasoning model. Use `.effort(20+).grok420()` or `.effort(-1).grok420()` to select the reasoning model.
|
|
265
|
+
|
|
342
266
|
## 🔄 Templates
|
|
343
267
|
|
|
344
268
|
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.
|
|
@@ -800,6 +724,39 @@ console.log(model.lastRaw.tokens);
|
|
|
800
724
|
|
|
801
725
|
`thinking` contains internal reasoning tokens when a provider reports them separately; cost calculation bills them at the output rate. `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).
|
|
802
726
|
|
|
727
|
+
## 🧠 Prompt Caching
|
|
728
|
+
|
|
729
|
+
Prompt caching reuses the stable beginning of a prompt at the provider level. It does not cache the answer: every call still generates a new response.
|
|
730
|
+
|
|
731
|
+
For GPT-5.6, keep the long, reusable instructions first, mark the end of that stable prefix, and add the changing request afterward:
|
|
732
|
+
|
|
733
|
+
```javascript
|
|
734
|
+
async function ask(question) {
|
|
735
|
+
const model = ModelMix.new()
|
|
736
|
+
.gpt56luna({
|
|
737
|
+
options: {
|
|
738
|
+
prompt_cache_key: 'support-rules-v1',
|
|
739
|
+
prompt_cache_options: { mode: 'explicit', ttl: '30m' }
|
|
740
|
+
}
|
|
741
|
+
})
|
|
742
|
+
.addTextFromFile('./prompts/support.md', {
|
|
743
|
+
role: 'developer',
|
|
744
|
+
cache: { breakpoint: true }
|
|
745
|
+
})
|
|
746
|
+
.addText(question);
|
|
747
|
+
|
|
748
|
+
const answer = await model.message();
|
|
749
|
+
const { cached, cacheWrite, cacheHitRate } = model.lastRaw.tokens;
|
|
750
|
+
console.log({ cached, cacheWrite, cacheHitRate });
|
|
751
|
+
return answer;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
await ask('Summarize support ticket 123.');
|
|
755
|
+
await ask('Summarize support ticket 456.');
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
The contents of `support.md` and the cache key stay the same between calls; only the final question changes. The first request may report `cacheWrite > 0`, while later requests confirm reuse with `cached > 0`. For GPT-5.6, the stable prefix must contain at least 1,024 tokens. Keep all variable content after the breakpoint, and change `prompt_cache_key` when the stable instructions change.
|
|
759
|
+
|
|
803
760
|
### GPT-5.6 prompt caching
|
|
804
761
|
|
|
805
762
|
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.
|
|
@@ -856,6 +813,34 @@ const model = ModelMix.new()
|
|
|
856
813
|
|
|
857
814
|
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.
|
|
858
815
|
|
|
816
|
+
## 🔧 Model Context Protocol (MCP) Integration
|
|
817
|
+
|
|
818
|
+
ModelMix makes it incredibly easy to enhance your AI models with powerful capabilities through the Model Context Protocol. With just a few lines of code, you can add features like web search, code execution, or any custom functionality to your models.
|
|
819
|
+
|
|
820
|
+
### Example: Adding Web Search Capability
|
|
821
|
+
|
|
822
|
+
Include the API key for Brave Search in your .env file.
|
|
823
|
+
```
|
|
824
|
+
BRAVE_API_KEY="BSA0..._fm"
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
```javascript
|
|
828
|
+
const mmix = ModelMix.new({ config: { max_history: 10 } }).gpt56sol();
|
|
829
|
+
mmix.setSystem('You are an assistant and today is ' + new Date().toISOString());
|
|
830
|
+
|
|
831
|
+
// Add web search capability through MCP
|
|
832
|
+
await mmix.addMCP('@modelcontextprotocol/server-brave-search');
|
|
833
|
+
mmix.addText('Use Internet: When did the last Christian pope die?');
|
|
834
|
+
console.log(await mmix.message());
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
This simple integration allows your model to:
|
|
838
|
+
- Search the web in real-time
|
|
839
|
+
- Access up-to-date information
|
|
840
|
+
- Combine AI reasoning with external data
|
|
841
|
+
|
|
842
|
+
The Model Context Protocol makes it easy to add any capability to your models, from web search to code execution, database queries, or custom functions. All with just a few lines of code!
|
|
843
|
+
|
|
859
844
|
## 🐛 Enabling Debug Mode
|
|
860
845
|
|
|
861
846
|
To activate debug mode in ModelMix and view detailed request information, follow these two steps:
|
|
@@ -929,6 +914,91 @@ Behavior summary:
|
|
|
929
914
|
- If retry is enabled, ModelMix retries the same model only for configured transient status codes.
|
|
930
915
|
- After retries are exhausted (or for non-retryable errors), ModelMix continues with normal fallback chain.
|
|
931
916
|
|
|
917
|
+
## 🔌 Instance Plugins
|
|
918
|
+
|
|
919
|
+
Plugins wrap one ModelMix instance without changing global behavior. They run in registration order after templates are rendered and before provider-specific request conversion:
|
|
920
|
+
|
|
921
|
+
```javascript
|
|
922
|
+
const metrics = {
|
|
923
|
+
name: 'metrics',
|
|
924
|
+
async execute(context, next) {
|
|
925
|
+
const startedAt = Date.now();
|
|
926
|
+
const result = await next();
|
|
927
|
+
return { ...result, elapsedMs: Date.now() - startedAt };
|
|
928
|
+
}
|
|
929
|
+
};
|
|
930
|
+
|
|
931
|
+
const model = ModelMix.new()
|
|
932
|
+
.gpt56luna()
|
|
933
|
+
.use(metrics)
|
|
934
|
+
.addText('Summarize this request.');
|
|
935
|
+
```
|
|
936
|
+
|
|
937
|
+
A plugin may edit `context.request`, call `next()`, or return a complete ModelMix result itself. It can also create history-free child executions with `context.invoke()` and choose plugin inheritance:
|
|
938
|
+
|
|
939
|
+
```javascript
|
|
940
|
+
const child = await context.invoke({
|
|
941
|
+
systemFile: './prompts/extract-entities.md',
|
|
942
|
+
assign: { outputLanguage: 'Spanish' },
|
|
943
|
+
messages: [{ role: 'user', content: section }],
|
|
944
|
+
plugins: { exclude: ['recursive-plugin'] },
|
|
945
|
+
history: false
|
|
946
|
+
});
|
|
947
|
+
```
|
|
948
|
+
|
|
949
|
+
Supported policies are `'inherit'`, `'none'`, `{ include: [...] }`, and `{ exclude: [...] }`. Child metadata exposes `executionId`, `parentExecutionId`, and `depth` to middleware. `.new()` inherits registered plugins but not message history.
|
|
950
|
+
|
|
951
|
+
Child `systemFile` templates use the same EJS engine, `assign()` data contract, and relative Markdown includes as ordinary ModelMix templates. Use either `system` or `systemFile`, not both.
|
|
952
|
+
|
|
953
|
+
### Recursive Language Model plugin
|
|
954
|
+
|
|
955
|
+
The separately publishable `@modelmix/rlm` workspace package keeps document parsing, planner prompts, and `isolated-vm` out of the core `modelmix` dependency tree. It requires Node.js 22 or newer.
|
|
956
|
+
|
|
957
|
+
```javascript
|
|
958
|
+
const { ModelMix } = require('modelmix');
|
|
959
|
+
const { rlm } = require('@modelmix/rlm');
|
|
960
|
+
|
|
961
|
+
const fast = ModelMix.new().gpt41mini();
|
|
962
|
+
|
|
963
|
+
const result = await ModelMix.new()
|
|
964
|
+
.gpt56luna()
|
|
965
|
+
.use(rlm({
|
|
966
|
+
maxDepth: 2,
|
|
967
|
+
documents: {
|
|
968
|
+
book: {
|
|
969
|
+
format: 'markdown',
|
|
970
|
+
content: markdownBook
|
|
971
|
+
}
|
|
972
|
+
},
|
|
973
|
+
workers: {
|
|
974
|
+
fast: {
|
|
975
|
+
model: fast,
|
|
976
|
+
intelligence: 2,
|
|
977
|
+
cost: 1,
|
|
978
|
+
speed: 4,
|
|
979
|
+
description: 'Translation, extraction, and simple transformations'
|
|
980
|
+
}
|
|
981
|
+
},
|
|
982
|
+
limits: {
|
|
983
|
+
maxQueryBytes: 64 * 1024,
|
|
984
|
+
sandboxMemoryBytes: 64 * 1024 * 1024,
|
|
985
|
+
maxConcurrentQueries: 4,
|
|
986
|
+
maxCalls: 100,
|
|
987
|
+
maxOutputBytes: 8 * 1024 * 1024,
|
|
988
|
+
maxGeneratedTokens: 100000,
|
|
989
|
+
maxWallTimeMs: 120000
|
|
990
|
+
}
|
|
991
|
+
}))
|
|
992
|
+
.addText('Translate this book to neutral Latin American Spanish.')
|
|
993
|
+
.message();
|
|
994
|
+
```
|
|
995
|
+
|
|
996
|
+
Markdown headings become stable nested sections, lists expose item arrays, and the original source order remains reconstructable. The planner receives only a content-free variable manifest: paths, types, array item counts, serialized UTF-8 byte estimates, string lengths, line and paragraph counts, structural summaries, and partition hints. The document values enter only the isolated sandbox, where generated JavaScript can inspect `variables` and call registered workers through `query()`.
|
|
997
|
+
|
|
998
|
+
A worker normally supplies `model: anotherModelMixInstance`. To offer the current parent chain under a name, register it with `useParent: true` instead; defining both is rejected.
|
|
999
|
+
|
|
1000
|
+
Planner instructions live in Markdown templates under `plugins/rlm/prompts/`. The plugin supplies manifests and limits through ModelMix `assign()` and loads the system prompt through `systemFile`, so relative includes work and runtime values are rendered exactly once.
|
|
1001
|
+
|
|
932
1002
|
## 📚 ModelMix Class Overview
|
|
933
1003
|
|
|
934
1004
|
```javascript
|
package/demo/short.js
CHANGED
|
@@ -14,6 +14,9 @@ const mmix = await ModelMix.new(setup)
|
|
|
14
14
|
.gemini37flash() // (fallback 2) Google gemini-3.7-flash
|
|
15
15
|
.gpt41nano() // (fallback 3) OpenAI gpt-4.1-nano
|
|
16
16
|
.grok46() // (fallback 4) Grok grok-4.6
|
|
17
|
+
.qwen35397b() // (fallback 5) OpenRouter qwen/qwen3.5-397b-a17b
|
|
18
|
+
.hermes470b() // (fallback 6) OpenRouter nousresearch/hermes-4-70b
|
|
19
|
+
.hermes4405b() // (fallback 7) OpenRouter nousresearch/hermes-4-405b
|
|
17
20
|
.addText("What's your name?");
|
|
18
21
|
|
|
19
22
|
console.log(await mmix.message());
|
package/effort.js
CHANGED
|
@@ -35,6 +35,7 @@ const GEMINI_BANDS = [
|
|
|
35
35
|
|
|
36
36
|
/** Exact model → supported OpenAI reasoning_effort values */
|
|
37
37
|
const OPENAI_MODEL_LEVELS = {
|
|
38
|
+
'accounts/fireworks/models/qwen3p8-2p4t-a95b': ['none', 'low', 'medium', 'high'],
|
|
38
39
|
'grok-4.6': ['low', 'medium', 'high', 'xhigh'],
|
|
39
40
|
'gpt-5': ['minimal', 'low', 'medium', 'high'],
|
|
40
41
|
'gpt-5-mini': ['minimal', 'low', 'medium', 'high'],
|
package/index.d.ts
CHANGED
|
@@ -436,6 +436,8 @@ export declare class ModelMix {
|
|
|
436
436
|
assign(keyValues: Record<string, unknown>): this;
|
|
437
437
|
assignKey(key: string, value: unknown): this;
|
|
438
438
|
effort(value: EffortValue): this;
|
|
439
|
+
/** Attach an ordered model chain. Use `shortcut@effort` for a per-model override. */
|
|
440
|
+
chain(...modelSpecs: string[]): this;
|
|
439
441
|
attach(key: string, provider: MixCustom): this;
|
|
440
442
|
|
|
441
443
|
// OpenAI
|
|
@@ -502,9 +504,12 @@ export declare class ModelMix {
|
|
|
502
504
|
|
|
503
505
|
// Multi-provider
|
|
504
506
|
qwen3(args?: ModelAttachArgs): this;
|
|
507
|
+
qwen35397b(args?: ModelAttachArgs): this;
|
|
505
508
|
qwen36plus(args?: ModelAttachArgs): this;
|
|
506
509
|
qwen37plus(args?: ModelAttachArgs): this;
|
|
507
510
|
qwen38max(args?: ModelAttachArgs): this;
|
|
511
|
+
hermes470b(args?: ModelAttachArgs): this;
|
|
512
|
+
hermes4405b(args?: ModelAttachArgs): this;
|
|
508
513
|
hermes3(args?: ModelAttachArgs): this;
|
|
509
514
|
kimiK26(args?: ModelAttachArgs): this;
|
|
510
515
|
kimiK27Code(args?: ModelAttachArgs): this;
|
package/index.js
CHANGED
|
@@ -362,6 +362,7 @@ const MODEL_PRICING = {
|
|
|
362
362
|
// Fireworks
|
|
363
363
|
'accounts/fireworks/models/deepseek-v4-flash': { input: 0.14, output: 0.28 },
|
|
364
364
|
'accounts/fireworks/models/deepseek-v4-pro': { input: 1.74, output: 3.48 },
|
|
365
|
+
'accounts/fireworks/models/deepseek-v4-pro-0813': { input: 1.32, cachedInput: 0.044, output: 3.96 },
|
|
365
366
|
'deepseek-ai/DeepSeek-V4-Flash': { input: 0.14, output: 0.28 },
|
|
366
367
|
'deepseek-ai/DeepSeek-V4-Pro': { input: 2.10, output: 4.40 },
|
|
367
368
|
'deepseek/deepseek-v4-flash': { input: 0.09, output: 0.18 },
|
|
@@ -369,10 +370,12 @@ const MODEL_PRICING = {
|
|
|
369
370
|
'accounts/fireworks/models/glm-5p1': { input: 1.05, output: 3.50 },
|
|
370
371
|
'zai-org/GLM-5.2': { input: 1.40, output: 4.40 },
|
|
371
372
|
'accounts/fireworks/models/kimi-k2p5': { input: 0.50, output: 2.80 },
|
|
373
|
+
'qwen/qwen3.5-397b-a17b': { input: 0.385, output: 2.45 },
|
|
372
374
|
'accounts/fireworks/models/qwen3p6-plus': { input: 0.50, output: 3.00 },
|
|
373
375
|
'Qwen/Qwen3.6-Plus': { input: 0.50, output: 3.00 },
|
|
374
376
|
'accounts/fireworks/models/qwen3p7-plus': { input: 0.40, output: 1.60 },
|
|
375
377
|
'qwen/qwen3.7-plus': { input: 0.32, output: 1.28 },
|
|
378
|
+
'accounts/fireworks/models/qwen3p8-2p4t-a95b': { input: 2.00, cachedInput: 0.25, output: 6.00 },
|
|
376
379
|
'qwen/qwen3.8-max': { input: 2.00, output: 6.00 },
|
|
377
380
|
// MiniMax
|
|
378
381
|
'MiniMax-M2.5': { input: 0.30, output: 1.20 },
|
|
@@ -384,7 +387,10 @@ const MODEL_PRICING = {
|
|
|
384
387
|
// Perplexity
|
|
385
388
|
'sonar': { input: 1.00, output: 1.00 },
|
|
386
389
|
'sonar-pro': { input: 3.00, output: 15.00 },
|
|
387
|
-
//
|
|
390
|
+
// Hermes 4 (OpenRouter)
|
|
391
|
+
'nousresearch/hermes-4-70b': { input: 0.13, output: 0.40 },
|
|
392
|
+
'nousresearch/hermes-4-405b': { input: 1.00, output: 3.00 },
|
|
393
|
+
// Hermes 3 (Lambda/OpenRouter)
|
|
388
394
|
'Hermes-3-Llama-3.1-405B-FP8': { input: 0.80, output: 0.80 },
|
|
389
395
|
'nousresearch/hermes-3-llama-3.1-405b:free': { input: 0, output: 0 },
|
|
390
396
|
// Qwen3 (Together/Cerebras)
|
|
@@ -401,6 +407,51 @@ const MODEL_PRICING = {
|
|
|
401
407
|
'zai-glm-4.7': { input: 0.55, output: 2.19 },
|
|
402
408
|
};
|
|
403
409
|
|
|
410
|
+
const CHAIN_MODEL_SHORTCUTS = new Set([
|
|
411
|
+
'gpt41', 'gpt41mini', 'gpt41nano', 'gpt5', 'gpt5mini', 'gpt5nano',
|
|
412
|
+
'gpt51', 'gpt52', 'gpt54', 'gpt54mini', 'gpt54nano', 'gpt54pro',
|
|
413
|
+
'gpt55', 'gpt55pro', 'gpt56sol', 'gpt56terra', 'gpt56luna',
|
|
414
|
+
'gptRealtime', 'gptRealtimeMini', 'gpt53codex', 'gpt53chat', 'gptOss',
|
|
415
|
+
'fable50', 'fable5', 'opus50', 'opus5', 'opus48', 'opus47', 'opus46',
|
|
416
|
+
'sonnet50', 'sonnet5', 'sonnet46', 'sonnet45', 'haiku45',
|
|
417
|
+
'gemini25flash', 'gemini31pro', 'gemini3pro', 'gemini3flash',
|
|
418
|
+
'gemini37flash', 'gemini36flash', 'gemini35flash', 'gemini35flashLite',
|
|
419
|
+
'gemini31flashLite', 'gemini25pro', 'sonarPro', 'sonar',
|
|
420
|
+
'grok46', 'grok45', 'grok43', 'grok420multiAgent', 'grok420',
|
|
421
|
+
'qwen3', 'qwen35397b', 'qwen36plus', 'qwen37plus', 'qwen38max',
|
|
422
|
+
'hermes470b', 'hermes4405b', 'hermes3',
|
|
423
|
+
'kimiK26', 'kimiK27Code', 'kimiK3', 'kimiK25',
|
|
424
|
+
'minimaxM25', 'minimaxM27', 'minimaxM3', 'mimo25', 'mimo25pro',
|
|
425
|
+
'deepseekV4Pro', 'deepseekV4Flash', 'GLM51', 'GLM52'
|
|
426
|
+
]);
|
|
427
|
+
|
|
428
|
+
function parseChainModels(modelSpecs) {
|
|
429
|
+
if (modelSpecs.length === 0) {
|
|
430
|
+
throw new TypeError('chain() requires at least one model shortcut string.');
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return modelSpecs.map((modelSpec, index) => {
|
|
434
|
+
if (typeof modelSpec !== 'string') {
|
|
435
|
+
throw new TypeError(`Invalid chain model at index ${index}: expected a model shortcut string.`);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const match = /^([A-Za-z_$][A-Za-z0-9_$]*)(?:@(-?\d+))?$/.exec(modelSpec);
|
|
439
|
+
if (!match) {
|
|
440
|
+
throw new TypeError(`Invalid chain model "${modelSpec}": expected "shortcut" or "shortcut@effort".`);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const shortcut = match[1];
|
|
444
|
+
if (!CHAIN_MODEL_SHORTCUTS.has(shortcut)) {
|
|
445
|
+
throw new Error(`Unknown model shortcut "${shortcut}" in chain().`);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return {
|
|
449
|
+
shortcut,
|
|
450
|
+
effort: match[2] === undefined ? undefined : normalizeEffort(Number(match[2]))
|
|
451
|
+
};
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
404
455
|
class ModelMix {
|
|
405
456
|
|
|
406
457
|
constructor({ options = {}, config = {}, mix = {} } = {}) {
|
|
@@ -483,6 +534,18 @@ class ModelMix {
|
|
|
483
534
|
return this;
|
|
484
535
|
}
|
|
485
536
|
|
|
537
|
+
chain(...modelSpecs) {
|
|
538
|
+
const models = parseChainModels(modelSpecs);
|
|
539
|
+
for (const { shortcut, effort } of models) {
|
|
540
|
+
if (effort === undefined) {
|
|
541
|
+
this[shortcut]();
|
|
542
|
+
} else {
|
|
543
|
+
this[shortcut]({ config: { effort } });
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return this;
|
|
547
|
+
}
|
|
548
|
+
|
|
486
549
|
use(plugin) {
|
|
487
550
|
if (!isPlainObject(plugin)) {
|
|
488
551
|
throw new TypeError('plugin must be a plain object.');
|
|
@@ -1034,6 +1097,10 @@ class ModelMix {
|
|
|
1034
1097
|
return this;
|
|
1035
1098
|
}
|
|
1036
1099
|
|
|
1100
|
+
qwen35397b({ options = {}, config = {} } = {}) {
|
|
1101
|
+
return this.attach('qwen/qwen3.5-397b-a17b', new MixOpenRouter({ options, config }));
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1037
1104
|
qwen36plus({ options = {}, config = {}, mix = { fireworks: true } } = {}) {
|
|
1038
1105
|
mix = { ...this.mix, ...mix };
|
|
1039
1106
|
if (mix.fireworks) this.attach('accounts/fireworks/models/qwen3p6-plus', new MixFireworks({ options, config }));
|
|
@@ -1048,12 +1115,21 @@ class ModelMix {
|
|
|
1048
1115
|
return this;
|
|
1049
1116
|
}
|
|
1050
1117
|
|
|
1051
|
-
qwen38max({ options = {}, config = {}, mix = {
|
|
1118
|
+
qwen38max({ options = {}, config = {}, mix = { fireworks: true } } = {}) {
|
|
1052
1119
|
mix = { ...this.mix, ...mix };
|
|
1120
|
+
if (mix.fireworks) this.attach('accounts/fireworks/models/qwen3p8-2p4t-a95b', new MixFireworks({ options, config }));
|
|
1053
1121
|
if (mix.openrouter) this.attach('qwen/qwen3.8-max', new MixOpenRouter({ options, config }));
|
|
1054
1122
|
return this;
|
|
1055
1123
|
}
|
|
1056
1124
|
|
|
1125
|
+
hermes470b({ options = {}, config = {} } = {}) {
|
|
1126
|
+
return this.attach('nousresearch/hermes-4-70b', new MixOpenRouter({ options, config }));
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
hermes4405b({ options = {}, config = {} } = {}) {
|
|
1130
|
+
return this.attach('nousresearch/hermes-4-405b', new MixOpenRouter({ options, config }));
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1057
1133
|
hermes3({ options = {}, config = {}, mix = {} } = {}) {
|
|
1058
1134
|
mix = { ...this.mix, ...mix };
|
|
1059
1135
|
if (mix.lambda) this.attach('Hermes-3-Llama-3.1-405B-FP8', new MixLambda({ options, config }));
|
|
@@ -1135,7 +1211,7 @@ class ModelMix {
|
|
|
1135
1211
|
deepseekV4Pro({ options = {}, config = {}, mix = { fireworks: true } } = {}) {
|
|
1136
1212
|
mix = { ...this.mix, ...mix };
|
|
1137
1213
|
if (mix.nvidia) this.attach('deepseek-ai/deepseek-v4-pro', new MixNVIDIA({ options, config }));
|
|
1138
|
-
if (mix.fireworks) this.attach('accounts/fireworks/models/deepseek-v4-pro', new MixFireworks({ options, config }));
|
|
1214
|
+
if (mix.fireworks) this.attach('accounts/fireworks/models/deepseek-v4-pro-0813', new MixFireworks({ options, config }));
|
|
1139
1215
|
if (mix.openrouter) this.attach('deepseek/deepseek-v4-pro', new MixOpenRouter({ options, config }));
|
|
1140
1216
|
if (mix.together) this.attach('deepseek-ai/DeepSeek-V4-Pro', new MixTogether({ options, config }));
|
|
1141
1217
|
return this;
|
package/package.json
CHANGED
package/skills/modelmix/SKILL.md
CHANGED
|
@@ -92,6 +92,17 @@ const model = ModelMix.new()
|
|
|
92
92
|
|
|
93
93
|
If `sonnet46` fails, it automatically tries `gpt52`, then `gemini3flash`.
|
|
94
94
|
|
|
95
|
+
The equivalent `chain()` form accepts public shortcut names directly in the
|
|
96
|
+
same order. Append `@effort` for a per-model unified effort override (`-1` or
|
|
97
|
+
`0`–`100`). Without the suffix, the entry inherits `config.effort`, or keeps the
|
|
98
|
+
provider default when no chain effort is configured:
|
|
99
|
+
|
|
100
|
+
```javascript
|
|
101
|
+
const model = ModelMix.new()
|
|
102
|
+
.chain('sonnet46', 'gpt52@20', 'gemini3flash@-1')
|
|
103
|
+
.addText('Hello!');
|
|
104
|
+
```
|
|
105
|
+
|
|
95
106
|
### Instance plugins
|
|
96
107
|
|
|
97
108
|
Register middleware with `.use({ name, execute })`. Plugins are scoped to the instance, run in registration order, and receive the rendered provider-neutral request. They may edit `context.request`, call `next()`, or return a complete ModelMix result.
|
|
@@ -166,13 +177,13 @@ Use `.effort(n)` (or `config.effort`) to enable Anthropic thinking — e.g. `.ef
|
|
|
166
177
|
`minimaxM25()` `minimaxM27()` `minimaxM3()`
|
|
167
178
|
|
|
168
179
|
### Fireworks
|
|
169
|
-
`qwen36plus()` `qwen37plus()` `deepseekV4Flash()` `deepseekV4Pro()` `kimiK26()`
|
|
180
|
+
`qwen36plus()` `qwen37plus()` `qwen38max()` `deepseekV4Flash()` `deepseekV4Pro()` `kimiK26()`
|
|
170
181
|
|
|
171
182
|
### Cerebras
|
|
172
183
|
`GLM46()`
|
|
173
184
|
|
|
174
185
|
### OpenRouter
|
|
175
|
-
`qwen38max()` `GLM45()`
|
|
186
|
+
`qwen35397b()` `hermes470b()` `hermes4405b()` `qwen38max()` `GLM45()`
|
|
176
187
|
|
|
177
188
|
### Multi-provider (auto-fallback across free/paid tiers)
|
|
178
189
|
`hermes3()` `kimiK25()`
|
package/test/deepseek.test.js
CHANGED
|
@@ -7,7 +7,12 @@ describe('DeepSeek Model Registration Tests', () => {
|
|
|
7
7
|
model.deepseekV4Pro({ mix: { fireworks: true, openrouter: false } });
|
|
8
8
|
|
|
9
9
|
expect(model.models).to.have.length(1);
|
|
10
|
-
expect(model.models[0].key).to.equal('accounts/fireworks/models/deepseek-v4-pro');
|
|
10
|
+
expect(model.models[0].key).to.equal('accounts/fireworks/models/deepseek-v4-pro-0813');
|
|
11
|
+
expect(ModelMix.calculateCost('accounts/fireworks/models/deepseek-v4-pro-0813', {
|
|
12
|
+
input: 1_000_000,
|
|
13
|
+
cached: 250_000,
|
|
14
|
+
output: 1_000_000
|
|
15
|
+
})).to.be.closeTo(4.961, 1e-10);
|
|
11
16
|
});
|
|
12
17
|
|
|
13
18
|
it('should register Together DeepSeek V4 Pro when together mix is enabled', () => {
|
package/test/effort.test.js
CHANGED
|
@@ -81,6 +81,12 @@ describe('Unified effort scale', () => {
|
|
|
81
81
|
expect(mapEffort('openai', 10, 'gpt-oss-120b')).to.deep.equal({ reasoning_effort: 'low' });
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
+
it('clamps Fireworks Qwen 3.8 Max to its supported reasoning levels', () => {
|
|
85
|
+
const key = 'accounts/fireworks/models/qwen3p8-2p4t-a95b';
|
|
86
|
+
expect(mapEffort('openai', 0, key)).to.deep.equal({ reasoning_effort: 'none' });
|
|
87
|
+
expect(mapEffort('openai', 100, key)).to.deep.equal({ reasoning_effort: 'high' });
|
|
88
|
+
});
|
|
89
|
+
|
|
84
90
|
it('maps Anthropic adaptive models to thinking + output_config.effort', () => {
|
|
85
91
|
expect(mapEffort('anthropic', 10, 'claude-opus-5')).to.deep.equal({
|
|
86
92
|
thinking: { type: 'adaptive', display: 'summarized' },
|
|
@@ -178,6 +184,10 @@ describe('Unified effort scale', () => {
|
|
|
178
184
|
reasoning_effort: 'max',
|
|
179
185
|
thinking: { type: 'enabled' }
|
|
180
186
|
});
|
|
187
|
+
expect(mapEffort('openai', 100, 'accounts/fireworks/models/deepseek-v4-pro-0813')).to.deep.equal({
|
|
188
|
+
reasoning_effort: 'max',
|
|
189
|
+
thinking: { type: 'enabled' }
|
|
190
|
+
});
|
|
181
191
|
// No adaptive control on DeepSeek → no-op
|
|
182
192
|
expect(mapEffort('openai', -1, 'deepseek/deepseek-v4-flash')).to.equal(null);
|
|
183
193
|
});
|
package/test/fallback.test.js
CHANGED
|
@@ -26,6 +26,40 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
26
26
|
});
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
+
it('should attach chain shortcuts from arguments with optional per-model effort', () => {
|
|
30
|
+
model.chain('sonnet5', 'gpt56luna@20', 'gemini37flash@-1');
|
|
31
|
+
|
|
32
|
+
expect(model.models.map(({ key }) => key)).to.deep.equal([
|
|
33
|
+
'claude-sonnet-5',
|
|
34
|
+
'gpt-5.6-luna',
|
|
35
|
+
'gemini-3.7-flash'
|
|
36
|
+
]);
|
|
37
|
+
expect(model.models[0].provider.config).to.not.have.property('effort');
|
|
38
|
+
expect(model.models[1].provider.config.effort).to.equal(20);
|
|
39
|
+
expect(model.models[2].provider.config.effort).to.equal(-1);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should keep the chain default effort when an entry omits it', () => {
|
|
43
|
+
model.effort(60).chain('gpt56luna', 'sonnet5@20');
|
|
44
|
+
|
|
45
|
+
expect(model.config.effort).to.equal(60);
|
|
46
|
+
expect(model.models[0].provider.config).to.not.have.property('effort');
|
|
47
|
+
expect(model.models[1].provider.config.effort).to.equal(20);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should reject invalid chains before attaching any model', () => {
|
|
51
|
+
expect(() => model.chain('gpt56luna', 'unknownModel'))
|
|
52
|
+
.to.throw('Unknown model shortcut "unknownModel" in chain().');
|
|
53
|
+
expect(model.models).to.have.length(0);
|
|
54
|
+
|
|
55
|
+
expect(() => model.chain('gpt56luna@101'))
|
|
56
|
+
.to.throw(/Invalid effort/);
|
|
57
|
+
expect(() => model.chain())
|
|
58
|
+
.to.throw('chain() requires at least one model shortcut string.');
|
|
59
|
+
expect(() => model.chain(['gpt56luna']))
|
|
60
|
+
.to.throw('Invalid chain model at index 0: expected a model shortcut string.');
|
|
61
|
+
});
|
|
62
|
+
|
|
29
63
|
it('should use primary provider when available', async () => {
|
|
30
64
|
model.gpt5mini().sonnet46().addText('Hello');
|
|
31
65
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const { expect } = require('chai');
|
|
2
|
+
const { ModelMix, MixOpenRouter } = require('../index.js');
|
|
3
|
+
|
|
4
|
+
describe('Hermes Model Registration Tests', () => {
|
|
5
|
+
it('should register Hermes 4 70B through OpenRouter', () => {
|
|
6
|
+
const model = ModelMix.new().hermes470b();
|
|
7
|
+
|
|
8
|
+
expect(model.models).to.have.length(1);
|
|
9
|
+
expect(model.models[0].key).to.equal('nousresearch/hermes-4-70b');
|
|
10
|
+
expect(model.models[0].provider).to.be.instanceOf(MixOpenRouter);
|
|
11
|
+
expect(ModelMix.calculateCost('nousresearch/hermes-4-70b', {
|
|
12
|
+
input: 1_000_000,
|
|
13
|
+
output: 1_000_000
|
|
14
|
+
})).to.equal(0.53);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should register Hermes 4 405B through OpenRouter', () => {
|
|
18
|
+
const model = ModelMix.new().hermes4405b();
|
|
19
|
+
|
|
20
|
+
expect(model.models).to.have.length(1);
|
|
21
|
+
expect(model.models[0].key).to.equal('nousresearch/hermes-4-405b');
|
|
22
|
+
expect(model.models[0].provider).to.be.instanceOf(MixOpenRouter);
|
|
23
|
+
expect(ModelMix.calculateCost('nousresearch/hermes-4-405b', {
|
|
24
|
+
input: 1_000_000,
|
|
25
|
+
output: 1_000_000
|
|
26
|
+
})).to.equal(4);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should support both Hermes 4 shortcuts in chain()', () => {
|
|
30
|
+
const model = ModelMix.new().chain('hermes470b', 'hermes4405b');
|
|
31
|
+
|
|
32
|
+
expect(model.models.map(({ key }) => key)).to.deep.equal([
|
|
33
|
+
'nousresearch/hermes-4-70b',
|
|
34
|
+
'nousresearch/hermes-4-405b'
|
|
35
|
+
]);
|
|
36
|
+
});
|
|
37
|
+
});
|
package/test/qwen.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const { expect } = require('chai');
|
|
2
|
-
const { ModelMix } = require('../index.js');
|
|
2
|
+
const { ModelMix, MixOpenRouter } = require('../index.js');
|
|
3
3
|
|
|
4
4
|
describe('Qwen Model Registration Tests', () => {
|
|
5
5
|
it('should register Fireworks Qwen 3.6 Plus by default', () => {
|
|
@@ -38,15 +38,45 @@ describe('Qwen Model Registration Tests', () => {
|
|
|
38
38
|
expect(model.models[0].key).to.equal('qwen/qwen3.7-plus');
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
it('should register
|
|
41
|
+
it('should register Fireworks Qwen 3.8 Max before the OpenRouter fallback by default', () => {
|
|
42
42
|
const model = ModelMix.new();
|
|
43
43
|
model.qwen38max();
|
|
44
44
|
|
|
45
|
+
expect(model.models.map(({ key }) => key)).to.deep.equal([
|
|
46
|
+
'accounts/fireworks/models/qwen3p8-2p4t-a95b',
|
|
47
|
+
'qwen/qwen3.8-max'
|
|
48
|
+
]);
|
|
49
|
+
expect(ModelMix.calculateCost('accounts/fireworks/models/qwen3p8-2p4t-a95b', {
|
|
50
|
+
input: 1_000_000,
|
|
51
|
+
cached: 250_000,
|
|
52
|
+
output: 1_000_000
|
|
53
|
+
})).to.equal(7.5625);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should register only OpenRouter Qwen 3.8 Max when Fireworks is disabled', () => {
|
|
57
|
+
const model = ModelMix.new();
|
|
58
|
+
model.qwen38max({ mix: { fireworks: false, openrouter: true } });
|
|
59
|
+
|
|
45
60
|
expect(model.models).to.have.length(1);
|
|
46
61
|
expect(model.models[0].key).to.equal('qwen/qwen3.8-max');
|
|
47
|
-
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should register Qwen 3.5 397B A17B through OpenRouter', () => {
|
|
65
|
+
const model = ModelMix.new().qwen35397b();
|
|
66
|
+
|
|
67
|
+
expect(model.models).to.have.length(1);
|
|
68
|
+
expect(model.models[0].key).to.equal('qwen/qwen3.5-397b-a17b');
|
|
69
|
+
expect(model.models[0].provider).to.be.instanceOf(MixOpenRouter);
|
|
70
|
+
expect(ModelMix.calculateCost('qwen/qwen3.5-397b-a17b', {
|
|
48
71
|
input: 1_000_000,
|
|
49
72
|
output: 1_000_000
|
|
50
|
-
})).to.equal(
|
|
73
|
+
})).to.equal(2.835);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should support Qwen 3.5 397B A17B in chain()', () => {
|
|
77
|
+
const model = ModelMix.new().chain('qwen35397b');
|
|
78
|
+
|
|
79
|
+
expect(model.models).to.have.length(1);
|
|
80
|
+
expect(model.models[0].key).to.equal('qwen/qwen3.5-397b-a17b');
|
|
51
81
|
});
|
|
52
82
|
});
|