modelmix 4.6.7 β 4.6.10
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 +32 -5
- package/demo/fireworks.js +3 -6
- package/demo/free.js +0 -1
- package/demo/groq.js +1 -1
- package/demo/nvidia.js +1 -1
- package/demo/together.js +1 -1
- package/effort.js +370 -0
- package/index.js +53 -29
- package/package.json +2 -2
- package/skills/modelmix/SKILL.md +31 -5
- package/test/deepseek.test.js +40 -0
- package/test/effort.test.js +359 -0
- package/test/setup.js +2 -0
package/README.md
CHANGED
|
@@ -90,7 +90,6 @@ console.log(ETH.price);
|
|
|
90
90
|
ModelMix.new()
|
|
91
91
|
.gptOss()
|
|
92
92
|
.kimiK25think()
|
|
93
|
-
.deepseekR1()
|
|
94
93
|
.hermes3()
|
|
95
94
|
.addText('What is the capital of France?');
|
|
96
95
|
```
|
|
@@ -102,6 +101,31 @@ This pattern allows you to:
|
|
|
102
101
|
- Track token usage across all providers
|
|
103
102
|
- Keep your code clean and maintainable
|
|
104
103
|
|
|
104
|
+
## ποΈ Unified Effort Scale
|
|
105
|
+
|
|
106
|
+
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.
|
|
107
|
+
|
|
108
|
+
```javascript
|
|
109
|
+
// In config (ModelMix.new or per-model shorthand)
|
|
110
|
+
ModelMix.new({ config: { effort: 50 } }).opus5().addText('...').message();
|
|
111
|
+
ModelMix.new().deepseekV4Flash({ config: { effort: 100 } }).addText('...').message();
|
|
112
|
+
|
|
113
|
+
// Fluent
|
|
114
|
+
ModelMix.new().effort(-1).minimaxM3().addText('...').message();
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Native wins:** if you already set a provider-native field (`reasoning_effort`, `output_config.effort`, `thinkingConfig`, etc.), unified `effort` is ignored for that request.
|
|
118
|
+
|
|
119
|
+
| | 0β19 | 20β39 | 40β59 | 60β79 | 80β100 | `-1` |
|
|
120
|
+
|--|------|-------|-------|-------|--------|------|
|
|
121
|
+
| OpenAI | `none` | `low` | `medium` | `high` | `xhigh` | β |
|
|
122
|
+
| Anthropic | `low` | `medium` | `high` | `xhigh` | `max` | adaptive |
|
|
123
|
+
| Gemini 3+\* | `minimal` | `low` | `medium` | `high` | β | dynamic |
|
|
124
|
+
| DeepSeek V4 | off | `low`β | `high`β | `high`β | `max`β | β |
|
|
125
|
+
| MiniMax M3 | off | adaptive | adaptive | adaptive | adaptive | adaptive |
|
|
126
|
+
|
|
127
|
+
\* Gemini bands: 0β24 / 25β49 / 50β74 / 75β100. DeepSeek `β` = thinking on; `off` = thinking disabled. MiniMax `off`/`adaptive` = `thinking.disabled` / `thinking.type=adaptive`. Gemini 2.5 maps 0β100 to `thinkingBudget`. `-1` = provider adaptive/dynamic when available, else no-op. Levels clamp to what each model supports.
|
|
128
|
+
|
|
105
129
|
## π§ Model Context Protocol (MCP) Integration
|
|
106
130
|
|
|
107
131
|
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.
|
|
@@ -138,8 +162,8 @@ Here's a comprehensive list of available methods:
|
|
|
138
162
|
| Method | Provider | Model | Price (I/O) per 1 M tokens |
|
|
139
163
|
| ------------------- | ---------- | ---------------------------- | -------------------------- |
|
|
140
164
|
| `gpt56sol()` | OpenAI | gpt-5.6-sol | [\$5.00 / \$30.00][1] |
|
|
141
|
-
| `gpt56terra()` | OpenAI | gpt-5.6-terra | [\$2.
|
|
142
|
-
| `gpt56luna()` | OpenAI | gpt-5.6-luna | [\$
|
|
165
|
+
| `gpt56terra()` | OpenAI | gpt-5.6-terra | [\$2.00 / \$12.00][1] |
|
|
166
|
+
| `gpt56luna()` | OpenAI | gpt-5.6-luna | [\$0.20 / \$1.20][1] |
|
|
143
167
|
| `gpt55()` | OpenAI | gpt-5.5 | [\$5.00 / \$30.00][1] |
|
|
144
168
|
| `gpt54()` | OpenAI | gpt-5.4 | [\$2.50 / \$15.00][1] |
|
|
145
169
|
| `gpt54mini()` | OpenAI | gpt-5.4-mini | [\$0.75 / \$4.50][1] |
|
|
@@ -170,6 +194,7 @@ Here's a comprehensive list of available methods:
|
|
|
170
194
|
| `grok420[think]()` | Grok | grok-4.20-0309 | [\$1.25 / \$2.50][6] |
|
|
171
195
|
| `grok41[think]()` | Grok | grok-4-1-fast | [\$0.20 / \$0.50][6] |
|
|
172
196
|
| `qwen36plus()` | Fireworks/Together | qwen3p6-plus / Qwen3.6-Plus | [\$0.50 / \$3.00][10] |
|
|
197
|
+
| `deepseekV4Flash()` | Fireworks | models/deepseek-v4-flash | [\$0.14 / \$0.28][10] |
|
|
173
198
|
| `deepseekV4Pro()` | Fireworks | models/deepseek-v4-pro | [\$1.74 / \$3.48][10] |
|
|
174
199
|
| `GLM52()` | Together | zai-org/GLM-5.2 | [\$1.40 / \$4.40][7] |
|
|
175
200
|
| `GLM51()` | Fireworks | models/glm-5p1 | [\$1.05 / \$3.50][10] |
|
|
@@ -195,7 +220,7 @@ Here's a comprehensive list of available methods:
|
|
|
195
220
|
[11]: https://platform.kimi.ai/docs/guide/kimi-k3-pricing "Kimi K3 Pricing"
|
|
196
221
|
|
|
197
222
|
Each method accepts optional `options`, `config`, and (for multi-provider methods) `mix` parameters to customize behavior.
|
|
198
|
-
For NVIDIA on DeepSeek V4 Pro, use `deepseekV4Pro({ mix: { nvidia: true } })`.
|
|
223
|
+
For NVIDIA on DeepSeek V4 Flash/Pro, use `deepseekV4Flash({ mix: { nvidia: true } })` or `deepseekV4Pro({ mix: { nvidia: true } })`.
|
|
199
224
|
For Together on Qwen 3.6 Plus, use `qwen36plus({ mix: { fireworks: false, together: true } })`.
|
|
200
225
|
For OpenRouter instead of Moonshot's native API, use `kimiK3({ mix: { moonshot: false, openrouter: true } })`.
|
|
201
226
|
|
|
@@ -204,7 +229,7 @@ const result = await ModelMix.new({
|
|
|
204
229
|
options: { temperature: 0.7 },
|
|
205
230
|
config: { system: "You are a helpful assistant" }
|
|
206
231
|
})
|
|
207
|
-
.
|
|
232
|
+
.gpt56luna()
|
|
208
233
|
.addText("Tell me a story about a cat");
|
|
209
234
|
.message();
|
|
210
235
|
```
|
|
@@ -617,6 +642,7 @@ new ModelMix(args = { options: {}, config: {} })
|
|
|
617
642
|
- **config**: This object contains configuration settings that control the behavior of the `ModelMix` instance. These settings can also be overridden for specific model instances. Examples of configuration settings include:
|
|
618
643
|
- `system`: Sets the default system message for the model, e.g., "You are an assistant."
|
|
619
644
|
- `max_history`: Limits the number of historical messages to retain, e.g., 1.
|
|
645
|
+
- `effort`: Unified reasoning effort (`-1` adaptive, or `0`β`100`). Not a native provider field β use `config.effort` or `.effort(n)`.
|
|
620
646
|
- `roundRobin`: When `true`, rotates through attached models on each request for load balancing. When `false` (default), uses fallback mode where models are tried sequentially only if previous ones fail.
|
|
621
647
|
- `bottleneck`: Configures the rate limiting behavior using Bottleneck. For example:
|
|
622
648
|
- `maxConcurrent`: Maximum number of concurrent requests
|
|
@@ -637,6 +663,7 @@ new ModelMix(args = { options: {}, config: {} })
|
|
|
637
663
|
- `attach(modelKey, modelInstance)`: Attaches a model instance to the `ModelMix`.
|
|
638
664
|
- `new()`: `static` Creates a new `ModelMix`.
|
|
639
665
|
- `new()`: Creates a new `ModelMix` using instance setup.
|
|
666
|
+
- `effort(n)`: Sets unified effort (`-1` or `0`β`100`) on `config.effort`.
|
|
640
667
|
|
|
641
668
|
- `setSystem(text)`: Sets the system prompt.
|
|
642
669
|
- `setSystemFromFile(filePath)`: Sets the system prompt from a file.
|
package/demo/fireworks.js
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import { ModelMix } from '../index.js';
|
|
2
|
-
try { process.loadEnvFile(); } catch {}
|
|
2
|
+
try { process.loadEnvFile(); } catch { }
|
|
3
3
|
|
|
4
4
|
async function main() {
|
|
5
5
|
try {
|
|
6
6
|
const ai = ModelMix.new();
|
|
7
7
|
|
|
8
|
-
const response = await ai
|
|
9
|
-
.
|
|
10
|
-
.deepseekV32()
|
|
11
|
-
.GLM47()
|
|
8
|
+
const response = await ai.effort(50)
|
|
9
|
+
.deepseekV4Flash()
|
|
12
10
|
.addText('What is the capital of France?')
|
|
13
11
|
.message();
|
|
14
12
|
|
|
@@ -20,4 +18,3 @@ async function main() {
|
|
|
20
18
|
}
|
|
21
19
|
|
|
22
20
|
main();
|
|
23
|
-
|
package/demo/free.js
CHANGED
package/demo/groq.js
CHANGED
package/demo/nvidia.js
CHANGED
|
@@ -2,7 +2,7 @@ import { ModelMix } from '../index.js';
|
|
|
2
2
|
try { process.loadEnvFile(); } catch { }
|
|
3
3
|
|
|
4
4
|
const model = ModelMix.new()
|
|
5
|
-
.deepseekV4Flash()
|
|
5
|
+
.deepseekV4Flash({ mix: { nvidia: true } })
|
|
6
6
|
.addText("Create exactly 5 characters for a narrative game.")
|
|
7
7
|
|
|
8
8
|
const jsonResult = await model.json([], [{
|
package/demo/together.js
CHANGED
|
@@ -4,7 +4,7 @@ try { process.loadEnvFile(); } catch {}
|
|
|
4
4
|
const setup = { config: { system: "You are ALF from Melmac." } };
|
|
5
5
|
|
|
6
6
|
let r = ModelMix.new()
|
|
7
|
-
.attach('deepseek-ai/DeepSeek-
|
|
7
|
+
.attach('deepseek-ai/DeepSeek-V4-Flash', new MixTogether(setup))
|
|
8
8
|
.addText('hi there')
|
|
9
9
|
.addText('do you like cats?')
|
|
10
10
|
.message();
|
package/effort.js
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified effort scale for ModelMix.
|
|
3
|
+
*
|
|
4
|
+
* Policy value: integer -1 (adaptive) or 0..100.
|
|
5
|
+
* Stored in config.effort (never in native options).
|
|
6
|
+
* Mapped to provider-native fields only when native effort controls are absent.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const OPENAI_LEVELS = ['none', 'low', 'medium', 'high', 'xhigh'];
|
|
10
|
+
const ANTHROPIC_LEVELS = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
11
|
+
const GEMINI_LEVELS = ['minimal', 'low', 'medium', 'high'];
|
|
12
|
+
|
|
13
|
+
const OPENAI_BANDS = [
|
|
14
|
+
[0, 19, 'none'],
|
|
15
|
+
[20, 39, 'low'],
|
|
16
|
+
[40, 59, 'medium'],
|
|
17
|
+
[60, 79, 'high'],
|
|
18
|
+
[80, 100, 'xhigh'],
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const ANTHROPIC_BANDS = [
|
|
22
|
+
[0, 19, 'low'],
|
|
23
|
+
[20, 39, 'medium'],
|
|
24
|
+
[40, 59, 'high'],
|
|
25
|
+
[60, 79, 'xhigh'],
|
|
26
|
+
[80, 100, 'max'],
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const GEMINI_BANDS = [
|
|
30
|
+
[0, 24, 'minimal'],
|
|
31
|
+
[25, 49, 'low'],
|
|
32
|
+
[50, 74, 'medium'],
|
|
33
|
+
[75, 100, 'high'],
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
/** Exact model β supported OpenAI reasoning_effort values */
|
|
37
|
+
const OPENAI_MODEL_LEVELS = {
|
|
38
|
+
'gpt-5': ['minimal', 'low', 'medium', 'high'],
|
|
39
|
+
'gpt-5-mini': ['minimal', 'low', 'medium', 'high'],
|
|
40
|
+
'gpt-5-nano': ['minimal', 'low', 'medium', 'high'],
|
|
41
|
+
'gpt-5.3-codex': ['low', 'medium', 'high', 'xhigh'],
|
|
42
|
+
'gpt-oss-120b': ['low', 'medium', 'high'],
|
|
43
|
+
'openai/gpt-oss-120b': ['low', 'medium', 'high'],
|
|
44
|
+
'openai/gpt-oss-120b:free': ['low', 'medium', 'high'],
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* DeepSeek V4 (OpenAI-compatible): reasoning_effort low|high|max + thinking toggle.
|
|
49
|
+
* Official mapping: low/high/max; xhigh remaps per model (flashβhigh, proβmax).
|
|
50
|
+
* @see https://api-docs.deepseek.com/guides/thinking_mode
|
|
51
|
+
*/
|
|
52
|
+
const DEEPSEEK_LEVELS = ['none', 'low', 'high', 'max'];
|
|
53
|
+
const DEEPSEEK_BANDS = [
|
|
54
|
+
[0, 19, 'none'],
|
|
55
|
+
[20, 39, 'low'],
|
|
56
|
+
[40, 79, 'high'],
|
|
57
|
+
[80, 100, 'max'],
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* MiniMax M3 (OpenAI-compatible): thinking.type disabled | adaptive.
|
|
62
|
+
* @see https://platform.minimax.io/docs/api-reference/text-openai-api
|
|
63
|
+
*/
|
|
64
|
+
const MINIMAX_LEVELS = ['disabled', 'adaptive'];
|
|
65
|
+
const MINIMAX_BANDS = [
|
|
66
|
+
[0, 19, 'disabled'],
|
|
67
|
+
[20, 100, 'adaptive'],
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
/** Exact model β supported Gemini thinkingLevel values */
|
|
71
|
+
const GEMINI_MODEL_LEVELS = {
|
|
72
|
+
'gemini-3-pro-preview': ['low', 'high'],
|
|
73
|
+
'gemini-3.1-pro-preview': ['low', 'medium', 'high'],
|
|
74
|
+
'gemini-2.5-pro': ['low', 'medium', 'high'],
|
|
75
|
+
'gemini-2.5-flash': ['low', 'medium', 'high'],
|
|
76
|
+
'gemini-2.5-flash-lite': ['low', 'medium', 'high'],
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const GEMINI_25_BUDGET_MAX = {
|
|
80
|
+
'gemini-2.5-pro': 32768,
|
|
81
|
+
'gemini-2.5-flash': 24576,
|
|
82
|
+
'gemini-2.5-flash-lite': 24576,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const PROVIDER_FAMILY_BY_CLASS = {
|
|
86
|
+
MixAnthropic: 'anthropic',
|
|
87
|
+
MixGoogle: 'google',
|
|
88
|
+
MixOpenAI: 'openai',
|
|
89
|
+
MixOpenAIResponses: 'openai',
|
|
90
|
+
MixOpenAIWebSocket: 'openai',
|
|
91
|
+
MixOpenRouter: 'openai',
|
|
92
|
+
MixKimi: 'openai',
|
|
93
|
+
MixMiniMax: 'openai',
|
|
94
|
+
MixMiMo: 'openai',
|
|
95
|
+
MixGrok: 'openai',
|
|
96
|
+
MixGroq: 'openai',
|
|
97
|
+
MixTogether: 'openai',
|
|
98
|
+
MixCerebras: 'openai',
|
|
99
|
+
MixFireworks: 'openai',
|
|
100
|
+
MixNVIDIA: 'openai',
|
|
101
|
+
MixPerplexity: null,
|
|
102
|
+
MixOllama: null,
|
|
103
|
+
MixLMStudio: null,
|
|
104
|
+
MixLambda: null,
|
|
105
|
+
MixCustom: null,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
function normalizeEffort(value) {
|
|
109
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || !Number.isInteger(value)) {
|
|
110
|
+
throw new Error(`Invalid effort: expected integer -1 or 0..100, got ${JSON.stringify(value)}`);
|
|
111
|
+
}
|
|
112
|
+
if (value === -1) return -1;
|
|
113
|
+
if (value < 0 || value > 100) {
|
|
114
|
+
throw new Error(`Invalid effort: expected integer -1 or 0..100, got ${value}`);
|
|
115
|
+
}
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function levelFromBands(effort, bands) {
|
|
120
|
+
for (const [lo, hi, level] of bands) {
|
|
121
|
+
if (effort >= lo && effort <= hi) return level;
|
|
122
|
+
}
|
|
123
|
+
return bands[bands.length - 1][2];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function pickNearestLevel(desired, ladder, supported) {
|
|
127
|
+
if (supported.includes(desired)) return desired;
|
|
128
|
+
const desiredIdx = ladder.indexOf(desired);
|
|
129
|
+
if (desiredIdx === -1) return supported[Math.floor(supported.length / 2)];
|
|
130
|
+
|
|
131
|
+
let best = supported[0];
|
|
132
|
+
let bestDist = Infinity;
|
|
133
|
+
for (const level of supported) {
|
|
134
|
+
const idx = ladder.indexOf(level);
|
|
135
|
+
if (idx === -1) continue;
|
|
136
|
+
const dist = Math.abs(idx - desiredIdx);
|
|
137
|
+
if (dist < bestDist) {
|
|
138
|
+
bestDist = dist;
|
|
139
|
+
best = level;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return best;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function supportedOpenAILevels(modelKey) {
|
|
146
|
+
if (modelKey && OPENAI_MODEL_LEVELS[modelKey]) {
|
|
147
|
+
return OPENAI_MODEL_LEVELS[modelKey];
|
|
148
|
+
}
|
|
149
|
+
return OPENAI_LEVELS;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function supportedGeminiLevels(modelKey) {
|
|
153
|
+
if (modelKey && GEMINI_MODEL_LEVELS[modelKey]) {
|
|
154
|
+
return GEMINI_MODEL_LEVELS[modelKey];
|
|
155
|
+
}
|
|
156
|
+
return GEMINI_LEVELS;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function isGemini25(modelKey) {
|
|
160
|
+
return typeof modelKey === 'string' && modelKey.includes('2.5');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function isDeepSeekV4(modelKey) {
|
|
164
|
+
if (typeof modelKey !== 'string') return false;
|
|
165
|
+
const key = modelKey.toLowerCase();
|
|
166
|
+
return key.includes('deepseek-v4') || key.includes('deepseek_v4');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function isMiniMax(modelKey) {
|
|
170
|
+
return typeof modelKey === 'string' && modelKey.toLowerCase().includes('minimax');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function mapDeepSeekEffort(normalized) {
|
|
174
|
+
// DeepSeek V4 has no adaptive mode (only enabled/disabled + low|high|max)
|
|
175
|
+
if (normalized === -1) return null;
|
|
176
|
+
const level = levelFromBands(normalized, DEEPSEEK_BANDS);
|
|
177
|
+
if (level === 'none') {
|
|
178
|
+
return { thinking: { type: 'disabled' } };
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
reasoning_effort: level,
|
|
182
|
+
thinking: { type: 'enabled' }
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function mapMiniMaxEffort(normalized) {
|
|
187
|
+
if (normalized === -1) {
|
|
188
|
+
return { thinking: { type: 'adaptive' } };
|
|
189
|
+
}
|
|
190
|
+
const level = levelFromBands(normalized, MINIMAX_BANDS);
|
|
191
|
+
return { thinking: { type: level } };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Explicit adaptive mapping when the provider/model exposes it.
|
|
196
|
+
* Returns null when adaptive is not a settable native control.
|
|
197
|
+
*/
|
|
198
|
+
function mapAdaptiveEffort(providerFamily, modelKey) {
|
|
199
|
+
if (providerFamily === 'anthropic') {
|
|
200
|
+
return { thinking: { type: 'adaptive' } };
|
|
201
|
+
}
|
|
202
|
+
if (providerFamily === 'google') {
|
|
203
|
+
// Gemini dynamic thinking: thinkingBudget -1 (2.5 official; accepted on 3.x as dynamic)
|
|
204
|
+
return { thinkingConfig: { thinkingBudget: -1 } };
|
|
205
|
+
}
|
|
206
|
+
if (providerFamily === 'openai' && isMiniMax(modelKey)) {
|
|
207
|
+
return { thinking: { type: 'adaptive' } };
|
|
208
|
+
}
|
|
209
|
+
// OpenAI / DeepSeek: no adaptive enum β cannot set adaptive
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function gemini25BudgetMax(modelKey) {
|
|
214
|
+
if (modelKey && GEMINI_25_BUDGET_MAX[modelKey] != null) {
|
|
215
|
+
return GEMINI_25_BUDGET_MAX[modelKey];
|
|
216
|
+
}
|
|
217
|
+
return 24576;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* @returns {'openai'|'anthropic'|'google'|null}
|
|
222
|
+
*/
|
|
223
|
+
function resolveProviderFamily(providerInstance) {
|
|
224
|
+
if (!providerInstance || !providerInstance.constructor) return null;
|
|
225
|
+
let proto = providerInstance;
|
|
226
|
+
while (proto) {
|
|
227
|
+
const name = proto.constructor?.name;
|
|
228
|
+
if (name && Object.prototype.hasOwnProperty.call(PROVIDER_FAMILY_BY_CLASS, name)) {
|
|
229
|
+
return PROVIDER_FAMILY_BY_CLASS[name];
|
|
230
|
+
}
|
|
231
|
+
proto = Object.getPrototypeOf(proto);
|
|
232
|
+
if (!proto || proto === Object.prototype) break;
|
|
233
|
+
}
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function hasNativeEffort(family, options = {}, modelKey) {
|
|
238
|
+
if (family === 'openai') {
|
|
239
|
+
if (options.reasoning_effort != null && options.reasoning_effort !== '') return true;
|
|
240
|
+
// DeepSeek / MiniMax use thinking.type as the on/off (or adaptive) control
|
|
241
|
+
if ((isDeepSeekV4(modelKey) || isMiniMax(modelKey)) && options.thinking != null) return true;
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
if (family === 'anthropic') {
|
|
245
|
+
return options.output_config?.effort != null && options.output_config.effort !== '';
|
|
246
|
+
}
|
|
247
|
+
if (family === 'google') {
|
|
248
|
+
if (options.thinkingConfig != null) return true;
|
|
249
|
+
if (options.thinkingLevel != null) return true;
|
|
250
|
+
if (options.thinkingBudget != null) return true;
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Map unified effort to a provider-native patch object.
|
|
258
|
+
* Returns null when there is nothing to set (unsupported family, or -1 on a
|
|
259
|
+
* model with no adaptive control).
|
|
260
|
+
*
|
|
261
|
+
* @returns {object|null}
|
|
262
|
+
*/
|
|
263
|
+
function mapEffort(providerFamily, effort, modelKey) {
|
|
264
|
+
const normalized = normalizeEffort(effort);
|
|
265
|
+
|
|
266
|
+
if (!providerFamily) return null;
|
|
267
|
+
|
|
268
|
+
if (normalized === -1) {
|
|
269
|
+
return mapAdaptiveEffort(providerFamily, modelKey);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (providerFamily === 'openai') {
|
|
273
|
+
if (isDeepSeekV4(modelKey)) {
|
|
274
|
+
return mapDeepSeekEffort(normalized);
|
|
275
|
+
}
|
|
276
|
+
if (isMiniMax(modelKey)) {
|
|
277
|
+
return mapMiniMaxEffort(normalized);
|
|
278
|
+
}
|
|
279
|
+
const desired = levelFromBands(normalized, OPENAI_BANDS);
|
|
280
|
+
const level = pickNearestLevel(desired, OPENAI_LEVELS, supportedOpenAILevels(modelKey));
|
|
281
|
+
return { reasoning_effort: level };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (providerFamily === 'anthropic') {
|
|
285
|
+
const desired = levelFromBands(normalized, ANTHROPIC_BANDS);
|
|
286
|
+
const level = pickNearestLevel(desired, ANTHROPIC_LEVELS, ANTHROPIC_LEVELS);
|
|
287
|
+
return { output_config: { effort: level } };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (providerFamily === 'google') {
|
|
291
|
+
if (isGemini25(modelKey)) {
|
|
292
|
+
const max = gemini25BudgetMax(modelKey);
|
|
293
|
+
const budget = Math.round((normalized / 100) * max);
|
|
294
|
+
return { thinkingConfig: { thinkingBudget: budget } };
|
|
295
|
+
}
|
|
296
|
+
const desired = levelFromBands(normalized, GEMINI_BANDS);
|
|
297
|
+
const level = pickNearestLevel(desired, GEMINI_LEVELS, supportedGeminiLevels(modelKey));
|
|
298
|
+
return { thinkingConfig: { thinkingLevel: level } };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Apply config.effort onto options when native controls are absent.
|
|
306
|
+
* Mutates and returns options. Never writes ModelMix `effort` into the HTTP body.
|
|
307
|
+
*/
|
|
308
|
+
function applyUnifiedEffort(options, config, providerFamily, modelKey) {
|
|
309
|
+
if (!config || config.effort === undefined || config.effort === null) {
|
|
310
|
+
return options;
|
|
311
|
+
}
|
|
312
|
+
if (!providerFamily) return options;
|
|
313
|
+
if (hasNativeEffort(providerFamily, options, modelKey)) return options;
|
|
314
|
+
|
|
315
|
+
const patch = mapEffort(providerFamily, config.effort, modelKey);
|
|
316
|
+
if (!patch) return options;
|
|
317
|
+
|
|
318
|
+
if (patch.reasoning_effort !== undefined) {
|
|
319
|
+
options.reasoning_effort = patch.reasoning_effort;
|
|
320
|
+
}
|
|
321
|
+
if (patch.output_config) {
|
|
322
|
+
options.output_config = {
|
|
323
|
+
...(options.output_config || {}),
|
|
324
|
+
...patch.output_config
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
if (patch.thinking) {
|
|
328
|
+
options.thinking = {
|
|
329
|
+
...(options.thinking || {}),
|
|
330
|
+
...patch.thinking
|
|
331
|
+
};
|
|
332
|
+
// Adaptive thinking rejects budget_tokens; drop it if we switched mode.
|
|
333
|
+
if (patch.thinking.type === 'adaptive') {
|
|
334
|
+
delete options.thinking.budget_tokens;
|
|
335
|
+
}
|
|
336
|
+
// DeepSeek non-thinking: do not send reasoning_effort
|
|
337
|
+
if (patch.thinking.type === 'disabled') {
|
|
338
|
+
delete options.reasoning_effort;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (patch.thinkingConfig) {
|
|
342
|
+
options.thinkingConfig = {
|
|
343
|
+
...(options.thinkingConfig || {}),
|
|
344
|
+
...patch.thinkingConfig
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return options;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
module.exports = {
|
|
352
|
+
normalizeEffort,
|
|
353
|
+
mapEffort,
|
|
354
|
+
mapAdaptiveEffort,
|
|
355
|
+
applyUnifiedEffort,
|
|
356
|
+
hasNativeEffort,
|
|
357
|
+
resolveProviderFamily,
|
|
358
|
+
isDeepSeekV4,
|
|
359
|
+
isMiniMax,
|
|
360
|
+
levelFromBands,
|
|
361
|
+
pickNearestLevel,
|
|
362
|
+
OPENAI_BANDS,
|
|
363
|
+
ANTHROPIC_BANDS,
|
|
364
|
+
GEMINI_BANDS,
|
|
365
|
+
DEEPSEEK_BANDS,
|
|
366
|
+
OPENAI_LEVELS,
|
|
367
|
+
ANTHROPIC_LEVELS,
|
|
368
|
+
GEMINI_LEVELS,
|
|
369
|
+
DEEPSEEK_LEVELS,
|
|
370
|
+
};
|
package/index.js
CHANGED
|
@@ -20,6 +20,11 @@ const {
|
|
|
20
20
|
fetchBinaryResponse,
|
|
21
21
|
fetchStreamResponse
|
|
22
22
|
} = require('./http-client');
|
|
23
|
+
const {
|
|
24
|
+
normalizeEffort,
|
|
25
|
+
applyUnifiedEffort,
|
|
26
|
+
resolveProviderFamily
|
|
27
|
+
} = require('./effort');
|
|
23
28
|
|
|
24
29
|
const DEFAULT_RETRYABLE_STATUS_CODES = [408, 425, 429, 500, 502, 503, 504, 529];
|
|
25
30
|
|
|
@@ -38,8 +43,8 @@ const MODEL_PRICING = {
|
|
|
38
43
|
'gpt-realtime-mini': [0.60, 2.40],
|
|
39
44
|
'gpt-realtime': [4.00, 16.00],
|
|
40
45
|
'gpt-5.6-sol': [5.00, 30.00],
|
|
41
|
-
'gpt-5.6-terra': [2.
|
|
42
|
-
'gpt-5.6-luna': [
|
|
46
|
+
'gpt-5.6-terra': [2.00, 12.00],
|
|
47
|
+
'gpt-5.6-luna': [0.20, 1.20],
|
|
43
48
|
'gpt-5.5-pro': [30.00, 180.00],
|
|
44
49
|
'gpt-5.5': [5.00, 30.00],
|
|
45
50
|
'gpt-5.4': [2.50, 15.00],
|
|
@@ -90,9 +95,11 @@ const MODEL_PRICING = {
|
|
|
90
95
|
'grok-4-1-fast-reasoning': [0.20, 0.50],
|
|
91
96
|
'grok-4-1-fast-non-reasoning': [0.20, 0.50],
|
|
92
97
|
// Fireworks
|
|
93
|
-
'accounts/fireworks/models/deepseek-
|
|
98
|
+
'accounts/fireworks/models/deepseek-v4-flash': [0.14, 0.28],
|
|
94
99
|
'accounts/fireworks/models/deepseek-v4-pro': [1.74, 3.48],
|
|
100
|
+
'deepseek-ai/DeepSeek-V4-Flash': [0.14, 0.28],
|
|
95
101
|
'deepseek-ai/DeepSeek-V4-Pro': [2.10, 4.40],
|
|
102
|
+
'deepseek/deepseek-v4-flash': [0.09, 0.18],
|
|
96
103
|
'accounts/fireworks/models/glm-4p7': [0.55, 2.19],
|
|
97
104
|
'accounts/fireworks/models/glm-5p1': [1.05, 3.50],
|
|
98
105
|
'zai-org/GLM-5.2': [1.40, 4.40],
|
|
@@ -123,13 +130,9 @@ const MODEL_PRICING = {
|
|
|
123
130
|
// Kimi K3
|
|
124
131
|
'kimi-k3': [3.00, 15.00],
|
|
125
132
|
'moonshotai/kimi-k3': [3.00, 15.00],
|
|
126
|
-
// DeepSeek V3.2 (OpenRouter)
|
|
127
|
-
'deepseek/deepseek-v3.2': [0.56, 1.68],
|
|
128
133
|
// GLM 4.7 (OpenRouter/Cerebras)
|
|
129
134
|
'z-ai/glm-4.7': [0.55, 2.19],
|
|
130
135
|
'zai-glm-4.7': [0.55, 2.19],
|
|
131
|
-
// DeepSeek R1 (OpenRouter free)
|
|
132
|
-
'deepseek/deepseek-r1-0528:free': [0, 0],
|
|
133
136
|
};
|
|
134
137
|
|
|
135
138
|
class ModelMix {
|
|
@@ -168,6 +171,10 @@ class ModelMix {
|
|
|
168
171
|
},
|
|
169
172
|
roundRobin: false, // false=fallback mode, true=round robin rotation
|
|
170
173
|
...config
|
|
174
|
+
};
|
|
175
|
+
// Unified effort is ModelMix policy (config.effort / .effort()), not a native option.
|
|
176
|
+
if (this.config.effort !== undefined && this.config.effort !== null) {
|
|
177
|
+
this.config.effort = normalizeEffort(this.config.effort);
|
|
171
178
|
}
|
|
172
179
|
const freeMix = { openrouter: true, cerebras: true, groq: true, together: false, lambda: false };
|
|
173
180
|
this.mix = { ...freeMix, ...mix };
|
|
@@ -181,12 +188,26 @@ class ModelMix {
|
|
|
181
188
|
return this;
|
|
182
189
|
}
|
|
183
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Set unified reasoning effort: -1 (adaptive) or 0..100.
|
|
193
|
+
* Stored in config.effort; mapped to provider-native fields at request time
|
|
194
|
+
* unless a native effort control is already set (native wins).
|
|
195
|
+
*/
|
|
196
|
+
effort(value) {
|
|
197
|
+
this.config.effort = normalizeEffort(value);
|
|
198
|
+
return this;
|
|
199
|
+
}
|
|
200
|
+
|
|
184
201
|
static new({ options = {}, config = {}, mix = {} } = {}) {
|
|
185
202
|
return new ModelMix({ options, config, mix });
|
|
186
203
|
}
|
|
187
204
|
|
|
188
205
|
new({ options = {}, config = {}, mix = {} } = {}) {
|
|
189
|
-
const instance = new ModelMix({
|
|
206
|
+
const instance = new ModelMix({
|
|
207
|
+
options: { ...this.options, ...options },
|
|
208
|
+
config: { ...this.config, ...config },
|
|
209
|
+
mix: { ...this.mix, ...mix }
|
|
210
|
+
});
|
|
190
211
|
instance.models = this.models; // Share models array for round-robin rotation
|
|
191
212
|
return instance;
|
|
192
213
|
}
|
|
@@ -508,15 +529,6 @@ class ModelMix {
|
|
|
508
529
|
return this;
|
|
509
530
|
}
|
|
510
531
|
|
|
511
|
-
deepseekR1({ options = {}, config = {}, mix = {} } = {}) {
|
|
512
|
-
mix = { ...this.mix, ...mix };
|
|
513
|
-
if (mix.groq) this.attach('deepseek-r1-distill-llama-70b', new MixGroq({ options, config }));
|
|
514
|
-
if (mix.together) this.attach('deepseek-ai/DeepSeek-R1', new MixTogether({ options, config }));
|
|
515
|
-
if (mix.cerebras) this.attach('deepseek-r1-distill-llama-70b', new MixCerebras({ options, config }));
|
|
516
|
-
if (mix.openrouter) this.attach('deepseek/deepseek-r1-0528:free', new MixOpenRouter({ options, config }));
|
|
517
|
-
return this;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
532
|
hermes3({ options = {}, config = {}, mix = {} } = {}) {
|
|
521
533
|
mix = { ...this.mix, ...mix };
|
|
522
534
|
if (mix.lambda) this.attach('Hermes-3-Llama-3.1-405B-FP8', new MixLambda({ options, config }));
|
|
@@ -576,8 +588,8 @@ class ModelMix {
|
|
|
576
588
|
|
|
577
589
|
minimaxM3({ options = {}, config = {}, mix = { minimax: true, openrouter: false } } = {}) {
|
|
578
590
|
mix = { ...this.mix, ...mix };
|
|
579
|
-
if (mix.minimax) this.attach('MiniMax-M3', new MixMiniMax({ options, config }));
|
|
580
591
|
if (mix.openrouter) this.attach('minimax/minimax-m3', new MixOpenRouter({ options, config }));
|
|
592
|
+
if (mix.minimax) this.attach('MiniMax-M3', new MixMiniMax({ options, config }));
|
|
581
593
|
if (mix.together) this.attach('MiniMaxAI/MiniMax-M3', new MixTogether({ options, config }));
|
|
582
594
|
return this;
|
|
583
595
|
}
|
|
@@ -605,11 +617,14 @@ class ModelMix {
|
|
|
605
617
|
return this;
|
|
606
618
|
}
|
|
607
619
|
|
|
608
|
-
deepseekV4Flash({ options = {}, config = {}, mix = {
|
|
620
|
+
deepseekV4Flash({ options = {}, config = {}, mix = { fireworks: true } } = {}) {
|
|
609
621
|
mix = { ...this.mix, ...mix };
|
|
610
622
|
if (mix.nvidia) this.attach('deepseek-ai/deepseek-v4-flash', new MixNVIDIA({ options, config }));
|
|
623
|
+
if (mix.fireworks) this.attach('accounts/fireworks/models/deepseek-v4-flash', new MixFireworks({ options, config }));
|
|
624
|
+
if (mix.openrouter) this.attach('deepseek/deepseek-v4-flash', new MixOpenRouter({ options, config }));
|
|
625
|
+
if (mix.together) this.attach('deepseek-ai/DeepSeek-V4-Flash', new MixTogether({ options, config }));
|
|
611
626
|
return this;
|
|
612
|
-
}
|
|
627
|
+
}
|
|
613
628
|
|
|
614
629
|
GLM51({ options = {}, config = {}, mix = { fireworks: true } } = {}) {
|
|
615
630
|
mix = { ...this.mix, ...mix };
|
|
@@ -632,14 +647,6 @@ class ModelMix {
|
|
|
632
647
|
return this;
|
|
633
648
|
}
|
|
634
649
|
|
|
635
|
-
GLM47({ options = {}, config = {}, mix = { fireworks: true } } = {}) {
|
|
636
|
-
mix = { ...this.mix, ...mix };
|
|
637
|
-
if (mix.fireworks) this.attach('accounts/fireworks/models/glm-4p7', new MixFireworks({ options, config }));
|
|
638
|
-
if (mix.openrouter) this.attach('z-ai/glm-4.7', new MixOpenRouter({ options, config }));
|
|
639
|
-
if (mix.cerebras) this.attach('zai-glm-4.7', new MixCerebras({ options, config }));
|
|
640
|
-
return this;
|
|
641
|
-
}
|
|
642
|
-
|
|
643
650
|
addText(text, { role = "user" } = {}) {
|
|
644
651
|
const content = [{
|
|
645
652
|
type: "text",
|
|
@@ -1038,6 +1045,10 @@ class ModelMix {
|
|
|
1038
1045
|
}
|
|
1039
1046
|
};
|
|
1040
1047
|
|
|
1048
|
+
// Unified effort β native provider fields (skipped if native already set)
|
|
1049
|
+
const providerFamily = resolveProviderFamily(providerInstance);
|
|
1050
|
+
applyUnifiedEffort(currentOptions, currentConfig, providerFamily, currentModelKey);
|
|
1051
|
+
|
|
1041
1052
|
if (currentConfig.debug >= 1) {
|
|
1042
1053
|
const isPrimary = i === 0;
|
|
1043
1054
|
const prefix = isPrimary ? 'β' : 'β»';
|
|
@@ -2892,6 +2903,19 @@ class MixGoogle extends MixCustom {
|
|
|
2892
2903
|
generationConfig.topP = options.top_p;
|
|
2893
2904
|
}
|
|
2894
2905
|
|
|
2906
|
+
// Thinking / effort (from unified config.effort or native options)
|
|
2907
|
+
if (options.thinkingConfig) {
|
|
2908
|
+
generationConfig.thinkingConfig = options.thinkingConfig;
|
|
2909
|
+
} else if (options.thinkingLevel != null || options.thinkingBudget != null) {
|
|
2910
|
+
generationConfig.thinkingConfig = {};
|
|
2911
|
+
if (options.thinkingLevel != null) {
|
|
2912
|
+
generationConfig.thinkingConfig.thinkingLevel = options.thinkingLevel;
|
|
2913
|
+
}
|
|
2914
|
+
if (options.thinkingBudget != null) {
|
|
2915
|
+
generationConfig.thinkingConfig.thinkingBudget = options.thinkingBudget;
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2895
2919
|
// Gemini does not support responseMimeType when function calling is used
|
|
2896
2920
|
const hasTools = options.tools && options.tools.length > 0 &&
|
|
2897
2921
|
options.tools.some(t => t.functionDeclarations && t.functionDeclarations.length > 0);
|
|
@@ -3028,4 +3052,4 @@ class MixGoogle extends MixCustom {
|
|
|
3028
3052
|
}
|
|
3029
3053
|
}
|
|
3030
3054
|
|
|
3031
|
-
module.exports = { MixCustom, ModelMix, MixAnthropic, MixKimi, MixMiniMax, MixMiMo, MixOpenAI, MixOpenAIResponses, MixOpenAIWebSocket, MixOpenRouter, MixPerplexity, MixOllama, MixLMStudio, MixGroq, MixTogether, MixGrok, MixCerebras, MixGoogle, MixFireworks, MixNVIDIA };
|
|
3055
|
+
module.exports = { MixCustom, ModelMix, MixAnthropic, MixKimi, MixMiniMax, MixMiMo, MixOpenAI, MixOpenAIResponses, MixOpenAIWebSocket, MixOpenRouter, MixPerplexity, MixOllama, MixLMStudio, MixGroq, MixTogether, MixGrok, MixCerebras, MixGoogle, MixFireworks, MixNVIDIA, normalizeEffort, applyUnifiedEffort, resolveProviderFamily };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "modelmix",
|
|
3
|
-
"version": "4.6.
|
|
3
|
+
"version": "4.6.10",
|
|
4
4
|
"description": "𧬠Reliable interface with automatic fallback for AI LLMs.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"repository": {
|
|
@@ -71,6 +71,6 @@
|
|
|
71
71
|
"test:live": "mocha test/live.test.js --timeout 10000 --require test/setup.js",
|
|
72
72
|
"test:live.mcp": "mocha test/live.mcp.js --timeout 60000 --require test/setup.js",
|
|
73
73
|
"test:tokens": "mocha test/tokens.test.js --timeout 10000 --require test/setup.js",
|
|
74
|
-
"test:offline": "mocha test/json.test.js test/fallback.test.js test/templates.test.js test/images.test.js test/bottleneck.test.js test/tokens.test.js test/history.test.js test/anthropic.test.js --timeout 10000 --require test/setup.js"
|
|
74
|
+
"test:offline": "mocha test/json.test.js test/fallback.test.js test/templates.test.js test/images.test.js test/bottleneck.test.js test/tokens.test.js test/history.test.js test/anthropic.test.js test/effort.test.js --timeout 10000 --require test/setup.js"
|
|
75
75
|
}
|
|
76
76
|
}
|
package/skills/modelmix/SKILL.md
CHANGED
|
@@ -29,6 +29,7 @@ Do NOT use for:
|
|
|
29
29
|
- [Installation](#installation)
|
|
30
30
|
- [Creating an instance](#creating-an-instance)
|
|
31
31
|
- [Attaching models](#attaching-models)
|
|
32
|
+
- [Unified effort](#unified-effort)
|
|
32
33
|
- [Get a text response](#get-a-text-response)
|
|
33
34
|
- [Get structured JSON](#get-structured-json)
|
|
34
35
|
- [Stream a response](#stream-a-response)
|
|
@@ -71,7 +72,8 @@ const model = ModelMix.new({
|
|
|
71
72
|
system: "You are a helpful assistant.",
|
|
72
73
|
max_history: 5, // -1 = unlimited, 0 = none (default), N = keep last N
|
|
73
74
|
debug: 0, // 0=silent, 1=minimal, 2=summary, 3=full, 4=verbose
|
|
74
|
-
roundRobin: false
|
|
75
|
+
roundRobin: false, // false=fallback, true=rotate models
|
|
76
|
+
effort: 50 // unified 0..100, or -1 adaptive
|
|
75
77
|
}
|
|
76
78
|
});
|
|
77
79
|
```
|
|
@@ -90,6 +92,30 @@ const model = ModelMix.new()
|
|
|
90
92
|
|
|
91
93
|
If `sonnet46` fails, it automatically tries `gpt52`, then `gemini3flash`.
|
|
92
94
|
|
|
95
|
+
### Unified effort
|
|
96
|
+
|
|
97
|
+
Provider-agnostic reasoning intensity. **Not** an `options` field β use `config.effort` or `.effort(n)`.
|
|
98
|
+
|
|
99
|
+
```javascript
|
|
100
|
+
ModelMix.new({ config: { effort: 40 } }).sonnet46().addText('Plan this refactor').message();
|
|
101
|
+
ModelMix.new().deepseekV4Flash({ config: { effort: 100 } }).addText('...').message();
|
|
102
|
+
ModelMix.new().effort(-1).minimaxM3().addText('Quick question').message();
|
|
103
|
+
|
|
104
|
+
// Native provider fields win when already set
|
|
105
|
+
ModelMix.new({ config: { effort: 80 } })
|
|
106
|
+
.gpt52({ options: { reasoning_effort: 'none' } }) // stays none
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
| | 0β19 | 20β39 | 40β59 | 60β79 | 80β100 | `-1` |
|
|
110
|
+
|--|------|-------|-------|-------|--------|------|
|
|
111
|
+
| OpenAI | `none` | `low` | `medium` | `high` | `xhigh` | β |
|
|
112
|
+
| Anthropic | `low` | `medium` | `high` | `xhigh` | `max` | adaptive |
|
|
113
|
+
| Gemini 3+\* | `minimal` | `low` | `medium` | `high` | β | dynamic |
|
|
114
|
+
| DeepSeek V4 | off | `low`β | `high`β | `high`β | `max`β | β |
|
|
115
|
+
| MiniMax M3 | off | adaptive | adaptive | adaptive | adaptive | adaptive |
|
|
116
|
+
|
|
117
|
+
\* Gemini bands: 0β24 / 25β49 / 50β74 / 75β100. DeepSeek `β` = thinking on; `off` = thinking disabled. MiniMax `off`/`adaptive` = `thinking.disabled` / `thinking.type=adaptive`. Gemini 2.5 maps 0β100 to `thinkingBudget`. `-1` = adaptive/dynamic when available, else no-op. Levels clamp per model. `*think()` shorthands still win over unified `effort`.
|
|
118
|
+
|
|
93
119
|
## Available Model Shorthands
|
|
94
120
|
|
|
95
121
|
### OpenAI
|
|
@@ -119,7 +145,7 @@ Thinking variants: append `think` β e.g. `fable5think()` `opus5think()` `opus4
|
|
|
119
145
|
`minimaxM25()` `minimaxM27()` `minimaxM3()`
|
|
120
146
|
|
|
121
147
|
### Fireworks
|
|
122
|
-
`
|
|
148
|
+
`deepseekV4Flash()` `deepseekV4Pro()` `GLM5()`
|
|
123
149
|
|
|
124
150
|
### Cerebras
|
|
125
151
|
`GLM46()`
|
|
@@ -128,7 +154,7 @@ Thinking variants: append `think` β e.g. `fable5think()` `opus5think()` `opus4
|
|
|
128
154
|
`GLM45()`
|
|
129
155
|
|
|
130
156
|
### Multi-provider (auto-fallback across free/paid tiers)
|
|
131
|
-
`
|
|
157
|
+
`hermes3()` `kimiK25think()`
|
|
132
158
|
|
|
133
159
|
### Local
|
|
134
160
|
`lmstudio()` β for LM Studio local models
|
|
@@ -403,7 +429,6 @@ For full debug output, also set: `DEBUG=ModelMix* node script.js`
|
|
|
403
429
|
const model = ModelMix.new()
|
|
404
430
|
.gptOss()
|
|
405
431
|
.kimiK25think()
|
|
406
|
-
.deepseekR1()
|
|
407
432
|
.hermes3()
|
|
408
433
|
.addText("What is the capital of France?");
|
|
409
434
|
console.log(await model.message());
|
|
@@ -426,7 +451,7 @@ const model = ModelMix.new({
|
|
|
426
451
|
minimax: false, // default: false
|
|
427
452
|
fireworks: false // default: false
|
|
428
453
|
}
|
|
429
|
-
}).
|
|
454
|
+
}).kimiK25think();
|
|
430
455
|
```
|
|
431
456
|
|
|
432
457
|
## Agent Usage Rules
|
|
@@ -439,6 +464,7 @@ const model = ModelMix.new({
|
|
|
439
464
|
- Use `.json()` for structured output instead of parsing text manually. Use descriptor objects `{ description, required, enum, default, nullable }` for richer schema control.
|
|
440
465
|
- Use `.message()` for simple text, `.raw()` when you need tokens/thinking/toolCalls.
|
|
441
466
|
- For thinking models, append `think` to the method name (e.g. `sonnet45think()`).
|
|
467
|
+
- For cross-provider reasoning intensity, use unified `effort` (`-1` or `0`β`100`) via `config.effort` or `.effort(n)` β never put it in `options`. Native fields win if already set.
|
|
442
468
|
- Template placeholders use `{key}` syntax in both system prompts and user messages.
|
|
443
469
|
- The library uses CommonJS internally but supports ESM import via `{ ModelMix }`.
|
|
444
470
|
- GPT-5+ models automatically use `max_completion_tokens` instead of `max_tokens`.
|
package/test/deepseek.test.js
CHANGED
|
@@ -17,4 +17,44 @@ describe('DeepSeek Model Registration Tests', () => {
|
|
|
17
17
|
expect(model.models).to.have.length(1);
|
|
18
18
|
expect(model.models[0].key).to.equal('deepseek-ai/DeepSeek-V4-Pro');
|
|
19
19
|
});
|
|
20
|
+
|
|
21
|
+
it('should register Fireworks DeepSeek V4 Flash by default', () => {
|
|
22
|
+
const model = ModelMix.new();
|
|
23
|
+
model.deepseekV4Flash({ mix: { fireworks: true, openrouter: false } });
|
|
24
|
+
|
|
25
|
+
expect(model.models).to.have.length(1);
|
|
26
|
+
expect(model.models[0].key).to.equal('accounts/fireworks/models/deepseek-v4-flash');
|
|
27
|
+
expect(ModelMix.calculateCost('accounts/fireworks/models/deepseek-v4-flash', {
|
|
28
|
+
input: 1_000_000,
|
|
29
|
+
output: 1_000_000
|
|
30
|
+
})).to.be.closeTo(0.42, 1e-10);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should register NVIDIA DeepSeek V4 Flash when nvidia mix is enabled', () => {
|
|
34
|
+
const model = ModelMix.new();
|
|
35
|
+
model.deepseekV4Flash({ mix: { fireworks: false, openrouter: false, nvidia: true } });
|
|
36
|
+
|
|
37
|
+
expect(model.models).to.have.length(1);
|
|
38
|
+
expect(model.models[0].key).to.equal('deepseek-ai/deepseek-v4-flash');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should register OpenRouter DeepSeek V4 Flash when openrouter mix is enabled', () => {
|
|
42
|
+
const model = ModelMix.new();
|
|
43
|
+
model.deepseekV4Flash({ mix: { fireworks: false, openrouter: true } });
|
|
44
|
+
|
|
45
|
+
expect(model.models).to.have.length(1);
|
|
46
|
+
expect(model.models[0].key).to.equal('deepseek/deepseek-v4-flash');
|
|
47
|
+
expect(ModelMix.calculateCost('deepseek/deepseek-v4-flash', {
|
|
48
|
+
input: 1_000_000,
|
|
49
|
+
output: 1_000_000
|
|
50
|
+
})).to.equal(0.27);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should register Together DeepSeek V4 Flash when together mix is enabled', () => {
|
|
54
|
+
const model = ModelMix.new();
|
|
55
|
+
model.deepseekV4Flash({ mix: { fireworks: false, openrouter: false, together: true } });
|
|
56
|
+
|
|
57
|
+
expect(model.models).to.have.length(1);
|
|
58
|
+
expect(model.models[0].key).to.equal('deepseek-ai/DeepSeek-V4-Flash');
|
|
59
|
+
});
|
|
20
60
|
});
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
const { expect } = require('chai');
|
|
2
|
+
const {
|
|
3
|
+
normalizeEffort,
|
|
4
|
+
mapEffort,
|
|
5
|
+
applyUnifiedEffort,
|
|
6
|
+
hasNativeEffort,
|
|
7
|
+
resolveProviderFamily,
|
|
8
|
+
levelFromBands,
|
|
9
|
+
OPENAI_BANDS,
|
|
10
|
+
ANTHROPIC_BANDS,
|
|
11
|
+
GEMINI_BANDS,
|
|
12
|
+
} = require('../effort.js');
|
|
13
|
+
const {
|
|
14
|
+
ModelMix,
|
|
15
|
+
MixOpenAI,
|
|
16
|
+
MixOpenAIResponses,
|
|
17
|
+
MixAnthropic,
|
|
18
|
+
MixGoogle,
|
|
19
|
+
MixPerplexity,
|
|
20
|
+
} = require('../index.js');
|
|
21
|
+
|
|
22
|
+
describe('Unified effort scale', () => {
|
|
23
|
+
describe('normalizeEffort', () => {
|
|
24
|
+
it('accepts -1 and 0..100 integers', () => {
|
|
25
|
+
expect(normalizeEffort(-1)).to.equal(-1);
|
|
26
|
+
expect(normalizeEffort(0)).to.equal(0);
|
|
27
|
+
expect(normalizeEffort(50)).to.equal(50);
|
|
28
|
+
expect(normalizeEffort(100)).to.equal(100);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('rejects invalid values', () => {
|
|
32
|
+
expect(() => normalizeEffort(1.5)).to.throw(/Invalid effort/);
|
|
33
|
+
expect(() => normalizeEffort(101)).to.throw(/Invalid effort/);
|
|
34
|
+
expect(() => normalizeEffort(-2)).to.throw(/Invalid effort/);
|
|
35
|
+
expect(() => normalizeEffort('medium')).to.throw(/Invalid effort/);
|
|
36
|
+
expect(() => normalizeEffort(null)).to.throw(/Invalid effort/);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('band mapping', () => {
|
|
41
|
+
it('maps OpenAI bands', () => {
|
|
42
|
+
expect(levelFromBands(0, OPENAI_BANDS)).to.equal('none');
|
|
43
|
+
expect(levelFromBands(19, OPENAI_BANDS)).to.equal('none');
|
|
44
|
+
expect(levelFromBands(20, OPENAI_BANDS)).to.equal('low');
|
|
45
|
+
expect(levelFromBands(40, OPENAI_BANDS)).to.equal('medium');
|
|
46
|
+
expect(levelFromBands(60, OPENAI_BANDS)).to.equal('high');
|
|
47
|
+
expect(levelFromBands(80, OPENAI_BANDS)).to.equal('xhigh');
|
|
48
|
+
expect(levelFromBands(100, OPENAI_BANDS)).to.equal('xhigh');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('maps Anthropic bands', () => {
|
|
52
|
+
expect(levelFromBands(0, ANTHROPIC_BANDS)).to.equal('low');
|
|
53
|
+
expect(levelFromBands(20, ANTHROPIC_BANDS)).to.equal('medium');
|
|
54
|
+
expect(levelFromBands(40, ANTHROPIC_BANDS)).to.equal('high');
|
|
55
|
+
expect(levelFromBands(60, ANTHROPIC_BANDS)).to.equal('xhigh');
|
|
56
|
+
expect(levelFromBands(80, ANTHROPIC_BANDS)).to.equal('max');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('maps Gemini bands', () => {
|
|
60
|
+
expect(levelFromBands(0, GEMINI_BANDS)).to.equal('minimal');
|
|
61
|
+
expect(levelFromBands(25, GEMINI_BANDS)).to.equal('low');
|
|
62
|
+
expect(levelFromBands(50, GEMINI_BANDS)).to.equal('medium');
|
|
63
|
+
expect(levelFromBands(75, GEMINI_BANDS)).to.equal('high');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('mapEffort', () => {
|
|
68
|
+
it('maps OpenAI effort to reasoning_effort', () => {
|
|
69
|
+
expect(mapEffort('openai', 10)).to.deep.equal({ reasoning_effort: 'none' });
|
|
70
|
+
expect(mapEffort('openai', 50)).to.deep.equal({ reasoning_effort: 'medium' });
|
|
71
|
+
expect(mapEffort('openai', 90)).to.deep.equal({ reasoning_effort: 'xhigh' });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('sets OpenAI adaptive only when supported (otherwise no-op)', () => {
|
|
75
|
+
expect(mapEffort('openai', -1)).to.equal(null);
|
|
76
|
+
expect(mapEffort('openai', -1, 'gpt-5.2')).to.equal(null);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('clamps OpenAI to model-supported levels', () => {
|
|
80
|
+
expect(mapEffort('openai', 10, 'gpt-5.3-codex')).to.deep.equal({ reasoning_effort: 'low' });
|
|
81
|
+
expect(mapEffort('openai', 10, 'gpt-oss-120b')).to.deep.equal({ reasoning_effort: 'low' });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('maps Anthropic effort to output_config.effort', () => {
|
|
85
|
+
expect(mapEffort('anthropic', 10)).to.deep.equal({ output_config: { effort: 'low' } });
|
|
86
|
+
expect(mapEffort('anthropic', 90)).to.deep.equal({ output_config: { effort: 'max' } });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('maps Anthropic adaptive to thinking.type=adaptive', () => {
|
|
90
|
+
expect(mapEffort('anthropic', -1)).to.deep.equal({ thinking: { type: 'adaptive' } });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('maps Gemini 3+ thinkingLevel', () => {
|
|
94
|
+
expect(mapEffort('google', 10, 'gemini-3.6-flash')).to.deep.equal({
|
|
95
|
+
thinkingConfig: { thinkingLevel: 'minimal' }
|
|
96
|
+
});
|
|
97
|
+
expect(mapEffort('google', 80, 'gemini-3.6-flash')).to.deep.equal({
|
|
98
|
+
thinkingConfig: { thinkingLevel: 'high' }
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('clamps Gemini levels for models with fewer steps', () => {
|
|
103
|
+
expect(mapEffort('google', 10, 'gemini-3-pro-preview')).to.deep.equal({
|
|
104
|
+
thinkingConfig: { thinkingLevel: 'low' }
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('maps Gemini adaptive (-1) to thinkingBudget -1', () => {
|
|
109
|
+
expect(mapEffort('google', -1, 'gemini-3.6-flash')).to.deep.equal({
|
|
110
|
+
thinkingConfig: { thinkingBudget: -1 }
|
|
111
|
+
});
|
|
112
|
+
expect(mapEffort('google', -1, 'gemini-2.5-flash')).to.deep.equal({
|
|
113
|
+
thinkingConfig: { thinkingBudget: -1 }
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('maps Gemini 2.5 to thinkingBudget', () => {
|
|
118
|
+
expect(mapEffort('google', 0, 'gemini-2.5-flash')).to.deep.equal({
|
|
119
|
+
thinkingConfig: { thinkingBudget: 0 }
|
|
120
|
+
});
|
|
121
|
+
expect(mapEffort('google', 100, 'gemini-2.5-flash')).to.deep.equal({
|
|
122
|
+
thinkingConfig: { thinkingBudget: 24576 }
|
|
123
|
+
});
|
|
124
|
+
expect(mapEffort('google', 50, 'gemini-2.5-pro')).to.deep.equal({
|
|
125
|
+
thinkingConfig: { thinkingBudget: 16384 }
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('maps DeepSeek V4 effort to thinking + reasoning_effort', () => {
|
|
130
|
+
const key = 'accounts/fireworks/models/deepseek-v4-flash';
|
|
131
|
+
expect(mapEffort('openai', 0, key)).to.deep.equal({
|
|
132
|
+
thinking: { type: 'disabled' }
|
|
133
|
+
});
|
|
134
|
+
expect(mapEffort('openai', 30, key)).to.deep.equal({
|
|
135
|
+
reasoning_effort: 'low',
|
|
136
|
+
thinking: { type: 'enabled' }
|
|
137
|
+
});
|
|
138
|
+
expect(mapEffort('openai', 50, key)).to.deep.equal({
|
|
139
|
+
reasoning_effort: 'high',
|
|
140
|
+
thinking: { type: 'enabled' }
|
|
141
|
+
});
|
|
142
|
+
expect(mapEffort('openai', 100, 'deepseek-ai/DeepSeek-V4-Pro')).to.deep.equal({
|
|
143
|
+
reasoning_effort: 'max',
|
|
144
|
+
thinking: { type: 'enabled' }
|
|
145
|
+
});
|
|
146
|
+
// No adaptive control on DeepSeek β no-op
|
|
147
|
+
expect(mapEffort('openai', -1, 'deepseek/deepseek-v4-flash')).to.equal(null);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('maps MiniMax thinking adaptive/disabled', () => {
|
|
151
|
+
expect(mapEffort('openai', -1, 'MiniMax-M3')).to.deep.equal({
|
|
152
|
+
thinking: { type: 'adaptive' }
|
|
153
|
+
});
|
|
154
|
+
expect(mapEffort('openai', 0, 'minimax/minimax-m3')).to.deep.equal({
|
|
155
|
+
thinking: { type: 'disabled' }
|
|
156
|
+
});
|
|
157
|
+
expect(mapEffort('openai', 50, 'MiniMaxAI/MiniMax-M3')).to.deep.equal({
|
|
158
|
+
thinking: { type: 'adaptive' }
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('returns null for unsupported families', () => {
|
|
163
|
+
expect(mapEffort(null, 50)).to.equal(null);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('applyUnifiedEffort / native wins', () => {
|
|
168
|
+
it('applies OpenAI mapping when native absent', () => {
|
|
169
|
+
const options = {};
|
|
170
|
+
applyUnifiedEffort(options, { effort: 50 }, 'openai', 'gpt-5.2');
|
|
171
|
+
expect(options.reasoning_effort).to.equal('medium');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('applies MiniMax adaptive from config.effort -1', () => {
|
|
175
|
+
const options = {};
|
|
176
|
+
applyUnifiedEffort(options, { effort: -1 }, 'openai', 'MiniMax-M3');
|
|
177
|
+
expect(options.thinking).to.deep.equal({ type: 'adaptive' });
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('applies DeepSeek mapping on Fireworks model key', () => {
|
|
181
|
+
const options = {};
|
|
182
|
+
applyUnifiedEffort(
|
|
183
|
+
options,
|
|
184
|
+
{ effort: 100 },
|
|
185
|
+
'openai',
|
|
186
|
+
'accounts/fireworks/models/deepseek-v4-flash'
|
|
187
|
+
);
|
|
188
|
+
expect(options.reasoning_effort).to.equal('max');
|
|
189
|
+
expect(options.thinking).to.deep.equal({ type: 'enabled' });
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('applies DeepSeek disabled thinking without reasoning_effort', () => {
|
|
193
|
+
const options = {};
|
|
194
|
+
applyUnifiedEffort(
|
|
195
|
+
options,
|
|
196
|
+
{ effort: 10 },
|
|
197
|
+
'openai',
|
|
198
|
+
'deepseek/deepseek-v4-flash'
|
|
199
|
+
);
|
|
200
|
+
expect(options.thinking).to.deep.equal({ type: 'disabled' });
|
|
201
|
+
expect(options.reasoning_effort).to.equal(undefined);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('skips DeepSeek mapping when thinking is already set', () => {
|
|
205
|
+
const options = { thinking: { type: 'disabled' } };
|
|
206
|
+
applyUnifiedEffort(
|
|
207
|
+
options,
|
|
208
|
+
{ effort: 100 },
|
|
209
|
+
'openai',
|
|
210
|
+
'accounts/fireworks/models/deepseek-v4-flash'
|
|
211
|
+
);
|
|
212
|
+
expect(options.thinking).to.deep.equal({ type: 'disabled' });
|
|
213
|
+
expect(options.reasoning_effort).to.equal(undefined);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('skips OpenAI mapping when reasoning_effort is set', () => {
|
|
217
|
+
const options = { reasoning_effort: 'none' };
|
|
218
|
+
applyUnifiedEffort(options, { effort: 90 }, 'openai', 'gpt-5.2');
|
|
219
|
+
expect(options.reasoning_effort).to.equal('none');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('applies Anthropic mapping when native absent', () => {
|
|
223
|
+
const options = {};
|
|
224
|
+
applyUnifiedEffort(options, { effort: 90 }, 'anthropic', 'claude-opus-5');
|
|
225
|
+
expect(options.output_config).to.deep.equal({ effort: 'max' });
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('skips Anthropic mapping when output_config.effort is set', () => {
|
|
229
|
+
const options = { output_config: { effort: 'low', format: { type: 'json_schema' } } };
|
|
230
|
+
applyUnifiedEffort(options, { effort: 90 }, 'anthropic', 'claude-opus-5');
|
|
231
|
+
expect(options.output_config.effort).to.equal('low');
|
|
232
|
+
expect(options.output_config.format).to.deep.equal({ type: 'json_schema' });
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('merges Anthropic adaptive without wiping display, drops budget_tokens', () => {
|
|
236
|
+
const options = {
|
|
237
|
+
thinking: { type: 'enabled', budget_tokens: 1638, display: 'summarized' }
|
|
238
|
+
};
|
|
239
|
+
applyUnifiedEffort(options, { effort: -1 }, 'anthropic', 'claude-sonnet-4-6');
|
|
240
|
+
expect(options.thinking).to.deep.equal({ type: 'adaptive', display: 'summarized' });
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('skips Google mapping when thinkingConfig is set', () => {
|
|
244
|
+
const options = { thinkingConfig: { thinkingLevel: 'low' } };
|
|
245
|
+
applyUnifiedEffort(options, { effort: 90 }, 'google', 'gemini-3.6-flash');
|
|
246
|
+
expect(options.thinkingConfig.thinkingLevel).to.equal('low');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('does nothing when config.effort is undefined', () => {
|
|
250
|
+
const options = {};
|
|
251
|
+
applyUnifiedEffort(options, {}, 'openai', 'gpt-5.2');
|
|
252
|
+
expect(options).to.deep.equal({});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('hasNativeEffort detects provider fields', () => {
|
|
256
|
+
expect(hasNativeEffort('openai', { reasoning_effort: 'high' })).to.equal(true);
|
|
257
|
+
expect(hasNativeEffort('openai', {})).to.equal(false);
|
|
258
|
+
expect(hasNativeEffort('anthropic', { output_config: { effort: 'max' } })).to.equal(true);
|
|
259
|
+
expect(hasNativeEffort('google', { thinkingBudget: 1024 })).to.equal(true);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe('resolveProviderFamily', () => {
|
|
264
|
+
it('resolves known providers', () => {
|
|
265
|
+
expect(resolveProviderFamily(new MixOpenAI())).to.equal('openai');
|
|
266
|
+
expect(resolveProviderFamily(new MixOpenAIResponses())).to.equal('openai');
|
|
267
|
+
expect(resolveProviderFamily(new MixAnthropic())).to.equal('anthropic');
|
|
268
|
+
expect(resolveProviderFamily(new MixGoogle())).to.equal('google');
|
|
269
|
+
expect(resolveProviderFamily(new MixPerplexity())).to.equal(null);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe('ModelMix API surface', () => {
|
|
274
|
+
it('accepts config.effort', () => {
|
|
275
|
+
const model = ModelMix.new({ config: { effort: 25 } });
|
|
276
|
+
expect(model.config.effort).to.equal(25);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('accepts config.effort on model shorthand', () => {
|
|
280
|
+
const model = ModelMix.new().deepseekV4Flash({ config: { effort: 100 } });
|
|
281
|
+
expect(model.models[0].provider.config.effort).to.equal(100);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('supports fluent .effort()', () => {
|
|
285
|
+
const model = ModelMix.new().effort(-1);
|
|
286
|
+
expect(model.config.effort).to.equal(-1);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('lets .new({ config: { effort } }) override inherited config.effort', () => {
|
|
290
|
+
const base = ModelMix.new({ config: { effort: 20 } });
|
|
291
|
+
const child = base.new({ config: { effort: 80 } });
|
|
292
|
+
expect(child.config.effort).to.equal(80);
|
|
293
|
+
expect(base.config.effort).to.equal(20);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('rejects invalid fluent effort', () => {
|
|
297
|
+
expect(() => ModelMix.new().effort(150)).to.throw(/Invalid effort/);
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
describe('provider request wiring', () => {
|
|
302
|
+
it('OpenAI Responses request uses mapped reasoning_effort', () => {
|
|
303
|
+
const options = { model: 'gpt-5.2', messages: [] };
|
|
304
|
+
applyUnifiedEffort(options, { effort: 15 }, 'openai', 'gpt-5.2');
|
|
305
|
+
const request = MixOpenAIResponses.buildResponsesRequest(options, {});
|
|
306
|
+
expect(request.reasoning).to.deep.equal({ effort: 'none' });
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('Anthropic *think() native effort wins over config.effort', () => {
|
|
310
|
+
const model = ModelMix.new({ config: { effort: 20 } }).opus5think();
|
|
311
|
+
expect(model.models[0].provider.options.output_config.effort).to.equal('max');
|
|
312
|
+
|
|
313
|
+
const options = {
|
|
314
|
+
...model.models[0].provider.options,
|
|
315
|
+
model: 'claude-opus-5'
|
|
316
|
+
};
|
|
317
|
+
applyUnifiedEffort(options, { effort: 20 }, 'anthropic', 'claude-opus-5');
|
|
318
|
+
expect(options.output_config.effort).to.equal('max');
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('MixGoogle generationConfig includes thinkingConfig from options', async () => {
|
|
322
|
+
const google = new MixGoogle({ config: { apiKey: 'test-key' } });
|
|
323
|
+
let capturedBody;
|
|
324
|
+
const originalFetch = global.fetch;
|
|
325
|
+
const responseBody = JSON.stringify({
|
|
326
|
+
candidates: [{ content: { parts: [{ text: 'ok' }] } }],
|
|
327
|
+
usageMetadata: { promptTokenCount: 1, candidatesTokenCount: 1 }
|
|
328
|
+
});
|
|
329
|
+
global.fetch = async (url, init) => {
|
|
330
|
+
capturedBody = JSON.parse(init.body);
|
|
331
|
+
return {
|
|
332
|
+
ok: true,
|
|
333
|
+
status: 200,
|
|
334
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
335
|
+
text: async () => responseBody,
|
|
336
|
+
json: async () => JSON.parse(responseBody)
|
|
337
|
+
};
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
await google.create({
|
|
342
|
+
config: { system: 'sys' },
|
|
343
|
+
options: {
|
|
344
|
+
model: 'gemini-3.6-flash',
|
|
345
|
+
max_tokens: 100,
|
|
346
|
+
messages: [{ role: 'user', content: [{ type: 'text', text: 'hi' }] }],
|
|
347
|
+
thinkingConfig: { thinkingLevel: 'low' }
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
} finally {
|
|
351
|
+
global.fetch = originalFetch;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
expect(capturedBody.generationConfig.thinkingConfig).to.deep.equal({
|
|
355
|
+
thinkingLevel: 'low'
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
});
|
package/test/setup.js
CHANGED
|
@@ -27,6 +27,8 @@ process.env.OPENAI_API_KEY = process.env.OPENAI_API_KEY || 'sk-proj-test-dummy-k
|
|
|
27
27
|
process.env.PPLX_API_KEY = process.env.PPLX_API_KEY || 'pplx-test-dummy-key-for-testing-purposes';
|
|
28
28
|
process.env.GROQ_API_KEY = process.env.GROQ_API_KEY || 'gsk_test-dummy-key-for-testing-purposes';
|
|
29
29
|
process.env.TOGETHER_API_KEY = process.env.TOGETHER_API_KEY || '49a96test-dummy-key-for-testing-purposes';
|
|
30
|
+
process.env.FIREWORKS_API_KEY = process.env.FIREWORKS_API_KEY || 'fw-test-dummy-key-for-testing-purposes';
|
|
31
|
+
process.env.OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY || 'sk-or-test-dummy-key-for-testing-purposes';
|
|
30
32
|
process.env.XAI_API_KEY = process.env.XAI_API_KEY || 'xai-test-dummy-key-for-testing-purposes';
|
|
31
33
|
process.env.CEREBRAS_API_KEY = process.env.CEREBRAS_API_KEY || 'csk-test-dummy-key-for-testing-purposes';
|
|
32
34
|
process.env.NVIDIA_API_KEY = process.env.NVIDIA_API_KEY || 'nvapi-test-dummy-key-for-testing-purposes';
|