modelmix 4.7.2 → 5.0.0
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 +210 -88
- package/demo/demo.js +4 -4
- package/demo/prompt.md +2 -2
- package/http-client.js +15 -2
- package/index.d.ts +101 -11
- package/index.js +899 -272
- package/package.json +4 -1
- package/skills/modelmix/SKILL.md +76 -19
- package/test/README.md +10 -9
- package/test/anthropic.test.js +146 -9
- package/test/effort.test.js +2 -2
- package/test/fallback.test.js +192 -2
- package/test/fixtures/account-details.txt +4 -0
- package/test/fixtures/choice-options.txt +6 -0
- package/test/fixtures/choice-template.txt +2 -0
- package/test/fixtures/system-rules.txt +1 -0
- package/test/fixtures/system-template.txt +2 -0
- package/test/fixtures/template.txt +4 -11
- package/test/grok.test.js +28 -1
- package/test/history.test.js +2 -2
- package/test/live.mcp.js +20 -20
- package/test/live.test.js +11 -11
- package/test/templates.test.js +471 -281
- package/test/tokens.test.js +414 -18
package/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
|
+
const ejs = require('ejs');
|
|
2
3
|
const fileType = require('file-type');
|
|
3
4
|
const detectFileTypeFromBuffer = fileType.fileTypeFromBuffer || fileType.fromBuffer;
|
|
4
5
|
const { inspect } = require('util');
|
|
@@ -24,7 +25,9 @@ const {
|
|
|
24
25
|
normalizeEffort,
|
|
25
26
|
applyUnifiedEffort,
|
|
26
27
|
resolveProviderFamily,
|
|
27
|
-
resolveGrok420ModelKey
|
|
28
|
+
resolveGrok420ModelKey,
|
|
29
|
+
GROK420_REASONING,
|
|
30
|
+
GROK420_NON_REASONING
|
|
28
31
|
} = require('./effort');
|
|
29
32
|
|
|
30
33
|
const DEFAULT_RETRYABLE_STATUS_CODES = [408, 425, 429, 500, 502, 503, 504, 529];
|
|
@@ -37,101 +40,309 @@ function sleep(ms) {
|
|
|
37
40
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
38
41
|
}
|
|
39
42
|
|
|
40
|
-
|
|
43
|
+
function isPlainObject(value) {
|
|
44
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
45
|
+
const prototype = Object.getPrototypeOf(value);
|
|
46
|
+
return prototype === Object.prototype || prototype === null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeContentCache(cache) {
|
|
50
|
+
if (cache !== undefined) {
|
|
51
|
+
if (!isPlainObject(cache) || cache.breakpoint !== true) {
|
|
52
|
+
throw new TypeError('cache must be { breakpoint: true }.');
|
|
53
|
+
}
|
|
54
|
+
return { breakpoint: true };
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function stripContentCacheMetadata(content) {
|
|
60
|
+
if (!content || typeof content !== 'object') return content;
|
|
61
|
+
const sanitized = { ...content };
|
|
62
|
+
delete sanitized.cache;
|
|
63
|
+
delete sanitized.cache_control;
|
|
64
|
+
delete sanitized.prompt_cache_breakpoint;
|
|
65
|
+
return sanitized;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function hasNeutralCacheBreakpoint(messages = []) {
|
|
69
|
+
return messages.some(message => Array.isArray(message?.content)
|
|
70
|
+
&& message.content.some(block => block?.cache?.breakpoint === true));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function validateTemplateData(value) {
|
|
74
|
+
if (!isPlainObject(value)) {
|
|
75
|
+
throw new TypeError('Template data must be a plain non-null object.');
|
|
76
|
+
}
|
|
77
|
+
if (Object.prototype.hasOwnProperty.call(value, '$mix')) {
|
|
78
|
+
throw new TypeError('Template data key "$mix" is reserved.');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function templateLocation({ filename, label }, lineNumber) {
|
|
83
|
+
return `${filename || label} at line ${lineNumber}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function preprocessChoiceDirectives(source, { filename = null, label = 'template' } = {}) {
|
|
87
|
+
const parts = source.split(/(\r\n|\n|\r)/);
|
|
88
|
+
const blocks = [];
|
|
89
|
+
|
|
90
|
+
for (let index = 0; index < parts.length; index += 2) {
|
|
91
|
+
const line = parts[index];
|
|
92
|
+
const trimmed = line.trim();
|
|
93
|
+
const lineNumber = (index / 2) + 1;
|
|
94
|
+
const location = templateLocation({ filename, label }, lineNumber);
|
|
95
|
+
const newline = parts[index + 1] || '';
|
|
96
|
+
|
|
97
|
+
if (/^<%\s*choice\s*%>$/.test(trimmed)) {
|
|
98
|
+
const parent = blocks[blocks.length - 1];
|
|
99
|
+
if (parent && parent.optionCount === 0) {
|
|
100
|
+
throw new Error(`A nested choice must be inside an option (${location}).`);
|
|
101
|
+
}
|
|
102
|
+
blocks.push({ lineNumber, optionCount: 0, weighted: null });
|
|
103
|
+
parts[index] = '<% $mix.choice(option => { -%>';
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const optionMatch = trimmed.match(/^<%\s*option(?:\s+(.+?))?\s*%>$/);
|
|
108
|
+
if (optionMatch) {
|
|
109
|
+
const block = blocks[blocks.length - 1];
|
|
110
|
+
if (!block) {
|
|
111
|
+
throw new Error(`Option directive must be inside a choice (${location}).`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const weightText = optionMatch[1];
|
|
115
|
+
const weighted = weightText !== undefined;
|
|
116
|
+
if (block.weighted !== null && block.weighted !== weighted) {
|
|
117
|
+
throw new Error(`Choice options must either all have weights or all omit them (${location}).`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let argument = '';
|
|
121
|
+
if (weighted) {
|
|
122
|
+
const weight = Number(weightText);
|
|
123
|
+
if (!Number.isFinite(weight) || weight <= 0) {
|
|
124
|
+
throw new Error(`Choice weight must be a positive finite number (${location}).`);
|
|
125
|
+
}
|
|
126
|
+
argument = `${weight}, `;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
block.weighted = weighted;
|
|
130
|
+
parts[index] = `<% ${block.optionCount > 0 ? '}); ' : ''}option(${argument}() => { -%>`;
|
|
131
|
+
block.optionCount += 1;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (/^<%\s*\/choice\s*%>$/.test(trimmed)) {
|
|
136
|
+
const block = blocks.pop();
|
|
137
|
+
if (!block) {
|
|
138
|
+
throw new Error(`Closing choice directive has no matching opening directive (${location}).`);
|
|
139
|
+
}
|
|
140
|
+
if (block.optionCount === 0) {
|
|
141
|
+
throw new Error(`Choice must contain at least one option (${location}).`);
|
|
142
|
+
}
|
|
143
|
+
parts[index] = '<% }); }); -%>';
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (/^<%\s*(?:choice|option|\/choice)(?:\s|%>)/.test(trimmed)) {
|
|
148
|
+
throw new Error(`Invalid choice directive (${location}).`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const block = blocks[blocks.length - 1];
|
|
152
|
+
if (block && block.optionCount === 0) {
|
|
153
|
+
if (trimmed) {
|
|
154
|
+
throw new Error(`Choice content must be inside an option (${location}).`);
|
|
155
|
+
}
|
|
156
|
+
parts[index] = '<%# -%>';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (newline) parts[index + 1] = newline;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (blocks.length > 0) {
|
|
163
|
+
const block = blocks[blocks.length - 1];
|
|
164
|
+
throw new Error(`Unclosed choice directive (${templateLocation({ filename, label }, block.lineNumber)}).`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return parts.join('');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function createTemplateRenderContext(random = Math.random) {
|
|
171
|
+
const choice = defineOptions => {
|
|
172
|
+
if (typeof defineOptions !== 'function') {
|
|
173
|
+
throw new TypeError('$mix.choice expects an option definition callback.');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const options = [];
|
|
177
|
+
let weighted = null;
|
|
178
|
+
const option = (weightOrRender, renderOption) => {
|
|
179
|
+
const hasWeight = renderOption !== undefined;
|
|
180
|
+
const weight = hasWeight ? weightOrRender : 1;
|
|
181
|
+
const render = hasWeight ? renderOption : weightOrRender;
|
|
182
|
+
|
|
183
|
+
if (weighted !== null && weighted !== hasWeight) {
|
|
184
|
+
throw new TypeError('$mix.choice options cannot mix weighted and unweighted forms.');
|
|
185
|
+
}
|
|
186
|
+
if (!Number.isFinite(weight) || weight <= 0) {
|
|
187
|
+
throw new TypeError('$mix.choice weights must be positive finite numbers.');
|
|
188
|
+
}
|
|
189
|
+
if (typeof render !== 'function') {
|
|
190
|
+
throw new TypeError('$mix.choice options require a render callback.');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
weighted = hasWeight;
|
|
194
|
+
options.push({ weight, render });
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
defineOptions(option);
|
|
198
|
+
if (options.length === 0) {
|
|
199
|
+
throw new Error('$mix.choice requires at least one option.');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const totalWeight = options.reduce((sum, current) => sum + current.weight, 0);
|
|
203
|
+
if (!Number.isFinite(totalWeight)) {
|
|
204
|
+
throw new TypeError('$mix.choice total weight must be finite.');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let target = random() * totalWeight;
|
|
208
|
+
for (const current of options) {
|
|
209
|
+
target -= current.weight;
|
|
210
|
+
if (target < 0) return current.render();
|
|
211
|
+
}
|
|
212
|
+
return options[options.length - 1].render();
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
helpers: Object.freeze({ choice }),
|
|
217
|
+
renderedMessages: new Map(),
|
|
218
|
+
renderedSystems: new Map()
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function configForDebug(config) {
|
|
223
|
+
const safeConfig = { ...config };
|
|
224
|
+
delete safeConfig.apiKey;
|
|
225
|
+
delete safeConfig.debug;
|
|
226
|
+
return safeConfig;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function redactSecret(value, secret, seen = new WeakSet()) {
|
|
230
|
+
if (!secret) return value;
|
|
231
|
+
if (typeof value === 'string') return value.split(secret).join('[REDACTED]');
|
|
232
|
+
if (!value || typeof value !== 'object') return value;
|
|
233
|
+
if (seen.has(value)) return '[Circular]';
|
|
234
|
+
|
|
235
|
+
seen.add(value);
|
|
236
|
+
if (Array.isArray(value)) {
|
|
237
|
+
return value.map(item => redactSecret(item, secret, seen));
|
|
238
|
+
}
|
|
239
|
+
return Object.fromEntries(
|
|
240
|
+
Object.entries(value).map(([key, item]) => [key, redactSecret(item, secret, seen)])
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Pricing per 1M tokens in USD
|
|
41
245
|
// Based on provider pricing pages linked in README
|
|
246
|
+
const GPT56_LONG_CONTEXT_PRICING = Object.freeze({
|
|
247
|
+
inputThreshold: 272_000,
|
|
248
|
+
inputMultiplier: 2,
|
|
249
|
+
outputMultiplier: 1.5
|
|
250
|
+
});
|
|
251
|
+
|
|
42
252
|
const MODEL_PRICING = {
|
|
43
253
|
// OpenAI
|
|
44
|
-
'gpt-realtime-mini':
|
|
45
|
-
'gpt-realtime':
|
|
46
|
-
'gpt-5.6-sol':
|
|
47
|
-
'gpt-5.6-terra':
|
|
48
|
-
'gpt-5.6-luna':
|
|
49
|
-
'gpt-5.5-pro':
|
|
50
|
-
'gpt-5.5':
|
|
51
|
-
'gpt-5.4':
|
|
52
|
-
'gpt-5.4-pro':
|
|
53
|
-
'gpt-5.4-mini':
|
|
54
|
-
'gpt-5.4-nano':
|
|
55
|
-
'gpt-5.3-codex':
|
|
56
|
-
'gpt-5.2':
|
|
57
|
-
'gpt-5.2-chat-latest':
|
|
58
|
-
'gpt-5.1':
|
|
59
|
-
'gpt-5':
|
|
60
|
-
'gpt-5-mini':
|
|
61
|
-
'gpt-5-nano':
|
|
62
|
-
'gpt-4.1':
|
|
63
|
-
'gpt-4.1-mini':
|
|
64
|
-
'gpt-4.1-nano':
|
|
254
|
+
'gpt-realtime-mini': { input: 0.60, cachedInput: 0.06, output: 2.40 },
|
|
255
|
+
'gpt-realtime': { input: 4.00, cachedInput: 0.40, output: 16.00 },
|
|
256
|
+
'gpt-5.6-sol': { input: 5.00, cachedInput: 0.50, cacheWrite: 6.25, output: 30.00, longContext: GPT56_LONG_CONTEXT_PRICING },
|
|
257
|
+
'gpt-5.6-terra': { input: 2.00, cachedInput: 0.20, cacheWrite: 2.50, output: 12.00, longContext: GPT56_LONG_CONTEXT_PRICING },
|
|
258
|
+
'gpt-5.6-luna': { input: 0.20, cachedInput: 0.02, cacheWrite: 0.25, output: 1.20, longContext: GPT56_LONG_CONTEXT_PRICING },
|
|
259
|
+
'gpt-5.5-pro': { input: 30.00, output: 180.00 },
|
|
260
|
+
'gpt-5.5': { input: 5.00, cachedInput: 0.50, output: 30.00 },
|
|
261
|
+
'gpt-5.4': { input: 2.50, cachedInput: 0.25, output: 15.00 },
|
|
262
|
+
'gpt-5.4-pro': { input: 30.00, output: 180.00 },
|
|
263
|
+
'gpt-5.4-mini': { input: 0.75, cachedInput: 0.075, output: 4.50 },
|
|
264
|
+
'gpt-5.4-nano': { input: 0.20, cachedInput: 0.02, output: 1.25 },
|
|
265
|
+
'gpt-5.3-codex': { input: 1.75, cachedInput: 0.175, output: 14.00 },
|
|
266
|
+
'gpt-5.2': { input: 1.75, cachedInput: 0.175, output: 14.00 },
|
|
267
|
+
'gpt-5.2-chat-latest': { input: 1.75, cachedInput: 0.175, output: 14.00 },
|
|
268
|
+
'gpt-5.1': { input: 1.25, cachedInput: 0.125, output: 10.00 },
|
|
269
|
+
'gpt-5': { input: 1.25, cachedInput: 0.125, output: 10.00 },
|
|
270
|
+
'gpt-5-mini': { input: 0.25, cachedInput: 0.025, output: 2.00 },
|
|
271
|
+
'gpt-5-nano': { input: 0.05, cachedInput: 0.005, output: 0.40 },
|
|
272
|
+
'gpt-4.1': { input: 2.00, cachedInput: 0.50, output: 8.00 },
|
|
273
|
+
'gpt-4.1-mini': { input: 0.40, cachedInput: 0.10, output: 1.60 },
|
|
274
|
+
'gpt-4.1-nano': { input: 0.10, cachedInput: 0.025, output: 0.40 },
|
|
65
275
|
// gptOss (Together/Groq/Cerebras/OpenRouter)
|
|
66
|
-
'openai/gpt-oss-120b':
|
|
67
|
-
'gpt-oss-120b':
|
|
68
|
-
'openai/gpt-oss-120b:free':
|
|
276
|
+
'openai/gpt-oss-120b': { input: 0.15, output: 0.60 },
|
|
277
|
+
'gpt-oss-120b': { input: 0.15, output: 0.60 },
|
|
278
|
+
'openai/gpt-oss-120b:free': { input: 0, output: 0 },
|
|
69
279
|
// Anthropic
|
|
70
|
-
'claude-fable-5':
|
|
71
|
-
'claude-opus-5':
|
|
72
|
-
'claude-sonnet-5':
|
|
73
|
-
'claude-opus-4-8':
|
|
74
|
-
'claude-opus-4-7':
|
|
75
|
-
'claude-opus-4-6':
|
|
76
|
-
'claude-sonnet-4-6':
|
|
77
|
-
'claude-sonnet-4-5-20250929':
|
|
78
|
-
'claude-haiku-4-5-20251001':
|
|
280
|
+
'claude-fable-5': { input: 10.00, cachedInput: 1.00, cacheWrite: 12.50, cacheWrite1h: 20.00, output: 50.00 },
|
|
281
|
+
'claude-opus-5': { input: 5.00, cachedInput: 0.50, cacheWrite: 6.25, cacheWrite1h: 10.00, output: 25.00 },
|
|
282
|
+
'claude-sonnet-5': { input: 3.00, cachedInput: 0.30, cacheWrite: 3.75, cacheWrite1h: 6.00, output: 15.00 },
|
|
283
|
+
'claude-opus-4-8': { input: 5.00, cachedInput: 0.50, cacheWrite: 6.25, cacheWrite1h: 10.00, output: 25.00 },
|
|
284
|
+
'claude-opus-4-7': { input: 5.00, cachedInput: 0.50, cacheWrite: 6.25, cacheWrite1h: 10.00, output: 25.00 },
|
|
285
|
+
'claude-opus-4-6': { input: 5.00, cachedInput: 0.50, cacheWrite: 6.25, cacheWrite1h: 10.00, output: 25.00 },
|
|
286
|
+
'claude-sonnet-4-6': { input: 3.00, cachedInput: 0.30, cacheWrite: 3.75, cacheWrite1h: 6.00, output: 15.00 },
|
|
287
|
+
'claude-sonnet-4-5-20250929': { input: 3.00, cachedInput: 0.30, cacheWrite: 3.75, cacheWrite1h: 6.00, output: 15.00 },
|
|
288
|
+
'claude-haiku-4-5-20251001': { input: 1.00, cachedInput: 0.10, cacheWrite: 1.25, cacheWrite1h: 2.00, output: 5.00 },
|
|
79
289
|
// Google
|
|
80
|
-
'gemini-3.1-pro-preview':
|
|
81
|
-
'gemini-3-pro-preview':
|
|
82
|
-
'gemini-3-flash-preview':
|
|
83
|
-
'gemini-3.6-flash':
|
|
84
|
-
'gemini-3.5-flash':
|
|
85
|
-
'gemini-
|
|
86
|
-
'gemini-2.5-
|
|
87
|
-
'gemini-
|
|
290
|
+
'gemini-3.1-pro-preview': { input: 2.00, output: 12.00 },
|
|
291
|
+
'gemini-3-pro-preview': { input: 2.00, output: 12.00 },
|
|
292
|
+
'gemini-3-flash-preview': { input: 0.50, output: 3.00 },
|
|
293
|
+
'gemini-3.6-flash': { input: 1.50, output: 7.50 },
|
|
294
|
+
'gemini-3.5-flash': { input: 0.75, output: 4.50 },
|
|
295
|
+
'gemini-3.5-flash-lite': { input: 0.30, output: 2.50 },
|
|
296
|
+
'gemini-2.5-pro': { input: 1.25, output: 10.00 },
|
|
297
|
+
'gemini-2.5-flash': { input: 0.30, output: 2.50 },
|
|
298
|
+
'gemini-3.1-flash-lite-preview': { input: 0.25, output: 1.50 },
|
|
88
299
|
// Grok
|
|
89
|
-
'grok-4.5':
|
|
90
|
-
'grok-4.3':
|
|
91
|
-
'grok-4.20-multi-agent-0309':
|
|
92
|
-
'grok-4.20-0309':
|
|
93
|
-
'grok-4.20-0309-reasoning':
|
|
94
|
-
'grok-4.20-0309-non-reasoning':
|
|
300
|
+
'grok-4.5': { input: 2.00, output: 6.00 },
|
|
301
|
+
'grok-4.3': { input: 1.25, output: 2.50 },
|
|
302
|
+
'grok-4.20-multi-agent-0309': { input: 1.25, output: 2.50 },
|
|
303
|
+
'grok-4.20-0309': { input: 1.25, output: 2.50 },
|
|
304
|
+
'grok-4.20-0309-reasoning': { input: 1.25, output: 2.50 },
|
|
305
|
+
'grok-4.20-0309-non-reasoning': { input: 1.25, output: 2.50 },
|
|
95
306
|
// Fireworks
|
|
96
|
-
'accounts/fireworks/models/deepseek-v4-flash':
|
|
97
|
-
'accounts/fireworks/models/deepseek-v4-pro':
|
|
98
|
-
'deepseek-ai/DeepSeek-V4-Flash':
|
|
99
|
-
'deepseek-ai/DeepSeek-V4-Pro':
|
|
100
|
-
'deepseek/deepseek-v4-flash':
|
|
101
|
-
'accounts/fireworks/models/glm-4p7':
|
|
102
|
-
'accounts/fireworks/models/glm-5p1':
|
|
103
|
-
'zai-org/GLM-5.2':
|
|
104
|
-
'accounts/fireworks/models/kimi-k2p5':
|
|
105
|
-
'accounts/fireworks/models/qwen3p6-plus':
|
|
106
|
-
'Qwen/Qwen3.6-Plus':
|
|
107
|
-
'accounts/fireworks/models/qwen3p7-plus':
|
|
108
|
-
'qwen/qwen3.7-plus':
|
|
109
|
-
'qwen/qwen3.8-max':
|
|
307
|
+
'accounts/fireworks/models/deepseek-v4-flash': { input: 0.14, output: 0.28 },
|
|
308
|
+
'accounts/fireworks/models/deepseek-v4-pro': { input: 1.74, output: 3.48 },
|
|
309
|
+
'deepseek-ai/DeepSeek-V4-Flash': { input: 0.14, output: 0.28 },
|
|
310
|
+
'deepseek-ai/DeepSeek-V4-Pro': { input: 2.10, output: 4.40 },
|
|
311
|
+
'deepseek/deepseek-v4-flash': { input: 0.09, output: 0.18 },
|
|
312
|
+
'accounts/fireworks/models/glm-4p7': { input: 0.55, output: 2.19 },
|
|
313
|
+
'accounts/fireworks/models/glm-5p1': { input: 1.05, output: 3.50 },
|
|
314
|
+
'zai-org/GLM-5.2': { input: 1.40, output: 4.40 },
|
|
315
|
+
'accounts/fireworks/models/kimi-k2p5': { input: 0.50, output: 2.80 },
|
|
316
|
+
'accounts/fireworks/models/qwen3p6-plus': { input: 0.50, output: 3.00 },
|
|
317
|
+
'Qwen/Qwen3.6-Plus': { input: 0.50, output: 3.00 },
|
|
318
|
+
'accounts/fireworks/models/qwen3p7-plus': { input: 0.40, output: 1.60 },
|
|
319
|
+
'qwen/qwen3.7-plus': { input: 0.32, output: 1.28 },
|
|
320
|
+
'qwen/qwen3.8-max': { input: 2.00, output: 6.00 },
|
|
110
321
|
// MiniMax
|
|
111
|
-
'MiniMax-M2.5':
|
|
112
|
-
'MiniMax-M2.7':
|
|
113
|
-
'MiniMax-M3':
|
|
114
|
-
'minimax/minimax-m2.7':
|
|
115
|
-
'minimax/minimax-m3':
|
|
116
|
-
'MiniMaxAI/MiniMax-M3':
|
|
322
|
+
'MiniMax-M2.5': { input: 0.30, output: 1.20 },
|
|
323
|
+
'MiniMax-M2.7': { input: 0.30, output: 1.20 },
|
|
324
|
+
'MiniMax-M3': { input: 0.30, output: 1.20 },
|
|
325
|
+
'minimax/minimax-m2.7': { input: 0.30, output: 1.20 },
|
|
326
|
+
'minimax/minimax-m3': { input: 0.30, output: 1.20 },
|
|
327
|
+
'MiniMaxAI/MiniMax-M3': { input: 0.30, output: 1.20 },
|
|
117
328
|
// Perplexity
|
|
118
|
-
'sonar':
|
|
119
|
-
'sonar-pro':
|
|
329
|
+
'sonar': { input: 1.00, output: 1.00 },
|
|
330
|
+
'sonar-pro': { input: 3.00, output: 15.00 },
|
|
120
331
|
// Hermes3 (Lambda/OpenRouter)
|
|
121
|
-
'Hermes-3-Llama-3.1-405B-FP8':
|
|
122
|
-
'nousresearch/hermes-3-llama-3.1-405b:free':
|
|
332
|
+
'Hermes-3-Llama-3.1-405B-FP8': { input: 0.80, output: 0.80 },
|
|
333
|
+
'nousresearch/hermes-3-llama-3.1-405b:free': { input: 0, output: 0 },
|
|
123
334
|
// Qwen3 (Together/Cerebras)
|
|
124
|
-
'Qwen/Qwen3-235B-A22B-fp8-tput':
|
|
125
|
-
'qwen-3-32b':
|
|
335
|
+
'Qwen/Qwen3-235B-A22B-fp8-tput': { input: 0.20, output: 0.60 },
|
|
336
|
+
'qwen-3-32b': { input: 0.20, output: 0.60 },
|
|
126
337
|
// Kimi K2.5 (Together/Fireworks/OpenRouter)
|
|
127
|
-
'moonshotai/Kimi-K2.5':
|
|
128
|
-
'moonshotai/kimi-k2.5':
|
|
338
|
+
'moonshotai/Kimi-K2.5': { input: 0.50, output: 2.80 },
|
|
339
|
+
'moonshotai/kimi-k2.5': { input: 0.50, output: 2.80 },
|
|
129
340
|
// Kimi K3
|
|
130
|
-
'kimi-k3':
|
|
131
|
-
'moonshotai/kimi-k3':
|
|
341
|
+
'kimi-k3': { input: 3.00, output: 15.00 },
|
|
342
|
+
'moonshotai/kimi-k3': { input: 3.00, output: 15.00 },
|
|
132
343
|
// GLM 4.7 (OpenRouter/Cerebras)
|
|
133
|
-
'z-ai/glm-4.7':
|
|
134
|
-
'zai-glm-4.7':
|
|
344
|
+
'z-ai/glm-4.7': { input: 0.55, output: 2.19 },
|
|
345
|
+
'zai-glm-4.7': { input: 0.55, output: 2.19 },
|
|
135
346
|
};
|
|
136
347
|
|
|
137
348
|
class ModelMix {
|
|
@@ -143,6 +354,7 @@ class ModelMix {
|
|
|
143
354
|
this.toolClient = {};
|
|
144
355
|
this.mcp = {};
|
|
145
356
|
this.mcpToolsManager = new MCPToolsManager();
|
|
357
|
+
this.messageTemplates = new WeakMap();
|
|
146
358
|
this.lastRaw = null;
|
|
147
359
|
this.options = {
|
|
148
360
|
max_tokens: 8192,
|
|
@@ -171,6 +383,13 @@ class ModelMix {
|
|
|
171
383
|
roundRobin: false, // false=fallback mode, true=round robin rotation
|
|
172
384
|
...config
|
|
173
385
|
};
|
|
386
|
+
this.systemTemplate = {
|
|
387
|
+
source: this.config.system,
|
|
388
|
+
filename: null
|
|
389
|
+
};
|
|
390
|
+
if (this.config.replace !== undefined) {
|
|
391
|
+
validateTemplateData(this.config.replace);
|
|
392
|
+
}
|
|
174
393
|
// Unified effort is ModelMix policy (config.effort / .effort()), not a native option.
|
|
175
394
|
if (this.config.effort !== undefined && this.config.effort !== null) {
|
|
176
395
|
this.config.effort = normalizeEffort(this.config.effort);
|
|
@@ -183,6 +402,7 @@ class ModelMix {
|
|
|
183
402
|
}
|
|
184
403
|
|
|
185
404
|
replace(keyValues) {
|
|
405
|
+
validateTemplateData(keyValues);
|
|
186
406
|
this.config.replace = { ...this.config.replace, ...keyValues };
|
|
187
407
|
return this;
|
|
188
408
|
}
|
|
@@ -202,11 +422,15 @@ class ModelMix {
|
|
|
202
422
|
}
|
|
203
423
|
|
|
204
424
|
new({ options = {}, config = {}, mix = {} } = {}) {
|
|
425
|
+
const hasSystemOverride = Object.prototype.hasOwnProperty.call(config, 'system');
|
|
205
426
|
const instance = new ModelMix({
|
|
206
427
|
options: { ...this.options, ...options },
|
|
207
428
|
config: { ...this.config, ...config },
|
|
208
429
|
mix: { ...this.mix, ...mix }
|
|
209
430
|
});
|
|
431
|
+
if (!hasSystemOverride) {
|
|
432
|
+
instance.systemTemplate = { ...this.systemTemplate };
|
|
433
|
+
}
|
|
210
434
|
instance.models = this.models; // Share models array for round-robin rotation
|
|
211
435
|
return instance;
|
|
212
436
|
}
|
|
@@ -237,20 +461,163 @@ class ModelMix {
|
|
|
237
461
|
return str.length > maxLen ? str.substring(0, maxLen) + '...' : str;
|
|
238
462
|
}
|
|
239
463
|
|
|
240
|
-
static
|
|
464
|
+
static normalizeTokenUsage({ input = 0, output = 0, total, cached = 0, cacheWrite = 0, cacheWrite5m = 0, cacheWrite1h = 0 } = {}) {
|
|
465
|
+
const tokenCount = value => Number.isFinite(value) ? Math.max(0, value) : 0;
|
|
466
|
+
const normalizedInput = tokenCount(input);
|
|
467
|
+
const normalizedOutput = tokenCount(output);
|
|
468
|
+
const normalizedCached = tokenCount(cached);
|
|
469
|
+
const normalizedCacheWrite5m = tokenCount(cacheWrite5m);
|
|
470
|
+
const normalizedCacheWrite1h = tokenCount(cacheWrite1h);
|
|
471
|
+
const normalizedCacheWrite = Math.max(
|
|
472
|
+
tokenCount(cacheWrite),
|
|
473
|
+
normalizedCacheWrite5m + normalizedCacheWrite1h
|
|
474
|
+
);
|
|
475
|
+
const normalizedTotal = Number.isFinite(total)
|
|
476
|
+
? Math.max(0, total)
|
|
477
|
+
: normalizedInput + normalizedOutput;
|
|
478
|
+
const uncachedInput = Math.max(0, normalizedInput - normalizedCached - normalizedCacheWrite);
|
|
479
|
+
const cacheHitRate = normalizedInput > 0
|
|
480
|
+
? Number((normalizedCached / normalizedInput).toFixed(4))
|
|
481
|
+
: 0;
|
|
482
|
+
|
|
483
|
+
return {
|
|
484
|
+
input: normalizedInput,
|
|
485
|
+
output: normalizedOutput,
|
|
486
|
+
total: normalizedTotal,
|
|
487
|
+
cached: normalizedCached,
|
|
488
|
+
cacheWrite: normalizedCacheWrite,
|
|
489
|
+
cacheWrite5m: normalizedCacheWrite5m,
|
|
490
|
+
cacheWrite1h: normalizedCacheWrite1h,
|
|
491
|
+
uncachedInput,
|
|
492
|
+
cacheHitRate,
|
|
493
|
+
cacheSavings: 0,
|
|
494
|
+
cacheWritePremium: 0,
|
|
495
|
+
breakEvenHits: 0,
|
|
496
|
+
cost: 0,
|
|
497
|
+
costBreakdown: {
|
|
498
|
+
uncachedInput: 0,
|
|
499
|
+
cachedInput: 0,
|
|
500
|
+
cacheWrite: 0,
|
|
501
|
+
cacheWrite5m: 0,
|
|
502
|
+
cacheWrite1h: 0,
|
|
503
|
+
output: 0,
|
|
504
|
+
total: 0
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
static calculateCostBreakdown(modelKey, tokens) {
|
|
241
510
|
const pricing = MODEL_PRICING[modelKey];
|
|
242
|
-
if (!pricing) return
|
|
243
|
-
|
|
244
|
-
|
|
511
|
+
if (!pricing) return ModelMix.normalizeTokenUsage().costBreakdown;
|
|
512
|
+
|
|
513
|
+
const normalized = ModelMix.normalizeTokenUsage(tokens);
|
|
514
|
+
const longContext = pricing.longContext;
|
|
515
|
+
const useLongContextRates = longContext && normalized.input > longContext.inputThreshold;
|
|
516
|
+
const inputMultiplier = useLongContextRates ? longContext.inputMultiplier : 1;
|
|
517
|
+
const outputMultiplier = useLongContextRates ? longContext.outputMultiplier : 1;
|
|
518
|
+
const {
|
|
519
|
+
input: inputPerMillion,
|
|
520
|
+
cachedInput: cachedInputPerMillion = inputPerMillion,
|
|
521
|
+
cacheWrite: cacheWritePerMillion = inputPerMillion,
|
|
522
|
+
cacheWrite1h: cacheWrite1hPerMillion = cacheWritePerMillion,
|
|
523
|
+
output: outputPerMillion
|
|
524
|
+
} = pricing;
|
|
525
|
+
const roundCost = value => Number(value.toFixed(12));
|
|
526
|
+
const genericCacheWrite = Math.max(
|
|
527
|
+
0,
|
|
528
|
+
normalized.cacheWrite - normalized.cacheWrite5m - normalized.cacheWrite1h
|
|
529
|
+
);
|
|
530
|
+
const cacheWrite5mCost = roundCost(
|
|
531
|
+
normalized.cacheWrite5m * cacheWritePerMillion * inputMultiplier / 1_000_000
|
|
532
|
+
);
|
|
533
|
+
const cacheWrite1hCost = roundCost(
|
|
534
|
+
normalized.cacheWrite1h * cacheWrite1hPerMillion * inputMultiplier / 1_000_000
|
|
535
|
+
);
|
|
536
|
+
const genericCacheWriteCost = roundCost(
|
|
537
|
+
genericCacheWrite * cacheWritePerMillion * inputMultiplier / 1_000_000
|
|
538
|
+
);
|
|
539
|
+
const breakdown = {
|
|
540
|
+
uncachedInput: roundCost(normalized.uncachedInput * inputPerMillion * inputMultiplier / 1_000_000),
|
|
541
|
+
cachedInput: roundCost(normalized.cached * cachedInputPerMillion * inputMultiplier / 1_000_000),
|
|
542
|
+
cacheWrite: roundCost(genericCacheWriteCost + cacheWrite5mCost + cacheWrite1hCost),
|
|
543
|
+
cacheWrite5m: cacheWrite5mCost,
|
|
544
|
+
cacheWrite1h: cacheWrite1hCost,
|
|
545
|
+
output: roundCost(normalized.output * outputPerMillion * outputMultiplier / 1_000_000)
|
|
546
|
+
};
|
|
547
|
+
breakdown.total = roundCost(
|
|
548
|
+
breakdown.uncachedInput
|
|
549
|
+
+ breakdown.cachedInput
|
|
550
|
+
+ breakdown.cacheWrite
|
|
551
|
+
+ breakdown.output
|
|
552
|
+
);
|
|
553
|
+
return breakdown;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
static calculateCacheMetrics(modelKey, tokens) {
|
|
557
|
+
const pricing = MODEL_PRICING[modelKey];
|
|
558
|
+
const emptyMetrics = {
|
|
559
|
+
cacheSavings: 0,
|
|
560
|
+
cacheWritePremium: 0,
|
|
561
|
+
breakEvenHits: 0
|
|
562
|
+
};
|
|
563
|
+
if (!pricing) return emptyMetrics;
|
|
564
|
+
|
|
565
|
+
const normalized = ModelMix.normalizeTokenUsage(tokens);
|
|
566
|
+
const longContext = pricing.longContext;
|
|
567
|
+
const inputMultiplier = longContext && normalized.input > longContext.inputThreshold
|
|
568
|
+
? longContext.inputMultiplier
|
|
569
|
+
: 1;
|
|
570
|
+
const cachedInputPerMillion = pricing.cachedInput ?? pricing.input;
|
|
571
|
+
const cacheWritePerMillion = pricing.cacheWrite ?? pricing.input;
|
|
572
|
+
const cacheWrite1hPerMillion = pricing.cacheWrite1h ?? cacheWritePerMillion;
|
|
573
|
+
const readSavingsPerMillion = Math.max(0, pricing.input - cachedInputPerMillion) * inputMultiplier;
|
|
574
|
+
const writePremiumPerMillion = Math.max(0, cacheWritePerMillion - pricing.input) * inputMultiplier;
|
|
575
|
+
const write1hPremiumPerMillion = Math.max(0, cacheWrite1hPerMillion - pricing.input) * inputMultiplier;
|
|
576
|
+
const roundCost = value => Number(value.toFixed(12));
|
|
577
|
+
const cacheSavings = roundCost(normalized.cached * readSavingsPerMillion / 1_000_000);
|
|
578
|
+
const genericCacheWrite = Math.max(
|
|
579
|
+
0,
|
|
580
|
+
normalized.cacheWrite - normalized.cacheWrite5m - normalized.cacheWrite1h
|
|
581
|
+
);
|
|
582
|
+
const cacheWritePremium = roundCost(
|
|
583
|
+
(
|
|
584
|
+
(genericCacheWrite + normalized.cacheWrite5m) * writePremiumPerMillion
|
|
585
|
+
+ normalized.cacheWrite1h * write1hPremiumPerMillion
|
|
586
|
+
) / 1_000_000
|
|
587
|
+
);
|
|
588
|
+
const fullHitSavings = normalized.cacheWrite * readSavingsPerMillion / 1_000_000;
|
|
589
|
+
|
|
590
|
+
return {
|
|
591
|
+
cacheSavings,
|
|
592
|
+
cacheWritePremium,
|
|
593
|
+
breakEvenHits: fullHitSavings > 0
|
|
594
|
+
? Number((cacheWritePremium / fullHitSavings).toFixed(4))
|
|
595
|
+
: 0
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
static calculateCost(modelKey, tokens) {
|
|
600
|
+
if (!MODEL_PRICING[modelKey]) return null;
|
|
601
|
+
return ModelMix.calculateCostBreakdown(modelKey, tokens).total;
|
|
245
602
|
}
|
|
246
603
|
|
|
247
604
|
static extractCacheTokens(usage = {}) {
|
|
248
605
|
return usage.input_tokens_details?.cached_tokens
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
606
|
+
?? usage.prompt_tokens_details?.cached_tokens
|
|
607
|
+
?? usage.cache_read_input_tokens
|
|
608
|
+
?? usage.cachedContentTokenCount
|
|
609
|
+
?? usage.cached_content_token_count
|
|
610
|
+
?? 0;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
static extractCacheWriteTokens(usage = {}) {
|
|
614
|
+
return usage.input_tokens_details?.cache_write_tokens
|
|
615
|
+
?? usage.prompt_tokens_details?.cache_write_tokens
|
|
616
|
+
?? usage.cache_creation_input_tokens
|
|
617
|
+
?? usage.cache_write_input_tokens
|
|
618
|
+
?? usage.cacheWriteTokenCount
|
|
619
|
+
?? usage.cache_write_token_count
|
|
620
|
+
?? 0;
|
|
254
621
|
}
|
|
255
622
|
|
|
256
623
|
static formatInputSummary(messages, system, debug = 2) {
|
|
@@ -384,12 +751,18 @@ class ModelMix {
|
|
|
384
751
|
if (mix.openrouter) this.attach('openai/gpt-oss-120b:free', new MixOpenRouter({ options, config }));
|
|
385
752
|
return this;
|
|
386
753
|
}
|
|
387
|
-
|
|
754
|
+
fable50({ options = {}, config = {} } = {}) {
|
|
388
755
|
return this.attach('claude-fable-5', new MixAnthropic({ options, config }));
|
|
389
756
|
}
|
|
390
|
-
|
|
757
|
+
fable5(args = {}) {
|
|
758
|
+
return this.fable50(args);
|
|
759
|
+
}
|
|
760
|
+
opus50({ options = {}, config = {} } = {}) {
|
|
391
761
|
return this.attach('claude-opus-5', new MixAnthropic({ options, config }));
|
|
392
762
|
}
|
|
763
|
+
opus5(args = {}) {
|
|
764
|
+
return this.opus50(args);
|
|
765
|
+
}
|
|
393
766
|
opus48({ options = {}, config = {} } = {}) {
|
|
394
767
|
return this.attach('claude-opus-4-8', new MixAnthropic({ options, config }));
|
|
395
768
|
}
|
|
@@ -399,9 +772,12 @@ class ModelMix {
|
|
|
399
772
|
opus46({ options = {}, config = {} } = {}) {
|
|
400
773
|
return this.attach('claude-opus-4-6', new MixAnthropic({ options, config }));
|
|
401
774
|
}
|
|
402
|
-
|
|
775
|
+
sonnet50({ options = {}, config = {} } = {}) {
|
|
403
776
|
return this.attach('claude-sonnet-5', new MixAnthropic({ options, config }));
|
|
404
777
|
}
|
|
778
|
+
sonnet5(args = {}) {
|
|
779
|
+
return this.sonnet50(args);
|
|
780
|
+
}
|
|
405
781
|
sonnet46({ options = {}, config = {} } = {}) {
|
|
406
782
|
return this.attach('claude-sonnet-4-6', new MixAnthropic({ options, config }));
|
|
407
783
|
}
|
|
@@ -429,6 +805,9 @@ class ModelMix {
|
|
|
429
805
|
gemini35flash({ options = {}, config = {} } = {}) {
|
|
430
806
|
return this.attach('gemini-3.5-flash', new MixGoogle({ options, config }));
|
|
431
807
|
}
|
|
808
|
+
gemini35flashLite({ options = {}, config = {} } = {}) {
|
|
809
|
+
return this.attach('gemini-3.5-flash-lite', new MixGoogle({ options, config }));
|
|
810
|
+
}
|
|
432
811
|
gemini31flashLite({ options = {}, config = {} } = {}) {
|
|
433
812
|
return this.attach('gemini-3.1-flash-lite-preview', new MixGoogle({ options, config }));
|
|
434
813
|
}
|
|
@@ -593,34 +972,54 @@ class ModelMix {
|
|
|
593
972
|
return this;
|
|
594
973
|
}
|
|
595
974
|
|
|
596
|
-
addText(text, { role = "user" } = {}) {
|
|
975
|
+
addText(text, { role = "user", cache } = {}) {
|
|
976
|
+
return this._addText(text, {
|
|
977
|
+
role,
|
|
978
|
+
cache: normalizeContentCache(cache),
|
|
979
|
+
template: { source: text, filename: null }
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
_addText(text, { role = "user", cache, template = null } = {}) {
|
|
597
984
|
const content = [{
|
|
598
985
|
type: "text",
|
|
599
|
-
text
|
|
986
|
+
text,
|
|
987
|
+
...(cache !== undefined && { cache })
|
|
600
988
|
}];
|
|
601
989
|
|
|
990
|
+
if (template) {
|
|
991
|
+
this.messageTemplates.set(content[0], template);
|
|
992
|
+
}
|
|
602
993
|
this.messages.push({ role, content });
|
|
603
994
|
return this;
|
|
604
995
|
}
|
|
605
996
|
|
|
606
|
-
addTextFromFile(filePath, { role = "user" } = {}) {
|
|
607
|
-
const
|
|
608
|
-
this.
|
|
609
|
-
return this
|
|
997
|
+
addTextFromFile(filePath, { role = "user", cache } = {}) {
|
|
998
|
+
const filename = path.resolve(filePath);
|
|
999
|
+
const content = this.readFile(filename);
|
|
1000
|
+
return this._addText(content, {
|
|
1001
|
+
role,
|
|
1002
|
+
cache: normalizeContentCache(cache),
|
|
1003
|
+
template: { source: content, filename }
|
|
1004
|
+
});
|
|
610
1005
|
}
|
|
611
1006
|
|
|
612
1007
|
setSystem(text) {
|
|
613
1008
|
this.config.system = text;
|
|
1009
|
+
this.systemTemplate = { source: text, filename: null };
|
|
614
1010
|
return this;
|
|
615
1011
|
}
|
|
616
1012
|
|
|
617
1013
|
setSystemFromFile(filePath) {
|
|
618
|
-
const
|
|
619
|
-
this.
|
|
1014
|
+
const filename = path.resolve(filePath);
|
|
1015
|
+
const content = this.readFile(filename);
|
|
1016
|
+
this.config.system = content;
|
|
1017
|
+
this.systemTemplate = { source: content, filename };
|
|
620
1018
|
return this;
|
|
621
1019
|
}
|
|
622
1020
|
|
|
623
|
-
addImageFromBuffer(buffer, { role = "user" } = {}) {
|
|
1021
|
+
addImageFromBuffer(buffer, { role = "user", cache } = {}) {
|
|
1022
|
+
const contentCache = normalizeContentCache(cache);
|
|
624
1023
|
this.messages.push({
|
|
625
1024
|
role,
|
|
626
1025
|
content: [{
|
|
@@ -628,19 +1027,21 @@ class ModelMix {
|
|
|
628
1027
|
source: {
|
|
629
1028
|
type: "buffer",
|
|
630
1029
|
data: buffer
|
|
631
|
-
}
|
|
1030
|
+
},
|
|
1031
|
+
...(contentCache !== undefined && { cache: contentCache })
|
|
632
1032
|
}]
|
|
633
1033
|
});
|
|
634
1034
|
return this;
|
|
635
1035
|
}
|
|
636
1036
|
|
|
637
|
-
addImage(filePath, { role = "user" } = {}) {
|
|
1037
|
+
addImage(filePath, { role = "user", cache } = {}) {
|
|
638
1038
|
const absolutePath = path.resolve(filePath);
|
|
639
1039
|
|
|
640
1040
|
if (!fs.existsSync(absolutePath)) {
|
|
641
1041
|
throw new Error(`Image file not found: ${filePath}`);
|
|
642
1042
|
}
|
|
643
1043
|
|
|
1044
|
+
const contentCache = normalizeContentCache(cache);
|
|
644
1045
|
this.messages.push({
|
|
645
1046
|
role,
|
|
646
1047
|
content: [{
|
|
@@ -648,13 +1049,14 @@ class ModelMix {
|
|
|
648
1049
|
source: {
|
|
649
1050
|
type: "file",
|
|
650
1051
|
data: filePath
|
|
651
|
-
}
|
|
1052
|
+
},
|
|
1053
|
+
...(contentCache !== undefined && { cache: contentCache })
|
|
652
1054
|
}]
|
|
653
1055
|
});
|
|
654
1056
|
return this;
|
|
655
1057
|
}
|
|
656
1058
|
|
|
657
|
-
addImageFromUrl(url, { role = "user" } = {}) {
|
|
1059
|
+
addImageFromUrl(url, { role = "user", cache } = {}) {
|
|
658
1060
|
let source;
|
|
659
1061
|
if (url.startsWith('data:')) {
|
|
660
1062
|
// Parse data URL: data:image/jpeg;base64,/9j/4AAQ...
|
|
@@ -675,11 +1077,13 @@ class ModelMix {
|
|
|
675
1077
|
};
|
|
676
1078
|
}
|
|
677
1079
|
|
|
1080
|
+
const contentCache = normalizeContentCache(cache);
|
|
678
1081
|
this.messages.push({
|
|
679
1082
|
role,
|
|
680
1083
|
content: [{
|
|
681
1084
|
type: "image",
|
|
682
|
-
source
|
|
1085
|
+
source,
|
|
1086
|
+
...(contentCache !== undefined && { cache: contentCache })
|
|
683
1087
|
}]
|
|
684
1088
|
});
|
|
685
1089
|
|
|
@@ -728,7 +1132,7 @@ class ModelMix {
|
|
|
728
1132
|
|
|
729
1133
|
// Update the content with processed image
|
|
730
1134
|
message.content[j] = {
|
|
731
|
-
|
|
1135
|
+
...content,
|
|
732
1136
|
source: {
|
|
733
1137
|
type: "base64",
|
|
734
1138
|
media_type: mimeType,
|
|
@@ -767,27 +1171,23 @@ class ModelMix {
|
|
|
767
1171
|
stream: false,
|
|
768
1172
|
}
|
|
769
1173
|
|
|
770
|
-
|
|
771
|
-
let
|
|
772
|
-
|
|
773
|
-
let config = {
|
|
774
|
-
system: systemWithReplacements,
|
|
775
|
-
}
|
|
1174
|
+
let config = {};
|
|
1175
|
+
let systemSuffix = '';
|
|
776
1176
|
|
|
777
1177
|
if (schemaExample) {
|
|
778
1178
|
config.schema = generateJsonSchema(schemaExample, schemaDescription);
|
|
779
1179
|
|
|
780
1180
|
if (addSchema) {
|
|
781
|
-
|
|
1181
|
+
systemSuffix += "\n\nOutput JSON Schema: \n```\n" + JSON.stringify(config.schema) + "\n```";
|
|
782
1182
|
}
|
|
783
1183
|
if (addExample) {
|
|
784
|
-
|
|
1184
|
+
systemSuffix += "\n\nOutput JSON Example: \n```\n" + JSON.stringify(schemaExample) + "\n```";
|
|
785
1185
|
}
|
|
786
1186
|
if (addNote) {
|
|
787
|
-
|
|
1187
|
+
systemSuffix += "\n\nOutput JSON Escape: double quotes, backslashes, and control characters inside JSON strings.\nEnsure the output contains no comments.";
|
|
788
1188
|
}
|
|
789
1189
|
}
|
|
790
|
-
const { message } = await this.execute({ options, config });
|
|
1190
|
+
const { message } = await this.execute({ options, config, systemSuffix });
|
|
791
1191
|
const parsed = JSON.parse(this._extractBlock(message));
|
|
792
1192
|
return isArrayWrap ? parsed.out : parsed;
|
|
793
1193
|
}
|
|
@@ -798,17 +1198,13 @@ class ModelMix {
|
|
|
798
1198
|
}
|
|
799
1199
|
|
|
800
1200
|
async block({ addSystemExtra = true } = {}) {
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
if (addSystemExtra) {
|
|
809
|
-
config.system += "\nReturn the result of the task between triple backtick block code tags ```";
|
|
810
|
-
}
|
|
811
|
-
const { message } = await this.execute({ options: { stream: false }, config });
|
|
1201
|
+
const systemSuffix = addSystemExtra
|
|
1202
|
+
? "\nReturn the result of the task between triple backtick block code tags ```"
|
|
1203
|
+
: '';
|
|
1204
|
+
const { message } = await this.execute({
|
|
1205
|
+
options: { stream: false },
|
|
1206
|
+
systemSuffix
|
|
1207
|
+
});
|
|
812
1208
|
return this._extractBlock(message);
|
|
813
1209
|
}
|
|
814
1210
|
|
|
@@ -822,22 +1218,52 @@ class ModelMix {
|
|
|
822
1218
|
}
|
|
823
1219
|
|
|
824
1220
|
replaceKeyFromFile(key, filePath) {
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
}
|
|
832
|
-
return this;
|
|
1221
|
+
const content = this.readFile(filePath);
|
|
1222
|
+
return this.replace({ [key]: content });
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
_choiceRandom() {
|
|
1226
|
+
return Math.random();
|
|
833
1227
|
}
|
|
834
1228
|
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
1229
|
+
_renderTemplate(
|
|
1230
|
+
source,
|
|
1231
|
+
{ filename = null, label = 'template' } = {},
|
|
1232
|
+
renderContext = createTemplateRenderContext(() => this._choiceRandom())
|
|
1233
|
+
) {
|
|
1234
|
+
if (typeof source !== 'string') {
|
|
1235
|
+
throw new TypeError(`${label} source must be a string.`);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
try {
|
|
1239
|
+
const template = preprocessChoiceDirectives(source, { filename, label });
|
|
1240
|
+
const data = { ...(this.config.replace || {}), $mix: renderContext.helpers };
|
|
1241
|
+
return ejs.render(template, data, {
|
|
1242
|
+
...(filename && { filename }),
|
|
1243
|
+
async: false,
|
|
1244
|
+
cache: false,
|
|
1245
|
+
compileDebug: true,
|
|
1246
|
+
unsafePrototypeLocals: false,
|
|
1247
|
+
includer: (originalPath, resolvedFilename) => {
|
|
1248
|
+
if (!resolvedFilename) {
|
|
1249
|
+
throw new Error(`Could not find the include file "${originalPath}"`);
|
|
1250
|
+
}
|
|
1251
|
+
const includedSource = fs.readFileSync(resolvedFilename, 'utf8').replace(/^\uFEFF/, '');
|
|
1252
|
+
return {
|
|
1253
|
+
filename: resolvedFilename,
|
|
1254
|
+
template: preprocessChoiceDirectives(includedSource, {
|
|
1255
|
+
filename: resolvedFilename,
|
|
1256
|
+
label: 'included template'
|
|
1257
|
+
})
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
});
|
|
1261
|
+
} catch (error) {
|
|
1262
|
+
const location = filename ? ` ${filename}` : '';
|
|
1263
|
+
const renderError = new Error(`Failed to render ${label}${location}: ${error.message}`);
|
|
1264
|
+
renderError.cause = error;
|
|
1265
|
+
throw renderError;
|
|
839
1266
|
}
|
|
840
|
-
return input;
|
|
841
1267
|
}
|
|
842
1268
|
|
|
843
1269
|
static hasToolInteraction(message) {
|
|
@@ -869,37 +1295,56 @@ class ModelMix {
|
|
|
869
1295
|
}, []);
|
|
870
1296
|
}
|
|
871
1297
|
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
1298
|
+
_renderMessageSnapshot(messages, renderContext) {
|
|
1299
|
+
return messages.map(message => ({
|
|
1300
|
+
...message,
|
|
1301
|
+
content: Array.isArray(message.content)
|
|
1302
|
+
? message.content.map(content => {
|
|
1303
|
+
if (!content || typeof content !== 'object') return content;
|
|
1304
|
+
|
|
1305
|
+
const snapshotContent = { ...content };
|
|
1306
|
+
const template = content.type === 'text'
|
|
1307
|
+
? this.messageTemplates.get(content)
|
|
1308
|
+
: null;
|
|
1309
|
+
if (!template) return snapshotContent;
|
|
1310
|
+
|
|
1311
|
+
let rendered = renderContext.renderedMessages.get(content)?.rendered;
|
|
1312
|
+
if (rendered === undefined) {
|
|
1313
|
+
rendered = this._renderTemplate(template.source, {
|
|
1314
|
+
filename: template.filename,
|
|
1315
|
+
label: 'message template'
|
|
1316
|
+
}, renderContext);
|
|
1317
|
+
renderContext.renderedMessages.set(content, { rendered, template });
|
|
882
1318
|
}
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
});
|
|
1319
|
+
snapshotContent.text = rendered;
|
|
1320
|
+
return snapshotContent;
|
|
1321
|
+
})
|
|
1322
|
+
: message.content
|
|
1323
|
+
}));
|
|
888
1324
|
}
|
|
889
1325
|
|
|
890
|
-
|
|
1326
|
+
_commitTemplateRenderContext(renderContext) {
|
|
1327
|
+
for (const [content, { rendered, template }] of renderContext.renderedMessages) {
|
|
1328
|
+
if (this.messageTemplates.get(content) !== template) continue;
|
|
1329
|
+
content.text = rendered;
|
|
1330
|
+
this.messageTemplates.delete(content);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
async prepareMessages(renderContext = createTemplateRenderContext(() => this._choiceRandom())) {
|
|
891
1335
|
await this.processImages();
|
|
892
|
-
|
|
1336
|
+
|
|
1337
|
+
let messages = this.messages;
|
|
893
1338
|
|
|
894
1339
|
// Smart message slicing based on max_history:
|
|
895
1340
|
// 0 = no history (stateless), N = keep last N messages, -1 = unlimited
|
|
896
1341
|
if (this.config.max_history > 0) {
|
|
897
|
-
let sliceStart = Math.max(0,
|
|
1342
|
+
let sliceStart = Math.max(0, messages.length - this.config.max_history);
|
|
898
1343
|
|
|
899
1344
|
// If we're slicing into the middle of a tool interaction,
|
|
900
1345
|
// backtrack to include the full sequence (user → assistant/tool_calls → tool results)
|
|
901
|
-
while (sliceStart > 0 && sliceStart <
|
|
902
|
-
const msg =
|
|
1346
|
+
while (sliceStart > 0 && sliceStart < messages.length) {
|
|
1347
|
+
const msg = messages[sliceStart];
|
|
903
1348
|
if (ModelMix.hasToolInteraction(msg)) {
|
|
904
1349
|
sliceStart--;
|
|
905
1350
|
} else {
|
|
@@ -907,13 +1352,13 @@ class ModelMix {
|
|
|
907
1352
|
}
|
|
908
1353
|
}
|
|
909
1354
|
|
|
910
|
-
this.messages =
|
|
1355
|
+
this.messages = messages.slice(sliceStart);
|
|
1356
|
+
messages = this.messages;
|
|
911
1357
|
}
|
|
912
1358
|
// max_history = -1: unlimited, no slicing
|
|
913
1359
|
// max_history = 0: no history, messages only contain what was added since last call
|
|
914
1360
|
|
|
915
|
-
|
|
916
|
-
this.options.messages = this.messages;
|
|
1361
|
+
return this.groupByRoles(this._renderMessageSnapshot(messages, renderContext));
|
|
917
1362
|
}
|
|
918
1363
|
|
|
919
1364
|
readFile(filePath, { encoding = 'utf8' } = {}) {
|
|
@@ -931,15 +1376,30 @@ class ModelMix {
|
|
|
931
1376
|
}
|
|
932
1377
|
}
|
|
933
1378
|
|
|
934
|
-
|
|
1379
|
+
_resolveSystemTemplate(config, providerConfig) {
|
|
1380
|
+
if (Object.prototype.hasOwnProperty.call(config, 'system')) {
|
|
1381
|
+
return { source: config.system, filename: null };
|
|
1382
|
+
}
|
|
1383
|
+
if (Object.prototype.hasOwnProperty.call(providerConfig, 'system')) {
|
|
1384
|
+
return { source: providerConfig.system, filename: null };
|
|
1385
|
+
}
|
|
1386
|
+
if (this.config.system !== this.systemTemplate.source) {
|
|
1387
|
+
return { source: this.config.system, filename: null };
|
|
1388
|
+
}
|
|
1389
|
+
return this.systemTemplate;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
async execute({ config = {}, options = {}, systemSuffix = '', _templateContext = null } = {}) {
|
|
935
1393
|
if (!this.models || this.models.length === 0) {
|
|
936
1394
|
throw new Error("No models specified. Use methods like .gpt5(), .sonnet46() first.");
|
|
937
1395
|
}
|
|
938
1396
|
|
|
939
|
-
|
|
940
|
-
|
|
1397
|
+
const isRootExecution = _templateContext === null;
|
|
1398
|
+
const templateContext = _templateContext || createTemplateRenderContext(() => this._choiceRandom());
|
|
1399
|
+
const execution = this.limiter.schedule(async () => {
|
|
1400
|
+
const preparedMessages = await this.prepareMessages(templateContext);
|
|
941
1401
|
|
|
942
|
-
if (
|
|
1402
|
+
if (preparedMessages.length === 0) {
|
|
943
1403
|
throw new Error("No user messages have been added. Use addText(prompt), addTextFromFile(filePath), addImage(filePath), or addImageFromUrl(url) to add a prompt.");
|
|
944
1404
|
}
|
|
945
1405
|
|
|
@@ -974,6 +1434,7 @@ class ModelMix {
|
|
|
974
1434
|
// Create clean copies for each provider to avoid contamination
|
|
975
1435
|
const currentOptions = {
|
|
976
1436
|
...this.options,
|
|
1437
|
+
messages: preparedMessages,
|
|
977
1438
|
...providerInstance.options,
|
|
978
1439
|
...optionsTools,
|
|
979
1440
|
...options,
|
|
@@ -990,6 +1451,18 @@ class ModelMix {
|
|
|
990
1451
|
...(config.retry || {})
|
|
991
1452
|
}
|
|
992
1453
|
};
|
|
1454
|
+
const systemTemplate = this._resolveSystemTemplate(config, providerInstance.config);
|
|
1455
|
+
const systemCacheKey = JSON.stringify([systemTemplate.filename, systemTemplate.source]);
|
|
1456
|
+
if (!templateContext.renderedSystems.has(systemCacheKey)) {
|
|
1457
|
+
templateContext.renderedSystems.set(
|
|
1458
|
+
systemCacheKey,
|
|
1459
|
+
this._renderTemplate(systemTemplate.source, {
|
|
1460
|
+
filename: systemTemplate.filename,
|
|
1461
|
+
label: 'system template'
|
|
1462
|
+
}, templateContext)
|
|
1463
|
+
);
|
|
1464
|
+
}
|
|
1465
|
+
currentConfig.system = templateContext.renderedSystems.get(systemCacheKey) + systemSuffix;
|
|
993
1466
|
|
|
994
1467
|
// Grok 4.20 alias → reasoning / non-reasoning from unified effort
|
|
995
1468
|
const resolvedModelKey = resolveGrok420ModelKey(
|
|
@@ -1014,7 +1487,7 @@ class ModelMix {
|
|
|
1014
1487
|
const header = `\n${prefix} [${providerName}:${resolvedModelKey}] #${originalIndex + 1}${suffix}`;
|
|
1015
1488
|
|
|
1016
1489
|
if (currentConfig.debug >= 2) {
|
|
1017
|
-
console.log(`${header}\n${ModelMix.formatInputSummary(
|
|
1490
|
+
console.log(`${header}\n${ModelMix.formatInputSummary(preparedMessages, currentConfig.system, currentConfig.debug)}`);
|
|
1018
1491
|
} else {
|
|
1019
1492
|
console.log(header);
|
|
1020
1493
|
}
|
|
@@ -1068,7 +1541,16 @@ class ModelMix {
|
|
|
1068
1541
|
const elapsedMs = Date.now() - startTime;
|
|
1069
1542
|
|
|
1070
1543
|
if (result.tokens) {
|
|
1071
|
-
|
|
1544
|
+
const normalizedTokens = ModelMix.normalizeTokenUsage(result.tokens);
|
|
1545
|
+
const costBreakdown = ModelMix.calculateCostBreakdown(resolvedModelKey, normalizedTokens);
|
|
1546
|
+
const cacheMetrics = ModelMix.calculateCacheMetrics(resolvedModelKey, normalizedTokens);
|
|
1547
|
+
result.tokens = {
|
|
1548
|
+
...result.tokens,
|
|
1549
|
+
...normalizedTokens,
|
|
1550
|
+
...cacheMetrics,
|
|
1551
|
+
cost: MODEL_PRICING[resolvedModelKey] ? costBreakdown.total : 0,
|
|
1552
|
+
costBreakdown
|
|
1553
|
+
};
|
|
1072
1554
|
const elapsedSec = elapsedMs / 1000;
|
|
1073
1555
|
result.tokens.speed = elapsedSec > 0 ? Math.round(result.tokens.output / elapsedSec) : 0;
|
|
1074
1556
|
}
|
|
@@ -1087,7 +1569,7 @@ class ModelMix {
|
|
|
1087
1569
|
}]
|
|
1088
1570
|
});
|
|
1089
1571
|
} else {
|
|
1090
|
-
this.
|
|
1572
|
+
this._addText(result.message, { role: "assistant" });
|
|
1091
1573
|
}
|
|
1092
1574
|
}
|
|
1093
1575
|
|
|
@@ -1105,7 +1587,7 @@ class ModelMix {
|
|
|
1105
1587
|
});
|
|
1106
1588
|
}
|
|
1107
1589
|
|
|
1108
|
-
return this.execute({ options, config });
|
|
1590
|
+
return this.execute({ options, config, systemSuffix, _templateContext: templateContext });
|
|
1109
1591
|
}
|
|
1110
1592
|
|
|
1111
1593
|
// debug level 1: Just success indicator
|
|
@@ -1167,7 +1649,7 @@ class ModelMix {
|
|
|
1167
1649
|
}]
|
|
1168
1650
|
});
|
|
1169
1651
|
} else {
|
|
1170
|
-
this.
|
|
1652
|
+
this._addText(result.message, { role: "assistant" });
|
|
1171
1653
|
}
|
|
1172
1654
|
}
|
|
1173
1655
|
|
|
@@ -1193,6 +1675,12 @@ class ModelMix {
|
|
|
1193
1675
|
log.error("Fallback logic completed without success or throwing the final error.");
|
|
1194
1676
|
throw lastError || new Error("Failed to get response from any model, and no specific error was caught.");
|
|
1195
1677
|
});
|
|
1678
|
+
|
|
1679
|
+
if (!isRootExecution) return execution;
|
|
1680
|
+
|
|
1681
|
+
const result = await execution;
|
|
1682
|
+
this._commitTemplateRenderContext(templateContext);
|
|
1683
|
+
return result;
|
|
1196
1684
|
}
|
|
1197
1685
|
|
|
1198
1686
|
async processToolCalls(toolCalls) {
|
|
@@ -1393,6 +1881,13 @@ class MixCustom {
|
|
|
1393
1881
|
return MixOpenAI.convertMessages(messages, config);
|
|
1394
1882
|
}
|
|
1395
1883
|
|
|
1884
|
+
sanitizeCacheOptions(options) {
|
|
1885
|
+
delete options.cache_control;
|
|
1886
|
+
delete options.prompt_cache_key;
|
|
1887
|
+
delete options.prompt_cache_options;
|
|
1888
|
+
delete options.prompt_cache_retention;
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1396
1891
|
static stripContentTypeHeader(headers = {}) {
|
|
1397
1892
|
return stripContentTypeHeader(headers);
|
|
1398
1893
|
}
|
|
@@ -1407,6 +1902,7 @@ class MixCustom {
|
|
|
1407
1902
|
|
|
1408
1903
|
async create({ config = {}, options = {} } = {}) {
|
|
1409
1904
|
try {
|
|
1905
|
+
this.sanitizeCacheOptions(options);
|
|
1410
1906
|
if (Array.isArray(options.messages)) {
|
|
1411
1907
|
options.messages = this.convertMessages(options.messages, config);
|
|
1412
1908
|
}
|
|
@@ -1418,9 +1914,7 @@ class MixCustom {
|
|
|
1418
1914
|
console.log('\n[REQUEST DETAILS]');
|
|
1419
1915
|
|
|
1420
1916
|
console.log('\n[CONFIG]');
|
|
1421
|
-
|
|
1422
|
-
delete configToLog.debug;
|
|
1423
|
-
console.log(ModelMix.formatJSON(configToLog));
|
|
1917
|
+
console.log(ModelMix.formatJSON(configForDebug(config)));
|
|
1424
1918
|
|
|
1425
1919
|
console.log('\n[OPTIONS]');
|
|
1426
1920
|
console.log(ModelMix.formatJSON(request.options));
|
|
@@ -1440,11 +1934,11 @@ class MixCustom {
|
|
|
1440
1934
|
}));
|
|
1441
1935
|
}
|
|
1442
1936
|
} catch (error) {
|
|
1443
|
-
throw this.handleError(error
|
|
1937
|
+
throw this.handleError(error);
|
|
1444
1938
|
}
|
|
1445
1939
|
}
|
|
1446
1940
|
|
|
1447
|
-
handleError(error
|
|
1941
|
+
handleError(error) {
|
|
1448
1942
|
let errorMessage = 'An error occurred in MixCustom';
|
|
1449
1943
|
let statusCode = null;
|
|
1450
1944
|
let errorDetails = null;
|
|
@@ -1458,12 +1952,10 @@ class MixCustom {
|
|
|
1458
1952
|
}
|
|
1459
1953
|
|
|
1460
1954
|
const formattedError = {
|
|
1461
|
-
message: errorMessage,
|
|
1955
|
+
message: redactSecret(errorMessage, this.config.apiKey),
|
|
1462
1956
|
statusCode,
|
|
1463
|
-
details: errorDetails,
|
|
1464
|
-
stack: error.stack,
|
|
1465
|
-
config: config,
|
|
1466
|
-
options: options
|
|
1957
|
+
details: redactSecret(errorDetails, this.config.apiKey),
|
|
1958
|
+
stack: redactSecret(error.stack, this.config.apiKey)
|
|
1467
1959
|
};
|
|
1468
1960
|
|
|
1469
1961
|
return formattedError;
|
|
@@ -1582,19 +2074,15 @@ class MixCustom {
|
|
|
1582
2074
|
static extractTokens(data) {
|
|
1583
2075
|
// OpenAI/Groq/Together/Lambda/Cerebras/Fireworks format
|
|
1584
2076
|
if (data.usage) {
|
|
1585
|
-
return {
|
|
2077
|
+
return ModelMix.normalizeTokenUsage({
|
|
1586
2078
|
input: data.usage.prompt_tokens || 0,
|
|
1587
2079
|
output: data.usage.completion_tokens || 0,
|
|
1588
|
-
total: data.usage.total_tokens
|
|
1589
|
-
cached: ModelMix.extractCacheTokens(data.usage)
|
|
1590
|
-
|
|
2080
|
+
total: data.usage.total_tokens,
|
|
2081
|
+
cached: ModelMix.extractCacheTokens(data.usage),
|
|
2082
|
+
cacheWrite: ModelMix.extractCacheWriteTokens(data.usage)
|
|
2083
|
+
});
|
|
1591
2084
|
}
|
|
1592
|
-
return
|
|
1593
|
-
input: 0,
|
|
1594
|
-
output: 0,
|
|
1595
|
-
total: 0,
|
|
1596
|
-
cached: 0
|
|
1597
|
-
};
|
|
2085
|
+
return ModelMix.normalizeTokenUsage();
|
|
1598
2086
|
}
|
|
1599
2087
|
|
|
1600
2088
|
processResponse(response) {
|
|
@@ -1613,6 +2101,11 @@ class MixCustom {
|
|
|
1613
2101
|
}
|
|
1614
2102
|
|
|
1615
2103
|
class MixOpenAI extends MixCustom {
|
|
2104
|
+
sanitizeCacheOptions(options) {
|
|
2105
|
+
delete options.cache_control;
|
|
2106
|
+
delete options.prompt_cache_options;
|
|
2107
|
+
}
|
|
2108
|
+
|
|
1616
2109
|
getDefaultConfig(customConfig) {
|
|
1617
2110
|
|
|
1618
2111
|
if (!process.env.OPENAI_API_KEY) {
|
|
@@ -1686,22 +2179,26 @@ class MixOpenAI extends MixCustom {
|
|
|
1686
2179
|
continue;
|
|
1687
2180
|
}
|
|
1688
2181
|
|
|
2182
|
+
let convertedMessage = { ...message };
|
|
1689
2183
|
if (Array.isArray(message.content)) {
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
2184
|
+
convertedMessage = {
|
|
2185
|
+
...message,
|
|
2186
|
+
content: message.content.filter(content => content !== null && content !== undefined).map(content => {
|
|
2187
|
+
if (content && content.type === 'image') {
|
|
2188
|
+
const { media_type, data } = content.source;
|
|
2189
|
+
return {
|
|
2190
|
+
type: 'image_url',
|
|
2191
|
+
image_url: {
|
|
2192
|
+
url: `data:${media_type};base64,${data}`
|
|
2193
|
+
}
|
|
2194
|
+
};
|
|
2195
|
+
}
|
|
2196
|
+
return stripContentCacheMetadata(content);
|
|
2197
|
+
})
|
|
2198
|
+
};
|
|
1702
2199
|
}
|
|
1703
2200
|
|
|
1704
|
-
results.push(
|
|
2201
|
+
results.push(convertedMessage);
|
|
1705
2202
|
}
|
|
1706
2203
|
|
|
1707
2204
|
return results;
|
|
@@ -1761,10 +2258,14 @@ class MixOpenAIResponses extends MixOpenAI {
|
|
|
1761
2258
|
}
|
|
1762
2259
|
|
|
1763
2260
|
static buildResponsesRequest(options = {}, config = {}) {
|
|
1764
|
-
const
|
|
2261
|
+
const isGPT56 = typeof options.model === 'string' && options.model.startsWith('gpt-5.6');
|
|
2262
|
+
const input = MixOpenAIResponses.messagesToResponsesInput(options.messages, {
|
|
2263
|
+
translateNeutralCache: isGPT56
|
|
2264
|
+
});
|
|
1765
2265
|
if (config.system) {
|
|
1766
2266
|
input.unshift({ role: 'developer', content: [{ type: 'input_text', text: config.system }] });
|
|
1767
2267
|
}
|
|
2268
|
+
MixOpenAIResponses.validatePromptCaching(options, input);
|
|
1768
2269
|
const request = {
|
|
1769
2270
|
model: options.model,
|
|
1770
2271
|
input,
|
|
@@ -1808,10 +2309,52 @@ class MixOpenAIResponses extends MixOpenAI {
|
|
|
1808
2309
|
if (options.user !== undefined) request.user = options.user;
|
|
1809
2310
|
if (options.prompt_cache_key !== undefined) request.prompt_cache_key = options.prompt_cache_key;
|
|
1810
2311
|
if (options.prompt_cache_retention !== undefined) request.prompt_cache_retention = options.prompt_cache_retention;
|
|
2312
|
+
if (options.prompt_cache_options !== undefined) request.prompt_cache_options = options.prompt_cache_options;
|
|
1811
2313
|
|
|
1812
2314
|
return request;
|
|
1813
2315
|
}
|
|
1814
2316
|
|
|
2317
|
+
static validatePromptCaching(options, input) {
|
|
2318
|
+
const isGPT56 = typeof options.model === 'string' && options.model.startsWith('gpt-5.6');
|
|
2319
|
+
const cacheOptions = options.prompt_cache_options;
|
|
2320
|
+
const breakpoints = input.flatMap(message => Array.isArray(message.content)
|
|
2321
|
+
? message.content
|
|
2322
|
+
.filter(block => block?.prompt_cache_breakpoint !== undefined)
|
|
2323
|
+
.map(block => block.prompt_cache_breakpoint)
|
|
2324
|
+
: []);
|
|
2325
|
+
|
|
2326
|
+
if (isGPT56 && options.prompt_cache_retention !== undefined) {
|
|
2327
|
+
throw new Error('GPT-5.6 does not support prompt_cache_retention; use prompt_cache_options.ttl instead.');
|
|
2328
|
+
}
|
|
2329
|
+
if (!isGPT56 && cacheOptions !== undefined) {
|
|
2330
|
+
throw new Error('prompt_cache_options is only supported by GPT-5.6 models.');
|
|
2331
|
+
}
|
|
2332
|
+
if (!isGPT56 && breakpoints.length > 0) {
|
|
2333
|
+
throw new Error('prompt_cache_breakpoint is only supported by GPT-5.6 models.');
|
|
2334
|
+
}
|
|
2335
|
+
if (cacheOptions !== undefined) {
|
|
2336
|
+
if (!isPlainObject(cacheOptions)) {
|
|
2337
|
+
throw new TypeError('prompt_cache_options must be a plain non-null object.');
|
|
2338
|
+
}
|
|
2339
|
+
if (cacheOptions.mode !== undefined
|
|
2340
|
+
&& cacheOptions.mode !== 'implicit'
|
|
2341
|
+
&& cacheOptions.mode !== 'explicit') {
|
|
2342
|
+
throw new TypeError('prompt_cache_options.mode must be "implicit" or "explicit".');
|
|
2343
|
+
}
|
|
2344
|
+
if (cacheOptions.ttl !== undefined && cacheOptions.ttl !== '30m') {
|
|
2345
|
+
throw new TypeError('prompt_cache_options.ttl must be "30m".');
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
for (const breakpoint of breakpoints) {
|
|
2349
|
+
if (!isPlainObject(breakpoint)) {
|
|
2350
|
+
throw new TypeError('prompt_cache_breakpoint must be a plain non-null object.');
|
|
2351
|
+
}
|
|
2352
|
+
if (breakpoint.mode !== 'explicit') {
|
|
2353
|
+
throw new TypeError('prompt_cache_breakpoint mode must be "explicit".');
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
|
|
1815
2358
|
static processResponsesResponse(response) {
|
|
1816
2359
|
const message = MixOpenAIResponses.extractResponsesMessage(response.data);
|
|
1817
2360
|
return {
|
|
@@ -1825,19 +2368,15 @@ class MixOpenAIResponses extends MixOpenAI {
|
|
|
1825
2368
|
|
|
1826
2369
|
static extractResponsesTokens(data) {
|
|
1827
2370
|
if (data.usage) {
|
|
1828
|
-
return {
|
|
2371
|
+
return ModelMix.normalizeTokenUsage({
|
|
1829
2372
|
input: data.usage.input_tokens || 0,
|
|
1830
2373
|
output: data.usage.output_tokens || 0,
|
|
1831
|
-
total: data.usage.total_tokens
|
|
1832
|
-
cached: ModelMix.extractCacheTokens(data.usage)
|
|
1833
|
-
|
|
2374
|
+
total: data.usage.total_tokens,
|
|
2375
|
+
cached: ModelMix.extractCacheTokens(data.usage),
|
|
2376
|
+
cacheWrite: ModelMix.extractCacheWriteTokens(data.usage)
|
|
2377
|
+
});
|
|
1834
2378
|
}
|
|
1835
|
-
return
|
|
1836
|
-
input: 0,
|
|
1837
|
-
output: 0,
|
|
1838
|
-
total: 0,
|
|
1839
|
-
cached: 0
|
|
1840
|
-
};
|
|
2379
|
+
return ModelMix.normalizeTokenUsage();
|
|
1841
2380
|
}
|
|
1842
2381
|
|
|
1843
2382
|
static extractResponsesMessage(data) {
|
|
@@ -1851,27 +2390,70 @@ class MixOpenAIResponses extends MixOpenAI {
|
|
|
1851
2390
|
.trim();
|
|
1852
2391
|
}
|
|
1853
2392
|
|
|
1854
|
-
static messagesToResponsesInput(messages = []) {
|
|
2393
|
+
static messagesToResponsesInput(messages = [], { translateNeutralCache = false } = {}) {
|
|
1855
2394
|
const mapped = [];
|
|
1856
2395
|
|
|
1857
2396
|
for (const message of messages) {
|
|
1858
2397
|
if (!message || !message.role) continue;
|
|
1859
2398
|
if (message.tool_calls || message.role === 'tool') continue;
|
|
1860
2399
|
|
|
1861
|
-
|
|
2400
|
+
const content = [];
|
|
2401
|
+
const isAssistant = message.role === 'assistant';
|
|
2402
|
+
const textType = isAssistant ? 'output_text' : 'input_text';
|
|
1862
2403
|
if (typeof message.content === 'string') {
|
|
1863
|
-
text
|
|
2404
|
+
if (message.content) content.push({ type: textType, text: message.content });
|
|
1864
2405
|
} else if (Array.isArray(message.content)) {
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
2406
|
+
for (const item of message.content) {
|
|
2407
|
+
if (!item || typeof item !== 'object') continue;
|
|
2408
|
+
const neutralCache = item.cache !== undefined
|
|
2409
|
+
? normalizeContentCache(item.cache)
|
|
2410
|
+
: undefined;
|
|
2411
|
+
const promptCacheBreakpoint = item.prompt_cache_breakpoint !== undefined
|
|
2412
|
+
? item.prompt_cache_breakpoint
|
|
2413
|
+
: (translateNeutralCache && neutralCache?.breakpoint
|
|
2414
|
+
? { mode: 'explicit' }
|
|
2415
|
+
: undefined);
|
|
2416
|
+
const breakpoint = !isAssistant && promptCacheBreakpoint !== undefined
|
|
2417
|
+
? { prompt_cache_breakpoint: promptCacheBreakpoint }
|
|
2418
|
+
: {};
|
|
2419
|
+
|
|
2420
|
+
if ((item.type === 'text' || item.type === 'input_text' || item.type === 'output_text')
|
|
2421
|
+
&& typeof item.text === 'string') {
|
|
2422
|
+
content.push({ type: textType, text: item.text, ...breakpoint });
|
|
2423
|
+
continue;
|
|
2424
|
+
}
|
|
2425
|
+
if (item.type === 'image' && item.source) {
|
|
2426
|
+
let imageUrl;
|
|
2427
|
+
if (item.source.type === 'base64') {
|
|
2428
|
+
if (!item.source.media_type || typeof item.source.data !== 'string') {
|
|
2429
|
+
throw new TypeError('Responses base64 images require source.media_type and string source.data.');
|
|
2430
|
+
}
|
|
2431
|
+
imageUrl = `data:${item.source.media_type};base64,${item.source.data}`;
|
|
2432
|
+
} else if (item.source.type === 'url' && typeof item.source.data === 'string') {
|
|
2433
|
+
imageUrl = item.source.data;
|
|
2434
|
+
} else {
|
|
2435
|
+
throw new TypeError('Responses images must be processed to base64 or use a URL source.');
|
|
2436
|
+
}
|
|
2437
|
+
content.push({ type: 'input_image', image_url: imageUrl, ...breakpoint });
|
|
2438
|
+
continue;
|
|
2439
|
+
}
|
|
2440
|
+
if (item.type === 'image_url' && typeof item.image_url?.url === 'string') {
|
|
2441
|
+
content.push({ type: 'input_image', image_url: item.image_url.url, ...breakpoint });
|
|
2442
|
+
continue;
|
|
2443
|
+
}
|
|
2444
|
+
if (item.type === 'input_image' || item.type === 'input_file') {
|
|
2445
|
+
content.push({
|
|
2446
|
+
...stripContentCacheMetadata(item),
|
|
2447
|
+
...breakpoint
|
|
2448
|
+
});
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
1869
2451
|
}
|
|
1870
2452
|
|
|
1871
|
-
if (
|
|
2453
|
+
if (content.length === 0) continue;
|
|
1872
2454
|
mapped.push({
|
|
1873
2455
|
role: message.role,
|
|
1874
|
-
content
|
|
2456
|
+
content
|
|
1875
2457
|
});
|
|
1876
2458
|
}
|
|
1877
2459
|
|
|
@@ -1924,9 +2506,7 @@ class MixOpenAIWebSocket extends MixOpenAIResponses {
|
|
|
1924
2506
|
reject({
|
|
1925
2507
|
message: `Realtime WebSocket timed out after ${timeoutMs}ms`,
|
|
1926
2508
|
statusCode: null,
|
|
1927
|
-
details: null
|
|
1928
|
-
config: mergedConfig,
|
|
1929
|
-
options
|
|
2509
|
+
details: null
|
|
1930
2510
|
});
|
|
1931
2511
|
}, timeoutMs);
|
|
1932
2512
|
|
|
@@ -2020,9 +2600,7 @@ class MixOpenAIWebSocket extends MixOpenAIResponses {
|
|
|
2020
2600
|
reject({
|
|
2021
2601
|
message: event.error?.message || 'Realtime WebSocket error',
|
|
2022
2602
|
statusCode: null,
|
|
2023
|
-
details: event.error || event
|
|
2024
|
-
config: mergedConfig,
|
|
2025
|
-
options
|
|
2603
|
+
details: event.error || event
|
|
2026
2604
|
});
|
|
2027
2605
|
}
|
|
2028
2606
|
});
|
|
@@ -2035,9 +2613,7 @@ class MixOpenAIWebSocket extends MixOpenAIResponses {
|
|
|
2035
2613
|
message: error.message || 'Realtime WebSocket connection error',
|
|
2036
2614
|
statusCode: null,
|
|
2037
2615
|
details: null,
|
|
2038
|
-
stack: error.stack
|
|
2039
|
-
config: mergedConfig,
|
|
2040
|
-
options
|
|
2616
|
+
stack: error.stack
|
|
2041
2617
|
});
|
|
2042
2618
|
});
|
|
2043
2619
|
|
|
@@ -2048,9 +2624,7 @@ class MixOpenAIWebSocket extends MixOpenAIResponses {
|
|
|
2048
2624
|
reject({
|
|
2049
2625
|
message: 'Realtime WebSocket closed before response.done',
|
|
2050
2626
|
statusCode: null,
|
|
2051
|
-
details: null
|
|
2052
|
-
config: mergedConfig,
|
|
2053
|
-
options
|
|
2627
|
+
details: null
|
|
2054
2628
|
});
|
|
2055
2629
|
});
|
|
2056
2630
|
});
|
|
@@ -2151,6 +2725,23 @@ class MixKimi extends MixOpenAI {
|
|
|
2151
2725
|
|
|
2152
2726
|
class MixAnthropic extends MixCustom {
|
|
2153
2727
|
|
|
2728
|
+
sanitizeCacheOptions(options) {
|
|
2729
|
+
delete options.prompt_cache_key;
|
|
2730
|
+
delete options.prompt_cache_options;
|
|
2731
|
+
delete options.prompt_cache_retention;
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
static validateCacheControl(cacheControl) {
|
|
2735
|
+
if (!isPlainObject(cacheControl) || cacheControl.type !== 'ephemeral') {
|
|
2736
|
+
throw new TypeError('Anthropic cache_control must have type "ephemeral".');
|
|
2737
|
+
}
|
|
2738
|
+
if (cacheControl.ttl !== undefined
|
|
2739
|
+
&& cacheControl.ttl !== '5m'
|
|
2740
|
+
&& cacheControl.ttl !== '1h') {
|
|
2741
|
+
throw new TypeError('Anthropic cache_control.ttl must be "5m" or "1h".');
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2154
2745
|
/**
|
|
2155
2746
|
* Opus 4.7+ and Claude 5 family reject sampling params (temperature/top_p/top_k).
|
|
2156
2747
|
* See: https://platform.claude.com/docs/en/about-claude/models/migration-guide
|
|
@@ -2196,10 +2787,20 @@ class MixAnthropic extends MixCustom {
|
|
|
2196
2787
|
delete options.top_k;
|
|
2197
2788
|
}
|
|
2198
2789
|
|
|
2790
|
+
const requestConfig = { ...config };
|
|
2791
|
+
if (hasNeutralCacheBreakpoint(options.messages)) {
|
|
2792
|
+
const contentCacheControl = options.cache_control ?? { type: 'ephemeral' };
|
|
2793
|
+
MixAnthropic.validateCacheControl(contentCacheControl);
|
|
2794
|
+
requestConfig._contentCacheControl = { ...contentCacheControl };
|
|
2795
|
+
delete options.cache_control;
|
|
2796
|
+
} else if (options.cache_control !== undefined) {
|
|
2797
|
+
MixAnthropic.validateCacheControl(options.cache_control);
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2199
2800
|
options.system = config.system;
|
|
2200
2801
|
|
|
2201
2802
|
try {
|
|
2202
|
-
return await super.create({ config, options });
|
|
2803
|
+
return await super.create({ config: requestConfig, options });
|
|
2203
2804
|
} catch (error) {
|
|
2204
2805
|
// Log the error details for debugging
|
|
2205
2806
|
if (error.response && error.response.data) {
|
|
@@ -2271,20 +2872,37 @@ class MixAnthropic extends MixCustom {
|
|
|
2271
2872
|
|
|
2272
2873
|
// Handle content conversion for other messages
|
|
2273
2874
|
if (message.content && Array.isArray(message.content)) {
|
|
2274
|
-
|
|
2875
|
+
const content = message.content.filter(content => content !== null && content !== undefined).map(content => {
|
|
2876
|
+
const neutralCache = content?.cache !== undefined
|
|
2877
|
+
? normalizeContentCache(content.cache)
|
|
2878
|
+
: undefined;
|
|
2879
|
+
if (neutralCache && content.cache_control !== undefined) {
|
|
2880
|
+
throw new TypeError('Use either cache or cache_control on an Anthropic content block, not both.');
|
|
2881
|
+
}
|
|
2882
|
+
let converted = content;
|
|
2275
2883
|
if (content && content.type === 'function') {
|
|
2276
|
-
|
|
2884
|
+
converted = {
|
|
2277
2885
|
type: 'tool_use',
|
|
2278
2886
|
id: content.id,
|
|
2279
2887
|
name: content.function.name,
|
|
2280
2888
|
input: JSON.parse(content.function.arguments)
|
|
2281
|
-
}
|
|
2889
|
+
};
|
|
2282
2890
|
}
|
|
2283
|
-
|
|
2891
|
+
const sanitized = stripContentCacheMetadata(converted);
|
|
2892
|
+
if (content.cache_control !== undefined) {
|
|
2893
|
+
MixAnthropic.validateCacheControl(content.cache_control);
|
|
2894
|
+
sanitized.cache_control = { ...content.cache_control };
|
|
2895
|
+
} else if (neutralCache?.breakpoint) {
|
|
2896
|
+
sanitized.cache_control = {
|
|
2897
|
+
...(config?._contentCacheControl || { type: 'ephemeral' })
|
|
2898
|
+
};
|
|
2899
|
+
}
|
|
2900
|
+
return sanitized;
|
|
2284
2901
|
});
|
|
2902
|
+
return { ...message, content };
|
|
2285
2903
|
}
|
|
2286
2904
|
|
|
2287
|
-
return message;
|
|
2905
|
+
return { ...message };
|
|
2288
2906
|
});
|
|
2289
2907
|
}
|
|
2290
2908
|
|
|
@@ -2366,19 +2984,26 @@ class MixAnthropic extends MixCustom {
|
|
|
2366
2984
|
static extractTokens(data) {
|
|
2367
2985
|
// Anthropic format
|
|
2368
2986
|
if (data.usage) {
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2987
|
+
const cached = ModelMix.extractCacheTokens(data.usage);
|
|
2988
|
+
const cacheWrite5m = data.usage.cache_creation?.ephemeral_5m_input_tokens ?? 0;
|
|
2989
|
+
const cacheWrite1h = data.usage.cache_creation?.ephemeral_1h_input_tokens ?? 0;
|
|
2990
|
+
const cacheWrite = Math.max(
|
|
2991
|
+
ModelMix.extractCacheWriteTokens(data.usage),
|
|
2992
|
+
cacheWrite5m + cacheWrite1h
|
|
2993
|
+
);
|
|
2994
|
+
const input = (data.usage.input_tokens || 0) + cached + cacheWrite;
|
|
2995
|
+
const output = data.usage.output_tokens || 0;
|
|
2996
|
+
return ModelMix.normalizeTokenUsage({
|
|
2997
|
+
input,
|
|
2998
|
+
output,
|
|
2999
|
+
total: input + output,
|
|
3000
|
+
cached,
|
|
3001
|
+
cacheWrite,
|
|
3002
|
+
cacheWrite5m,
|
|
3003
|
+
cacheWrite1h
|
|
3004
|
+
});
|
|
2375
3005
|
}
|
|
2376
|
-
return
|
|
2377
|
-
input: 0,
|
|
2378
|
-
output: 0,
|
|
2379
|
-
total: 0,
|
|
2380
|
-
cached: 0
|
|
2381
|
-
};
|
|
3006
|
+
return ModelMix.normalizeTokenUsage();
|
|
2382
3007
|
}
|
|
2383
3008
|
|
|
2384
3009
|
processResponse(response) {
|
|
@@ -2562,6 +3187,13 @@ class MixGrok extends MixOpenAI {
|
|
|
2562
3187
|
...customConfig
|
|
2563
3188
|
});
|
|
2564
3189
|
}
|
|
3190
|
+
|
|
3191
|
+
async create({ config = {}, options = {} } = {}) {
|
|
3192
|
+
if (options.model === GROK420_REASONING || options.model === GROK420_NON_REASONING) {
|
|
3193
|
+
delete options.reasoning_effort;
|
|
3194
|
+
}
|
|
3195
|
+
return super.create({ config, options });
|
|
3196
|
+
}
|
|
2565
3197
|
}
|
|
2566
3198
|
|
|
2567
3199
|
class MixLambda extends MixCustom {
|
|
@@ -2736,6 +3368,7 @@ class MixGoogle extends MixCustom {
|
|
|
2736
3368
|
return super.getDefaultConfig({
|
|
2737
3369
|
url: 'https://generativelanguage.googleapis.com/v1beta/models',
|
|
2738
3370
|
apiKey: process.env.GEMINI_API_KEY,
|
|
3371
|
+
...customConfig
|
|
2739
3372
|
});
|
|
2740
3373
|
}
|
|
2741
3374
|
|
|
@@ -2905,9 +3538,7 @@ class MixGoogle extends MixCustom {
|
|
|
2905
3538
|
console.log('\n[REQUEST DETAILS - GOOGLE]');
|
|
2906
3539
|
|
|
2907
3540
|
console.log('\n[CONFIG]');
|
|
2908
|
-
|
|
2909
|
-
delete configToLog.debug;
|
|
2910
|
-
console.log(ModelMix.formatJSON(configToLog));
|
|
3541
|
+
console.log(ModelMix.formatJSON(configForDebug(config)));
|
|
2911
3542
|
|
|
2912
3543
|
console.log('\n[PAYLOAD]');
|
|
2913
3544
|
console.log(ModelMix.formatJSON(payload));
|
|
@@ -2923,7 +3554,7 @@ class MixGoogle extends MixCustom {
|
|
|
2923
3554
|
}));
|
|
2924
3555
|
}
|
|
2925
3556
|
} catch (error) {
|
|
2926
|
-
throw this.handleError(error
|
|
3557
|
+
throw this.handleError(error);
|
|
2927
3558
|
}
|
|
2928
3559
|
}
|
|
2929
3560
|
|
|
@@ -2961,19 +3592,15 @@ class MixGoogle extends MixCustom {
|
|
|
2961
3592
|
static extractTokens(data) {
|
|
2962
3593
|
// Google Gemini format
|
|
2963
3594
|
if (data.usageMetadata) {
|
|
2964
|
-
return {
|
|
3595
|
+
return ModelMix.normalizeTokenUsage({
|
|
2965
3596
|
input: data.usageMetadata.promptTokenCount || 0,
|
|
2966
3597
|
output: data.usageMetadata.candidatesTokenCount || 0,
|
|
2967
|
-
total: data.usageMetadata.totalTokenCount
|
|
2968
|
-
cached: ModelMix.extractCacheTokens(data.usageMetadata)
|
|
2969
|
-
|
|
3598
|
+
total: data.usageMetadata.totalTokenCount,
|
|
3599
|
+
cached: ModelMix.extractCacheTokens(data.usageMetadata),
|
|
3600
|
+
cacheWrite: ModelMix.extractCacheWriteTokens(data.usageMetadata)
|
|
3601
|
+
});
|
|
2970
3602
|
}
|
|
2971
|
-
return
|
|
2972
|
-
input: 0,
|
|
2973
|
-
output: 0,
|
|
2974
|
-
total: 0,
|
|
2975
|
-
cached: 0
|
|
2976
|
-
};
|
|
3603
|
+
return ModelMix.normalizeTokenUsage();
|
|
2977
3604
|
}
|
|
2978
3605
|
|
|
2979
3606
|
static stripUnsupportedSchemaProps(schema) {
|
|
@@ -3020,4 +3647,4 @@ class MixGoogle extends MixCustom {
|
|
|
3020
3647
|
}
|
|
3021
3648
|
}
|
|
3022
3649
|
|
|
3023
|
-
module.exports = { MixCustom, ModelMix, MixAnthropic, MixKimi, MixMiniMax, MixMiMo, MixOpenAI, MixOpenAIResponses, MixOpenAIWebSocket, MixOpenRouter, MixPerplexity, MixOllama, MixLMStudio, MixGroq, MixTogether, MixGrok, MixCerebras, MixGoogle, MixFireworks, MixNVIDIA, normalizeEffort, applyUnifiedEffort, resolveProviderFamily };
|
|
3650
|
+
module.exports = { MixCustom, ModelMix, MixAnthropic, MixKimi, MixMiniMax, MixMiMo, MixOpenAI, MixOpenAIResponses, MixOpenAIWebSocket, MixOpenRouter, MixPerplexity, MixOllama, MixLMStudio, MixGroq, MixTogether, MixGrok, MixCerebras, MixGoogle, MixFireworks, MixNVIDIA, normalizeEffort, applyUnifiedEffort, resolveProviderFamily };
|