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.
@@ -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.apiKey || process.env.MINIMAX_API_KEY;
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.apiKey || process.env.MIMO_API_KEY;
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.apiKey || process.env.PPLX_API_KEY;
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.apiKey || process.env.XAI_API_KEY;
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.apiKey || process.env.LAMBDA_API_KEY;
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.apiKey || process.env.GROQ_API_KEY;
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.apiKey || process.env.TOGETHER_API_KEY;
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.apiKey || process.env.CEREBRAS_API_KEY;
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.apiKey || process.env.FIREWORKS_API_KEY;
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.apiKey || process.env.NVIDIA_API_KEY;
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 };
@@ -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.apiKey || process.env.OPENAI_API_KEY;
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
- if (options.model?.startsWith('o')) {
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.apiKey || process.env.OPENROUTER_API_KEY;
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.apiKey || process.env.MOONSHOT_API_KEY;
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 createAnthropicProviders = require('./providers/anthropic');
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,
@@ -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 Glimmer 30B (NVIDIA/OpenRouter, Fireworks, Together)
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 = MODEL_PRICING[modelKey];
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 = MODEL_PRICING[modelKey];
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 Object.prototype.hasOwnProperty.call(MODEL_PRICING, modelKey);
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.9",
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
- 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
- }
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
- 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
- }
1
+ const { isPlainObject } = require('./validation');
6
2
 
7
3
  function finiteRating(value, path) {
8
4
  if (!Number.isFinite(value) || value < 0) {
@@ -6,11 +6,16 @@ allowBuilds:
6
6
 
7
7
  minimumReleaseAgeExclude:
8
8
  - ws@8.21.0
9
- - hono@4.12.25
10
- - js-yaml@4.1.2
11
- - brace-expansion@5.0.8
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@<=5.0.7: ^5.0.8
15
+ brace-expansion: 5.0.9
14
16
  diff: 8.0.4
15
- ip-address: 10.2.0
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));
@@ -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
- `gpt52()` `gpt52chat()` `gpt51()` `gpt5()` `gpt5mini()` `gpt5nano()` `gpt45()` `o3()` `o4mini()`
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({ mix: { openrouter: false } })
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
- openrouter: true, // default: true
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
@@ -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();
@@ -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
+ });