modelmix 4.7.4 → 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 -89
- package/demo/demo.js +4 -4
- package/demo/prompt.md +2 -2
- package/http-client.js +15 -2
- package/index.d.ts +100 -11
- package/index.js +896 -273
- package/package.json +4 -1
- package/skills/modelmix/SKILL.md +75 -18
- 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 +409 -16
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,102 +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-3.5-flash-lite':
|
|
86
|
-
'gemini-2.5-pro':
|
|
87
|
-
'gemini-2.5-flash':
|
|
88
|
-
'gemini-3.1-flash-lite-preview':
|
|
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 },
|
|
89
299
|
// Grok
|
|
90
|
-
'grok-4.5':
|
|
91
|
-
'grok-4.3':
|
|
92
|
-
'grok-4.20-multi-agent-0309':
|
|
93
|
-
'grok-4.20-0309':
|
|
94
|
-
'grok-4.20-0309-reasoning':
|
|
95
|
-
'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 },
|
|
96
306
|
// Fireworks
|
|
97
|
-
'accounts/fireworks/models/deepseek-v4-flash':
|
|
98
|
-
'accounts/fireworks/models/deepseek-v4-pro':
|
|
99
|
-
'deepseek-ai/DeepSeek-V4-Flash':
|
|
100
|
-
'deepseek-ai/DeepSeek-V4-Pro':
|
|
101
|
-
'deepseek/deepseek-v4-flash':
|
|
102
|
-
'accounts/fireworks/models/glm-4p7':
|
|
103
|
-
'accounts/fireworks/models/glm-5p1':
|
|
104
|
-
'zai-org/GLM-5.2':
|
|
105
|
-
'accounts/fireworks/models/kimi-k2p5':
|
|
106
|
-
'accounts/fireworks/models/qwen3p6-plus':
|
|
107
|
-
'Qwen/Qwen3.6-Plus':
|
|
108
|
-
'accounts/fireworks/models/qwen3p7-plus':
|
|
109
|
-
'qwen/qwen3.7-plus':
|
|
110
|
-
'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 },
|
|
111
321
|
// MiniMax
|
|
112
|
-
'MiniMax-M2.5':
|
|
113
|
-
'MiniMax-M2.7':
|
|
114
|
-
'MiniMax-M3':
|
|
115
|
-
'minimax/minimax-m2.7':
|
|
116
|
-
'minimax/minimax-m3':
|
|
117
|
-
'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 },
|
|
118
328
|
// Perplexity
|
|
119
|
-
'sonar':
|
|
120
|
-
'sonar-pro':
|
|
329
|
+
'sonar': { input: 1.00, output: 1.00 },
|
|
330
|
+
'sonar-pro': { input: 3.00, output: 15.00 },
|
|
121
331
|
// Hermes3 (Lambda/OpenRouter)
|
|
122
|
-
'Hermes-3-Llama-3.1-405B-FP8':
|
|
123
|
-
'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 },
|
|
124
334
|
// Qwen3 (Together/Cerebras)
|
|
125
|
-
'Qwen/Qwen3-235B-A22B-fp8-tput':
|
|
126
|
-
'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 },
|
|
127
337
|
// Kimi K2.5 (Together/Fireworks/OpenRouter)
|
|
128
|
-
'moonshotai/Kimi-K2.5':
|
|
129
|
-
'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 },
|
|
130
340
|
// Kimi K3
|
|
131
|
-
'kimi-k3':
|
|
132
|
-
'moonshotai/kimi-k3':
|
|
341
|
+
'kimi-k3': { input: 3.00, output: 15.00 },
|
|
342
|
+
'moonshotai/kimi-k3': { input: 3.00, output: 15.00 },
|
|
133
343
|
// GLM 4.7 (OpenRouter/Cerebras)
|
|
134
|
-
'z-ai/glm-4.7':
|
|
135
|
-
'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 },
|
|
136
346
|
};
|
|
137
347
|
|
|
138
348
|
class ModelMix {
|
|
@@ -144,6 +354,7 @@ class ModelMix {
|
|
|
144
354
|
this.toolClient = {};
|
|
145
355
|
this.mcp = {};
|
|
146
356
|
this.mcpToolsManager = new MCPToolsManager();
|
|
357
|
+
this.messageTemplates = new WeakMap();
|
|
147
358
|
this.lastRaw = null;
|
|
148
359
|
this.options = {
|
|
149
360
|
max_tokens: 8192,
|
|
@@ -172,6 +383,13 @@ class ModelMix {
|
|
|
172
383
|
roundRobin: false, // false=fallback mode, true=round robin rotation
|
|
173
384
|
...config
|
|
174
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
|
+
}
|
|
175
393
|
// Unified effort is ModelMix policy (config.effort / .effort()), not a native option.
|
|
176
394
|
if (this.config.effort !== undefined && this.config.effort !== null) {
|
|
177
395
|
this.config.effort = normalizeEffort(this.config.effort);
|
|
@@ -184,6 +402,7 @@ class ModelMix {
|
|
|
184
402
|
}
|
|
185
403
|
|
|
186
404
|
replace(keyValues) {
|
|
405
|
+
validateTemplateData(keyValues);
|
|
187
406
|
this.config.replace = { ...this.config.replace, ...keyValues };
|
|
188
407
|
return this;
|
|
189
408
|
}
|
|
@@ -203,11 +422,15 @@ class ModelMix {
|
|
|
203
422
|
}
|
|
204
423
|
|
|
205
424
|
new({ options = {}, config = {}, mix = {} } = {}) {
|
|
425
|
+
const hasSystemOverride = Object.prototype.hasOwnProperty.call(config, 'system');
|
|
206
426
|
const instance = new ModelMix({
|
|
207
427
|
options: { ...this.options, ...options },
|
|
208
428
|
config: { ...this.config, ...config },
|
|
209
429
|
mix: { ...this.mix, ...mix }
|
|
210
430
|
});
|
|
431
|
+
if (!hasSystemOverride) {
|
|
432
|
+
instance.systemTemplate = { ...this.systemTemplate };
|
|
433
|
+
}
|
|
211
434
|
instance.models = this.models; // Share models array for round-robin rotation
|
|
212
435
|
return instance;
|
|
213
436
|
}
|
|
@@ -238,20 +461,163 @@ class ModelMix {
|
|
|
238
461
|
return str.length > maxLen ? str.substring(0, maxLen) + '...' : str;
|
|
239
462
|
}
|
|
240
463
|
|
|
241
|
-
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) {
|
|
242
510
|
const pricing = MODEL_PRICING[modelKey];
|
|
243
|
-
if (!pricing) return
|
|
244
|
-
|
|
245
|
-
|
|
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;
|
|
246
602
|
}
|
|
247
603
|
|
|
248
604
|
static extractCacheTokens(usage = {}) {
|
|
249
605
|
return usage.input_tokens_details?.cached_tokens
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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;
|
|
255
621
|
}
|
|
256
622
|
|
|
257
623
|
static formatInputSummary(messages, system, debug = 2) {
|
|
@@ -385,12 +751,18 @@ class ModelMix {
|
|
|
385
751
|
if (mix.openrouter) this.attach('openai/gpt-oss-120b:free', new MixOpenRouter({ options, config }));
|
|
386
752
|
return this;
|
|
387
753
|
}
|
|
388
|
-
|
|
754
|
+
fable50({ options = {}, config = {} } = {}) {
|
|
389
755
|
return this.attach('claude-fable-5', new MixAnthropic({ options, config }));
|
|
390
756
|
}
|
|
391
|
-
|
|
757
|
+
fable5(args = {}) {
|
|
758
|
+
return this.fable50(args);
|
|
759
|
+
}
|
|
760
|
+
opus50({ options = {}, config = {} } = {}) {
|
|
392
761
|
return this.attach('claude-opus-5', new MixAnthropic({ options, config }));
|
|
393
762
|
}
|
|
763
|
+
opus5(args = {}) {
|
|
764
|
+
return this.opus50(args);
|
|
765
|
+
}
|
|
394
766
|
opus48({ options = {}, config = {} } = {}) {
|
|
395
767
|
return this.attach('claude-opus-4-8', new MixAnthropic({ options, config }));
|
|
396
768
|
}
|
|
@@ -400,9 +772,12 @@ class ModelMix {
|
|
|
400
772
|
opus46({ options = {}, config = {} } = {}) {
|
|
401
773
|
return this.attach('claude-opus-4-6', new MixAnthropic({ options, config }));
|
|
402
774
|
}
|
|
403
|
-
|
|
775
|
+
sonnet50({ options = {}, config = {} } = {}) {
|
|
404
776
|
return this.attach('claude-sonnet-5', new MixAnthropic({ options, config }));
|
|
405
777
|
}
|
|
778
|
+
sonnet5(args = {}) {
|
|
779
|
+
return this.sonnet50(args);
|
|
780
|
+
}
|
|
406
781
|
sonnet46({ options = {}, config = {} } = {}) {
|
|
407
782
|
return this.attach('claude-sonnet-4-6', new MixAnthropic({ options, config }));
|
|
408
783
|
}
|
|
@@ -597,34 +972,54 @@ class ModelMix {
|
|
|
597
972
|
return this;
|
|
598
973
|
}
|
|
599
974
|
|
|
600
|
-
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 } = {}) {
|
|
601
984
|
const content = [{
|
|
602
985
|
type: "text",
|
|
603
|
-
text
|
|
986
|
+
text,
|
|
987
|
+
...(cache !== undefined && { cache })
|
|
604
988
|
}];
|
|
605
989
|
|
|
990
|
+
if (template) {
|
|
991
|
+
this.messageTemplates.set(content[0], template);
|
|
992
|
+
}
|
|
606
993
|
this.messages.push({ role, content });
|
|
607
994
|
return this;
|
|
608
995
|
}
|
|
609
996
|
|
|
610
|
-
addTextFromFile(filePath, { role = "user" } = {}) {
|
|
611
|
-
const
|
|
612
|
-
this.
|
|
613
|
-
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
|
+
});
|
|
614
1005
|
}
|
|
615
1006
|
|
|
616
1007
|
setSystem(text) {
|
|
617
1008
|
this.config.system = text;
|
|
1009
|
+
this.systemTemplate = { source: text, filename: null };
|
|
618
1010
|
return this;
|
|
619
1011
|
}
|
|
620
1012
|
|
|
621
1013
|
setSystemFromFile(filePath) {
|
|
622
|
-
const
|
|
623
|
-
this.
|
|
1014
|
+
const filename = path.resolve(filePath);
|
|
1015
|
+
const content = this.readFile(filename);
|
|
1016
|
+
this.config.system = content;
|
|
1017
|
+
this.systemTemplate = { source: content, filename };
|
|
624
1018
|
return this;
|
|
625
1019
|
}
|
|
626
1020
|
|
|
627
|
-
addImageFromBuffer(buffer, { role = "user" } = {}) {
|
|
1021
|
+
addImageFromBuffer(buffer, { role = "user", cache } = {}) {
|
|
1022
|
+
const contentCache = normalizeContentCache(cache);
|
|
628
1023
|
this.messages.push({
|
|
629
1024
|
role,
|
|
630
1025
|
content: [{
|
|
@@ -632,19 +1027,21 @@ class ModelMix {
|
|
|
632
1027
|
source: {
|
|
633
1028
|
type: "buffer",
|
|
634
1029
|
data: buffer
|
|
635
|
-
}
|
|
1030
|
+
},
|
|
1031
|
+
...(contentCache !== undefined && { cache: contentCache })
|
|
636
1032
|
}]
|
|
637
1033
|
});
|
|
638
1034
|
return this;
|
|
639
1035
|
}
|
|
640
1036
|
|
|
641
|
-
addImage(filePath, { role = "user" } = {}) {
|
|
1037
|
+
addImage(filePath, { role = "user", cache } = {}) {
|
|
642
1038
|
const absolutePath = path.resolve(filePath);
|
|
643
1039
|
|
|
644
1040
|
if (!fs.existsSync(absolutePath)) {
|
|
645
1041
|
throw new Error(`Image file not found: ${filePath}`);
|
|
646
1042
|
}
|
|
647
1043
|
|
|
1044
|
+
const contentCache = normalizeContentCache(cache);
|
|
648
1045
|
this.messages.push({
|
|
649
1046
|
role,
|
|
650
1047
|
content: [{
|
|
@@ -652,13 +1049,14 @@ class ModelMix {
|
|
|
652
1049
|
source: {
|
|
653
1050
|
type: "file",
|
|
654
1051
|
data: filePath
|
|
655
|
-
}
|
|
1052
|
+
},
|
|
1053
|
+
...(contentCache !== undefined && { cache: contentCache })
|
|
656
1054
|
}]
|
|
657
1055
|
});
|
|
658
1056
|
return this;
|
|
659
1057
|
}
|
|
660
1058
|
|
|
661
|
-
addImageFromUrl(url, { role = "user" } = {}) {
|
|
1059
|
+
addImageFromUrl(url, { role = "user", cache } = {}) {
|
|
662
1060
|
let source;
|
|
663
1061
|
if (url.startsWith('data:')) {
|
|
664
1062
|
// Parse data URL: data:image/jpeg;base64,/9j/4AAQ...
|
|
@@ -679,11 +1077,13 @@ class ModelMix {
|
|
|
679
1077
|
};
|
|
680
1078
|
}
|
|
681
1079
|
|
|
1080
|
+
const contentCache = normalizeContentCache(cache);
|
|
682
1081
|
this.messages.push({
|
|
683
1082
|
role,
|
|
684
1083
|
content: [{
|
|
685
1084
|
type: "image",
|
|
686
|
-
source
|
|
1085
|
+
source,
|
|
1086
|
+
...(contentCache !== undefined && { cache: contentCache })
|
|
687
1087
|
}]
|
|
688
1088
|
});
|
|
689
1089
|
|
|
@@ -732,7 +1132,7 @@ class ModelMix {
|
|
|
732
1132
|
|
|
733
1133
|
// Update the content with processed image
|
|
734
1134
|
message.content[j] = {
|
|
735
|
-
|
|
1135
|
+
...content,
|
|
736
1136
|
source: {
|
|
737
1137
|
type: "base64",
|
|
738
1138
|
media_type: mimeType,
|
|
@@ -771,27 +1171,23 @@ class ModelMix {
|
|
|
771
1171
|
stream: false,
|
|
772
1172
|
}
|
|
773
1173
|
|
|
774
|
-
|
|
775
|
-
let
|
|
776
|
-
|
|
777
|
-
let config = {
|
|
778
|
-
system: systemWithReplacements,
|
|
779
|
-
}
|
|
1174
|
+
let config = {};
|
|
1175
|
+
let systemSuffix = '';
|
|
780
1176
|
|
|
781
1177
|
if (schemaExample) {
|
|
782
1178
|
config.schema = generateJsonSchema(schemaExample, schemaDescription);
|
|
783
1179
|
|
|
784
1180
|
if (addSchema) {
|
|
785
|
-
|
|
1181
|
+
systemSuffix += "\n\nOutput JSON Schema: \n```\n" + JSON.stringify(config.schema) + "\n```";
|
|
786
1182
|
}
|
|
787
1183
|
if (addExample) {
|
|
788
|
-
|
|
1184
|
+
systemSuffix += "\n\nOutput JSON Example: \n```\n" + JSON.stringify(schemaExample) + "\n```";
|
|
789
1185
|
}
|
|
790
1186
|
if (addNote) {
|
|
791
|
-
|
|
1187
|
+
systemSuffix += "\n\nOutput JSON Escape: double quotes, backslashes, and control characters inside JSON strings.\nEnsure the output contains no comments.";
|
|
792
1188
|
}
|
|
793
1189
|
}
|
|
794
|
-
const { message } = await this.execute({ options, config });
|
|
1190
|
+
const { message } = await this.execute({ options, config, systemSuffix });
|
|
795
1191
|
const parsed = JSON.parse(this._extractBlock(message));
|
|
796
1192
|
return isArrayWrap ? parsed.out : parsed;
|
|
797
1193
|
}
|
|
@@ -802,17 +1198,13 @@ class ModelMix {
|
|
|
802
1198
|
}
|
|
803
1199
|
|
|
804
1200
|
async block({ addSystemExtra = true } = {}) {
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
if (addSystemExtra) {
|
|
813
|
-
config.system += "\nReturn the result of the task between triple backtick block code tags ```";
|
|
814
|
-
}
|
|
815
|
-
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
|
+
});
|
|
816
1208
|
return this._extractBlock(message);
|
|
817
1209
|
}
|
|
818
1210
|
|
|
@@ -826,22 +1218,52 @@ class ModelMix {
|
|
|
826
1218
|
}
|
|
827
1219
|
|
|
828
1220
|
replaceKeyFromFile(key, filePath) {
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
this.replace({ [key]: this._template(content, this.config.replace) });
|
|
832
|
-
} catch (error) {
|
|
833
|
-
// Gracefully handle file read errors without throwing
|
|
834
|
-
log.warn(`replaceKeyFromFile: ${error.message}`);
|
|
835
|
-
}
|
|
836
|
-
return this;
|
|
1221
|
+
const content = this.readFile(filePath);
|
|
1222
|
+
return this.replace({ [key]: content });
|
|
837
1223
|
}
|
|
838
1224
|
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
1225
|
+
_choiceRandom() {
|
|
1226
|
+
return Math.random();
|
|
1227
|
+
}
|
|
1228
|
+
|
|
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;
|
|
843
1266
|
}
|
|
844
|
-
return input;
|
|
845
1267
|
}
|
|
846
1268
|
|
|
847
1269
|
static hasToolInteraction(message) {
|
|
@@ -873,37 +1295,56 @@ class ModelMix {
|
|
|
873
1295
|
}, []);
|
|
874
1296
|
}
|
|
875
1297
|
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
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 });
|
|
886
1318
|
}
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
});
|
|
1319
|
+
snapshotContent.text = rendered;
|
|
1320
|
+
return snapshotContent;
|
|
1321
|
+
})
|
|
1322
|
+
: message.content
|
|
1323
|
+
}));
|
|
1324
|
+
}
|
|
1325
|
+
|
|
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
|
+
}
|
|
892
1332
|
}
|
|
893
1333
|
|
|
894
|
-
async prepareMessages() {
|
|
1334
|
+
async prepareMessages(renderContext = createTemplateRenderContext(() => this._choiceRandom())) {
|
|
895
1335
|
await this.processImages();
|
|
896
|
-
|
|
1336
|
+
|
|
1337
|
+
let messages = this.messages;
|
|
897
1338
|
|
|
898
1339
|
// Smart message slicing based on max_history:
|
|
899
1340
|
// 0 = no history (stateless), N = keep last N messages, -1 = unlimited
|
|
900
1341
|
if (this.config.max_history > 0) {
|
|
901
|
-
let sliceStart = Math.max(0,
|
|
1342
|
+
let sliceStart = Math.max(0, messages.length - this.config.max_history);
|
|
902
1343
|
|
|
903
1344
|
// If we're slicing into the middle of a tool interaction,
|
|
904
1345
|
// backtrack to include the full sequence (user → assistant/tool_calls → tool results)
|
|
905
|
-
while (sliceStart > 0 && sliceStart <
|
|
906
|
-
const msg =
|
|
1346
|
+
while (sliceStart > 0 && sliceStart < messages.length) {
|
|
1347
|
+
const msg = messages[sliceStart];
|
|
907
1348
|
if (ModelMix.hasToolInteraction(msg)) {
|
|
908
1349
|
sliceStart--;
|
|
909
1350
|
} else {
|
|
@@ -911,13 +1352,13 @@ class ModelMix {
|
|
|
911
1352
|
}
|
|
912
1353
|
}
|
|
913
1354
|
|
|
914
|
-
this.messages =
|
|
1355
|
+
this.messages = messages.slice(sliceStart);
|
|
1356
|
+
messages = this.messages;
|
|
915
1357
|
}
|
|
916
1358
|
// max_history = -1: unlimited, no slicing
|
|
917
1359
|
// max_history = 0: no history, messages only contain what was added since last call
|
|
918
1360
|
|
|
919
|
-
|
|
920
|
-
this.options.messages = this.messages;
|
|
1361
|
+
return this.groupByRoles(this._renderMessageSnapshot(messages, renderContext));
|
|
921
1362
|
}
|
|
922
1363
|
|
|
923
1364
|
readFile(filePath, { encoding = 'utf8' } = {}) {
|
|
@@ -935,15 +1376,30 @@ class ModelMix {
|
|
|
935
1376
|
}
|
|
936
1377
|
}
|
|
937
1378
|
|
|
938
|
-
|
|
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 } = {}) {
|
|
939
1393
|
if (!this.models || this.models.length === 0) {
|
|
940
1394
|
throw new Error("No models specified. Use methods like .gpt5(), .sonnet46() first.");
|
|
941
1395
|
}
|
|
942
1396
|
|
|
943
|
-
|
|
944
|
-
|
|
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);
|
|
945
1401
|
|
|
946
|
-
if (
|
|
1402
|
+
if (preparedMessages.length === 0) {
|
|
947
1403
|
throw new Error("No user messages have been added. Use addText(prompt), addTextFromFile(filePath), addImage(filePath), or addImageFromUrl(url) to add a prompt.");
|
|
948
1404
|
}
|
|
949
1405
|
|
|
@@ -978,6 +1434,7 @@ class ModelMix {
|
|
|
978
1434
|
// Create clean copies for each provider to avoid contamination
|
|
979
1435
|
const currentOptions = {
|
|
980
1436
|
...this.options,
|
|
1437
|
+
messages: preparedMessages,
|
|
981
1438
|
...providerInstance.options,
|
|
982
1439
|
...optionsTools,
|
|
983
1440
|
...options,
|
|
@@ -994,6 +1451,18 @@ class ModelMix {
|
|
|
994
1451
|
...(config.retry || {})
|
|
995
1452
|
}
|
|
996
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;
|
|
997
1466
|
|
|
998
1467
|
// Grok 4.20 alias → reasoning / non-reasoning from unified effort
|
|
999
1468
|
const resolvedModelKey = resolveGrok420ModelKey(
|
|
@@ -1018,7 +1487,7 @@ class ModelMix {
|
|
|
1018
1487
|
const header = `\n${prefix} [${providerName}:${resolvedModelKey}] #${originalIndex + 1}${suffix}`;
|
|
1019
1488
|
|
|
1020
1489
|
if (currentConfig.debug >= 2) {
|
|
1021
|
-
console.log(`${header}\n${ModelMix.formatInputSummary(
|
|
1490
|
+
console.log(`${header}\n${ModelMix.formatInputSummary(preparedMessages, currentConfig.system, currentConfig.debug)}`);
|
|
1022
1491
|
} else {
|
|
1023
1492
|
console.log(header);
|
|
1024
1493
|
}
|
|
@@ -1072,7 +1541,16 @@ class ModelMix {
|
|
|
1072
1541
|
const elapsedMs = Date.now() - startTime;
|
|
1073
1542
|
|
|
1074
1543
|
if (result.tokens) {
|
|
1075
|
-
|
|
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
|
+
};
|
|
1076
1554
|
const elapsedSec = elapsedMs / 1000;
|
|
1077
1555
|
result.tokens.speed = elapsedSec > 0 ? Math.round(result.tokens.output / elapsedSec) : 0;
|
|
1078
1556
|
}
|
|
@@ -1091,7 +1569,7 @@ class ModelMix {
|
|
|
1091
1569
|
}]
|
|
1092
1570
|
});
|
|
1093
1571
|
} else {
|
|
1094
|
-
this.
|
|
1572
|
+
this._addText(result.message, { role: "assistant" });
|
|
1095
1573
|
}
|
|
1096
1574
|
}
|
|
1097
1575
|
|
|
@@ -1109,7 +1587,7 @@ class ModelMix {
|
|
|
1109
1587
|
});
|
|
1110
1588
|
}
|
|
1111
1589
|
|
|
1112
|
-
return this.execute({ options, config });
|
|
1590
|
+
return this.execute({ options, config, systemSuffix, _templateContext: templateContext });
|
|
1113
1591
|
}
|
|
1114
1592
|
|
|
1115
1593
|
// debug level 1: Just success indicator
|
|
@@ -1171,7 +1649,7 @@ class ModelMix {
|
|
|
1171
1649
|
}]
|
|
1172
1650
|
});
|
|
1173
1651
|
} else {
|
|
1174
|
-
this.
|
|
1652
|
+
this._addText(result.message, { role: "assistant" });
|
|
1175
1653
|
}
|
|
1176
1654
|
}
|
|
1177
1655
|
|
|
@@ -1197,6 +1675,12 @@ class ModelMix {
|
|
|
1197
1675
|
log.error("Fallback logic completed without success or throwing the final error.");
|
|
1198
1676
|
throw lastError || new Error("Failed to get response from any model, and no specific error was caught.");
|
|
1199
1677
|
});
|
|
1678
|
+
|
|
1679
|
+
if (!isRootExecution) return execution;
|
|
1680
|
+
|
|
1681
|
+
const result = await execution;
|
|
1682
|
+
this._commitTemplateRenderContext(templateContext);
|
|
1683
|
+
return result;
|
|
1200
1684
|
}
|
|
1201
1685
|
|
|
1202
1686
|
async processToolCalls(toolCalls) {
|
|
@@ -1397,6 +1881,13 @@ class MixCustom {
|
|
|
1397
1881
|
return MixOpenAI.convertMessages(messages, config);
|
|
1398
1882
|
}
|
|
1399
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
|
+
|
|
1400
1891
|
static stripContentTypeHeader(headers = {}) {
|
|
1401
1892
|
return stripContentTypeHeader(headers);
|
|
1402
1893
|
}
|
|
@@ -1411,6 +1902,7 @@ class MixCustom {
|
|
|
1411
1902
|
|
|
1412
1903
|
async create({ config = {}, options = {} } = {}) {
|
|
1413
1904
|
try {
|
|
1905
|
+
this.sanitizeCacheOptions(options);
|
|
1414
1906
|
if (Array.isArray(options.messages)) {
|
|
1415
1907
|
options.messages = this.convertMessages(options.messages, config);
|
|
1416
1908
|
}
|
|
@@ -1422,9 +1914,7 @@ class MixCustom {
|
|
|
1422
1914
|
console.log('\n[REQUEST DETAILS]');
|
|
1423
1915
|
|
|
1424
1916
|
console.log('\n[CONFIG]');
|
|
1425
|
-
|
|
1426
|
-
delete configToLog.debug;
|
|
1427
|
-
console.log(ModelMix.formatJSON(configToLog));
|
|
1917
|
+
console.log(ModelMix.formatJSON(configForDebug(config)));
|
|
1428
1918
|
|
|
1429
1919
|
console.log('\n[OPTIONS]');
|
|
1430
1920
|
console.log(ModelMix.formatJSON(request.options));
|
|
@@ -1444,11 +1934,11 @@ class MixCustom {
|
|
|
1444
1934
|
}));
|
|
1445
1935
|
}
|
|
1446
1936
|
} catch (error) {
|
|
1447
|
-
throw this.handleError(error
|
|
1937
|
+
throw this.handleError(error);
|
|
1448
1938
|
}
|
|
1449
1939
|
}
|
|
1450
1940
|
|
|
1451
|
-
handleError(error
|
|
1941
|
+
handleError(error) {
|
|
1452
1942
|
let errorMessage = 'An error occurred in MixCustom';
|
|
1453
1943
|
let statusCode = null;
|
|
1454
1944
|
let errorDetails = null;
|
|
@@ -1462,12 +1952,10 @@ class MixCustom {
|
|
|
1462
1952
|
}
|
|
1463
1953
|
|
|
1464
1954
|
const formattedError = {
|
|
1465
|
-
message: errorMessage,
|
|
1955
|
+
message: redactSecret(errorMessage, this.config.apiKey),
|
|
1466
1956
|
statusCode,
|
|
1467
|
-
details: errorDetails,
|
|
1468
|
-
stack: error.stack,
|
|
1469
|
-
config: config,
|
|
1470
|
-
options: options
|
|
1957
|
+
details: redactSecret(errorDetails, this.config.apiKey),
|
|
1958
|
+
stack: redactSecret(error.stack, this.config.apiKey)
|
|
1471
1959
|
};
|
|
1472
1960
|
|
|
1473
1961
|
return formattedError;
|
|
@@ -1586,19 +2074,15 @@ class MixCustom {
|
|
|
1586
2074
|
static extractTokens(data) {
|
|
1587
2075
|
// OpenAI/Groq/Together/Lambda/Cerebras/Fireworks format
|
|
1588
2076
|
if (data.usage) {
|
|
1589
|
-
return {
|
|
2077
|
+
return ModelMix.normalizeTokenUsage({
|
|
1590
2078
|
input: data.usage.prompt_tokens || 0,
|
|
1591
2079
|
output: data.usage.completion_tokens || 0,
|
|
1592
|
-
total: data.usage.total_tokens
|
|
1593
|
-
cached: ModelMix.extractCacheTokens(data.usage)
|
|
1594
|
-
|
|
2080
|
+
total: data.usage.total_tokens,
|
|
2081
|
+
cached: ModelMix.extractCacheTokens(data.usage),
|
|
2082
|
+
cacheWrite: ModelMix.extractCacheWriteTokens(data.usage)
|
|
2083
|
+
});
|
|
1595
2084
|
}
|
|
1596
|
-
return
|
|
1597
|
-
input: 0,
|
|
1598
|
-
output: 0,
|
|
1599
|
-
total: 0,
|
|
1600
|
-
cached: 0
|
|
1601
|
-
};
|
|
2085
|
+
return ModelMix.normalizeTokenUsage();
|
|
1602
2086
|
}
|
|
1603
2087
|
|
|
1604
2088
|
processResponse(response) {
|
|
@@ -1617,6 +2101,11 @@ class MixCustom {
|
|
|
1617
2101
|
}
|
|
1618
2102
|
|
|
1619
2103
|
class MixOpenAI extends MixCustom {
|
|
2104
|
+
sanitizeCacheOptions(options) {
|
|
2105
|
+
delete options.cache_control;
|
|
2106
|
+
delete options.prompt_cache_options;
|
|
2107
|
+
}
|
|
2108
|
+
|
|
1620
2109
|
getDefaultConfig(customConfig) {
|
|
1621
2110
|
|
|
1622
2111
|
if (!process.env.OPENAI_API_KEY) {
|
|
@@ -1690,22 +2179,26 @@ class MixOpenAI extends MixCustom {
|
|
|
1690
2179
|
continue;
|
|
1691
2180
|
}
|
|
1692
2181
|
|
|
2182
|
+
let convertedMessage = { ...message };
|
|
1693
2183
|
if (Array.isArray(message.content)) {
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
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
|
+
};
|
|
1706
2199
|
}
|
|
1707
2200
|
|
|
1708
|
-
results.push(
|
|
2201
|
+
results.push(convertedMessage);
|
|
1709
2202
|
}
|
|
1710
2203
|
|
|
1711
2204
|
return results;
|
|
@@ -1765,10 +2258,14 @@ class MixOpenAIResponses extends MixOpenAI {
|
|
|
1765
2258
|
}
|
|
1766
2259
|
|
|
1767
2260
|
static buildResponsesRequest(options = {}, config = {}) {
|
|
1768
|
-
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
|
+
});
|
|
1769
2265
|
if (config.system) {
|
|
1770
2266
|
input.unshift({ role: 'developer', content: [{ type: 'input_text', text: config.system }] });
|
|
1771
2267
|
}
|
|
2268
|
+
MixOpenAIResponses.validatePromptCaching(options, input);
|
|
1772
2269
|
const request = {
|
|
1773
2270
|
model: options.model,
|
|
1774
2271
|
input,
|
|
@@ -1812,10 +2309,52 @@ class MixOpenAIResponses extends MixOpenAI {
|
|
|
1812
2309
|
if (options.user !== undefined) request.user = options.user;
|
|
1813
2310
|
if (options.prompt_cache_key !== undefined) request.prompt_cache_key = options.prompt_cache_key;
|
|
1814
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;
|
|
1815
2313
|
|
|
1816
2314
|
return request;
|
|
1817
2315
|
}
|
|
1818
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
|
+
|
|
1819
2358
|
static processResponsesResponse(response) {
|
|
1820
2359
|
const message = MixOpenAIResponses.extractResponsesMessage(response.data);
|
|
1821
2360
|
return {
|
|
@@ -1829,19 +2368,15 @@ class MixOpenAIResponses extends MixOpenAI {
|
|
|
1829
2368
|
|
|
1830
2369
|
static extractResponsesTokens(data) {
|
|
1831
2370
|
if (data.usage) {
|
|
1832
|
-
return {
|
|
2371
|
+
return ModelMix.normalizeTokenUsage({
|
|
1833
2372
|
input: data.usage.input_tokens || 0,
|
|
1834
2373
|
output: data.usage.output_tokens || 0,
|
|
1835
|
-
total: data.usage.total_tokens
|
|
1836
|
-
cached: ModelMix.extractCacheTokens(data.usage)
|
|
1837
|
-
|
|
2374
|
+
total: data.usage.total_tokens,
|
|
2375
|
+
cached: ModelMix.extractCacheTokens(data.usage),
|
|
2376
|
+
cacheWrite: ModelMix.extractCacheWriteTokens(data.usage)
|
|
2377
|
+
});
|
|
1838
2378
|
}
|
|
1839
|
-
return
|
|
1840
|
-
input: 0,
|
|
1841
|
-
output: 0,
|
|
1842
|
-
total: 0,
|
|
1843
|
-
cached: 0
|
|
1844
|
-
};
|
|
2379
|
+
return ModelMix.normalizeTokenUsage();
|
|
1845
2380
|
}
|
|
1846
2381
|
|
|
1847
2382
|
static extractResponsesMessage(data) {
|
|
@@ -1855,27 +2390,70 @@ class MixOpenAIResponses extends MixOpenAI {
|
|
|
1855
2390
|
.trim();
|
|
1856
2391
|
}
|
|
1857
2392
|
|
|
1858
|
-
static messagesToResponsesInput(messages = []) {
|
|
2393
|
+
static messagesToResponsesInput(messages = [], { translateNeutralCache = false } = {}) {
|
|
1859
2394
|
const mapped = [];
|
|
1860
2395
|
|
|
1861
2396
|
for (const message of messages) {
|
|
1862
2397
|
if (!message || !message.role) continue;
|
|
1863
2398
|
if (message.tool_calls || message.role === 'tool') continue;
|
|
1864
2399
|
|
|
1865
|
-
|
|
2400
|
+
const content = [];
|
|
2401
|
+
const isAssistant = message.role === 'assistant';
|
|
2402
|
+
const textType = isAssistant ? 'output_text' : 'input_text';
|
|
1866
2403
|
if (typeof message.content === 'string') {
|
|
1867
|
-
text
|
|
2404
|
+
if (message.content) content.push({ type: textType, text: message.content });
|
|
1868
2405
|
} else if (Array.isArray(message.content)) {
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
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
|
+
}
|
|
1873
2451
|
}
|
|
1874
2452
|
|
|
1875
|
-
if (
|
|
2453
|
+
if (content.length === 0) continue;
|
|
1876
2454
|
mapped.push({
|
|
1877
2455
|
role: message.role,
|
|
1878
|
-
content
|
|
2456
|
+
content
|
|
1879
2457
|
});
|
|
1880
2458
|
}
|
|
1881
2459
|
|
|
@@ -1928,9 +2506,7 @@ class MixOpenAIWebSocket extends MixOpenAIResponses {
|
|
|
1928
2506
|
reject({
|
|
1929
2507
|
message: `Realtime WebSocket timed out after ${timeoutMs}ms`,
|
|
1930
2508
|
statusCode: null,
|
|
1931
|
-
details: null
|
|
1932
|
-
config: mergedConfig,
|
|
1933
|
-
options
|
|
2509
|
+
details: null
|
|
1934
2510
|
});
|
|
1935
2511
|
}, timeoutMs);
|
|
1936
2512
|
|
|
@@ -2024,9 +2600,7 @@ class MixOpenAIWebSocket extends MixOpenAIResponses {
|
|
|
2024
2600
|
reject({
|
|
2025
2601
|
message: event.error?.message || 'Realtime WebSocket error',
|
|
2026
2602
|
statusCode: null,
|
|
2027
|
-
details: event.error || event
|
|
2028
|
-
config: mergedConfig,
|
|
2029
|
-
options
|
|
2603
|
+
details: event.error || event
|
|
2030
2604
|
});
|
|
2031
2605
|
}
|
|
2032
2606
|
});
|
|
@@ -2039,9 +2613,7 @@ class MixOpenAIWebSocket extends MixOpenAIResponses {
|
|
|
2039
2613
|
message: error.message || 'Realtime WebSocket connection error',
|
|
2040
2614
|
statusCode: null,
|
|
2041
2615
|
details: null,
|
|
2042
|
-
stack: error.stack
|
|
2043
|
-
config: mergedConfig,
|
|
2044
|
-
options
|
|
2616
|
+
stack: error.stack
|
|
2045
2617
|
});
|
|
2046
2618
|
});
|
|
2047
2619
|
|
|
@@ -2052,9 +2624,7 @@ class MixOpenAIWebSocket extends MixOpenAIResponses {
|
|
|
2052
2624
|
reject({
|
|
2053
2625
|
message: 'Realtime WebSocket closed before response.done',
|
|
2054
2626
|
statusCode: null,
|
|
2055
|
-
details: null
|
|
2056
|
-
config: mergedConfig,
|
|
2057
|
-
options
|
|
2627
|
+
details: null
|
|
2058
2628
|
});
|
|
2059
2629
|
});
|
|
2060
2630
|
});
|
|
@@ -2155,6 +2725,23 @@ class MixKimi extends MixOpenAI {
|
|
|
2155
2725
|
|
|
2156
2726
|
class MixAnthropic extends MixCustom {
|
|
2157
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
|
+
|
|
2158
2745
|
/**
|
|
2159
2746
|
* Opus 4.7+ and Claude 5 family reject sampling params (temperature/top_p/top_k).
|
|
2160
2747
|
* See: https://platform.claude.com/docs/en/about-claude/models/migration-guide
|
|
@@ -2200,10 +2787,20 @@ class MixAnthropic extends MixCustom {
|
|
|
2200
2787
|
delete options.top_k;
|
|
2201
2788
|
}
|
|
2202
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
|
+
|
|
2203
2800
|
options.system = config.system;
|
|
2204
2801
|
|
|
2205
2802
|
try {
|
|
2206
|
-
return await super.create({ config, options });
|
|
2803
|
+
return await super.create({ config: requestConfig, options });
|
|
2207
2804
|
} catch (error) {
|
|
2208
2805
|
// Log the error details for debugging
|
|
2209
2806
|
if (error.response && error.response.data) {
|
|
@@ -2275,20 +2872,37 @@ class MixAnthropic extends MixCustom {
|
|
|
2275
2872
|
|
|
2276
2873
|
// Handle content conversion for other messages
|
|
2277
2874
|
if (message.content && Array.isArray(message.content)) {
|
|
2278
|
-
|
|
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;
|
|
2279
2883
|
if (content && content.type === 'function') {
|
|
2280
|
-
|
|
2884
|
+
converted = {
|
|
2281
2885
|
type: 'tool_use',
|
|
2282
2886
|
id: content.id,
|
|
2283
2887
|
name: content.function.name,
|
|
2284
2888
|
input: JSON.parse(content.function.arguments)
|
|
2285
|
-
}
|
|
2889
|
+
};
|
|
2286
2890
|
}
|
|
2287
|
-
|
|
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;
|
|
2288
2901
|
});
|
|
2902
|
+
return { ...message, content };
|
|
2289
2903
|
}
|
|
2290
2904
|
|
|
2291
|
-
return message;
|
|
2905
|
+
return { ...message };
|
|
2292
2906
|
});
|
|
2293
2907
|
}
|
|
2294
2908
|
|
|
@@ -2370,19 +2984,26 @@ class MixAnthropic extends MixCustom {
|
|
|
2370
2984
|
static extractTokens(data) {
|
|
2371
2985
|
// Anthropic format
|
|
2372
2986
|
if (data.usage) {
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
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
|
+
});
|
|
2379
3005
|
}
|
|
2380
|
-
return
|
|
2381
|
-
input: 0,
|
|
2382
|
-
output: 0,
|
|
2383
|
-
total: 0,
|
|
2384
|
-
cached: 0
|
|
2385
|
-
};
|
|
3006
|
+
return ModelMix.normalizeTokenUsage();
|
|
2386
3007
|
}
|
|
2387
3008
|
|
|
2388
3009
|
processResponse(response) {
|
|
@@ -2566,6 +3187,13 @@ class MixGrok extends MixOpenAI {
|
|
|
2566
3187
|
...customConfig
|
|
2567
3188
|
});
|
|
2568
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
|
+
}
|
|
2569
3197
|
}
|
|
2570
3198
|
|
|
2571
3199
|
class MixLambda extends MixCustom {
|
|
@@ -2740,6 +3368,7 @@ class MixGoogle extends MixCustom {
|
|
|
2740
3368
|
return super.getDefaultConfig({
|
|
2741
3369
|
url: 'https://generativelanguage.googleapis.com/v1beta/models',
|
|
2742
3370
|
apiKey: process.env.GEMINI_API_KEY,
|
|
3371
|
+
...customConfig
|
|
2743
3372
|
});
|
|
2744
3373
|
}
|
|
2745
3374
|
|
|
@@ -2909,9 +3538,7 @@ class MixGoogle extends MixCustom {
|
|
|
2909
3538
|
console.log('\n[REQUEST DETAILS - GOOGLE]');
|
|
2910
3539
|
|
|
2911
3540
|
console.log('\n[CONFIG]');
|
|
2912
|
-
|
|
2913
|
-
delete configToLog.debug;
|
|
2914
|
-
console.log(ModelMix.formatJSON(configToLog));
|
|
3541
|
+
console.log(ModelMix.formatJSON(configForDebug(config)));
|
|
2915
3542
|
|
|
2916
3543
|
console.log('\n[PAYLOAD]');
|
|
2917
3544
|
console.log(ModelMix.formatJSON(payload));
|
|
@@ -2927,7 +3554,7 @@ class MixGoogle extends MixCustom {
|
|
|
2927
3554
|
}));
|
|
2928
3555
|
}
|
|
2929
3556
|
} catch (error) {
|
|
2930
|
-
throw this.handleError(error
|
|
3557
|
+
throw this.handleError(error);
|
|
2931
3558
|
}
|
|
2932
3559
|
}
|
|
2933
3560
|
|
|
@@ -2965,19 +3592,15 @@ class MixGoogle extends MixCustom {
|
|
|
2965
3592
|
static extractTokens(data) {
|
|
2966
3593
|
// Google Gemini format
|
|
2967
3594
|
if (data.usageMetadata) {
|
|
2968
|
-
return {
|
|
3595
|
+
return ModelMix.normalizeTokenUsage({
|
|
2969
3596
|
input: data.usageMetadata.promptTokenCount || 0,
|
|
2970
3597
|
output: data.usageMetadata.candidatesTokenCount || 0,
|
|
2971
|
-
total: data.usageMetadata.totalTokenCount
|
|
2972
|
-
cached: ModelMix.extractCacheTokens(data.usageMetadata)
|
|
2973
|
-
|
|
3598
|
+
total: data.usageMetadata.totalTokenCount,
|
|
3599
|
+
cached: ModelMix.extractCacheTokens(data.usageMetadata),
|
|
3600
|
+
cacheWrite: ModelMix.extractCacheWriteTokens(data.usageMetadata)
|
|
3601
|
+
});
|
|
2974
3602
|
}
|
|
2975
|
-
return
|
|
2976
|
-
input: 0,
|
|
2977
|
-
output: 0,
|
|
2978
|
-
total: 0,
|
|
2979
|
-
cached: 0
|
|
2980
|
-
};
|
|
3603
|
+
return ModelMix.normalizeTokenUsage();
|
|
2981
3604
|
}
|
|
2982
3605
|
|
|
2983
3606
|
static stripUnsupportedSchemaProps(schema) {
|
|
@@ -3024,4 +3647,4 @@ class MixGoogle extends MixCustom {
|
|
|
3024
3647
|
}
|
|
3025
3648
|
}
|
|
3026
3649
|
|
|
3027
|
-
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 };
|