modelmix 5.0.6 → 5.1.2
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 +88 -112
- package/RLM_PLUGIN_SPEC.md +2 -2
- package/demo/fallback.js +2 -2
- package/demo/free.js +2 -3
- package/demo/gemini.js +2 -2
- package/demo/json.js +2 -2
- package/demo/mcp-simple.js +2 -2
- package/demo/mcp-tools.js +6 -6
- package/demo/mcp.js +1 -1
- package/demo/parallel-strategy.js +3 -3
- package/demo/parallel.js +2 -2
- package/demo/repl-powers.js +2 -3
- package/demo/rlm-basic.js +2 -2
- package/demo/rlm-fast.js +3 -3
- package/demo/rlm-simple.js +3 -3
- package/demo/short.js +1 -1
- package/demo/stream.js +1 -1
- package/demo/tokens.js +1 -1
- package/demo/verbose.js +5 -5
- package/effort.js +9 -3
- package/index.d.ts +0 -8
- package/index.js +499 -2861
- package/lib/content-cache.js +31 -0
- package/lib/model-chain.js +51 -0
- package/lib/object-utils.js +7 -0
- package/lib/provider-debug.js +23 -0
- package/lib/providers/anthropic.js +337 -0
- package/lib/providers/base.js +409 -0
- package/lib/providers/google.js +293 -0
- package/lib/providers/openai-compatible.js +338 -0
- package/lib/providers/openai.js +622 -0
- package/lib/providers.js +26 -0
- package/lib/template-engine.js +168 -0
- package/lib/token-usage.js +299 -0
- package/package.json +1 -1
- package/skills/modelmix/SKILL.md +10 -10
- package/test/effort.test.js +36 -0
- package/test/fallback.test.js +15 -3
- package/test/history.test.js +1 -1
- package/test/model-chain.test.js +12 -0
- package/test/moderation.test.js +1 -1
- package/test/public-api.test.js +54 -0
- package/test/tokens.test.js +20 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
const { isPlainObject } = require('./object-utils');
|
|
2
|
+
|
|
3
|
+
function validateTemplateData(value) {
|
|
4
|
+
if (!isPlainObject(value)) {
|
|
5
|
+
throw new TypeError('Template data must be a plain non-null object.');
|
|
6
|
+
}
|
|
7
|
+
if (Object.prototype.hasOwnProperty.call(value, '$mix')) {
|
|
8
|
+
throw new TypeError('Template data key "$mix" is reserved.');
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function validateTemplateDataKey(key) {
|
|
13
|
+
if (typeof key !== 'string' || key.length === 0) {
|
|
14
|
+
throw new TypeError('Template data key must be a non-empty string.');
|
|
15
|
+
}
|
|
16
|
+
if (key === '$mix') {
|
|
17
|
+
throw new TypeError('Template data key "$mix" is reserved.');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function templateLocation({ filename, label }, lineNumber) {
|
|
22
|
+
return `${filename || label} at line ${lineNumber}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function preprocessChoiceDirectives(source, { filename = null, label = 'template' } = {}) {
|
|
26
|
+
const parts = source.split(/(\r\n|\n|\r)/);
|
|
27
|
+
const blocks = [];
|
|
28
|
+
|
|
29
|
+
for (let index = 0; index < parts.length; index += 2) {
|
|
30
|
+
const line = parts[index];
|
|
31
|
+
const trimmed = line.trim();
|
|
32
|
+
const lineNumber = (index / 2) + 1;
|
|
33
|
+
const location = templateLocation({ filename, label }, lineNumber);
|
|
34
|
+
const newline = parts[index + 1] || '';
|
|
35
|
+
|
|
36
|
+
if (/^<%\s*choice\s*%>$/.test(trimmed)) {
|
|
37
|
+
const parent = blocks[blocks.length - 1];
|
|
38
|
+
if (parent && parent.optionCount === 0) {
|
|
39
|
+
throw new Error(`A nested choice must be inside an option (${location}).`);
|
|
40
|
+
}
|
|
41
|
+
blocks.push({ lineNumber, optionCount: 0, weighted: null });
|
|
42
|
+
parts[index] = '<% $mix.choice(option => { -%>';
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const optionMatch = trimmed.match(/^<%\s*option(?:\s+(.+?))?\s*%>$/);
|
|
47
|
+
if (optionMatch) {
|
|
48
|
+
const block = blocks[blocks.length - 1];
|
|
49
|
+
if (!block) {
|
|
50
|
+
throw new Error(`Option directive must be inside a choice (${location}).`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const weightText = optionMatch[1];
|
|
54
|
+
const weighted = weightText !== undefined;
|
|
55
|
+
if (block.weighted !== null && block.weighted !== weighted) {
|
|
56
|
+
throw new Error(`Choice options must either all have weights or all omit them (${location}).`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let argument = '';
|
|
60
|
+
if (weighted) {
|
|
61
|
+
const weight = Number(weightText);
|
|
62
|
+
if (!Number.isFinite(weight) || weight <= 0) {
|
|
63
|
+
throw new Error(`Choice weight must be a positive finite number (${location}).`);
|
|
64
|
+
}
|
|
65
|
+
argument = `${weight}, `;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
block.weighted = weighted;
|
|
69
|
+
parts[index] = `<% ${block.optionCount > 0 ? '}); ' : ''}option(${argument}() => { -%>`;
|
|
70
|
+
block.optionCount += 1;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (/^<%\s*\/choice\s*%>$/.test(trimmed)) {
|
|
75
|
+
const block = blocks.pop();
|
|
76
|
+
if (!block) {
|
|
77
|
+
throw new Error(`Closing choice directive has no matching opening directive (${location}).`);
|
|
78
|
+
}
|
|
79
|
+
if (block.optionCount === 0) {
|
|
80
|
+
throw new Error(`Choice must contain at least one option (${location}).`);
|
|
81
|
+
}
|
|
82
|
+
parts[index] = '<% }); }); -%>';
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (/^<%\s*(?:choice|option|\/choice)(?:\s|%>)/.test(trimmed)) {
|
|
87
|
+
throw new Error(`Invalid choice directive (${location}).`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const block = blocks[blocks.length - 1];
|
|
91
|
+
if (block && block.optionCount === 0) {
|
|
92
|
+
if (trimmed) {
|
|
93
|
+
throw new Error(`Choice content must be inside an option (${location}).`);
|
|
94
|
+
}
|
|
95
|
+
parts[index] = '<%# -%>';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (newline) parts[index + 1] = newline;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (blocks.length > 0) {
|
|
102
|
+
const block = blocks[blocks.length - 1];
|
|
103
|
+
throw new Error(`Unclosed choice directive (${templateLocation({ filename, label }, block.lineNumber)}).`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return parts.join('');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function createTemplateRenderContext(random = Math.random) {
|
|
110
|
+
const choice = defineOptions => {
|
|
111
|
+
if (typeof defineOptions !== 'function') {
|
|
112
|
+
throw new TypeError('$mix.choice expects an option definition callback.');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const options = [];
|
|
116
|
+
let weighted = null;
|
|
117
|
+
const option = (weightOrRender, renderOption) => {
|
|
118
|
+
const hasWeight = renderOption !== undefined;
|
|
119
|
+
const weight = hasWeight ? weightOrRender : 1;
|
|
120
|
+
const render = hasWeight ? renderOption : weightOrRender;
|
|
121
|
+
|
|
122
|
+
if (weighted !== null && weighted !== hasWeight) {
|
|
123
|
+
throw new TypeError('$mix.choice options cannot mix weighted and unweighted forms.');
|
|
124
|
+
}
|
|
125
|
+
if (!Number.isFinite(weight) || weight <= 0) {
|
|
126
|
+
throw new TypeError('$mix.choice weights must be positive finite numbers.');
|
|
127
|
+
}
|
|
128
|
+
if (typeof render !== 'function') {
|
|
129
|
+
throw new TypeError('$mix.choice options require a render callback.');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
weighted = hasWeight;
|
|
133
|
+
options.push({ weight, render });
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
defineOptions(option);
|
|
137
|
+
if (options.length === 0) {
|
|
138
|
+
throw new Error('$mix.choice requires at least one option.');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const totalWeight = options.reduce((sum, current) => sum + current.weight, 0);
|
|
142
|
+
if (!Number.isFinite(totalWeight)) {
|
|
143
|
+
throw new TypeError('$mix.choice total weight must be finite.');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let target = random() * totalWeight;
|
|
147
|
+
for (const current of options) {
|
|
148
|
+
target -= current.weight;
|
|
149
|
+
if (target < 0) return current.render();
|
|
150
|
+
}
|
|
151
|
+
return options[options.length - 1].render();
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
helpers: Object.freeze({ choice }),
|
|
156
|
+
renderedTemplateData: new Map(),
|
|
157
|
+
renderedMessages: new Map(),
|
|
158
|
+
renderedSystems: new Map()
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = {
|
|
163
|
+
validateTemplateData,
|
|
164
|
+
validateTemplateDataKey,
|
|
165
|
+
preprocessChoiceDirectives,
|
|
166
|
+
createTemplateRenderContext
|
|
167
|
+
};
|
|
168
|
+
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
const GPT56_LONG_CONTEXT_PRICING = Object.freeze({
|
|
2
|
+
inputThreshold: 272_000,
|
|
3
|
+
inputMultiplier: 2,
|
|
4
|
+
outputMultiplier: 1.5
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
const GROK46_LONG_CONTEXT_PRICING = Object.freeze({
|
|
8
|
+
inputThreshold: 200_000,
|
|
9
|
+
inputMultiplier: 2,
|
|
10
|
+
outputMultiplier: 2,
|
|
11
|
+
inclusive: true
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
function usesLongContextRates(pricing, inputTokens) {
|
|
15
|
+
const longContext = pricing.longContext;
|
|
16
|
+
if (!longContext) return false;
|
|
17
|
+
return longContext.inclusive
|
|
18
|
+
? inputTokens >= longContext.inputThreshold
|
|
19
|
+
: inputTokens > longContext.inputThreshold;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const MODEL_PRICING = {
|
|
23
|
+
// OpenAI
|
|
24
|
+
'gpt-realtime-mini': { input: 0.60, cachedInput: 0.06, output: 2.40 },
|
|
25
|
+
'gpt-realtime': { input: 4.00, cachedInput: 0.40, output: 16.00 },
|
|
26
|
+
'gpt-5.6-sol': { input: 5.00, cachedInput: 0.50, cacheWrite: 6.25, output: 30.00, longContext: GPT56_LONG_CONTEXT_PRICING },
|
|
27
|
+
'gpt-5.6-terra': { input: 2.00, cachedInput: 0.20, cacheWrite: 2.50, output: 12.00, longContext: GPT56_LONG_CONTEXT_PRICING },
|
|
28
|
+
'gpt-5.6-luna': { input: 0.20, cachedInput: 0.02, cacheWrite: 0.25, output: 1.20, longContext: GPT56_LONG_CONTEXT_PRICING },
|
|
29
|
+
'gpt-5.5-pro': { input: 30.00, output: 180.00 },
|
|
30
|
+
'gpt-5.5': { input: 5.00, cachedInput: 0.50, output: 30.00 },
|
|
31
|
+
'gpt-5.4': { input: 2.50, cachedInput: 0.25, output: 15.00 },
|
|
32
|
+
'gpt-5.4-pro': { input: 30.00, output: 180.00 },
|
|
33
|
+
'gpt-5.4-mini': { input: 0.75, cachedInput: 0.075, output: 4.50 },
|
|
34
|
+
'gpt-5.4-nano': { input: 0.20, cachedInput: 0.02, output: 1.25 },
|
|
35
|
+
'gpt-5.3-codex': { input: 1.75, cachedInput: 0.175, output: 14.00 },
|
|
36
|
+
'gpt-5.2': { input: 1.75, cachedInput: 0.175, output: 14.00 },
|
|
37
|
+
'gpt-5.2-chat-latest': { input: 1.75, cachedInput: 0.175, output: 14.00 },
|
|
38
|
+
'gpt-5.1': { input: 1.25, cachedInput: 0.125, output: 10.00 },
|
|
39
|
+
'gpt-5': { input: 1.25, cachedInput: 0.125, output: 10.00 },
|
|
40
|
+
'gpt-5-mini': { input: 0.25, cachedInput: 0.025, output: 2.00 },
|
|
41
|
+
'gpt-5-nano': { input: 0.05, cachedInput: 0.005, output: 0.40 },
|
|
42
|
+
'gpt-4.1': { input: 2.00, cachedInput: 0.50, output: 8.00 },
|
|
43
|
+
'gpt-4.1-mini': { input: 0.40, cachedInput: 0.10, output: 1.60 },
|
|
44
|
+
'gpt-4.1-nano': { input: 0.10, cachedInput: 0.025, output: 0.40 },
|
|
45
|
+
// gptOss (Together/Groq/Cerebras/OpenRouter)
|
|
46
|
+
'openai/gpt-oss-120b': { input: 0.15, output: 0.60 },
|
|
47
|
+
'gpt-oss-120b': { input: 0.15, output: 0.60 },
|
|
48
|
+
// Anthropic
|
|
49
|
+
'claude-fable-5': { input: 10.00, cachedInput: 1.00, cacheWrite: 12.50, cacheWrite1h: 20.00, output: 50.00 },
|
|
50
|
+
'claude-opus-5': { input: 5.00, cachedInput: 0.50, cacheWrite: 6.25, cacheWrite1h: 10.00, output: 25.00 },
|
|
51
|
+
'claude-sonnet-5': { input: 3.00, cachedInput: 0.30, cacheWrite: 3.75, cacheWrite1h: 6.00, output: 15.00 },
|
|
52
|
+
'claude-opus-4-8': { input: 5.00, cachedInput: 0.50, cacheWrite: 6.25, cacheWrite1h: 10.00, output: 25.00 },
|
|
53
|
+
'claude-opus-4-7': { input: 5.00, cachedInput: 0.50, cacheWrite: 6.25, cacheWrite1h: 10.00, output: 25.00 },
|
|
54
|
+
'claude-opus-4-6': { input: 5.00, cachedInput: 0.50, cacheWrite: 6.25, cacheWrite1h: 10.00, output: 25.00 },
|
|
55
|
+
'claude-sonnet-4-6': { input: 3.00, cachedInput: 0.30, cacheWrite: 3.75, cacheWrite1h: 6.00, output: 15.00 },
|
|
56
|
+
'claude-sonnet-4-5-20250929': { input: 3.00, cachedInput: 0.30, cacheWrite: 3.75, cacheWrite1h: 6.00, output: 15.00 },
|
|
57
|
+
'claude-haiku-4-5-20251001': { input: 1.00, cachedInput: 0.10, cacheWrite: 1.25, cacheWrite1h: 2.00, output: 5.00 },
|
|
58
|
+
// Google
|
|
59
|
+
'gemini-3.1-pro-preview': { input: 2.00, output: 12.00 },
|
|
60
|
+
'gemini-3-pro-preview': { input: 2.00, output: 12.00 },
|
|
61
|
+
'gemini-3-flash-preview': { input: 0.50, output: 3.00 },
|
|
62
|
+
'gemini-3.7-flash': { input: 0.75, cachedInput: 0.075, output: 3.75 },
|
|
63
|
+
'gemini-3.6-flash': { input: 0.75, cachedInput: 0.075, output: 3.75 },
|
|
64
|
+
'gemini-3.5-flash': { input: 0.75, output: 4.50 },
|
|
65
|
+
'gemini-3.5-flash-lite': { input: 0.30, output: 2.50 },
|
|
66
|
+
'gemini-2.5-pro': { input: 1.25, output: 10.00 },
|
|
67
|
+
'gemini-2.5-flash': { input: 0.30, output: 2.50 },
|
|
68
|
+
'gemini-3.1-flash-lite-preview': { input: 0.25, output: 1.50 },
|
|
69
|
+
// Grok
|
|
70
|
+
'grok-4.6': { input: 2.00, cachedInput: 0.50, output: 6.00, longContext: GROK46_LONG_CONTEXT_PRICING },
|
|
71
|
+
'grok-4.5': { input: 2.00, output: 6.00 },
|
|
72
|
+
'grok-4.3': { input: 1.25, output: 2.50 },
|
|
73
|
+
'grok-4.20-multi-agent-0309': { input: 1.25, output: 2.50 },
|
|
74
|
+
'grok-4.20-0309': { input: 1.25, output: 2.50 },
|
|
75
|
+
'grok-4.20-0309-reasoning': { input: 1.25, output: 2.50 },
|
|
76
|
+
'grok-4.20-0309-non-reasoning': { input: 1.25, output: 2.50 },
|
|
77
|
+
// Fireworks
|
|
78
|
+
'accounts/fireworks/models/deepseek-v4-flash': { input: 0.14, output: 0.28 },
|
|
79
|
+
'accounts/fireworks/models/deepseek-v4-pro': { input: 1.74, output: 3.48 },
|
|
80
|
+
'accounts/fireworks/models/deepseek-v4-pro-0813': { input: 1.32, cachedInput: 0.044, output: 3.96 },
|
|
81
|
+
'deepseek-ai/DeepSeek-V4-Flash': { input: 0.14, output: 0.28 },
|
|
82
|
+
'deepseek-ai/DeepSeek-V4-Pro': { input: 2.10, output: 4.40 },
|
|
83
|
+
'deepseek/deepseek-v4-flash': { input: 0.09, output: 0.18 },
|
|
84
|
+
'accounts/fireworks/models/glm-4p7': { input: 0.55, output: 2.19 },
|
|
85
|
+
'accounts/fireworks/models/glm-5p1': { input: 1.05, output: 3.50 },
|
|
86
|
+
'zai-org/GLM-5.2': { input: 1.40, output: 4.40 },
|
|
87
|
+
'accounts/fireworks/models/kimi-k2p5': { input: 0.50, output: 2.80 },
|
|
88
|
+
'qwen/qwen3.5-397b-a17b': { input: 0.385, output: 2.45 },
|
|
89
|
+
'accounts/fireworks/models/qwen3p6-plus': { input: 0.50, output: 3.00 },
|
|
90
|
+
'Qwen/Qwen3.6-Plus': { input: 0.50, output: 3.00 },
|
|
91
|
+
'accounts/fireworks/models/qwen3p7-plus': { input: 0.40, output: 1.60 },
|
|
92
|
+
'qwen/qwen3.7-plus': { input: 0.32, output: 1.28 },
|
|
93
|
+
'accounts/fireworks/models/qwen3p8-2p4t-a95b': { input: 2.00, cachedInput: 0.25, output: 6.00 },
|
|
94
|
+
'qwen/qwen3.8-max': { input: 2.00, output: 6.00 },
|
|
95
|
+
// MiniMax
|
|
96
|
+
'MiniMax-M2.5': { input: 0.30, output: 1.20 },
|
|
97
|
+
'MiniMax-M2.7': { input: 0.30, output: 1.20 },
|
|
98
|
+
'MiniMax-M3': { input: 0.30, output: 1.20 },
|
|
99
|
+
'minimax/minimax-m2.7': { input: 0.30, output: 1.20 },
|
|
100
|
+
'minimax/minimax-m3': { input: 0.30, output: 1.20 },
|
|
101
|
+
'MiniMaxAI/MiniMax-M3': { input: 0.30, output: 1.20 },
|
|
102
|
+
// Perplexity
|
|
103
|
+
'sonar': { input: 1.00, output: 1.00 },
|
|
104
|
+
'sonar-pro': { input: 3.00, output: 15.00 },
|
|
105
|
+
// Hermes 4 (OpenRouter)
|
|
106
|
+
'nousresearch/hermes-4-70b': { input: 0.13, output: 0.40 },
|
|
107
|
+
'nousresearch/hermes-4-405b': { input: 1.00, output: 3.00 },
|
|
108
|
+
// Hermes 3 (Lambda/OpenRouter)
|
|
109
|
+
'Hermes-3-Llama-3.1-405B-FP8': { input: 0.80, output: 0.80 },
|
|
110
|
+
'nousresearch/hermes-3-llama-3.1-405b:free': { input: 0, output: 0 },
|
|
111
|
+
// Qwen3 (Together/Cerebras)
|
|
112
|
+
'Qwen/Qwen3-235B-A22B-fp8-tput': { input: 0.20, output: 0.60 },
|
|
113
|
+
'qwen-3-32b': { input: 0.20, output: 0.60 },
|
|
114
|
+
// Kimi K2.5 (Together/Fireworks/OpenRouter)
|
|
115
|
+
'moonshotai/Kimi-K2.5': { input: 0.50, output: 2.80 },
|
|
116
|
+
'moonshotai/kimi-k2.5': { input: 0.50, output: 2.80 },
|
|
117
|
+
// Kimi K3
|
|
118
|
+
'kimi-k3': { input: 3.00, output: 15.00 },
|
|
119
|
+
'moonshotai/kimi-k3': { input: 3.00, output: 15.00 },
|
|
120
|
+
// GLM 4.7 (OpenRouter/Cerebras)
|
|
121
|
+
'z-ai/glm-4.7': { input: 0.55, output: 2.19 },
|
|
122
|
+
'zai-glm-4.7': { input: 0.55, output: 2.19 },
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
function normalizeTokenUsage({ input = 0, output = 0, thinking = 0, total, cached = 0, cacheWrite = 0, cacheWrite5m = 0, cacheWrite1h = 0 } = {}) {
|
|
126
|
+
const tokenCount = value => Number.isFinite(value) ? Math.max(0, value) : 0;
|
|
127
|
+
const normalizedInput = tokenCount(input);
|
|
128
|
+
const normalizedOutput = tokenCount(output);
|
|
129
|
+
const normalizedThinking = tokenCount(thinking);
|
|
130
|
+
const normalizedCached = tokenCount(cached);
|
|
131
|
+
const normalizedCacheWrite5m = tokenCount(cacheWrite5m);
|
|
132
|
+
const normalizedCacheWrite1h = tokenCount(cacheWrite1h);
|
|
133
|
+
const normalizedCacheWrite = Math.max(
|
|
134
|
+
tokenCount(cacheWrite),
|
|
135
|
+
normalizedCacheWrite5m + normalizedCacheWrite1h
|
|
136
|
+
);
|
|
137
|
+
const normalizedTotal = Number.isFinite(total)
|
|
138
|
+
? Math.max(0, total)
|
|
139
|
+
: normalizedInput + normalizedOutput + normalizedThinking;
|
|
140
|
+
const uncachedInput = Math.max(0, normalizedInput - normalizedCached - normalizedCacheWrite);
|
|
141
|
+
const cacheHitRate = normalizedInput > 0
|
|
142
|
+
? Number((normalizedCached / normalizedInput).toFixed(4))
|
|
143
|
+
: 0;
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
input: normalizedInput,
|
|
147
|
+
output: normalizedOutput,
|
|
148
|
+
thinking: normalizedThinking,
|
|
149
|
+
total: normalizedTotal,
|
|
150
|
+
cached: normalizedCached,
|
|
151
|
+
cacheWrite: normalizedCacheWrite,
|
|
152
|
+
cacheWrite5m: normalizedCacheWrite5m,
|
|
153
|
+
cacheWrite1h: normalizedCacheWrite1h,
|
|
154
|
+
uncachedInput,
|
|
155
|
+
cacheHitRate,
|
|
156
|
+
cacheSavings: 0,
|
|
157
|
+
cacheWritePremium: 0,
|
|
158
|
+
breakEvenHits: 0,
|
|
159
|
+
cost: 0,
|
|
160
|
+
costBreakdown: {
|
|
161
|
+
uncachedInput: 0,
|
|
162
|
+
cachedInput: 0,
|
|
163
|
+
cacheWrite: 0,
|
|
164
|
+
cacheWrite5m: 0,
|
|
165
|
+
cacheWrite1h: 0,
|
|
166
|
+
output: 0,
|
|
167
|
+
total: 0
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function calculateCostBreakdown(modelKey, tokens) {
|
|
173
|
+
const pricing = MODEL_PRICING[modelKey];
|
|
174
|
+
if (!pricing) return normalizeTokenUsage().costBreakdown;
|
|
175
|
+
|
|
176
|
+
const normalized = normalizeTokenUsage(tokens);
|
|
177
|
+
const longContext = pricing.longContext;
|
|
178
|
+
const useLongContextRates = usesLongContextRates(pricing, normalized.input);
|
|
179
|
+
const inputMultiplier = useLongContextRates ? longContext.inputMultiplier : 1;
|
|
180
|
+
const outputMultiplier = useLongContextRates ? longContext.outputMultiplier : 1;
|
|
181
|
+
const {
|
|
182
|
+
input: inputPerMillion,
|
|
183
|
+
cachedInput: cachedInputPerMillion = inputPerMillion,
|
|
184
|
+
cacheWrite: cacheWritePerMillion = inputPerMillion,
|
|
185
|
+
cacheWrite1h: cacheWrite1hPerMillion = cacheWritePerMillion,
|
|
186
|
+
output: outputPerMillion
|
|
187
|
+
} = pricing;
|
|
188
|
+
const roundCost = value => Number(value.toFixed(12));
|
|
189
|
+
const genericCacheWrite = Math.max(
|
|
190
|
+
0,
|
|
191
|
+
normalized.cacheWrite - normalized.cacheWrite5m - normalized.cacheWrite1h
|
|
192
|
+
);
|
|
193
|
+
const cacheWrite5mCost = roundCost(
|
|
194
|
+
normalized.cacheWrite5m * cacheWritePerMillion * inputMultiplier / 1_000_000
|
|
195
|
+
);
|
|
196
|
+
const cacheWrite1hCost = roundCost(
|
|
197
|
+
normalized.cacheWrite1h * cacheWrite1hPerMillion * inputMultiplier / 1_000_000
|
|
198
|
+
);
|
|
199
|
+
const genericCacheWriteCost = roundCost(
|
|
200
|
+
genericCacheWrite * cacheWritePerMillion * inputMultiplier / 1_000_000
|
|
201
|
+
);
|
|
202
|
+
const breakdown = {
|
|
203
|
+
uncachedInput: roundCost(normalized.uncachedInput * inputPerMillion * inputMultiplier / 1_000_000),
|
|
204
|
+
cachedInput: roundCost(normalized.cached * cachedInputPerMillion * inputMultiplier / 1_000_000),
|
|
205
|
+
cacheWrite: roundCost(genericCacheWriteCost + cacheWrite5mCost + cacheWrite1hCost),
|
|
206
|
+
cacheWrite5m: cacheWrite5mCost,
|
|
207
|
+
cacheWrite1h: cacheWrite1hCost,
|
|
208
|
+
output: roundCost(
|
|
209
|
+
(normalized.output + normalized.thinking) * outputPerMillion * outputMultiplier / 1_000_000
|
|
210
|
+
)
|
|
211
|
+
};
|
|
212
|
+
breakdown.total = roundCost(
|
|
213
|
+
breakdown.uncachedInput
|
|
214
|
+
+ breakdown.cachedInput
|
|
215
|
+
+ breakdown.cacheWrite
|
|
216
|
+
+ breakdown.output
|
|
217
|
+
);
|
|
218
|
+
return breakdown;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function calculateCacheMetrics(modelKey, tokens) {
|
|
222
|
+
const pricing = MODEL_PRICING[modelKey];
|
|
223
|
+
const emptyMetrics = {
|
|
224
|
+
cacheSavings: 0,
|
|
225
|
+
cacheWritePremium: 0,
|
|
226
|
+
breakEvenHits: 0
|
|
227
|
+
};
|
|
228
|
+
if (!pricing) return emptyMetrics;
|
|
229
|
+
|
|
230
|
+
const normalized = normalizeTokenUsage(tokens);
|
|
231
|
+
const longContext = pricing.longContext;
|
|
232
|
+
const useLongContextRates = usesLongContextRates(pricing, normalized.input);
|
|
233
|
+
const inputMultiplier = useLongContextRates ? longContext.inputMultiplier : 1;
|
|
234
|
+
const cachedInputPerMillion = pricing.cachedInput ?? pricing.input;
|
|
235
|
+
const cacheWritePerMillion = pricing.cacheWrite ?? pricing.input;
|
|
236
|
+
const cacheWrite1hPerMillion = pricing.cacheWrite1h ?? cacheWritePerMillion;
|
|
237
|
+
const readSavingsPerMillion = Math.max(0, pricing.input - cachedInputPerMillion) * inputMultiplier;
|
|
238
|
+
const writePremiumPerMillion = Math.max(0, cacheWritePerMillion - pricing.input) * inputMultiplier;
|
|
239
|
+
const write1hPremiumPerMillion = Math.max(0, cacheWrite1hPerMillion - pricing.input) * inputMultiplier;
|
|
240
|
+
const roundCost = value => Number(value.toFixed(12));
|
|
241
|
+
const cacheSavings = roundCost(normalized.cached * readSavingsPerMillion / 1_000_000);
|
|
242
|
+
const genericCacheWrite = Math.max(
|
|
243
|
+
0,
|
|
244
|
+
normalized.cacheWrite - normalized.cacheWrite5m - normalized.cacheWrite1h
|
|
245
|
+
);
|
|
246
|
+
const cacheWritePremium = roundCost(
|
|
247
|
+
(
|
|
248
|
+
(genericCacheWrite + normalized.cacheWrite5m) * writePremiumPerMillion
|
|
249
|
+
+ normalized.cacheWrite1h * write1hPremiumPerMillion
|
|
250
|
+
) / 1_000_000
|
|
251
|
+
);
|
|
252
|
+
const fullHitSavings = normalized.cacheWrite * readSavingsPerMillion / 1_000_000;
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
cacheSavings,
|
|
256
|
+
cacheWritePremium,
|
|
257
|
+
breakEvenHits: fullHitSavings > 0
|
|
258
|
+
? Number((cacheWritePremium / fullHitSavings).toFixed(4))
|
|
259
|
+
: 0
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function calculateCost(modelKey, tokens) {
|
|
264
|
+
if (!hasModelPricing(modelKey)) return null;
|
|
265
|
+
return calculateCostBreakdown(modelKey, tokens).total;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function hasModelPricing(modelKey) {
|
|
269
|
+
return Object.prototype.hasOwnProperty.call(MODEL_PRICING, modelKey);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function extractCacheTokens(usage = {}) {
|
|
273
|
+
return usage.input_tokens_details?.cached_tokens
|
|
274
|
+
?? usage.prompt_tokens_details?.cached_tokens
|
|
275
|
+
?? usage.cache_read_input_tokens
|
|
276
|
+
?? usage.cachedContentTokenCount
|
|
277
|
+
?? usage.cached_content_token_count
|
|
278
|
+
?? 0;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function extractCacheWriteTokens(usage = {}) {
|
|
282
|
+
return usage.input_tokens_details?.cache_write_tokens
|
|
283
|
+
?? usage.prompt_tokens_details?.cache_write_tokens
|
|
284
|
+
?? usage.cache_creation_input_tokens
|
|
285
|
+
?? usage.cache_write_input_tokens
|
|
286
|
+
?? usage.cacheWriteTokenCount
|
|
287
|
+
?? usage.cache_write_token_count
|
|
288
|
+
?? 0;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
module.exports = {
|
|
292
|
+
normalizeTokenUsage,
|
|
293
|
+
calculateCostBreakdown,
|
|
294
|
+
calculateCacheMetrics,
|
|
295
|
+
calculateCost,
|
|
296
|
+
hasModelPricing,
|
|
297
|
+
extractCacheTokens,
|
|
298
|
+
extractCacheWriteTokens
|
|
299
|
+
};
|
package/package.json
CHANGED
package/skills/modelmix/SKILL.md
CHANGED
|
@@ -86,11 +86,11 @@ Chain shorthand methods to attach providers. First model is primary; others are
|
|
|
86
86
|
const model = ModelMix.new()
|
|
87
87
|
.sonnet46() // primary
|
|
88
88
|
.gpt52() // fallback 1
|
|
89
|
-
.
|
|
89
|
+
.gemini37flash() // fallback 2
|
|
90
90
|
.addText("Hello!")
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
If `sonnet46` fails, it automatically tries `gpt52`, then `
|
|
93
|
+
If `sonnet46` fails, it automatically tries `gpt52`, then `gemini37flash`.
|
|
94
94
|
|
|
95
95
|
The equivalent `chain()` form accepts public shortcut names directly in the
|
|
96
96
|
same order. Append `@effort` for a per-model unified effort override (`-1` or
|
|
@@ -99,7 +99,7 @@ provider default when no chain effort is configured:
|
|
|
99
99
|
|
|
100
100
|
```javascript
|
|
101
101
|
const model = ModelMix.new()
|
|
102
|
-
.chain('sonnet46', 'gpt52@20', '
|
|
102
|
+
.chain('sonnet46', 'gpt52@20', 'gemini37flash@-1')
|
|
103
103
|
.addText('Hello!');
|
|
104
104
|
```
|
|
105
105
|
|
|
@@ -143,7 +143,7 @@ ModelMix.new({ config: { effort: 80 } })
|
|
|
143
143
|
| DeepSeek V4 | off | `low`↑ | `high`↑ | `high`↑ | `max`↑ | — |
|
|
144
144
|
| MiniMax M3 | off | adaptive | adaptive | adaptive | adaptive | adaptive |
|
|
145
145
|
|
|
146
|
-
\* Gemini bands: 0–24 / 25–49 / 50–74 / 75–100. Gemini 3.7 Flash supports only `low` / `medium` / `high`, so the first two bands clamp to `low`; `-1` keeps its native `medium` default. DeepSeek `↑` = thinking on; `off` = thinking disabled. MiniMax `off`/`adaptive` = `thinking.disabled` / `thinking.type=adaptive`. Gemini 2.5 maps 0–100 to `thinkingBudget`. Anthropic: adaptive + `output_config.effort` on Claude 5 / Fable / Opus 4.6+ / Sonnet 4.6+; Sonnet 4.5 / Haiku 4.5 use `thinking.type=enabled` + `budget_tokens`. Grok 4.6 maps 0–39 / 40–59 / 60–79 / 80–100 to `low` / `medium` / `high` / `xhigh`; without effort it uses native `high`. `-1` = adaptive/dynamic when available, else no-op. Levels clamp per model. Former `*think()` methods are removed — use `.effort(n).<model>()`. Kimi: `kimiK25()` / `kimiK26()`. Grok 4.20: `.grok420()` non-reasoning; `.effort(20+|-1).grok420()` selects reasoning.
|
|
146
|
+
\* GPT-5.6 maps `100` to `max`; 80–99 remains `xhigh`. Gemini bands: 0–24 / 25–49 / 50–74 / 75–100. Gemini 3.7 Flash supports only `low` / `medium` / `high`, so the first two bands clamp to `low`; `-1` keeps its native `medium` default. DeepSeek `↑` = thinking on; `off` = thinking disabled. MiniMax `off`/`adaptive` = `thinking.disabled` / `thinking.type=adaptive`. Gemini 2.5 maps 0–100 to `thinkingBudget`. Anthropic: adaptive + `output_config.effort` on Claude 5 / Fable / Opus 4.6+ / Sonnet 4.6+; Sonnet 4.5 / Haiku 4.5 use `thinking.type=enabled` + `budget_tokens`. Grok 4.6 maps 0–39 / 40–59 / 60–79 / 80–100 to `low` / `medium` / `high` / `xhigh`; without effort it uses native `high`. `-1` = adaptive/dynamic when available, else no-op. Levels clamp per model. Former `*think()` methods are removed — use `.effort(n).<model>()`. Kimi: `kimiK25()` / `kimiK26()`. Grok 4.20: `.grok420()` non-reasoning; `.effort(20+|-1).grok420()` selects reasoning.
|
|
147
147
|
|
|
148
148
|
## Available Model Shorthands
|
|
149
149
|
|
|
@@ -151,7 +151,7 @@ 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()` `
|
|
154
|
+
`gpt52()` `gpt52chat()` `gpt51()` `gpt5()` `gpt5mini()` `gpt5nano()` `gpt45()` `o3()` `o4mini()`
|
|
155
155
|
|
|
156
156
|
### Anthropic
|
|
157
157
|
`fable50()` `opus50()` `opus48()` `opus47()` `opus46()` `sonnet5()` `sonnet46()` `sonnet45()` `haiku45()`
|
|
@@ -159,7 +159,7 @@ Use `ModerationMix.new().openai()` with `.raw()` to classify text and images thr
|
|
|
159
159
|
Use `.effort(n)` (or `config.effort`) to enable Anthropic thinking — e.g. `.effort(100).opus50()`. `fable5()` and `opus5()` remain available as compatibility aliases.
|
|
160
160
|
|
|
161
161
|
### Google
|
|
162
|
-
`
|
|
162
|
+
`gemini31pro()` `gemini37flash()` `gemini36flash()` `gemini35flash()` `gemini35flashLite()` `gemini31flashLite()`
|
|
163
163
|
|
|
164
164
|
### Grok
|
|
165
165
|
`grok46()` `grok45()` `grok43()` `grok420multiAgent()` `grok420()`
|
|
@@ -174,7 +174,7 @@ Use `.effort(n)` (or `config.effort`) to enable Anthropic thinking — e.g. `.ef
|
|
|
174
174
|
`kimiK3()` — requires `MOONSHOT_API_KEY`; use `{ mix: { moonshot: false, openrouter: true } }` for OpenRouter.
|
|
175
175
|
|
|
176
176
|
### MiniMax
|
|
177
|
-
`
|
|
177
|
+
`minimaxM27()` `minimaxM3()`
|
|
178
178
|
|
|
179
179
|
### Fireworks
|
|
180
180
|
`qwen36plus()` `qwen37plus()` `qwen38max()` `deepseekV4Flash()` `deepseekV4Pro()` `kimiK26()`
|
|
@@ -460,7 +460,7 @@ Omit all weights for equal probabilities. Otherwise every option needs a positiv
|
|
|
460
460
|
const pool = ModelMix.new({ config: { roundRobin: true } })
|
|
461
461
|
.gpt5mini()
|
|
462
462
|
.sonnet45()
|
|
463
|
-
.
|
|
463
|
+
.gemini37flash();
|
|
464
464
|
|
|
465
465
|
const r1 = await pool.new().addText("Request 1").message();
|
|
466
466
|
const r2 = await pool.new().addText("Request 2").message();
|
|
@@ -548,7 +548,7 @@ For full debug output, also set: `DEBUG=ModelMix* node script.js`
|
|
|
548
548
|
### Free-tier models
|
|
549
549
|
|
|
550
550
|
```javascript
|
|
551
|
-
const model = ModelMix.new()
|
|
551
|
+
const model = ModelMix.new({ mix: { openrouter: false } })
|
|
552
552
|
.gptOss()
|
|
553
553
|
.kimiK25()
|
|
554
554
|
.hermes3()
|
|
@@ -556,7 +556,7 @@ const model = ModelMix.new()
|
|
|
556
556
|
console.log(await model.message());
|
|
557
557
|
```
|
|
558
558
|
|
|
559
|
-
These use providers with free quotas (
|
|
559
|
+
These use providers with free quotas (Groq, Cerebras, and Together). OpenRouter is disabled because its GPT-OSS 120B route is no longer free. If one runs out of quota, ModelMix falls back to the next.
|
|
560
560
|
|
|
561
561
|
### Multi-provider routing
|
|
562
562
|
|
package/test/effort.test.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { expect } = require('chai');
|
|
2
|
+
const sinon = require('sinon');
|
|
2
3
|
const {
|
|
3
4
|
normalizeEffort,
|
|
4
5
|
mapEffort,
|
|
@@ -69,6 +70,14 @@ describe('Unified effort scale', () => {
|
|
|
69
70
|
expect(mapEffort('openai', 10)).to.deep.equal({ reasoning_effort: 'none' });
|
|
70
71
|
expect(mapEffort('openai', 50)).to.deep.equal({ reasoning_effort: 'medium' });
|
|
71
72
|
expect(mapEffort('openai', 90)).to.deep.equal({ reasoning_effort: 'xhigh' });
|
|
73
|
+
expect(mapEffort('openai', 100)).to.deep.equal({ reasoning_effort: 'xhigh' });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('maps GPT-5.6 maximum unified effort to max', () => {
|
|
77
|
+
expect(mapEffort('openai', 99, 'gpt-5.6-luna')).to.deep.equal({ reasoning_effort: 'xhigh' });
|
|
78
|
+
for (const model of ['gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.6-luna']) {
|
|
79
|
+
expect(mapEffort('openai', 100, model)).to.deep.equal({ reasoning_effort: 'max' });
|
|
80
|
+
}
|
|
72
81
|
});
|
|
73
82
|
|
|
74
83
|
it('sets OpenAI adaptive only when supported (otherwise no-op)', () => {
|
|
@@ -351,6 +360,25 @@ describe('Unified effort scale', () => {
|
|
|
351
360
|
});
|
|
352
361
|
});
|
|
353
362
|
|
|
363
|
+
describe('debug logging', () => {
|
|
364
|
+
afterEach(() => {
|
|
365
|
+
sinon.restore();
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('includes the unified effort in the model header', async () => {
|
|
369
|
+
const provider = new MixOpenAIResponses();
|
|
370
|
+
sinon.stub(provider, 'create').resolves({ message: 'ok', toolCalls: [] });
|
|
371
|
+
const log = sinon.spy(console, 'log');
|
|
372
|
+
const model = ModelMix.new({ config: { debug: 1, effort: 60 } })
|
|
373
|
+
.attach('gpt-5.6-luna', provider)
|
|
374
|
+
.addText('Hello');
|
|
375
|
+
|
|
376
|
+
await model.message();
|
|
377
|
+
|
|
378
|
+
expect(log.calledWithMatch(/→ \[openairesponses:gpt-5\.6-luna@60\] #1/)).to.equal(true);
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
354
382
|
describe('provider request wiring', () => {
|
|
355
383
|
it('OpenAI Responses request uses mapped reasoning_effort', () => {
|
|
356
384
|
const options = { model: 'gpt-5.2', messages: [] };
|
|
@@ -359,6 +387,14 @@ describe('Unified effort scale', () => {
|
|
|
359
387
|
expect(request.reasoning).to.deep.equal({ effort: 'none' });
|
|
360
388
|
});
|
|
361
389
|
|
|
390
|
+
it('GPT-5.6 Luna .effort(100) sends max reasoning effort', () => {
|
|
391
|
+
const model = ModelMix.new().effort(100).gpt56luna();
|
|
392
|
+
const options = { model: 'gpt-5.6-luna', messages: [] };
|
|
393
|
+
applyUnifiedEffort(options, model.config, 'openai', 'gpt-5.6-luna');
|
|
394
|
+
const request = MixOpenAIResponses.buildResponsesRequest(options, {});
|
|
395
|
+
expect(request.reasoning).to.deep.equal({ effort: 'max' });
|
|
396
|
+
});
|
|
397
|
+
|
|
362
398
|
it('Anthropic config.effort maps through .effort().opus50()', () => {
|
|
363
399
|
const model = ModelMix.new().effort(100).opus50();
|
|
364
400
|
expect(model.config.effort).to.equal(100);
|
package/test/fallback.test.js
CHANGED
|
@@ -60,6 +60,18 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
60
60
|
.to.throw('Invalid chain model at index 0: expected a model shortcut string.');
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
+
it('should reject removed shortcuts from the public chain API', () => {
|
|
64
|
+
for (const shortcut of [
|
|
65
|
+
'gpt41', 'gpt41mini', 'gpt41nano',
|
|
66
|
+
'gemini25flash', 'gemini25pro', 'gemini3pro', 'gemini3flash',
|
|
67
|
+
'minimaxM25'
|
|
68
|
+
]) {
|
|
69
|
+
expect(model[shortcut]).to.equal(undefined);
|
|
70
|
+
expect(() => model.chain(shortcut))
|
|
71
|
+
.to.throw(`Unknown model shortcut "${shortcut}" in chain().`);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
63
75
|
it('should use primary provider when available', async () => {
|
|
64
76
|
model.gpt5mini().sonnet46().addText('Hello');
|
|
65
77
|
|
|
@@ -195,7 +207,7 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
195
207
|
});
|
|
196
208
|
|
|
197
209
|
it('should cascade through multiple fallbacks', async () => {
|
|
198
|
-
model.gpt5mini().sonnet46().
|
|
210
|
+
model.gpt5mini().sonnet46().gemini37flash().addText('Hello');
|
|
199
211
|
|
|
200
212
|
// Mock failed OpenAI response
|
|
201
213
|
nock('https://api.openai.com')
|
|
@@ -279,7 +291,7 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
279
291
|
});
|
|
280
292
|
|
|
281
293
|
it('should fallback from Anthropic to Google', async () => {
|
|
282
|
-
model.sonnet46().
|
|
294
|
+
model.sonnet46().gemini37flash().addText('Test message');
|
|
283
295
|
|
|
284
296
|
// Mock Anthropic failure
|
|
285
297
|
nock('https://api.anthropic.com')
|
|
@@ -665,7 +677,7 @@ describe('Provider Fallback Chain Tests', () => {
|
|
|
665
677
|
});
|
|
666
678
|
|
|
667
679
|
it('should provide detailed error information when all fallbacks fail', async () => {
|
|
668
|
-
model.gpt5mini().sonnet46().
|
|
680
|
+
model.gpt5mini().sonnet46().gemini37flash().addText('Test');
|
|
669
681
|
|
|
670
682
|
// Mock all providers failing with different errors
|
|
671
683
|
nock('https://api.openai.com')
|
package/test/history.test.js
CHANGED
|
@@ -570,7 +570,7 @@ describe('Conversation History Tests', () => {
|
|
|
570
570
|
const model = ModelMix.new({
|
|
571
571
|
config: { debug: false, max_history: 10 }
|
|
572
572
|
});
|
|
573
|
-
model.
|
|
573
|
+
model.gemini37flash();
|
|
574
574
|
|
|
575
575
|
model.addText('Hello');
|
|
576
576
|
nock('https://generativelanguage.googleapis.com')
|