modelmix 5.1.9 → 5.1.12
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 +18 -6
- package/demo/free.js +1 -3
- package/demo/short.js +15 -13
- package/effort.js +13 -7
- package/index.d.ts +3 -0
- package/index.js +71 -71
- package/lib/model-chain.js +2 -2
- package/lib/provider-api-key.js +9 -0
- package/lib/providers/anthropic.js +22 -20
- package/lib/providers/base.js +4 -22
- package/lib/providers/google.js +3 -6
- package/lib/providers/openai-compatible.js +11 -40
- package/lib/providers/openai-options.js +16 -0
- package/lib/providers/openai.js +18 -39
- package/lib/providers.js +5 -2
- package/lib/token-usage.js +19 -4
- package/package.json +2 -1
- package/plugins/rlm/lib/budget.js +2 -7
- package/plugins/rlm/lib/planner-prompt.js +1 -7
- package/plugins/rlm/lib/plugin.js +1 -6
- package/plugins/rlm/lib/validation.js +14 -0
- package/plugins/rlm/lib/variable-descriptors.js +1 -5
- package/plugins/rlm/lib/worker-catalog.js +1 -5
- package/pnpm-workspace.yaml +10 -5
- package/schema.js +0 -28
- package/skills/modelmix/SKILL.md +10 -6
- package/test/anthropic.test.js +77 -1
- package/test/bottleneck.test.js +2 -2
- package/test/effort.test.js +24 -1
- package/test/fallback.test.js +101 -1
- package/test/glm.test.js +2 -6
- package/test/kimi.test.js +2 -6
- package/test/live.test.js +1 -1
- package/test/muse.test.js +24 -7
- package/test/provider-expansion.test.js +17 -0
- package/test/public-api.test.js +2 -2
- package/test/qwen.test.js +2 -3
- package/test/tokens.test.js +23 -8
|
@@ -2,14 +2,12 @@ const {
|
|
|
2
2
|
GROK420_REASONING,
|
|
3
3
|
GROK420_NON_REASONING
|
|
4
4
|
} = require('../../effort');
|
|
5
|
+
const { requireProviderApiKey } = require('../provider-api-key');
|
|
5
6
|
|
|
6
7
|
function createCompatibleProviders({ MixCustom, MixOpenAI }) {
|
|
7
8
|
class MixMiniMax extends MixOpenAI {
|
|
8
9
|
getDefaultConfig(customConfig) {
|
|
9
|
-
const apiKey = customConfig
|
|
10
|
-
if (!apiKey) {
|
|
11
|
-
throw new Error('MiniMax API key not found. Please provide it in config or set MINIMAX_API_KEY environment variable.');
|
|
12
|
-
}
|
|
10
|
+
const apiKey = requireProviderApiKey(customConfig, 'MINIMAX_API_KEY', 'MiniMax');
|
|
13
11
|
|
|
14
12
|
return MixCustom.prototype.getDefaultConfig.call(this, {
|
|
15
13
|
url: 'https://api.minimax.io/v1/chat/completions',
|
|
@@ -29,10 +27,7 @@ function createCompatibleProviders({ MixCustom, MixOpenAI }) {
|
|
|
29
27
|
|
|
30
28
|
class MixMiMo extends MixOpenAI {
|
|
31
29
|
getDefaultConfig(customConfig) {
|
|
32
|
-
const apiKey = customConfig
|
|
33
|
-
if (!apiKey) {
|
|
34
|
-
throw new Error('MiMo API key not found. Please provide it in config or set MIMO_API_KEY environment variable.');
|
|
35
|
-
}
|
|
30
|
+
const apiKey = requireProviderApiKey(customConfig, 'MIMO_API_KEY', 'MiMo');
|
|
36
31
|
|
|
37
32
|
return MixCustom.prototype.getDefaultConfig.call(this, {
|
|
38
33
|
url: 'https://api.xiaomimimo.com/v1/chat/completions',
|
|
@@ -53,10 +48,7 @@ function createCompatibleProviders({ MixCustom, MixOpenAI }) {
|
|
|
53
48
|
|
|
54
49
|
class MixPerplexity extends MixCustom {
|
|
55
50
|
getDefaultConfig(customConfig) {
|
|
56
|
-
const apiKey = customConfig
|
|
57
|
-
if (!apiKey) {
|
|
58
|
-
throw new Error('Perplexity API key not found. Please provide it in config or set PPLX_API_KEY environment variable.');
|
|
59
|
-
}
|
|
51
|
+
const apiKey = requireProviderApiKey(customConfig, 'PPLX_API_KEY', 'Perplexity');
|
|
60
52
|
|
|
61
53
|
return super.getDefaultConfig({
|
|
62
54
|
url: 'https://api.perplexity.ai/chat/completions',
|
|
@@ -133,10 +125,7 @@ function createCompatibleProviders({ MixCustom, MixOpenAI }) {
|
|
|
133
125
|
|
|
134
126
|
class MixGrok extends MixOpenAI {
|
|
135
127
|
getDefaultConfig(customConfig) {
|
|
136
|
-
const apiKey = customConfig
|
|
137
|
-
if (!apiKey) {
|
|
138
|
-
throw new Error('Grok API key not found. Please provide it in config or set XAI_API_KEY environment variable.');
|
|
139
|
-
}
|
|
128
|
+
const apiKey = requireProviderApiKey(customConfig, 'XAI_API_KEY', 'Grok');
|
|
140
129
|
|
|
141
130
|
return super.getDefaultConfig({
|
|
142
131
|
url: 'https://api.x.ai/v1/chat/completions',
|
|
@@ -155,10 +144,7 @@ function createCompatibleProviders({ MixCustom, MixOpenAI }) {
|
|
|
155
144
|
|
|
156
145
|
class MixLambda extends MixCustom {
|
|
157
146
|
getDefaultConfig(customConfig) {
|
|
158
|
-
const apiKey = customConfig
|
|
159
|
-
if (!apiKey) {
|
|
160
|
-
throw new Error('Lambda API key not found. Please provide it in config or set LAMBDA_API_KEY environment variable.');
|
|
161
|
-
}
|
|
147
|
+
const apiKey = requireProviderApiKey(customConfig, 'LAMBDA_API_KEY', 'Lambda');
|
|
162
148
|
|
|
163
149
|
return super.getDefaultConfig({
|
|
164
150
|
url: 'https://api.lambda.ai/v1/chat/completions',
|
|
@@ -235,10 +221,7 @@ function createCompatibleProviders({ MixCustom, MixOpenAI }) {
|
|
|
235
221
|
|
|
236
222
|
class MixGroq extends MixCustom {
|
|
237
223
|
getDefaultConfig(customConfig) {
|
|
238
|
-
const apiKey = customConfig
|
|
239
|
-
if (!apiKey) {
|
|
240
|
-
throw new Error('Groq API key not found. Please provide it in config or set GROQ_API_KEY environment variable.');
|
|
241
|
-
}
|
|
224
|
+
const apiKey = requireProviderApiKey(customConfig, 'GROQ_API_KEY', 'Groq');
|
|
242
225
|
|
|
243
226
|
return super.getDefaultConfig({
|
|
244
227
|
url: 'https://api.groq.com/openai/v1/chat/completions',
|
|
@@ -250,10 +233,7 @@ function createCompatibleProviders({ MixCustom, MixOpenAI }) {
|
|
|
250
233
|
|
|
251
234
|
class MixTogether extends MixCustom {
|
|
252
235
|
getDefaultConfig(customConfig) {
|
|
253
|
-
const apiKey = customConfig
|
|
254
|
-
if (!apiKey) {
|
|
255
|
-
throw new Error('Together API key not found. Please provide it in config or set TOGETHER_API_KEY environment variable.');
|
|
256
|
-
}
|
|
236
|
+
const apiKey = requireProviderApiKey(customConfig, 'TOGETHER_API_KEY', 'Together');
|
|
257
237
|
|
|
258
238
|
return super.getDefaultConfig({
|
|
259
239
|
url: 'https://api.together.xyz/v1/chat/completions',
|
|
@@ -272,10 +252,7 @@ function createCompatibleProviders({ MixCustom, MixOpenAI }) {
|
|
|
272
252
|
|
|
273
253
|
class MixCerebras extends MixCustom {
|
|
274
254
|
getDefaultConfig(customConfig) {
|
|
275
|
-
const apiKey = customConfig
|
|
276
|
-
if (!apiKey) {
|
|
277
|
-
throw new Error('Cerebras API key not found. Please provide it in config or set CEREBRAS_API_KEY environment variable.');
|
|
278
|
-
}
|
|
255
|
+
const apiKey = requireProviderApiKey(customConfig, 'CEREBRAS_API_KEY', 'Cerebras');
|
|
279
256
|
|
|
280
257
|
return super.getDefaultConfig({
|
|
281
258
|
url: 'https://api.cerebras.ai/v1/chat/completions',
|
|
@@ -292,10 +269,7 @@ function createCompatibleProviders({ MixCustom, MixOpenAI }) {
|
|
|
292
269
|
|
|
293
270
|
class MixFireworks extends MixCustom {
|
|
294
271
|
getDefaultConfig(customConfig) {
|
|
295
|
-
const apiKey = customConfig
|
|
296
|
-
if (!apiKey) {
|
|
297
|
-
throw new Error('Fireworks API key not found. Please provide it in config or set FIREWORKS_API_KEY environment variable.');
|
|
298
|
-
}
|
|
272
|
+
const apiKey = requireProviderApiKey(customConfig, 'FIREWORKS_API_KEY', 'Fireworks');
|
|
299
273
|
|
|
300
274
|
return super.getDefaultConfig({
|
|
301
275
|
url: 'https://api.fireworks.ai/inference/v1/chat/completions',
|
|
@@ -307,10 +281,7 @@ function createCompatibleProviders({ MixCustom, MixOpenAI }) {
|
|
|
307
281
|
|
|
308
282
|
class MixNVIDIA extends MixCustom {
|
|
309
283
|
getDefaultConfig(customConfig) {
|
|
310
|
-
const apiKey = customConfig
|
|
311
|
-
if (!apiKey) {
|
|
312
|
-
throw new Error('NVIDIA API key not found. Please provide it in config or set NVIDIA_API_KEY environment variable.');
|
|
313
|
-
}
|
|
284
|
+
const apiKey = requireProviderApiKey(customConfig, 'NVIDIA_API_KEY', 'NVIDIA');
|
|
314
285
|
|
|
315
286
|
return super.getDefaultConfig({
|
|
316
287
|
url: 'https://integrate.api.nvidia.com/v1/chat/completions',
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
function normalizeOpenAIOptions(options) {
|
|
2
|
+
if (/^o\d/.test(options.model || '')) {
|
|
3
|
+
delete options.max_tokens;
|
|
4
|
+
delete options.temperature;
|
|
5
|
+
}
|
|
6
|
+
if (options.model?.includes('gpt-5')) {
|
|
7
|
+
if (options.max_tokens) {
|
|
8
|
+
options.max_completion_tokens = options.max_tokens;
|
|
9
|
+
delete options.max_tokens;
|
|
10
|
+
}
|
|
11
|
+
delete options.temperature;
|
|
12
|
+
}
|
|
13
|
+
return options;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = { normalizeOpenAIOptions };
|
package/lib/providers/openai.js
CHANGED
|
@@ -4,31 +4,21 @@ const {
|
|
|
4
4
|
normalizeContentCache,
|
|
5
5
|
stripContentCacheMetadata
|
|
6
6
|
} = require('../content-cache');
|
|
7
|
+
const { requireProviderApiKey } = require('../provider-api-key');
|
|
8
|
+
const { normalizeOpenAIOptions } = require('./openai-options');
|
|
7
9
|
|
|
8
10
|
function createOpenAIProviders({
|
|
9
11
|
ModelMix,
|
|
10
12
|
MixCustom,
|
|
11
13
|
MixOpenAI,
|
|
12
|
-
MixModeration
|
|
14
|
+
MixModeration,
|
|
15
|
+
rejectsAnthropicSamplingParams
|
|
13
16
|
}) {
|
|
14
17
|
const WebSocket = require('ws');
|
|
15
18
|
|
|
16
19
|
class MixOpenAIResponses extends MixOpenAI {
|
|
17
20
|
async create({ config = {}, options = {} } = {}) {
|
|
18
|
-
|
|
19
|
-
// Keep GPT/o-model option normalization behavior
|
|
20
|
-
if (options.model?.startsWith('o')) {
|
|
21
|
-
delete options.max_tokens;
|
|
22
|
-
delete options.temperature;
|
|
23
|
-
}
|
|
24
|
-
if (options.model?.includes('gpt-5')) {
|
|
25
|
-
if (options.max_tokens) {
|
|
26
|
-
options.max_completion_tokens = options.max_tokens;
|
|
27
|
-
delete options.max_tokens;
|
|
28
|
-
}
|
|
29
|
-
delete options.temperature;
|
|
30
|
-
}
|
|
31
|
-
|
|
21
|
+
normalizeOpenAIOptions(options);
|
|
32
22
|
const responsesUrl = this.config.url.replace('/chat/completions', '/responses');
|
|
33
23
|
const request = MixOpenAIResponses.buildResponsesRequest(options, config);
|
|
34
24
|
const response = await fetchJsonResponse(responsesUrl, {
|
|
@@ -246,10 +236,7 @@ function createOpenAIProviders({
|
|
|
246
236
|
|
|
247
237
|
class MixOpenAIModeration extends MixModeration {
|
|
248
238
|
getDefaultConfig(customConfig) {
|
|
249
|
-
const apiKey = customConfig
|
|
250
|
-
if (!apiKey) {
|
|
251
|
-
throw new Error('OpenAI API key not found. Please provide it in config or set OPENAI_API_KEY environment variable.');
|
|
252
|
-
}
|
|
239
|
+
const apiKey = requireProviderApiKey(customConfig, 'OPENAI_API_KEY', 'OpenAI');
|
|
253
240
|
|
|
254
241
|
return super.getDefaultConfig({
|
|
255
242
|
url: 'https://api.openai.com/v1/moderations',
|
|
@@ -357,18 +344,7 @@ function createOpenAIProviders({
|
|
|
357
344
|
}
|
|
358
345
|
|
|
359
346
|
async create({ config = {}, options = {} } = {}) {
|
|
360
|
-
|
|
361
|
-
delete options.max_tokens;
|
|
362
|
-
delete options.temperature;
|
|
363
|
-
}
|
|
364
|
-
if (options.model?.includes('gpt-5')) {
|
|
365
|
-
if (options.max_tokens) {
|
|
366
|
-
options.max_completion_tokens = options.max_tokens;
|
|
367
|
-
delete options.max_tokens;
|
|
368
|
-
}
|
|
369
|
-
delete options.temperature;
|
|
370
|
-
}
|
|
371
|
-
|
|
347
|
+
normalizeOpenAIOptions(options);
|
|
372
348
|
const mergedConfig = { ...this.config, ...config };
|
|
373
349
|
const realtimeUrl = `${mergedConfig.realtimeUrl}?model=${encodeURIComponent(options.model)}`;
|
|
374
350
|
const timeoutMs = mergedConfig.websocketTimeoutMs || 120000;
|
|
@@ -556,10 +532,7 @@ function createOpenAIProviders({
|
|
|
556
532
|
|
|
557
533
|
class MixOpenRouter extends MixOpenAI {
|
|
558
534
|
getDefaultConfig(customConfig) {
|
|
559
|
-
const apiKey = customConfig
|
|
560
|
-
if (!apiKey) {
|
|
561
|
-
throw new Error('OpenRouter API key not found. Please provide it in config or set OPENROUTER_API_KEY environment variable.');
|
|
562
|
-
}
|
|
535
|
+
const apiKey = requireProviderApiKey(customConfig, 'OPENROUTER_API_KEY', 'OpenRouter');
|
|
563
536
|
|
|
564
537
|
return MixCustom.prototype.getDefaultConfig.call(this, {
|
|
565
538
|
url: 'https://openrouter.ai/api/v1/chat/completions',
|
|
@@ -567,14 +540,20 @@ function createOpenAIProviders({
|
|
|
567
540
|
...customConfig
|
|
568
541
|
});
|
|
569
542
|
}
|
|
543
|
+
|
|
544
|
+
async create({ config = {}, options = {} } = {}) {
|
|
545
|
+
if (rejectsAnthropicSamplingParams(options.model)) {
|
|
546
|
+
delete options.temperature;
|
|
547
|
+
delete options.top_p;
|
|
548
|
+
delete options.top_k;
|
|
549
|
+
}
|
|
550
|
+
return super.create({ config, options });
|
|
551
|
+
}
|
|
570
552
|
}
|
|
571
553
|
|
|
572
554
|
class MixKimi extends MixOpenAI {
|
|
573
555
|
getDefaultConfig(customConfig) {
|
|
574
|
-
const apiKey = customConfig
|
|
575
|
-
if (!apiKey) {
|
|
576
|
-
throw new Error('Moonshot API key not found. Please provide it in config or set MOONSHOT_API_KEY environment variable.');
|
|
577
|
-
}
|
|
556
|
+
const apiKey = requireProviderApiKey(customConfig, 'MOONSHOT_API_KEY', 'Moonshot');
|
|
578
557
|
|
|
579
558
|
return MixCustom.prototype.getDefaultConfig.call(this, {
|
|
580
559
|
url: 'https://api.moonshot.ai/v1/chat/completions',
|
package/lib/providers.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
const createBaseProviders = require('./providers/base');
|
|
2
2
|
const createOpenAIProviders = require('./providers/openai');
|
|
3
|
-
const
|
|
3
|
+
const {
|
|
4
|
+
createAnthropicProviders,
|
|
5
|
+
rejectsAnthropicSamplingParams
|
|
6
|
+
} = require('./providers/anthropic');
|
|
4
7
|
const createCompatibleProviders = require('./providers/openai-compatible');
|
|
5
8
|
const createGoogleProviders = require('./providers/google');
|
|
6
9
|
|
|
7
10
|
function createProviders({ ModelMix, log }) {
|
|
8
11
|
const base = createBaseProviders({ ModelMix });
|
|
9
|
-
const openai = createOpenAIProviders({ ModelMix, ...base });
|
|
12
|
+
const openai = createOpenAIProviders({ ModelMix, rejectsAnthropicSamplingParams, ...base });
|
|
10
13
|
const anthropic = createAnthropicProviders({ ModelMix, MixCustom: base.MixCustom, log });
|
|
11
14
|
const compatible = createCompatibleProviders({
|
|
12
15
|
MixCustom: base.MixCustom,
|
package/lib/token-usage.js
CHANGED
|
@@ -35,6 +35,8 @@ const MODEL_PRICING = {
|
|
|
35
35
|
'gpt-5.3-codex': { input: 1.75, cachedInput: 0.175, output: 14.00 },
|
|
36
36
|
'gpt-5.2': { input: 1.75, cachedInput: 0.175, output: 14.00 },
|
|
37
37
|
'gpt-5.2-chat-latest': { input: 1.75, cachedInput: 0.175, output: 14.00 },
|
|
38
|
+
'gpt-5.3-chat-latest': { input: 1.75, cachedInput: 0.175, output: 14.00 },
|
|
39
|
+
'openai/gpt-5.3-chat': { input: 1.75, cachedInput: 0.175, output: 14.00 },
|
|
38
40
|
'gpt-5.1': { input: 1.25, cachedInput: 0.125, output: 10.00 },
|
|
39
41
|
'gpt-5': { input: 1.25, cachedInput: 0.125, output: 10.00 },
|
|
40
42
|
'gpt-5-mini': { input: 0.25, cachedInput: 0.025, output: 2.00 },
|
|
@@ -47,6 +49,8 @@ const MODEL_PRICING = {
|
|
|
47
49
|
'gpt-oss-120b': { input: 0.15, output: 0.60 },
|
|
48
50
|
'accounts/fireworks/models/gpt-oss-120b': { input: 0.15, cachedInput: 0.014, output: 0.60 },
|
|
49
51
|
// Anthropic
|
|
52
|
+
'claude-fable-5-1': { input: 10.00, cachedInput: 0.25, cacheWrite: 12.50, cacheWrite1h: 20.00, output: 50.00 },
|
|
53
|
+
'anthropic/claude-fable-5.1': { input: 10.00, cachedInput: 0.25, cacheWrite: 12.50, cacheWrite1h: 20.00, output: 50.00 },
|
|
50
54
|
'claude-fable-5': { input: 10.00, cachedInput: 1.00, cacheWrite: 12.50, cacheWrite1h: 20.00, output: 50.00 },
|
|
51
55
|
'claude-opus-5': { input: 5.00, cachedInput: 0.50, cacheWrite: 6.25, cacheWrite1h: 10.00, output: 25.00 },
|
|
52
56
|
'claude-sonnet-5': { input: 3.00, cachedInput: 0.30, cacheWrite: 3.75, cacheWrite1h: 6.00, output: 15.00 },
|
|
@@ -75,10 +79,11 @@ const MODEL_PRICING = {
|
|
|
75
79
|
'grok-4.20-0309': { input: 1.25, output: 2.50 },
|
|
76
80
|
'grok-4.20-0309-reasoning': { input: 1.25, output: 2.50 },
|
|
77
81
|
'grok-4.20-0309-non-reasoning': { input: 1.25, output: 2.50 },
|
|
78
|
-
// Muse
|
|
82
|
+
// Muse
|
|
79
83
|
'meta/muse-glimmer-30b': { input: 0.35, cachedInput: 0.04, output: 1.50 },
|
|
80
84
|
'accounts/fireworks/models/muse-glimmer-30b': { input: 0.35, cachedInput: 0.04, output: 1.50 },
|
|
81
85
|
'meta-models/Muse-Glimmer-30B': { input: 0.35, cachedInput: 0.04, output: 1.50 },
|
|
86
|
+
'meta/muse-spark-1.2-contributor': { input: 0.10, cachedInput: 0.002, output: 0.20 },
|
|
82
87
|
// Fireworks
|
|
83
88
|
'accounts/fireworks/models/deepseek-v4-flash': { input: 0.14, output: 0.28 },
|
|
84
89
|
'accounts/fireworks/models/deepseek-v4-pro': { input: 1.74, output: 3.48 },
|
|
@@ -200,7 +205,7 @@ function normalizeTokenUsage({ input = 0, output = 0, thinking = 0, total, cache
|
|
|
200
205
|
}
|
|
201
206
|
|
|
202
207
|
function calculateCostBreakdown(modelKey, tokens) {
|
|
203
|
-
const pricing =
|
|
208
|
+
const pricing = getModelPricing(modelKey);
|
|
204
209
|
if (!pricing) return normalizeTokenUsage().costBreakdown;
|
|
205
210
|
|
|
206
211
|
const normalized = normalizeTokenUsage(tokens);
|
|
@@ -249,7 +254,7 @@ function calculateCostBreakdown(modelKey, tokens) {
|
|
|
249
254
|
}
|
|
250
255
|
|
|
251
256
|
function calculateCacheMetrics(modelKey, tokens) {
|
|
252
|
-
const pricing =
|
|
257
|
+
const pricing = getModelPricing(modelKey);
|
|
253
258
|
const emptyMetrics = {
|
|
254
259
|
cacheSavings: 0,
|
|
255
260
|
cacheWritePremium: 0,
|
|
@@ -296,7 +301,17 @@ function calculateCost(modelKey, tokens) {
|
|
|
296
301
|
}
|
|
297
302
|
|
|
298
303
|
function hasModelPricing(modelKey) {
|
|
299
|
-
return
|
|
304
|
+
return getModelPricing(modelKey) !== undefined;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function getModelPricing(modelKey) {
|
|
308
|
+
if (Object.prototype.hasOwnProperty.call(MODEL_PRICING, modelKey)) {
|
|
309
|
+
return MODEL_PRICING[modelKey];
|
|
310
|
+
}
|
|
311
|
+
if (typeof modelKey === 'string' && modelKey.startsWith('openai/')) {
|
|
312
|
+
return MODEL_PRICING[modelKey.slice('openai/'.length)];
|
|
313
|
+
}
|
|
314
|
+
return undefined;
|
|
300
315
|
}
|
|
301
316
|
|
|
302
317
|
function extractCacheTokens(usage = {}) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "modelmix",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.12",
|
|
4
4
|
"description": "🧬 Reliable interface with automatic fallback for AI LLMs.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"gpt5",
|
|
28
28
|
"opus50",
|
|
29
29
|
"opus5",
|
|
30
|
+
"fable51",
|
|
30
31
|
"fable50",
|
|
31
32
|
"fable",
|
|
32
33
|
"sonnet",
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { positiveInteger } = require('./validation');
|
|
2
|
+
|
|
1
3
|
class RlmLimitError extends Error {
|
|
2
4
|
constructor(limit, message) {
|
|
3
5
|
super(message);
|
|
@@ -6,13 +8,6 @@ class RlmLimitError extends Error {
|
|
|
6
8
|
}
|
|
7
9
|
}
|
|
8
10
|
|
|
9
|
-
function positiveInteger(value, name) {
|
|
10
|
-
if (!Number.isInteger(value) || value <= 0) {
|
|
11
|
-
throw new TypeError(`${name} must be a positive integer.`);
|
|
12
|
-
}
|
|
13
|
-
return value;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
11
|
function validateRuntimeLimits(limits) {
|
|
17
12
|
if (!limits || typeof limits !== 'object' || Array.isArray(limits)) {
|
|
18
13
|
throw new TypeError('limits must be a plain object.');
|
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const { describeVariables } = require('./variable-descriptors');
|
|
3
|
+
const { positiveInteger } = require('./validation');
|
|
3
4
|
|
|
4
5
|
const PLANNER_SYSTEM_TEMPLATE = path.resolve(__dirname, '../prompts/planner.md');
|
|
5
6
|
|
|
6
|
-
function positiveInteger(value, name) {
|
|
7
|
-
if (!Number.isInteger(value) || value <= 0) {
|
|
8
|
-
throw new TypeError(`${name} must be a positive integer.`);
|
|
9
|
-
}
|
|
10
|
-
return value;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
7
|
function planningHint(name, descriptor, maxQueryBytes) {
|
|
14
8
|
const payloadBytes = descriptor.utf8Bytes ?? descriptor.estimatedBytes;
|
|
15
9
|
if (payloadBytes <= maxQueryBytes) {
|
|
@@ -9,12 +9,7 @@ const {
|
|
|
9
9
|
sumTokens
|
|
10
10
|
} = require('./runtime');
|
|
11
11
|
const { createWorkerCatalog } = require('./worker-catalog');
|
|
12
|
-
|
|
13
|
-
function isPlainObject(value) {
|
|
14
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
15
|
-
const prototype = Object.getPrototypeOf(value);
|
|
16
|
-
return prototype === Object.prototype || prototype === null;
|
|
17
|
-
}
|
|
12
|
+
const { isPlainObject } = require('./validation');
|
|
18
13
|
|
|
19
14
|
function validateSandbox(sandbox) {
|
|
20
15
|
if (!sandbox || typeof sandbox !== 'object' || typeof sandbox.execute !== 'function') {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
function isPlainObject(value) {
|
|
2
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
3
|
+
const prototype = Object.getPrototypeOf(value);
|
|
4
|
+
return prototype === Object.prototype || prototype === null;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function positiveInteger(value, name) {
|
|
8
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
9
|
+
throw new TypeError(`${name} must be a positive integer.`);
|
|
10
|
+
}
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = { isPlainObject, positiveInteger };
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
3
|
-
const prototype = Object.getPrototypeOf(value);
|
|
4
|
-
return prototype === Object.prototype || prototype === null;
|
|
5
|
-
}
|
|
1
|
+
const { isPlainObject } = require('./validation');
|
|
6
2
|
|
|
7
3
|
function roundAverage(total, count) {
|
|
8
4
|
return count === 0 ? 0 : Number((total / count).toFixed(2));
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
3
|
-
const prototype = Object.getPrototypeOf(value);
|
|
4
|
-
return prototype === Object.prototype || prototype === null;
|
|
5
|
-
}
|
|
1
|
+
const { isPlainObject } = require('./validation');
|
|
6
2
|
|
|
7
3
|
function finiteRating(value, path) {
|
|
8
4
|
if (!Number.isFinite(value) || value < 0) {
|
package/pnpm-workspace.yaml
CHANGED
|
@@ -6,11 +6,16 @@ allowBuilds:
|
|
|
6
6
|
|
|
7
7
|
minimumReleaseAgeExclude:
|
|
8
8
|
- ws@8.21.0
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
9
|
+
- fast-uri@3.1.5
|
|
10
|
+
- hono@4.12.34
|
|
11
|
+
- ip-address@10.3.1
|
|
12
|
+
- js-yaml@4.3.1
|
|
13
|
+
- brace-expansion@5.0.9
|
|
12
14
|
overrides:
|
|
13
|
-
brace-expansion
|
|
15
|
+
brace-expansion: 5.0.9
|
|
14
16
|
diff: 8.0.4
|
|
15
|
-
|
|
17
|
+
fast-uri: 3.1.5
|
|
18
|
+
hono: 4.12.34
|
|
19
|
+
ip-address: 10.3.1
|
|
20
|
+
js-yaml: 4.3.1
|
|
16
21
|
serialize-javascript: 7.0.5
|
package/schema.js
CHANGED
|
@@ -122,31 +122,3 @@ function generateJsonSchema(example, descriptions = {}) {
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
module.exports = generateJsonSchema;
|
|
125
|
-
|
|
126
|
-
// const example = {
|
|
127
|
-
// name: 'Alice',
|
|
128
|
-
// age: 30,
|
|
129
|
-
// email: 'alice@example.com',
|
|
130
|
-
// birthDate: '1990-01-01',
|
|
131
|
-
// isAdmin: false,
|
|
132
|
-
// preferences: {
|
|
133
|
-
// theme: 'dark',
|
|
134
|
-
// notifications: true
|
|
135
|
-
// },
|
|
136
|
-
// tags: ['admin', 'user']
|
|
137
|
-
// };
|
|
138
|
-
|
|
139
|
-
// const descriptions = {
|
|
140
|
-
// name: 'Full name of the user',
|
|
141
|
-
// age: 'Age must be 0 or greater',
|
|
142
|
-
// email: 'User email address',
|
|
143
|
-
// // birthDate: 'User birth date in YYYY-MM-DD format',
|
|
144
|
-
// preferences: {
|
|
145
|
-
// theme: 'Theme preference (light/dark)',
|
|
146
|
-
// notifications: 'Whether notifications are enabled'
|
|
147
|
-
// }
|
|
148
|
-
// };
|
|
149
|
-
|
|
150
|
-
// const schema = generateJsonSchema(example);
|
|
151
|
-
|
|
152
|
-
// console.log(JSON.stringify(schema, null, 2));
|
package/skills/modelmix/SKILL.md
CHANGED
|
@@ -151,13 +151,17 @@ ModelMix.new({ config: { effort: 80 } })
|
|
|
151
151
|
|
|
152
152
|
Use `ModerationMix.new().openai()` with `.raw()` to classify text and images through OpenAI's Moderations endpoint. Read the results from `raw.moderation`. `ModerationMix` accepts moderation providers as ordered fallbacks, rejects generative providers, and does not generate text or support streaming.
|
|
153
153
|
|
|
154
|
-
`
|
|
154
|
+
`gpt56sol()` `gpt56terra()` `gpt56luna()` `gpt55()` `gpt55pro()` `gpt54()` `gpt54mini()` `gpt54nano()` `gpt54pro()` `gpt53codex()` `gpt53chat()` `gpt52()` `gpt51()` `gpt5()` `gpt5mini()` `gpt5nano()` `gptRealtime()` `gptRealtimeMini()` `gptOss()`
|
|
155
|
+
|
|
156
|
+
Every textual GPT-5 shortcut registers only the official OpenAI model by default. Pass `mix: { openrouter: true }` to `ModelMix.new()` or to an individual shortcut to append its `openai/*` OpenRouter route as a fallback. `gpt53chat()` uses `gpt-5.3-chat-latest` officially and `openai/gpt-5.3-chat` through OpenRouter. Both API keys are required when that fallback is enabled. Realtime shortcuts remain official-only.
|
|
155
157
|
|
|
156
158
|
### Anthropic
|
|
157
|
-
`fable50()` `opus50()` `opus48()` `opus47()` `opus46()` `sonnet5()` `sonnet46()` `sonnet45()` `haiku45()`
|
|
159
|
+
`fable51()` `fable50()` `opus50()` `opus48()` `opus47()` `opus46()` `sonnet5()` `sonnet46()` `sonnet45()` `haiku45()`
|
|
158
160
|
|
|
159
161
|
Use `.effort(n)` (or `config.effort`) to enable Anthropic thinking — e.g. `.effort(100).opus50()`. `fable5()` and `opus5()` remain available as compatibility aliases.
|
|
160
162
|
|
|
163
|
+
`fable51()` registers `claude-fable-5-1` through Anthropic by default. Pass `mix: { openrouter: true }` to append `anthropic/claude-fable-5.1` as its fallback.
|
|
164
|
+
|
|
161
165
|
### Google
|
|
162
166
|
`gemini31pro()` `gemini37flash()` `gemini36flash()` `gemini35flash()` `gemini35flashLite()` `gemini31flashLite()`
|
|
163
167
|
|
|
@@ -186,7 +190,7 @@ Use `.effort(n)` (or `config.effort`) to enable Anthropic thinking — e.g. `.ef
|
|
|
186
190
|
`GLM46()`
|
|
187
191
|
|
|
188
192
|
### OpenRouter
|
|
189
|
-
`museGlimmer30b()` `gptOss()` `qwen35397b()` `qwen36plus()` `qwen37plus()` `qwen3827b()` `qwen38flash()` `hermes470b()` `hermes4405b()` `qwen38max()` `kimiK27Code()` `kimiK3()` `minimaxM27()` `minimaxM3()` `GLM45()` `GLM52()` `GLM53()` `GLM53Flash()`
|
|
193
|
+
`museGlimmer30b()` `museSpark12Contributor()` `gptOss()` `qwen35397b()` `qwen36plus()` `qwen37plus()` `qwen3827b()` `qwen38flash()` `hermes470b()` `hermes4405b()` `qwen38max()` `kimiK27Code()` `kimiK3()` `minimaxM27()` `minimaxM3()` `GLM45()` `GLM52()` `GLM53()` `GLM53Flash()`
|
|
190
194
|
|
|
191
195
|
### Multi-provider (auto-fallback across free/paid tiers)
|
|
192
196
|
`hermes3()` `kimiK25()`
|
|
@@ -551,10 +555,9 @@ For full debug output, also set: `DEBUG=ModelMix* node script.js`
|
|
|
551
555
|
### Free-tier models
|
|
552
556
|
|
|
553
557
|
```javascript
|
|
554
|
-
const model = ModelMix.new(
|
|
558
|
+
const model = ModelMix.new()
|
|
555
559
|
.gptOss()
|
|
556
560
|
.kimiK25()
|
|
557
|
-
.hermes3()
|
|
558
561
|
.addText("What is the capital of France?");
|
|
559
562
|
console.log(await model.message());
|
|
560
563
|
```
|
|
@@ -568,7 +571,8 @@ Some model shorthands register the same model across multiple providers for maxi
|
|
|
568
571
|
```javascript
|
|
569
572
|
const model = ModelMix.new({
|
|
570
573
|
mix: {
|
|
571
|
-
|
|
574
|
+
anthropic: true, // fable51() default: true
|
|
575
|
+
openrouter: false, // fallback default: false
|
|
572
576
|
cerebras: true, // default: true
|
|
573
577
|
groq: true, // default: true
|
|
574
578
|
together: false, // default: false
|
package/test/anthropic.test.js
CHANGED
|
@@ -1,8 +1,84 @@
|
|
|
1
1
|
const { expect } = require('chai');
|
|
2
2
|
const nock = require('nock');
|
|
3
|
-
const { ModelMix, MixAnthropic } = require('../index.js');
|
|
3
|
+
const { ModelMix, MixAnthropic, MixOpenRouter } = require('../index.js');
|
|
4
4
|
|
|
5
5
|
describe('Anthropic Model Registration Tests', () => {
|
|
6
|
+
it('should register Claude Fable 5.1 through Anthropic by default', () => {
|
|
7
|
+
const model = ModelMix.new().fable51();
|
|
8
|
+
|
|
9
|
+
expect(model.models.map(({ key }) => key)).to.deep.equal(['claude-fable-5-1']);
|
|
10
|
+
expect(model.models[0].provider).to.be.instanceOf(MixAnthropic);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should allow enabling or selecting the OpenRouter Claude Fable 5.1 route', () => {
|
|
14
|
+
const both = ModelMix.new().fable51({ mix: { openrouter: true } });
|
|
15
|
+
const routed = ModelMix.new().fable51({
|
|
16
|
+
mix: { anthropic: false, openrouter: true }
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
expect(both.models.map(({ key }) => key)).to.deep.equal([
|
|
20
|
+
'claude-fable-5-1',
|
|
21
|
+
'anthropic/claude-fable-5.1'
|
|
22
|
+
]);
|
|
23
|
+
expect(both.models[1].provider).to.be.instanceOf(MixOpenRouter);
|
|
24
|
+
expect(routed.models.map(({ key }) => key)).to.deep.equal(['anthropic/claude-fable-5.1']);
|
|
25
|
+
expect(routed.models[0].provider).to.be.instanceOf(MixOpenRouter);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should strip unsupported sampling params from OpenRouter Claude Fable 5.1 requests', async () => {
|
|
29
|
+
const provider = new MixOpenRouter();
|
|
30
|
+
let requestBody;
|
|
31
|
+
nock('https://openrouter.ai')
|
|
32
|
+
.post('/api/v1/chat/completions', body => {
|
|
33
|
+
requestBody = body;
|
|
34
|
+
return true;
|
|
35
|
+
})
|
|
36
|
+
.reply(200, {
|
|
37
|
+
choices: [{ message: { content: 'Done' } }],
|
|
38
|
+
usage: { prompt_tokens: 1, completion_tokens: 1 }
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
await provider.create({
|
|
42
|
+
config: { system: 'You are an assistant.' },
|
|
43
|
+
options: {
|
|
44
|
+
model: 'anthropic/claude-fable-5.1',
|
|
45
|
+
messages: [{ role: 'user', content: 'Hello' }],
|
|
46
|
+
max_tokens: 100,
|
|
47
|
+
temperature: 1,
|
|
48
|
+
top_p: 0.9,
|
|
49
|
+
top_k: 40
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(requestBody).to.not.have.property('temperature');
|
|
54
|
+
expect(requestBody).to.not.have.property('top_p');
|
|
55
|
+
expect(requestBody).to.not.have.property('top_k');
|
|
56
|
+
expect(requestBody.model).to.equal('anthropic/claude-fable-5.1');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should price Claude Fable 5.1 cache usage equally across providers', () => {
|
|
60
|
+
const tokens = {
|
|
61
|
+
input: 3_000_000,
|
|
62
|
+
uncachedInput: 1_000_000,
|
|
63
|
+
cached: 1_000_000,
|
|
64
|
+
cacheWrite: 1_000_000,
|
|
65
|
+
cacheWrite5m: 1_000_000,
|
|
66
|
+
output: 1_000_000
|
|
67
|
+
};
|
|
68
|
+
const expected = {
|
|
69
|
+
uncachedInput: 10,
|
|
70
|
+
cachedInput: 0.25,
|
|
71
|
+
cacheWrite: 12.5,
|
|
72
|
+
cacheWrite5m: 12.5,
|
|
73
|
+
cacheWrite1h: 0,
|
|
74
|
+
output: 50,
|
|
75
|
+
total: 72.75
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
expect(ModelMix.calculateCostBreakdown('claude-fable-5-1', tokens)).to.deep.equal(expected);
|
|
79
|
+
expect(ModelMix.calculateCostBreakdown('anthropic/claude-fable-5.1', tokens)).to.deep.equal(expected);
|
|
80
|
+
});
|
|
81
|
+
|
|
6
82
|
it('should register Claude Fable 5', () => {
|
|
7
83
|
const model = ModelMix.new();
|
|
8
84
|
model.fable50();
|
package/test/bottleneck.test.js
CHANGED
|
@@ -247,7 +247,7 @@ describe('Rate Limiting with Bottleneck Tests', () => {
|
|
|
247
247
|
});
|
|
248
248
|
|
|
249
249
|
it('should handle rate limiting with API errors', async () => {
|
|
250
|
-
model.gpt51();
|
|
250
|
+
model.gpt51({ mix: { openrouter: false } });
|
|
251
251
|
|
|
252
252
|
nock('https://api.openai.com')
|
|
253
253
|
.post('/v1/responses')
|
|
@@ -455,4 +455,4 @@ describe('Rate Limiting with Bottleneck Tests', () => {
|
|
|
455
455
|
model.addText('Event test').message();
|
|
456
456
|
});
|
|
457
457
|
});
|
|
458
|
-
});
|
|
458
|
+
});
|