modelmix 4.6.6 → 4.6.8
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 +30 -2
- 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/opus5.js +23 -0
- package/demo/together.js +1 -1
- package/effort.js +370 -0
- package/index.js +92 -35
- package/package.json +2 -2
- package/skills/modelmix/SKILL.md +32 -5
- package/test/anthropic.test.js +106 -0
- package/test/deepseek.test.js +40 -0
- package/test/effort.test.js +359 -0
- package/test/history.test.js +110 -0
- package/test/live.mcp.js +43 -1
- package/test/setup.js +2 -0
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
|
|
|
@@ -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 };
|
|
@@ -887,13 +902,21 @@ class ModelMix {
|
|
|
887
902
|
return input;
|
|
888
903
|
}
|
|
889
904
|
|
|
905
|
+
static hasToolInteraction(message) {
|
|
906
|
+
if (!message) return false;
|
|
907
|
+
if (message.role === 'tool' || message.tool_calls || message.tool_call_id) return true;
|
|
908
|
+
// Anthropic-native assistant turns store tool_use blocks in content (no tool_calls).
|
|
909
|
+
if (message.role === 'assistant' && Array.isArray(message.content)) {
|
|
910
|
+
return message.content.some(block => block?.type === 'tool_use');
|
|
911
|
+
}
|
|
912
|
+
return false;
|
|
913
|
+
}
|
|
914
|
+
|
|
890
915
|
groupByRoles(messages) {
|
|
891
916
|
return messages.reduce((acc, currentMessage, index) => {
|
|
892
917
|
// Don't group tool messages or assistant messages with tool_calls
|
|
893
918
|
// Each tool response must be separate with its own tool_call_id
|
|
894
|
-
const shouldNotGroup = currentMessage
|
|
895
|
-
currentMessage.tool_calls ||
|
|
896
|
-
currentMessage.tool_call_id;
|
|
919
|
+
const shouldNotGroup = ModelMix.hasToolInteraction(currentMessage);
|
|
897
920
|
|
|
898
921
|
if (index === 0 || currentMessage.role !== messages[index - 1].role || shouldNotGroup) {
|
|
899
922
|
// acc.push({
|
|
@@ -939,7 +962,7 @@ class ModelMix {
|
|
|
939
962
|
// backtrack to include the full sequence (user → assistant/tool_calls → tool results)
|
|
940
963
|
while (sliceStart > 0 && sliceStart < this.messages.length) {
|
|
941
964
|
const msg = this.messages[sliceStart];
|
|
942
|
-
if (
|
|
965
|
+
if (ModelMix.hasToolInteraction(msg)) {
|
|
943
966
|
sliceStart--;
|
|
944
967
|
} else {
|
|
945
968
|
break;
|
|
@@ -1030,6 +1053,10 @@ class ModelMix {
|
|
|
1030
1053
|
}
|
|
1031
1054
|
};
|
|
1032
1055
|
|
|
1056
|
+
// Unified effort → native provider fields (skipped if native already set)
|
|
1057
|
+
const providerFamily = resolveProviderFamily(providerInstance);
|
|
1058
|
+
applyUnifiedEffort(currentOptions, currentConfig, providerFamily, currentModelKey);
|
|
1059
|
+
|
|
1033
1060
|
if (currentConfig.debug >= 1) {
|
|
1034
1061
|
const isPrimary = i === 0;
|
|
1035
1062
|
const prefix = isPrimary ? '→' : '↻';
|
|
@@ -1108,7 +1135,8 @@ class ModelMix {
|
|
|
1108
1135
|
this.messages.push({
|
|
1109
1136
|
role: "assistant", content: [{
|
|
1110
1137
|
type: "thinking",
|
|
1111
|
-
|
|
1138
|
+
// Empty string is valid (Anthropic display: "omitted").
|
|
1139
|
+
thinking: result.think ?? '',
|
|
1112
1140
|
signature: result.signature
|
|
1113
1141
|
}]
|
|
1114
1142
|
});
|
|
@@ -1184,7 +1212,8 @@ class ModelMix {
|
|
|
1184
1212
|
this.messages.push({
|
|
1185
1213
|
role: "assistant", content: [{
|
|
1186
1214
|
type: "thinking",
|
|
1187
|
-
|
|
1215
|
+
// Empty string is valid (Anthropic display: "omitted").
|
|
1216
|
+
thinking: result.think ?? '',
|
|
1188
1217
|
signature: result.signature
|
|
1189
1218
|
}, {
|
|
1190
1219
|
type: "text",
|
|
@@ -2229,10 +2258,10 @@ class MixAnthropic extends MixCustom {
|
|
|
2229
2258
|
const filteredMessages = [];
|
|
2230
2259
|
for (let i = 0; i < messages.length; i++) {
|
|
2231
2260
|
if (messages[i].role === 'tool') {
|
|
2232
|
-
//
|
|
2261
|
+
// Preceding assistant may use OpenAI tool_calls or Anthropic tool_use blocks.
|
|
2233
2262
|
let foundToolCall = false;
|
|
2234
2263
|
for (let j = i - 1; j >= 0; j--) {
|
|
2235
|
-
if (messages[j].role === 'assistant'
|
|
2264
|
+
if (ModelMix.hasToolInteraction(messages[j]) && messages[j].role === 'assistant') {
|
|
2236
2265
|
foundToolCall = true;
|
|
2237
2266
|
break;
|
|
2238
2267
|
}
|
|
@@ -2356,12 +2385,22 @@ class MixAnthropic extends MixCustom {
|
|
|
2356
2385
|
throw new Error(`Anthropic content blocks are missing .text (stop_reason: ${stopReason ?? 'unknown'}, content_types: ${contentTypes}).`);
|
|
2357
2386
|
}
|
|
2358
2387
|
|
|
2388
|
+
static extractThinkingBlock(data) {
|
|
2389
|
+
const content = Array.isArray(data?.content) ? data.content : [];
|
|
2390
|
+
return content.find(block => block?.type === 'thinking') || null;
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2359
2393
|
static extractThink(data) {
|
|
2360
|
-
|
|
2394
|
+
const block = MixAnthropic.extractThinkingBlock(data);
|
|
2395
|
+
// Preserve empty string: display "omitted" returns thinking: "" with a signature.
|
|
2396
|
+
return typeof block?.thinking === 'string' ? block.thinking : null;
|
|
2361
2397
|
}
|
|
2362
2398
|
|
|
2363
2399
|
static extractSignature(data) {
|
|
2364
|
-
|
|
2400
|
+
const block = MixAnthropic.extractThinkingBlock(data);
|
|
2401
|
+
return typeof block?.signature === 'string' && block.signature
|
|
2402
|
+
? block.signature
|
|
2403
|
+
: null;
|
|
2365
2404
|
}
|
|
2366
2405
|
|
|
2367
2406
|
static extractTokens(data) {
|
|
@@ -2383,13 +2422,18 @@ class MixAnthropic extends MixCustom {
|
|
|
2383
2422
|
}
|
|
2384
2423
|
|
|
2385
2424
|
processResponse(response) {
|
|
2425
|
+
const data = response.data;
|
|
2386
2426
|
return {
|
|
2387
|
-
message: MixAnthropic.extractMessage(
|
|
2388
|
-
think: MixAnthropic.extractThink(
|
|
2389
|
-
toolCalls: MixAnthropic.extractToolCalls(
|
|
2390
|
-
tokens: MixAnthropic.extractTokens(
|
|
2391
|
-
response:
|
|
2392
|
-
signature: MixAnthropic.extractSignature(
|
|
2427
|
+
message: MixAnthropic.extractMessage(data),
|
|
2428
|
+
think: MixAnthropic.extractThink(data),
|
|
2429
|
+
toolCalls: MixAnthropic.extractToolCalls(data),
|
|
2430
|
+
tokens: MixAnthropic.extractTokens(data),
|
|
2431
|
+
response: data,
|
|
2432
|
+
signature: MixAnthropic.extractSignature(data),
|
|
2433
|
+
// Replay Anthropic content blocks verbatim (including empty thinking).
|
|
2434
|
+
assistantMessage: Array.isArray(data?.content)
|
|
2435
|
+
? { role: 'assistant', content: data.content }
|
|
2436
|
+
: undefined
|
|
2393
2437
|
}
|
|
2394
2438
|
}
|
|
2395
2439
|
|
|
@@ -2867,6 +2911,19 @@ class MixGoogle extends MixCustom {
|
|
|
2867
2911
|
generationConfig.topP = options.top_p;
|
|
2868
2912
|
}
|
|
2869
2913
|
|
|
2914
|
+
// Thinking / effort (from unified config.effort or native options)
|
|
2915
|
+
if (options.thinkingConfig) {
|
|
2916
|
+
generationConfig.thinkingConfig = options.thinkingConfig;
|
|
2917
|
+
} else if (options.thinkingLevel != null || options.thinkingBudget != null) {
|
|
2918
|
+
generationConfig.thinkingConfig = {};
|
|
2919
|
+
if (options.thinkingLevel != null) {
|
|
2920
|
+
generationConfig.thinkingConfig.thinkingLevel = options.thinkingLevel;
|
|
2921
|
+
}
|
|
2922
|
+
if (options.thinkingBudget != null) {
|
|
2923
|
+
generationConfig.thinkingConfig.thinkingBudget = options.thinkingBudget;
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2870
2927
|
// Gemini does not support responseMimeType when function calling is used
|
|
2871
2928
|
const hasTools = options.tools && options.tools.length > 0 &&
|
|
2872
2929
|
options.tools.some(t => t.functionDeclarations && t.functionDeclarations.length > 0);
|
|
@@ -3003,4 +3060,4 @@ class MixGoogle extends MixCustom {
|
|
|
3003
3060
|
}
|
|
3004
3061
|
}
|
|
3005
3062
|
|
|
3006
|
-
module.exports = { MixCustom, ModelMix, MixAnthropic, MixKimi, MixMiniMax, MixMiMo, MixOpenAI, MixOpenAIResponses, MixOpenAIWebSocket, MixOpenRouter, MixPerplexity, MixOllama, MixLMStudio, MixGroq, MixTogether, MixGrok, MixCerebras, MixGoogle, MixFireworks, MixNVIDIA };
|
|
3063
|
+
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.8",
|
|
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,31 @@ 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
|
+
| Value | OpenAI | Anthropic | Gemini 3+ | DeepSeek V4 | MiniMax M3 |
|
|
110
|
+
|------|--------|-----------|-----------|-------------|------------|
|
|
111
|
+
| 0–19 | `none` | `low` | `minimal` (0–24) | thinking `disabled` | `thinking.disabled` |
|
|
112
|
+
| 20–39 | `low` | `medium` | `low` (25–49) | `low` + thinking on | `thinking.adaptive` |
|
|
113
|
+
| 40–59 | `medium` | `high` | `medium` (50–74) | `high` + thinking on | `thinking.adaptive` |
|
|
114
|
+
| 60–79 | `high` | `xhigh` | `high` (75–100) | `high` + thinking on | `thinking.adaptive` |
|
|
115
|
+
| 80–100 | `xhigh` | `max` | — | `max` + thinking on | `thinking.adaptive` |
|
|
116
|
+
| `-1` | no-op (no adaptive API) | `thinking.type=adaptive` | `thinkingBudget: -1` | no-op (no adaptive API) | `thinking.type=adaptive` |
|
|
117
|
+
|
|
118
|
+
Gemini 2.5 maps 0–100 to numeric `thinkingBudget`. `-1` sets the provider adaptive/dynamic control when available; otherwise no-op. Levels clamp to what each model supports. `*think()` shorthands that set native effort still win over unified `effort`.
|
|
119
|
+
|
|
93
120
|
## Available Model Shorthands
|
|
94
121
|
|
|
95
122
|
### OpenAI
|
|
@@ -119,7 +146,7 @@ Thinking variants: append `think` — e.g. `fable5think()` `opus5think()` `opus4
|
|
|
119
146
|
`minimaxM25()` `minimaxM27()` `minimaxM3()`
|
|
120
147
|
|
|
121
148
|
### Fireworks
|
|
122
|
-
`
|
|
149
|
+
`deepseekV4Flash()` `deepseekV4Pro()` `GLM5()` `GLM47()`
|
|
123
150
|
|
|
124
151
|
### Cerebras
|
|
125
152
|
`GLM46()`
|
|
@@ -128,7 +155,7 @@ Thinking variants: append `think` — e.g. `fable5think()` `opus5think()` `opus4
|
|
|
128
155
|
`GLM45()`
|
|
129
156
|
|
|
130
157
|
### Multi-provider (auto-fallback across free/paid tiers)
|
|
131
|
-
`
|
|
158
|
+
`hermes3()` `kimiK25think()` `GLM47()`
|
|
132
159
|
|
|
133
160
|
### Local
|
|
134
161
|
`lmstudio()` — for LM Studio local models
|
|
@@ -403,7 +430,6 @@ For full debug output, also set: `DEBUG=ModelMix* node script.js`
|
|
|
403
430
|
const model = ModelMix.new()
|
|
404
431
|
.gptOss()
|
|
405
432
|
.kimiK25think()
|
|
406
|
-
.deepseekR1()
|
|
407
433
|
.hermes3()
|
|
408
434
|
.addText("What is the capital of France?");
|
|
409
435
|
console.log(await model.message());
|
|
@@ -426,7 +452,7 @@ const model = ModelMix.new({
|
|
|
426
452
|
minimax: false, // default: false
|
|
427
453
|
fireworks: false // default: false
|
|
428
454
|
}
|
|
429
|
-
}).
|
|
455
|
+
}).GLM47();
|
|
430
456
|
```
|
|
431
457
|
|
|
432
458
|
## Agent Usage Rules
|
|
@@ -439,6 +465,7 @@ const model = ModelMix.new({
|
|
|
439
465
|
- Use `.json()` for structured output instead of parsing text manually. Use descriptor objects `{ description, required, enum, default, nullable }` for richer schema control.
|
|
440
466
|
- Use `.message()` for simple text, `.raw()` when you need tokens/thinking/toolCalls.
|
|
441
467
|
- For thinking models, append `think` to the method name (e.g. `sonnet45think()`).
|
|
468
|
+
- 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
469
|
- Template placeholders use `{key}` syntax in both system prompts and user messages.
|
|
443
470
|
- The library uses CommonJS internally but supports ESM import via `{ ModelMix }`.
|
|
444
471
|
- GPT-5+ models automatically use `max_completion_tokens` instead of `max_tokens`.
|
package/test/anthropic.test.js
CHANGED
|
@@ -81,4 +81,110 @@ describe('Anthropic Model Registration Tests', () => {
|
|
|
81
81
|
budget_tokens: 1638
|
|
82
82
|
});
|
|
83
83
|
});
|
|
84
|
+
|
|
85
|
+
describe('Thinking block extraction', () => {
|
|
86
|
+
it('should preserve empty thinking text from display omitted', () => {
|
|
87
|
+
const data = {
|
|
88
|
+
content: [{
|
|
89
|
+
type: 'thinking',
|
|
90
|
+
thinking: '',
|
|
91
|
+
signature: 'sig-omitted'
|
|
92
|
+
}, {
|
|
93
|
+
type: 'text',
|
|
94
|
+
text: 'Hello'
|
|
95
|
+
}]
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
expect(MixAnthropic.extractThink(data)).to.equal('');
|
|
99
|
+
expect(MixAnthropic.extractSignature(data)).to.equal('sig-omitted');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should extract summarized thinking text', () => {
|
|
103
|
+
const data = {
|
|
104
|
+
content: [{
|
|
105
|
+
type: 'thinking',
|
|
106
|
+
thinking: 'Step by step...',
|
|
107
|
+
signature: 'sig-summarized'
|
|
108
|
+
}, {
|
|
109
|
+
type: 'text',
|
|
110
|
+
text: 'Answer'
|
|
111
|
+
}]
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
expect(MixAnthropic.extractThink(data)).to.equal('Step by step...');
|
|
115
|
+
expect(MixAnthropic.extractSignature(data)).to.equal('sig-summarized');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should return null when thinking block is missing', () => {
|
|
119
|
+
const data = {
|
|
120
|
+
content: [{ type: 'text', text: 'Hello' }]
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
expect(MixAnthropic.extractThink(data)).to.equal(null);
|
|
124
|
+
expect(MixAnthropic.extractSignature(data)).to.equal(null);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should persist Anthropic content blocks as assistantMessage', () => {
|
|
128
|
+
const content = [{
|
|
129
|
+
type: 'thinking',
|
|
130
|
+
thinking: '',
|
|
131
|
+
signature: 'sig-omitted'
|
|
132
|
+
}, {
|
|
133
|
+
type: 'text',
|
|
134
|
+
text: 'Hello'
|
|
135
|
+
}];
|
|
136
|
+
const provider = new MixAnthropic();
|
|
137
|
+
const result = provider.processResponse({ data: { content, usage: {} } });
|
|
138
|
+
|
|
139
|
+
expect(result.think).to.equal('');
|
|
140
|
+
expect(result.signature).to.equal('sig-omitted');
|
|
141
|
+
expect(result.assistantMessage).to.deep.equal({
|
|
142
|
+
role: 'assistant',
|
|
143
|
+
content
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should keep tool_result after native Anthropic tool_use assistantMessage', () => {
|
|
148
|
+
// processResponse stores assistant content as Anthropic blocks (tool_use),
|
|
149
|
+
// not OpenAI-style tool_calls. convertMessages must still pair tool results.
|
|
150
|
+
const toolUseId = 'toolu_01TestToolUseId';
|
|
151
|
+
const converted = MixAnthropic.convertMessages([
|
|
152
|
+
{ role: 'user', content: [{ type: 'text', text: 'What time is it?' }] },
|
|
153
|
+
{
|
|
154
|
+
role: 'assistant',
|
|
155
|
+
content: [{
|
|
156
|
+
type: 'tool_use',
|
|
157
|
+
id: toolUseId,
|
|
158
|
+
name: 'get_current_time',
|
|
159
|
+
input: {}
|
|
160
|
+
}]
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
role: 'tool',
|
|
164
|
+
tool_call_id: toolUseId,
|
|
165
|
+
name: 'get_current_time',
|
|
166
|
+
content: '2026-07-30T12:00:00Z'
|
|
167
|
+
}
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
expect(converted).to.have.length(3);
|
|
171
|
+
expect(converted[1]).to.deep.equal({
|
|
172
|
+
role: 'assistant',
|
|
173
|
+
content: [{
|
|
174
|
+
type: 'tool_use',
|
|
175
|
+
id: toolUseId,
|
|
176
|
+
name: 'get_current_time',
|
|
177
|
+
input: {}
|
|
178
|
+
}]
|
|
179
|
+
});
|
|
180
|
+
expect(converted[2]).to.deep.equal({
|
|
181
|
+
role: 'user',
|
|
182
|
+
content: [{
|
|
183
|
+
type: 'tool_result',
|
|
184
|
+
tool_use_id: toolUseId,
|
|
185
|
+
content: '2026-07-30T12:00:00Z'
|
|
186
|
+
}]
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
});
|
|
84
190
|
});
|
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
|
});
|