modelmix 4.7.4 → 5.0.1
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/AGENTS.md +43 -0
- package/README.md +283 -92
- package/demo/demo.js +4 -4
- package/demo/prompt.md +2 -2
- package/http-client.js +15 -2
- package/index.d.ts +102 -12
- package/index.js +965 -275
- package/package.json +4 -1
- package/skills/modelmix/SKILL.md +109 -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/dynamic-include.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/fixtures/tree.ejs +6 -0
- 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 +621 -280
- 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,319 @@ 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 validateTemplateDataKey(key) {
|
|
83
|
+
if (typeof key !== 'string' || key.length === 0) {
|
|
84
|
+
throw new TypeError('Template data key must be a non-empty string.');
|
|
85
|
+
}
|
|
86
|
+
if (key === '$mix') {
|
|
87
|
+
throw new TypeError('Template data key "$mix" is reserved.');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function templateLocation({ filename, label }, lineNumber) {
|
|
92
|
+
return `${filename || label} at line ${lineNumber}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function preprocessChoiceDirectives(source, { filename = null, label = 'template' } = {}) {
|
|
96
|
+
const parts = source.split(/(\r\n|\n|\r)/);
|
|
97
|
+
const blocks = [];
|
|
98
|
+
|
|
99
|
+
for (let index = 0; index < parts.length; index += 2) {
|
|
100
|
+
const line = parts[index];
|
|
101
|
+
const trimmed = line.trim();
|
|
102
|
+
const lineNumber = (index / 2) + 1;
|
|
103
|
+
const location = templateLocation({ filename, label }, lineNumber);
|
|
104
|
+
const newline = parts[index + 1] || '';
|
|
105
|
+
|
|
106
|
+
if (/^<%\s*choice\s*%>$/.test(trimmed)) {
|
|
107
|
+
const parent = blocks[blocks.length - 1];
|
|
108
|
+
if (parent && parent.optionCount === 0) {
|
|
109
|
+
throw new Error(`A nested choice must be inside an option (${location}).`);
|
|
110
|
+
}
|
|
111
|
+
blocks.push({ lineNumber, optionCount: 0, weighted: null });
|
|
112
|
+
parts[index] = '<% $mix.choice(option => { -%>';
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const optionMatch = trimmed.match(/^<%\s*option(?:\s+(.+?))?\s*%>$/);
|
|
117
|
+
if (optionMatch) {
|
|
118
|
+
const block = blocks[blocks.length - 1];
|
|
119
|
+
if (!block) {
|
|
120
|
+
throw new Error(`Option directive must be inside a choice (${location}).`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const weightText = optionMatch[1];
|
|
124
|
+
const weighted = weightText !== undefined;
|
|
125
|
+
if (block.weighted !== null && block.weighted !== weighted) {
|
|
126
|
+
throw new Error(`Choice options must either all have weights or all omit them (${location}).`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let argument = '';
|
|
130
|
+
if (weighted) {
|
|
131
|
+
const weight = Number(weightText);
|
|
132
|
+
if (!Number.isFinite(weight) || weight <= 0) {
|
|
133
|
+
throw new Error(`Choice weight must be a positive finite number (${location}).`);
|
|
134
|
+
}
|
|
135
|
+
argument = `${weight}, `;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
block.weighted = weighted;
|
|
139
|
+
parts[index] = `<% ${block.optionCount > 0 ? '}); ' : ''}option(${argument}() => { -%>`;
|
|
140
|
+
block.optionCount += 1;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (/^<%\s*\/choice\s*%>$/.test(trimmed)) {
|
|
145
|
+
const block = blocks.pop();
|
|
146
|
+
if (!block) {
|
|
147
|
+
throw new Error(`Closing choice directive has no matching opening directive (${location}).`);
|
|
148
|
+
}
|
|
149
|
+
if (block.optionCount === 0) {
|
|
150
|
+
throw new Error(`Choice must contain at least one option (${location}).`);
|
|
151
|
+
}
|
|
152
|
+
parts[index] = '<% }); }); -%>';
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (/^<%\s*(?:choice|option|\/choice)(?:\s|%>)/.test(trimmed)) {
|
|
157
|
+
throw new Error(`Invalid choice directive (${location}).`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const block = blocks[blocks.length - 1];
|
|
161
|
+
if (block && block.optionCount === 0) {
|
|
162
|
+
if (trimmed) {
|
|
163
|
+
throw new Error(`Choice content must be inside an option (${location}).`);
|
|
164
|
+
}
|
|
165
|
+
parts[index] = '<%# -%>';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (newline) parts[index + 1] = newline;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (blocks.length > 0) {
|
|
172
|
+
const block = blocks[blocks.length - 1];
|
|
173
|
+
throw new Error(`Unclosed choice directive (${templateLocation({ filename, label }, block.lineNumber)}).`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return parts.join('');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function createTemplateRenderContext(random = Math.random) {
|
|
180
|
+
const choice = defineOptions => {
|
|
181
|
+
if (typeof defineOptions !== 'function') {
|
|
182
|
+
throw new TypeError('$mix.choice expects an option definition callback.');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const options = [];
|
|
186
|
+
let weighted = null;
|
|
187
|
+
const option = (weightOrRender, renderOption) => {
|
|
188
|
+
const hasWeight = renderOption !== undefined;
|
|
189
|
+
const weight = hasWeight ? weightOrRender : 1;
|
|
190
|
+
const render = hasWeight ? renderOption : weightOrRender;
|
|
191
|
+
|
|
192
|
+
if (weighted !== null && weighted !== hasWeight) {
|
|
193
|
+
throw new TypeError('$mix.choice options cannot mix weighted and unweighted forms.');
|
|
194
|
+
}
|
|
195
|
+
if (!Number.isFinite(weight) || weight <= 0) {
|
|
196
|
+
throw new TypeError('$mix.choice weights must be positive finite numbers.');
|
|
197
|
+
}
|
|
198
|
+
if (typeof render !== 'function') {
|
|
199
|
+
throw new TypeError('$mix.choice options require a render callback.');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
weighted = hasWeight;
|
|
203
|
+
options.push({ weight, render });
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
defineOptions(option);
|
|
207
|
+
if (options.length === 0) {
|
|
208
|
+
throw new Error('$mix.choice requires at least one option.');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const totalWeight = options.reduce((sum, current) => sum + current.weight, 0);
|
|
212
|
+
if (!Number.isFinite(totalWeight)) {
|
|
213
|
+
throw new TypeError('$mix.choice total weight must be finite.');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let target = random() * totalWeight;
|
|
217
|
+
for (const current of options) {
|
|
218
|
+
target -= current.weight;
|
|
219
|
+
if (target < 0) return current.render();
|
|
220
|
+
}
|
|
221
|
+
return options[options.length - 1].render();
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
helpers: Object.freeze({ choice }),
|
|
226
|
+
renderedTemplateData: new Map(),
|
|
227
|
+
renderedMessages: new Map(),
|
|
228
|
+
renderedSystems: new Map()
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function configForDebug(config) {
|
|
233
|
+
const safeConfig = { ...config };
|
|
234
|
+
delete safeConfig.apiKey;
|
|
235
|
+
delete safeConfig.debug;
|
|
236
|
+
return safeConfig;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function redactSecret(value, secret, seen = new WeakSet()) {
|
|
240
|
+
if (!secret) return value;
|
|
241
|
+
if (typeof value === 'string') return value.split(secret).join('[REDACTED]');
|
|
242
|
+
if (!value || typeof value !== 'object') return value;
|
|
243
|
+
if (seen.has(value)) return '[Circular]';
|
|
244
|
+
|
|
245
|
+
seen.add(value);
|
|
246
|
+
if (Array.isArray(value)) {
|
|
247
|
+
return value.map(item => redactSecret(item, secret, seen));
|
|
248
|
+
}
|
|
249
|
+
return Object.fromEntries(
|
|
250
|
+
Object.entries(value).map(([key, item]) => [key, redactSecret(item, secret, seen)])
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Pricing per 1M tokens in USD
|
|
41
255
|
// Based on provider pricing pages linked in README
|
|
256
|
+
const GPT56_LONG_CONTEXT_PRICING = Object.freeze({
|
|
257
|
+
inputThreshold: 272_000,
|
|
258
|
+
inputMultiplier: 2,
|
|
259
|
+
outputMultiplier: 1.5
|
|
260
|
+
});
|
|
261
|
+
|
|
42
262
|
const MODEL_PRICING = {
|
|
43
263
|
// 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':
|
|
264
|
+
'gpt-realtime-mini': { input: 0.60, cachedInput: 0.06, output: 2.40 },
|
|
265
|
+
'gpt-realtime': { input: 4.00, cachedInput: 0.40, output: 16.00 },
|
|
266
|
+
'gpt-5.6-sol': { input: 5.00, cachedInput: 0.50, cacheWrite: 6.25, output: 30.00, longContext: GPT56_LONG_CONTEXT_PRICING },
|
|
267
|
+
'gpt-5.6-terra': { input: 2.00, cachedInput: 0.20, cacheWrite: 2.50, output: 12.00, longContext: GPT56_LONG_CONTEXT_PRICING },
|
|
268
|
+
'gpt-5.6-luna': { input: 0.20, cachedInput: 0.02, cacheWrite: 0.25, output: 1.20, longContext: GPT56_LONG_CONTEXT_PRICING },
|
|
269
|
+
'gpt-5.5-pro': { input: 30.00, output: 180.00 },
|
|
270
|
+
'gpt-5.5': { input: 5.00, cachedInput: 0.50, output: 30.00 },
|
|
271
|
+
'gpt-5.4': { input: 2.50, cachedInput: 0.25, output: 15.00 },
|
|
272
|
+
'gpt-5.4-pro': { input: 30.00, output: 180.00 },
|
|
273
|
+
'gpt-5.4-mini': { input: 0.75, cachedInput: 0.075, output: 4.50 },
|
|
274
|
+
'gpt-5.4-nano': { input: 0.20, cachedInput: 0.02, output: 1.25 },
|
|
275
|
+
'gpt-5.3-codex': { input: 1.75, cachedInput: 0.175, output: 14.00 },
|
|
276
|
+
'gpt-5.2': { input: 1.75, cachedInput: 0.175, output: 14.00 },
|
|
277
|
+
'gpt-5.2-chat-latest': { input: 1.75, cachedInput: 0.175, output: 14.00 },
|
|
278
|
+
'gpt-5.1': { input: 1.25, cachedInput: 0.125, output: 10.00 },
|
|
279
|
+
'gpt-5': { input: 1.25, cachedInput: 0.125, output: 10.00 },
|
|
280
|
+
'gpt-5-mini': { input: 0.25, cachedInput: 0.025, output: 2.00 },
|
|
281
|
+
'gpt-5-nano': { input: 0.05, cachedInput: 0.005, output: 0.40 },
|
|
282
|
+
'gpt-4.1': { input: 2.00, cachedInput: 0.50, output: 8.00 },
|
|
283
|
+
'gpt-4.1-mini': { input: 0.40, cachedInput: 0.10, output: 1.60 },
|
|
284
|
+
'gpt-4.1-nano': { input: 0.10, cachedInput: 0.025, output: 0.40 },
|
|
65
285
|
// gptOss (Together/Groq/Cerebras/OpenRouter)
|
|
66
|
-
'openai/gpt-oss-120b':
|
|
67
|
-
'gpt-oss-120b':
|
|
68
|
-
'openai/gpt-oss-120b:free':
|
|
286
|
+
'openai/gpt-oss-120b': { input: 0.15, output: 0.60 },
|
|
287
|
+
'gpt-oss-120b': { input: 0.15, output: 0.60 },
|
|
288
|
+
'openai/gpt-oss-120b:free': { input: 0, output: 0 },
|
|
69
289
|
// 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':
|
|
290
|
+
'claude-fable-5': { input: 10.00, cachedInput: 1.00, cacheWrite: 12.50, cacheWrite1h: 20.00, output: 50.00 },
|
|
291
|
+
'claude-opus-5': { input: 5.00, cachedInput: 0.50, cacheWrite: 6.25, cacheWrite1h: 10.00, output: 25.00 },
|
|
292
|
+
'claude-sonnet-5': { input: 3.00, cachedInput: 0.30, cacheWrite: 3.75, cacheWrite1h: 6.00, output: 15.00 },
|
|
293
|
+
'claude-opus-4-8': { input: 5.00, cachedInput: 0.50, cacheWrite: 6.25, cacheWrite1h: 10.00, output: 25.00 },
|
|
294
|
+
'claude-opus-4-7': { input: 5.00, cachedInput: 0.50, cacheWrite: 6.25, cacheWrite1h: 10.00, output: 25.00 },
|
|
295
|
+
'claude-opus-4-6': { input: 5.00, cachedInput: 0.50, cacheWrite: 6.25, cacheWrite1h: 10.00, output: 25.00 },
|
|
296
|
+
'claude-sonnet-4-6': { input: 3.00, cachedInput: 0.30, cacheWrite: 3.75, cacheWrite1h: 6.00, output: 15.00 },
|
|
297
|
+
'claude-sonnet-4-5-20250929': { input: 3.00, cachedInput: 0.30, cacheWrite: 3.75, cacheWrite1h: 6.00, output: 15.00 },
|
|
298
|
+
'claude-haiku-4-5-20251001': { input: 1.00, cachedInput: 0.10, cacheWrite: 1.25, cacheWrite1h: 2.00, output: 5.00 },
|
|
79
299
|
// 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':
|
|
300
|
+
'gemini-3.1-pro-preview': { input: 2.00, output: 12.00 },
|
|
301
|
+
'gemini-3-pro-preview': { input: 2.00, output: 12.00 },
|
|
302
|
+
'gemini-3-flash-preview': { input: 0.50, output: 3.00 },
|
|
303
|
+
'gemini-3.6-flash': { input: 1.50, output: 7.50 },
|
|
304
|
+
'gemini-3.5-flash': { input: 0.75, output: 4.50 },
|
|
305
|
+
'gemini-3.5-flash-lite': { input: 0.30, output: 2.50 },
|
|
306
|
+
'gemini-2.5-pro': { input: 1.25, output: 10.00 },
|
|
307
|
+
'gemini-2.5-flash': { input: 0.30, output: 2.50 },
|
|
308
|
+
'gemini-3.1-flash-lite-preview': { input: 0.25, output: 1.50 },
|
|
89
309
|
// 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':
|
|
310
|
+
'grok-4.5': { input: 2.00, output: 6.00 },
|
|
311
|
+
'grok-4.3': { input: 1.25, output: 2.50 },
|
|
312
|
+
'grok-4.20-multi-agent-0309': { input: 1.25, output: 2.50 },
|
|
313
|
+
'grok-4.20-0309': { input: 1.25, output: 2.50 },
|
|
314
|
+
'grok-4.20-0309-reasoning': { input: 1.25, output: 2.50 },
|
|
315
|
+
'grok-4.20-0309-non-reasoning': { input: 1.25, output: 2.50 },
|
|
96
316
|
// 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':
|
|
317
|
+
'accounts/fireworks/models/deepseek-v4-flash': { input: 0.14, output: 0.28 },
|
|
318
|
+
'accounts/fireworks/models/deepseek-v4-pro': { input: 1.74, output: 3.48 },
|
|
319
|
+
'deepseek-ai/DeepSeek-V4-Flash': { input: 0.14, output: 0.28 },
|
|
320
|
+
'deepseek-ai/DeepSeek-V4-Pro': { input: 2.10, output: 4.40 },
|
|
321
|
+
'deepseek/deepseek-v4-flash': { input: 0.09, output: 0.18 },
|
|
322
|
+
'accounts/fireworks/models/glm-4p7': { input: 0.55, output: 2.19 },
|
|
323
|
+
'accounts/fireworks/models/glm-5p1': { input: 1.05, output: 3.50 },
|
|
324
|
+
'zai-org/GLM-5.2': { input: 1.40, output: 4.40 },
|
|
325
|
+
'accounts/fireworks/models/kimi-k2p5': { input: 0.50, output: 2.80 },
|
|
326
|
+
'accounts/fireworks/models/qwen3p6-plus': { input: 0.50, output: 3.00 },
|
|
327
|
+
'Qwen/Qwen3.6-Plus': { input: 0.50, output: 3.00 },
|
|
328
|
+
'accounts/fireworks/models/qwen3p7-plus': { input: 0.40, output: 1.60 },
|
|
329
|
+
'qwen/qwen3.7-plus': { input: 0.32, output: 1.28 },
|
|
330
|
+
'qwen/qwen3.8-max': { input: 2.00, output: 6.00 },
|
|
111
331
|
// 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':
|
|
332
|
+
'MiniMax-M2.5': { input: 0.30, output: 1.20 },
|
|
333
|
+
'MiniMax-M2.7': { input: 0.30, output: 1.20 },
|
|
334
|
+
'MiniMax-M3': { input: 0.30, output: 1.20 },
|
|
335
|
+
'minimax/minimax-m2.7': { input: 0.30, output: 1.20 },
|
|
336
|
+
'minimax/minimax-m3': { input: 0.30, output: 1.20 },
|
|
337
|
+
'MiniMaxAI/MiniMax-M3': { input: 0.30, output: 1.20 },
|
|
118
338
|
// Perplexity
|
|
119
|
-
'sonar':
|
|
120
|
-
'sonar-pro':
|
|
339
|
+
'sonar': { input: 1.00, output: 1.00 },
|
|
340
|
+
'sonar-pro': { input: 3.00, output: 15.00 },
|
|
121
341
|
// Hermes3 (Lambda/OpenRouter)
|
|
122
|
-
'Hermes-3-Llama-3.1-405B-FP8':
|
|
123
|
-
'nousresearch/hermes-3-llama-3.1-405b:free':
|
|
342
|
+
'Hermes-3-Llama-3.1-405B-FP8': { input: 0.80, output: 0.80 },
|
|
343
|
+
'nousresearch/hermes-3-llama-3.1-405b:free': { input: 0, output: 0 },
|
|
124
344
|
// Qwen3 (Together/Cerebras)
|
|
125
|
-
'Qwen/Qwen3-235B-A22B-fp8-tput':
|
|
126
|
-
'qwen-3-32b':
|
|
345
|
+
'Qwen/Qwen3-235B-A22B-fp8-tput': { input: 0.20, output: 0.60 },
|
|
346
|
+
'qwen-3-32b': { input: 0.20, output: 0.60 },
|
|
127
347
|
// Kimi K2.5 (Together/Fireworks/OpenRouter)
|
|
128
|
-
'moonshotai/Kimi-K2.5':
|
|
129
|
-
'moonshotai/kimi-k2.5':
|
|
348
|
+
'moonshotai/Kimi-K2.5': { input: 0.50, output: 2.80 },
|
|
349
|
+
'moonshotai/kimi-k2.5': { input: 0.50, output: 2.80 },
|
|
130
350
|
// Kimi K3
|
|
131
|
-
'kimi-k3':
|
|
132
|
-
'moonshotai/kimi-k3':
|
|
351
|
+
'kimi-k3': { input: 3.00, output: 15.00 },
|
|
352
|
+
'moonshotai/kimi-k3': { input: 3.00, output: 15.00 },
|
|
133
353
|
// GLM 4.7 (OpenRouter/Cerebras)
|
|
134
|
-
'z-ai/glm-4.7':
|
|
135
|
-
'zai-glm-4.7':
|
|
354
|
+
'z-ai/glm-4.7': { input: 0.55, output: 2.19 },
|
|
355
|
+
'zai-glm-4.7': { input: 0.55, output: 2.19 },
|
|
136
356
|
};
|
|
137
357
|
|
|
138
358
|
class ModelMix {
|
|
@@ -144,6 +364,8 @@ class ModelMix {
|
|
|
144
364
|
this.toolClient = {};
|
|
145
365
|
this.mcp = {};
|
|
146
366
|
this.mcpToolsManager = new MCPToolsManager();
|
|
367
|
+
this.templateFileAssignments = new Map();
|
|
368
|
+
this.messageTemplates = new WeakMap();
|
|
147
369
|
this.lastRaw = null;
|
|
148
370
|
this.options = {
|
|
149
371
|
max_tokens: 8192,
|
|
@@ -172,6 +394,13 @@ class ModelMix {
|
|
|
172
394
|
roundRobin: false, // false=fallback mode, true=round robin rotation
|
|
173
395
|
...config
|
|
174
396
|
};
|
|
397
|
+
this.systemTemplate = {
|
|
398
|
+
source: this.config.system,
|
|
399
|
+
filename: null
|
|
400
|
+
};
|
|
401
|
+
if (this.config.templateData !== undefined) {
|
|
402
|
+
validateTemplateData(this.config.templateData);
|
|
403
|
+
}
|
|
175
404
|
// Unified effort is ModelMix policy (config.effort / .effort()), not a native option.
|
|
176
405
|
if (this.config.effort !== undefined && this.config.effort !== null) {
|
|
177
406
|
this.config.effort = normalizeEffort(this.config.effort);
|
|
@@ -183,11 +412,20 @@ class ModelMix {
|
|
|
183
412
|
|
|
184
413
|
}
|
|
185
414
|
|
|
186
|
-
|
|
187
|
-
|
|
415
|
+
assign(keyValues) {
|
|
416
|
+
validateTemplateData(keyValues);
|
|
417
|
+
for (const key of Object.keys(keyValues)) {
|
|
418
|
+
this.templateFileAssignments.delete(key);
|
|
419
|
+
}
|
|
420
|
+
this.config.templateData = { ...this.config.templateData, ...keyValues };
|
|
188
421
|
return this;
|
|
189
422
|
}
|
|
190
423
|
|
|
424
|
+
assignKey(key, value) {
|
|
425
|
+
validateTemplateDataKey(key);
|
|
426
|
+
return this.assign({ [key]: value });
|
|
427
|
+
}
|
|
428
|
+
|
|
191
429
|
/**
|
|
192
430
|
* Set unified reasoning effort: -1 (adaptive) or 0..100.
|
|
193
431
|
* Stored in config.effort; mapped to provider-native fields at request time
|
|
@@ -203,11 +441,19 @@ class ModelMix {
|
|
|
203
441
|
}
|
|
204
442
|
|
|
205
443
|
new({ options = {}, config = {}, mix = {} } = {}) {
|
|
444
|
+
const hasSystemOverride = Object.prototype.hasOwnProperty.call(config, 'system');
|
|
206
445
|
const instance = new ModelMix({
|
|
207
446
|
options: { ...this.options, ...options },
|
|
208
447
|
config: { ...this.config, ...config },
|
|
209
448
|
mix: { ...this.mix, ...mix }
|
|
210
449
|
});
|
|
450
|
+
if (!hasSystemOverride) {
|
|
451
|
+
instance.systemTemplate = { ...this.systemTemplate };
|
|
452
|
+
}
|
|
453
|
+
instance.templateFileAssignments = new Map(this.templateFileAssignments);
|
|
454
|
+
for (const key of Object.keys(config.templateData || {})) {
|
|
455
|
+
instance.templateFileAssignments.delete(key);
|
|
456
|
+
}
|
|
211
457
|
instance.models = this.models; // Share models array for round-robin rotation
|
|
212
458
|
return instance;
|
|
213
459
|
}
|
|
@@ -238,20 +484,163 @@ class ModelMix {
|
|
|
238
484
|
return str.length > maxLen ? str.substring(0, maxLen) + '...' : str;
|
|
239
485
|
}
|
|
240
486
|
|
|
241
|
-
static
|
|
487
|
+
static normalizeTokenUsage({ input = 0, output = 0, total, cached = 0, cacheWrite = 0, cacheWrite5m = 0, cacheWrite1h = 0 } = {}) {
|
|
488
|
+
const tokenCount = value => Number.isFinite(value) ? Math.max(0, value) : 0;
|
|
489
|
+
const normalizedInput = tokenCount(input);
|
|
490
|
+
const normalizedOutput = tokenCount(output);
|
|
491
|
+
const normalizedCached = tokenCount(cached);
|
|
492
|
+
const normalizedCacheWrite5m = tokenCount(cacheWrite5m);
|
|
493
|
+
const normalizedCacheWrite1h = tokenCount(cacheWrite1h);
|
|
494
|
+
const normalizedCacheWrite = Math.max(
|
|
495
|
+
tokenCount(cacheWrite),
|
|
496
|
+
normalizedCacheWrite5m + normalizedCacheWrite1h
|
|
497
|
+
);
|
|
498
|
+
const normalizedTotal = Number.isFinite(total)
|
|
499
|
+
? Math.max(0, total)
|
|
500
|
+
: normalizedInput + normalizedOutput;
|
|
501
|
+
const uncachedInput = Math.max(0, normalizedInput - normalizedCached - normalizedCacheWrite);
|
|
502
|
+
const cacheHitRate = normalizedInput > 0
|
|
503
|
+
? Number((normalizedCached / normalizedInput).toFixed(4))
|
|
504
|
+
: 0;
|
|
505
|
+
|
|
506
|
+
return {
|
|
507
|
+
input: normalizedInput,
|
|
508
|
+
output: normalizedOutput,
|
|
509
|
+
total: normalizedTotal,
|
|
510
|
+
cached: normalizedCached,
|
|
511
|
+
cacheWrite: normalizedCacheWrite,
|
|
512
|
+
cacheWrite5m: normalizedCacheWrite5m,
|
|
513
|
+
cacheWrite1h: normalizedCacheWrite1h,
|
|
514
|
+
uncachedInput,
|
|
515
|
+
cacheHitRate,
|
|
516
|
+
cacheSavings: 0,
|
|
517
|
+
cacheWritePremium: 0,
|
|
518
|
+
breakEvenHits: 0,
|
|
519
|
+
cost: 0,
|
|
520
|
+
costBreakdown: {
|
|
521
|
+
uncachedInput: 0,
|
|
522
|
+
cachedInput: 0,
|
|
523
|
+
cacheWrite: 0,
|
|
524
|
+
cacheWrite5m: 0,
|
|
525
|
+
cacheWrite1h: 0,
|
|
526
|
+
output: 0,
|
|
527
|
+
total: 0
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
static calculateCostBreakdown(modelKey, tokens) {
|
|
533
|
+
const pricing = MODEL_PRICING[modelKey];
|
|
534
|
+
if (!pricing) return ModelMix.normalizeTokenUsage().costBreakdown;
|
|
535
|
+
|
|
536
|
+
const normalized = ModelMix.normalizeTokenUsage(tokens);
|
|
537
|
+
const longContext = pricing.longContext;
|
|
538
|
+
const useLongContextRates = longContext && normalized.input > longContext.inputThreshold;
|
|
539
|
+
const inputMultiplier = useLongContextRates ? longContext.inputMultiplier : 1;
|
|
540
|
+
const outputMultiplier = useLongContextRates ? longContext.outputMultiplier : 1;
|
|
541
|
+
const {
|
|
542
|
+
input: inputPerMillion,
|
|
543
|
+
cachedInput: cachedInputPerMillion = inputPerMillion,
|
|
544
|
+
cacheWrite: cacheWritePerMillion = inputPerMillion,
|
|
545
|
+
cacheWrite1h: cacheWrite1hPerMillion = cacheWritePerMillion,
|
|
546
|
+
output: outputPerMillion
|
|
547
|
+
} = pricing;
|
|
548
|
+
const roundCost = value => Number(value.toFixed(12));
|
|
549
|
+
const genericCacheWrite = Math.max(
|
|
550
|
+
0,
|
|
551
|
+
normalized.cacheWrite - normalized.cacheWrite5m - normalized.cacheWrite1h
|
|
552
|
+
);
|
|
553
|
+
const cacheWrite5mCost = roundCost(
|
|
554
|
+
normalized.cacheWrite5m * cacheWritePerMillion * inputMultiplier / 1_000_000
|
|
555
|
+
);
|
|
556
|
+
const cacheWrite1hCost = roundCost(
|
|
557
|
+
normalized.cacheWrite1h * cacheWrite1hPerMillion * inputMultiplier / 1_000_000
|
|
558
|
+
);
|
|
559
|
+
const genericCacheWriteCost = roundCost(
|
|
560
|
+
genericCacheWrite * cacheWritePerMillion * inputMultiplier / 1_000_000
|
|
561
|
+
);
|
|
562
|
+
const breakdown = {
|
|
563
|
+
uncachedInput: roundCost(normalized.uncachedInput * inputPerMillion * inputMultiplier / 1_000_000),
|
|
564
|
+
cachedInput: roundCost(normalized.cached * cachedInputPerMillion * inputMultiplier / 1_000_000),
|
|
565
|
+
cacheWrite: roundCost(genericCacheWriteCost + cacheWrite5mCost + cacheWrite1hCost),
|
|
566
|
+
cacheWrite5m: cacheWrite5mCost,
|
|
567
|
+
cacheWrite1h: cacheWrite1hCost,
|
|
568
|
+
output: roundCost(normalized.output * outputPerMillion * outputMultiplier / 1_000_000)
|
|
569
|
+
};
|
|
570
|
+
breakdown.total = roundCost(
|
|
571
|
+
breakdown.uncachedInput
|
|
572
|
+
+ breakdown.cachedInput
|
|
573
|
+
+ breakdown.cacheWrite
|
|
574
|
+
+ breakdown.output
|
|
575
|
+
);
|
|
576
|
+
return breakdown;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
static calculateCacheMetrics(modelKey, tokens) {
|
|
242
580
|
const pricing = MODEL_PRICING[modelKey];
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
581
|
+
const emptyMetrics = {
|
|
582
|
+
cacheSavings: 0,
|
|
583
|
+
cacheWritePremium: 0,
|
|
584
|
+
breakEvenHits: 0
|
|
585
|
+
};
|
|
586
|
+
if (!pricing) return emptyMetrics;
|
|
587
|
+
|
|
588
|
+
const normalized = ModelMix.normalizeTokenUsage(tokens);
|
|
589
|
+
const longContext = pricing.longContext;
|
|
590
|
+
const inputMultiplier = longContext && normalized.input > longContext.inputThreshold
|
|
591
|
+
? longContext.inputMultiplier
|
|
592
|
+
: 1;
|
|
593
|
+
const cachedInputPerMillion = pricing.cachedInput ?? pricing.input;
|
|
594
|
+
const cacheWritePerMillion = pricing.cacheWrite ?? pricing.input;
|
|
595
|
+
const cacheWrite1hPerMillion = pricing.cacheWrite1h ?? cacheWritePerMillion;
|
|
596
|
+
const readSavingsPerMillion = Math.max(0, pricing.input - cachedInputPerMillion) * inputMultiplier;
|
|
597
|
+
const writePremiumPerMillion = Math.max(0, cacheWritePerMillion - pricing.input) * inputMultiplier;
|
|
598
|
+
const write1hPremiumPerMillion = Math.max(0, cacheWrite1hPerMillion - pricing.input) * inputMultiplier;
|
|
599
|
+
const roundCost = value => Number(value.toFixed(12));
|
|
600
|
+
const cacheSavings = roundCost(normalized.cached * readSavingsPerMillion / 1_000_000);
|
|
601
|
+
const genericCacheWrite = Math.max(
|
|
602
|
+
0,
|
|
603
|
+
normalized.cacheWrite - normalized.cacheWrite5m - normalized.cacheWrite1h
|
|
604
|
+
);
|
|
605
|
+
const cacheWritePremium = roundCost(
|
|
606
|
+
(
|
|
607
|
+
(genericCacheWrite + normalized.cacheWrite5m) * writePremiumPerMillion
|
|
608
|
+
+ normalized.cacheWrite1h * write1hPremiumPerMillion
|
|
609
|
+
) / 1_000_000
|
|
610
|
+
);
|
|
611
|
+
const fullHitSavings = normalized.cacheWrite * readSavingsPerMillion / 1_000_000;
|
|
612
|
+
|
|
613
|
+
return {
|
|
614
|
+
cacheSavings,
|
|
615
|
+
cacheWritePremium,
|
|
616
|
+
breakEvenHits: fullHitSavings > 0
|
|
617
|
+
? Number((cacheWritePremium / fullHitSavings).toFixed(4))
|
|
618
|
+
: 0
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
static calculateCost(modelKey, tokens) {
|
|
623
|
+
if (!MODEL_PRICING[modelKey]) return null;
|
|
624
|
+
return ModelMix.calculateCostBreakdown(modelKey, tokens).total;
|
|
246
625
|
}
|
|
247
626
|
|
|
248
627
|
static extractCacheTokens(usage = {}) {
|
|
249
628
|
return usage.input_tokens_details?.cached_tokens
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
629
|
+
?? usage.prompt_tokens_details?.cached_tokens
|
|
630
|
+
?? usage.cache_read_input_tokens
|
|
631
|
+
?? usage.cachedContentTokenCount
|
|
632
|
+
?? usage.cached_content_token_count
|
|
633
|
+
?? 0;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
static extractCacheWriteTokens(usage = {}) {
|
|
637
|
+
return usage.input_tokens_details?.cache_write_tokens
|
|
638
|
+
?? usage.prompt_tokens_details?.cache_write_tokens
|
|
639
|
+
?? usage.cache_creation_input_tokens
|
|
640
|
+
?? usage.cache_write_input_tokens
|
|
641
|
+
?? usage.cacheWriteTokenCount
|
|
642
|
+
?? usage.cache_write_token_count
|
|
643
|
+
?? 0;
|
|
255
644
|
}
|
|
256
645
|
|
|
257
646
|
static formatInputSummary(messages, system, debug = 2) {
|
|
@@ -385,12 +774,18 @@ class ModelMix {
|
|
|
385
774
|
if (mix.openrouter) this.attach('openai/gpt-oss-120b:free', new MixOpenRouter({ options, config }));
|
|
386
775
|
return this;
|
|
387
776
|
}
|
|
388
|
-
|
|
777
|
+
fable50({ options = {}, config = {} } = {}) {
|
|
389
778
|
return this.attach('claude-fable-5', new MixAnthropic({ options, config }));
|
|
390
779
|
}
|
|
391
|
-
|
|
780
|
+
fable5(args = {}) {
|
|
781
|
+
return this.fable50(args);
|
|
782
|
+
}
|
|
783
|
+
opus50({ options = {}, config = {} } = {}) {
|
|
392
784
|
return this.attach('claude-opus-5', new MixAnthropic({ options, config }));
|
|
393
785
|
}
|
|
786
|
+
opus5(args = {}) {
|
|
787
|
+
return this.opus50(args);
|
|
788
|
+
}
|
|
394
789
|
opus48({ options = {}, config = {} } = {}) {
|
|
395
790
|
return this.attach('claude-opus-4-8', new MixAnthropic({ options, config }));
|
|
396
791
|
}
|
|
@@ -400,9 +795,12 @@ class ModelMix {
|
|
|
400
795
|
opus46({ options = {}, config = {} } = {}) {
|
|
401
796
|
return this.attach('claude-opus-4-6', new MixAnthropic({ options, config }));
|
|
402
797
|
}
|
|
403
|
-
|
|
798
|
+
sonnet50({ options = {}, config = {} } = {}) {
|
|
404
799
|
return this.attach('claude-sonnet-5', new MixAnthropic({ options, config }));
|
|
405
800
|
}
|
|
801
|
+
sonnet5(args = {}) {
|
|
802
|
+
return this.sonnet50(args);
|
|
803
|
+
}
|
|
406
804
|
sonnet46({ options = {}, config = {} } = {}) {
|
|
407
805
|
return this.attach('claude-sonnet-4-6', new MixAnthropic({ options, config }));
|
|
408
806
|
}
|
|
@@ -597,34 +995,54 @@ class ModelMix {
|
|
|
597
995
|
return this;
|
|
598
996
|
}
|
|
599
997
|
|
|
600
|
-
addText(text, { role = "user" } = {}) {
|
|
998
|
+
addText(text, { role = "user", cache } = {}) {
|
|
999
|
+
return this._addText(text, {
|
|
1000
|
+
role,
|
|
1001
|
+
cache: normalizeContentCache(cache),
|
|
1002
|
+
template: { source: text, filename: null }
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
_addText(text, { role = "user", cache, template = null } = {}) {
|
|
601
1007
|
const content = [{
|
|
602
1008
|
type: "text",
|
|
603
|
-
text
|
|
1009
|
+
text,
|
|
1010
|
+
...(cache !== undefined && { cache })
|
|
604
1011
|
}];
|
|
605
1012
|
|
|
1013
|
+
if (template) {
|
|
1014
|
+
this.messageTemplates.set(content[0], template);
|
|
1015
|
+
}
|
|
606
1016
|
this.messages.push({ role, content });
|
|
607
1017
|
return this;
|
|
608
1018
|
}
|
|
609
1019
|
|
|
610
|
-
addTextFromFile(filePath, { role = "user" } = {}) {
|
|
611
|
-
const
|
|
612
|
-
this.
|
|
613
|
-
return this
|
|
1020
|
+
addTextFromFile(filePath, { role = "user", cache } = {}) {
|
|
1021
|
+
const filename = path.resolve(filePath);
|
|
1022
|
+
const content = this.readFile(filename);
|
|
1023
|
+
return this._addText(content, {
|
|
1024
|
+
role,
|
|
1025
|
+
cache: normalizeContentCache(cache),
|
|
1026
|
+
template: { source: content, filename }
|
|
1027
|
+
});
|
|
614
1028
|
}
|
|
615
1029
|
|
|
616
1030
|
setSystem(text) {
|
|
617
1031
|
this.config.system = text;
|
|
1032
|
+
this.systemTemplate = { source: text, filename: null };
|
|
618
1033
|
return this;
|
|
619
1034
|
}
|
|
620
1035
|
|
|
621
1036
|
setSystemFromFile(filePath) {
|
|
622
|
-
const
|
|
623
|
-
this.
|
|
1037
|
+
const filename = path.resolve(filePath);
|
|
1038
|
+
const content = this.readFile(filename);
|
|
1039
|
+
this.config.system = content;
|
|
1040
|
+
this.systemTemplate = { source: content, filename };
|
|
624
1041
|
return this;
|
|
625
1042
|
}
|
|
626
1043
|
|
|
627
|
-
addImageFromBuffer(buffer, { role = "user" } = {}) {
|
|
1044
|
+
addImageFromBuffer(buffer, { role = "user", cache } = {}) {
|
|
1045
|
+
const contentCache = normalizeContentCache(cache);
|
|
628
1046
|
this.messages.push({
|
|
629
1047
|
role,
|
|
630
1048
|
content: [{
|
|
@@ -632,19 +1050,21 @@ class ModelMix {
|
|
|
632
1050
|
source: {
|
|
633
1051
|
type: "buffer",
|
|
634
1052
|
data: buffer
|
|
635
|
-
}
|
|
1053
|
+
},
|
|
1054
|
+
...(contentCache !== undefined && { cache: contentCache })
|
|
636
1055
|
}]
|
|
637
1056
|
});
|
|
638
1057
|
return this;
|
|
639
1058
|
}
|
|
640
1059
|
|
|
641
|
-
addImage(filePath, { role = "user" } = {}) {
|
|
1060
|
+
addImage(filePath, { role = "user", cache } = {}) {
|
|
642
1061
|
const absolutePath = path.resolve(filePath);
|
|
643
1062
|
|
|
644
1063
|
if (!fs.existsSync(absolutePath)) {
|
|
645
1064
|
throw new Error(`Image file not found: ${filePath}`);
|
|
646
1065
|
}
|
|
647
1066
|
|
|
1067
|
+
const contentCache = normalizeContentCache(cache);
|
|
648
1068
|
this.messages.push({
|
|
649
1069
|
role,
|
|
650
1070
|
content: [{
|
|
@@ -652,13 +1072,14 @@ class ModelMix {
|
|
|
652
1072
|
source: {
|
|
653
1073
|
type: "file",
|
|
654
1074
|
data: filePath
|
|
655
|
-
}
|
|
1075
|
+
},
|
|
1076
|
+
...(contentCache !== undefined && { cache: contentCache })
|
|
656
1077
|
}]
|
|
657
1078
|
});
|
|
658
1079
|
return this;
|
|
659
1080
|
}
|
|
660
1081
|
|
|
661
|
-
addImageFromUrl(url, { role = "user" } = {}) {
|
|
1082
|
+
addImageFromUrl(url, { role = "user", cache } = {}) {
|
|
662
1083
|
let source;
|
|
663
1084
|
if (url.startsWith('data:')) {
|
|
664
1085
|
// Parse data URL: data:image/jpeg;base64,/9j/4AAQ...
|
|
@@ -679,11 +1100,13 @@ class ModelMix {
|
|
|
679
1100
|
};
|
|
680
1101
|
}
|
|
681
1102
|
|
|
1103
|
+
const contentCache = normalizeContentCache(cache);
|
|
682
1104
|
this.messages.push({
|
|
683
1105
|
role,
|
|
684
1106
|
content: [{
|
|
685
1107
|
type: "image",
|
|
686
|
-
source
|
|
1108
|
+
source,
|
|
1109
|
+
...(contentCache !== undefined && { cache: contentCache })
|
|
687
1110
|
}]
|
|
688
1111
|
});
|
|
689
1112
|
|
|
@@ -732,7 +1155,7 @@ class ModelMix {
|
|
|
732
1155
|
|
|
733
1156
|
// Update the content with processed image
|
|
734
1157
|
message.content[j] = {
|
|
735
|
-
|
|
1158
|
+
...content,
|
|
736
1159
|
source: {
|
|
737
1160
|
type: "base64",
|
|
738
1161
|
media_type: mimeType,
|
|
@@ -771,27 +1194,23 @@ class ModelMix {
|
|
|
771
1194
|
stream: false,
|
|
772
1195
|
}
|
|
773
1196
|
|
|
774
|
-
|
|
775
|
-
let
|
|
776
|
-
|
|
777
|
-
let config = {
|
|
778
|
-
system: systemWithReplacements,
|
|
779
|
-
}
|
|
1197
|
+
let config = {};
|
|
1198
|
+
let systemSuffix = '';
|
|
780
1199
|
|
|
781
1200
|
if (schemaExample) {
|
|
782
1201
|
config.schema = generateJsonSchema(schemaExample, schemaDescription);
|
|
783
1202
|
|
|
784
1203
|
if (addSchema) {
|
|
785
|
-
|
|
1204
|
+
systemSuffix += "\n\nOutput JSON Schema: \n```\n" + JSON.stringify(config.schema) + "\n```";
|
|
786
1205
|
}
|
|
787
1206
|
if (addExample) {
|
|
788
|
-
|
|
1207
|
+
systemSuffix += "\n\nOutput JSON Example: \n```\n" + JSON.stringify(schemaExample) + "\n```";
|
|
789
1208
|
}
|
|
790
1209
|
if (addNote) {
|
|
791
|
-
|
|
1210
|
+
systemSuffix += "\n\nOutput JSON Escape: double quotes, backslashes, and control characters inside JSON strings.\nEnsure the output contains no comments.";
|
|
792
1211
|
}
|
|
793
1212
|
}
|
|
794
|
-
const { message } = await this.execute({ options, config });
|
|
1213
|
+
const { message } = await this.execute({ options, config, systemSuffix });
|
|
795
1214
|
const parsed = JSON.parse(this._extractBlock(message));
|
|
796
1215
|
return isArrayWrap ? parsed.out : parsed;
|
|
797
1216
|
}
|
|
@@ -802,17 +1221,13 @@ class ModelMix {
|
|
|
802
1221
|
}
|
|
803
1222
|
|
|
804
1223
|
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 });
|
|
1224
|
+
const systemSuffix = addSystemExtra
|
|
1225
|
+
? "\nReturn the result of the task between triple backtick block code tags ```"
|
|
1226
|
+
: '';
|
|
1227
|
+
const { message } = await this.execute({
|
|
1228
|
+
options: { stream: false },
|
|
1229
|
+
systemSuffix
|
|
1230
|
+
});
|
|
816
1231
|
return this._extractBlock(message);
|
|
817
1232
|
}
|
|
818
1233
|
|
|
@@ -825,23 +1240,97 @@ class ModelMix {
|
|
|
825
1240
|
return this.execute({ options: { stream: true } });
|
|
826
1241
|
}
|
|
827
1242
|
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
1243
|
+
assignKeyFromFile(key, filePath) {
|
|
1244
|
+
validateTemplateDataKey(key);
|
|
1245
|
+
this.readFile(filePath);
|
|
1246
|
+
|
|
1247
|
+
const templateData = { ...this.config.templateData };
|
|
1248
|
+
delete templateData[key];
|
|
1249
|
+
this.config.templateData = templateData;
|
|
1250
|
+
this.templateFileAssignments.set(key, Object.freeze({
|
|
1251
|
+
key,
|
|
1252
|
+
filename: path.resolve(filePath)
|
|
1253
|
+
}));
|
|
836
1254
|
return this;
|
|
837
1255
|
}
|
|
838
1256
|
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
1257
|
+
_choiceRandom() {
|
|
1258
|
+
return Math.random();
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
_templateData(renderContext) {
|
|
1262
|
+
const assigned = { ...(this.config.templateData || {}), $mix: renderContext.helpers };
|
|
1263
|
+
const data = { ...assigned };
|
|
1264
|
+
|
|
1265
|
+
for (const [key, assignment] of this.templateFileAssignments) {
|
|
1266
|
+
data[key] = this._renderAssignedTemplate(assignment, assigned, renderContext);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
return data;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
_renderAssignedTemplate(assignment, data, renderContext) {
|
|
1273
|
+
if (renderContext.renderedTemplateData.has(assignment)) {
|
|
1274
|
+
return renderContext.renderedTemplateData.get(assignment);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
const rendered = this._renderTemplateWithData(
|
|
1278
|
+
`<%- include(${JSON.stringify(assignment.filename)}) %>`,
|
|
1279
|
+
{
|
|
1280
|
+
filename: assignment.filename,
|
|
1281
|
+
label: `template data "${assignment.key}"`
|
|
1282
|
+
},
|
|
1283
|
+
data
|
|
1284
|
+
);
|
|
1285
|
+
renderContext.renderedTemplateData.set(assignment, rendered);
|
|
1286
|
+
return rendered;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
_renderTemplate(
|
|
1290
|
+
source,
|
|
1291
|
+
{ filename = null, label = 'template' } = {},
|
|
1292
|
+
renderContext = createTemplateRenderContext(() => this._choiceRandom())
|
|
1293
|
+
) {
|
|
1294
|
+
return this._renderTemplateWithData(
|
|
1295
|
+
source,
|
|
1296
|
+
{ filename, label },
|
|
1297
|
+
this._templateData(renderContext)
|
|
1298
|
+
);
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
_renderTemplateWithData(source, { filename = null, label = 'template' }, data) {
|
|
1302
|
+
if (typeof source !== 'string') {
|
|
1303
|
+
throw new TypeError(`${label} source must be a string.`);
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
try {
|
|
1307
|
+
const template = preprocessChoiceDirectives(source, { filename, label });
|
|
1308
|
+
return ejs.render(template, data, {
|
|
1309
|
+
...(filename && { filename }),
|
|
1310
|
+
async: false,
|
|
1311
|
+
cache: false,
|
|
1312
|
+
compileDebug: true,
|
|
1313
|
+
unsafePrototypeLocals: false,
|
|
1314
|
+
includer: (originalPath, resolvedFilename) => {
|
|
1315
|
+
if (!resolvedFilename) {
|
|
1316
|
+
throw new Error(`Could not find the include file "${originalPath}"`);
|
|
1317
|
+
}
|
|
1318
|
+
const includedSource = fs.readFileSync(resolvedFilename, 'utf8').replace(/^\uFEFF/, '');
|
|
1319
|
+
return {
|
|
1320
|
+
filename: resolvedFilename,
|
|
1321
|
+
template: preprocessChoiceDirectives(includedSource, {
|
|
1322
|
+
filename: resolvedFilename,
|
|
1323
|
+
label: 'included template'
|
|
1324
|
+
})
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
});
|
|
1328
|
+
} catch (error) {
|
|
1329
|
+
const location = filename ? ` ${filename}` : '';
|
|
1330
|
+
const renderError = new Error(`Failed to render ${label}${location}: ${error.message}`);
|
|
1331
|
+
renderError.cause = error;
|
|
1332
|
+
throw renderError;
|
|
843
1333
|
}
|
|
844
|
-
return input;
|
|
845
1334
|
}
|
|
846
1335
|
|
|
847
1336
|
static hasToolInteraction(message) {
|
|
@@ -873,37 +1362,56 @@ class ModelMix {
|
|
|
873
1362
|
}, []);
|
|
874
1363
|
}
|
|
875
1364
|
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
1365
|
+
_renderMessageSnapshot(messages, renderContext) {
|
|
1366
|
+
return messages.map(message => ({
|
|
1367
|
+
...message,
|
|
1368
|
+
content: Array.isArray(message.content)
|
|
1369
|
+
? message.content.map(content => {
|
|
1370
|
+
if (!content || typeof content !== 'object') return content;
|
|
1371
|
+
|
|
1372
|
+
const snapshotContent = { ...content };
|
|
1373
|
+
const template = content.type === 'text'
|
|
1374
|
+
? this.messageTemplates.get(content)
|
|
1375
|
+
: null;
|
|
1376
|
+
if (!template) return snapshotContent;
|
|
1377
|
+
|
|
1378
|
+
let rendered = renderContext.renderedMessages.get(content)?.rendered;
|
|
1379
|
+
if (rendered === undefined) {
|
|
1380
|
+
rendered = this._renderTemplate(template.source, {
|
|
1381
|
+
filename: template.filename,
|
|
1382
|
+
label: 'message template'
|
|
1383
|
+
}, renderContext);
|
|
1384
|
+
renderContext.renderedMessages.set(content, { rendered, template });
|
|
886
1385
|
}
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
});
|
|
1386
|
+
snapshotContent.text = rendered;
|
|
1387
|
+
return snapshotContent;
|
|
1388
|
+
})
|
|
1389
|
+
: message.content
|
|
1390
|
+
}));
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
_commitTemplateRenderContext(renderContext) {
|
|
1394
|
+
for (const [content, { rendered, template }] of renderContext.renderedMessages) {
|
|
1395
|
+
if (this.messageTemplates.get(content) !== template) continue;
|
|
1396
|
+
content.text = rendered;
|
|
1397
|
+
this.messageTemplates.delete(content);
|
|
1398
|
+
}
|
|
892
1399
|
}
|
|
893
1400
|
|
|
894
|
-
async prepareMessages() {
|
|
1401
|
+
async prepareMessages(renderContext = createTemplateRenderContext(() => this._choiceRandom())) {
|
|
895
1402
|
await this.processImages();
|
|
896
|
-
|
|
1403
|
+
|
|
1404
|
+
let messages = this.messages;
|
|
897
1405
|
|
|
898
1406
|
// Smart message slicing based on max_history:
|
|
899
1407
|
// 0 = no history (stateless), N = keep last N messages, -1 = unlimited
|
|
900
1408
|
if (this.config.max_history > 0) {
|
|
901
|
-
let sliceStart = Math.max(0,
|
|
1409
|
+
let sliceStart = Math.max(0, messages.length - this.config.max_history);
|
|
902
1410
|
|
|
903
1411
|
// If we're slicing into the middle of a tool interaction,
|
|
904
1412
|
// backtrack to include the full sequence (user → assistant/tool_calls → tool results)
|
|
905
|
-
while (sliceStart > 0 && sliceStart <
|
|
906
|
-
const msg =
|
|
1413
|
+
while (sliceStart > 0 && sliceStart < messages.length) {
|
|
1414
|
+
const msg = messages[sliceStart];
|
|
907
1415
|
if (ModelMix.hasToolInteraction(msg)) {
|
|
908
1416
|
sliceStart--;
|
|
909
1417
|
} else {
|
|
@@ -911,13 +1419,13 @@ class ModelMix {
|
|
|
911
1419
|
}
|
|
912
1420
|
}
|
|
913
1421
|
|
|
914
|
-
this.messages =
|
|
1422
|
+
this.messages = messages.slice(sliceStart);
|
|
1423
|
+
messages = this.messages;
|
|
915
1424
|
}
|
|
916
1425
|
// max_history = -1: unlimited, no slicing
|
|
917
1426
|
// max_history = 0: no history, messages only contain what was added since last call
|
|
918
1427
|
|
|
919
|
-
|
|
920
|
-
this.options.messages = this.messages;
|
|
1428
|
+
return this.groupByRoles(this._renderMessageSnapshot(messages, renderContext));
|
|
921
1429
|
}
|
|
922
1430
|
|
|
923
1431
|
readFile(filePath, { encoding = 'utf8' } = {}) {
|
|
@@ -935,15 +1443,30 @@ class ModelMix {
|
|
|
935
1443
|
}
|
|
936
1444
|
}
|
|
937
1445
|
|
|
938
|
-
|
|
1446
|
+
_resolveSystemTemplate(config, providerConfig) {
|
|
1447
|
+
if (Object.prototype.hasOwnProperty.call(config, 'system')) {
|
|
1448
|
+
return { source: config.system, filename: null };
|
|
1449
|
+
}
|
|
1450
|
+
if (Object.prototype.hasOwnProperty.call(providerConfig, 'system')) {
|
|
1451
|
+
return { source: providerConfig.system, filename: null };
|
|
1452
|
+
}
|
|
1453
|
+
if (this.config.system !== this.systemTemplate.source) {
|
|
1454
|
+
return { source: this.config.system, filename: null };
|
|
1455
|
+
}
|
|
1456
|
+
return this.systemTemplate;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
async execute({ config = {}, options = {}, systemSuffix = '', _templateContext = null } = {}) {
|
|
939
1460
|
if (!this.models || this.models.length === 0) {
|
|
940
1461
|
throw new Error("No models specified. Use methods like .gpt5(), .sonnet46() first.");
|
|
941
1462
|
}
|
|
942
1463
|
|
|
943
|
-
|
|
944
|
-
|
|
1464
|
+
const isRootExecution = _templateContext === null;
|
|
1465
|
+
const templateContext = _templateContext || createTemplateRenderContext(() => this._choiceRandom());
|
|
1466
|
+
const execution = this.limiter.schedule(async () => {
|
|
1467
|
+
const preparedMessages = await this.prepareMessages(templateContext);
|
|
945
1468
|
|
|
946
|
-
if (
|
|
1469
|
+
if (preparedMessages.length === 0) {
|
|
947
1470
|
throw new Error("No user messages have been added. Use addText(prompt), addTextFromFile(filePath), addImage(filePath), or addImageFromUrl(url) to add a prompt.");
|
|
948
1471
|
}
|
|
949
1472
|
|
|
@@ -978,6 +1501,7 @@ class ModelMix {
|
|
|
978
1501
|
// Create clean copies for each provider to avoid contamination
|
|
979
1502
|
const currentOptions = {
|
|
980
1503
|
...this.options,
|
|
1504
|
+
messages: preparedMessages,
|
|
981
1505
|
...providerInstance.options,
|
|
982
1506
|
...optionsTools,
|
|
983
1507
|
...options,
|
|
@@ -994,6 +1518,18 @@ class ModelMix {
|
|
|
994
1518
|
...(config.retry || {})
|
|
995
1519
|
}
|
|
996
1520
|
};
|
|
1521
|
+
const systemTemplate = this._resolveSystemTemplate(config, providerInstance.config);
|
|
1522
|
+
const systemCacheKey = JSON.stringify([systemTemplate.filename, systemTemplate.source]);
|
|
1523
|
+
if (!templateContext.renderedSystems.has(systemCacheKey)) {
|
|
1524
|
+
templateContext.renderedSystems.set(
|
|
1525
|
+
systemCacheKey,
|
|
1526
|
+
this._renderTemplate(systemTemplate.source, {
|
|
1527
|
+
filename: systemTemplate.filename,
|
|
1528
|
+
label: 'system template'
|
|
1529
|
+
}, templateContext)
|
|
1530
|
+
);
|
|
1531
|
+
}
|
|
1532
|
+
currentConfig.system = templateContext.renderedSystems.get(systemCacheKey) + systemSuffix;
|
|
997
1533
|
|
|
998
1534
|
// Grok 4.20 alias → reasoning / non-reasoning from unified effort
|
|
999
1535
|
const resolvedModelKey = resolveGrok420ModelKey(
|
|
@@ -1018,7 +1554,7 @@ class ModelMix {
|
|
|
1018
1554
|
const header = `\n${prefix} [${providerName}:${resolvedModelKey}] #${originalIndex + 1}${suffix}`;
|
|
1019
1555
|
|
|
1020
1556
|
if (currentConfig.debug >= 2) {
|
|
1021
|
-
console.log(`${header}\n${ModelMix.formatInputSummary(
|
|
1557
|
+
console.log(`${header}\n${ModelMix.formatInputSummary(preparedMessages, currentConfig.system, currentConfig.debug)}`);
|
|
1022
1558
|
} else {
|
|
1023
1559
|
console.log(header);
|
|
1024
1560
|
}
|
|
@@ -1072,7 +1608,16 @@ class ModelMix {
|
|
|
1072
1608
|
const elapsedMs = Date.now() - startTime;
|
|
1073
1609
|
|
|
1074
1610
|
if (result.tokens) {
|
|
1075
|
-
|
|
1611
|
+
const normalizedTokens = ModelMix.normalizeTokenUsage(result.tokens);
|
|
1612
|
+
const costBreakdown = ModelMix.calculateCostBreakdown(resolvedModelKey, normalizedTokens);
|
|
1613
|
+
const cacheMetrics = ModelMix.calculateCacheMetrics(resolvedModelKey, normalizedTokens);
|
|
1614
|
+
result.tokens = {
|
|
1615
|
+
...result.tokens,
|
|
1616
|
+
...normalizedTokens,
|
|
1617
|
+
...cacheMetrics,
|
|
1618
|
+
cost: MODEL_PRICING[resolvedModelKey] ? costBreakdown.total : 0,
|
|
1619
|
+
costBreakdown
|
|
1620
|
+
};
|
|
1076
1621
|
const elapsedSec = elapsedMs / 1000;
|
|
1077
1622
|
result.tokens.speed = elapsedSec > 0 ? Math.round(result.tokens.output / elapsedSec) : 0;
|
|
1078
1623
|
}
|
|
@@ -1091,7 +1636,7 @@ class ModelMix {
|
|
|
1091
1636
|
}]
|
|
1092
1637
|
});
|
|
1093
1638
|
} else {
|
|
1094
|
-
this.
|
|
1639
|
+
this._addText(result.message, { role: "assistant" });
|
|
1095
1640
|
}
|
|
1096
1641
|
}
|
|
1097
1642
|
|
|
@@ -1109,7 +1654,7 @@ class ModelMix {
|
|
|
1109
1654
|
});
|
|
1110
1655
|
}
|
|
1111
1656
|
|
|
1112
|
-
return this.execute({ options, config });
|
|
1657
|
+
return this.execute({ options, config, systemSuffix, _templateContext: templateContext });
|
|
1113
1658
|
}
|
|
1114
1659
|
|
|
1115
1660
|
// debug level 1: Just success indicator
|
|
@@ -1171,7 +1716,7 @@ class ModelMix {
|
|
|
1171
1716
|
}]
|
|
1172
1717
|
});
|
|
1173
1718
|
} else {
|
|
1174
|
-
this.
|
|
1719
|
+
this._addText(result.message, { role: "assistant" });
|
|
1175
1720
|
}
|
|
1176
1721
|
}
|
|
1177
1722
|
|
|
@@ -1197,6 +1742,12 @@ class ModelMix {
|
|
|
1197
1742
|
log.error("Fallback logic completed without success or throwing the final error.");
|
|
1198
1743
|
throw lastError || new Error("Failed to get response from any model, and no specific error was caught.");
|
|
1199
1744
|
});
|
|
1745
|
+
|
|
1746
|
+
if (!isRootExecution) return execution;
|
|
1747
|
+
|
|
1748
|
+
const result = await execution;
|
|
1749
|
+
this._commitTemplateRenderContext(templateContext);
|
|
1750
|
+
return result;
|
|
1200
1751
|
}
|
|
1201
1752
|
|
|
1202
1753
|
async processToolCalls(toolCalls) {
|
|
@@ -1397,6 +1948,13 @@ class MixCustom {
|
|
|
1397
1948
|
return MixOpenAI.convertMessages(messages, config);
|
|
1398
1949
|
}
|
|
1399
1950
|
|
|
1951
|
+
sanitizeCacheOptions(options) {
|
|
1952
|
+
delete options.cache_control;
|
|
1953
|
+
delete options.prompt_cache_key;
|
|
1954
|
+
delete options.prompt_cache_options;
|
|
1955
|
+
delete options.prompt_cache_retention;
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1400
1958
|
static stripContentTypeHeader(headers = {}) {
|
|
1401
1959
|
return stripContentTypeHeader(headers);
|
|
1402
1960
|
}
|
|
@@ -1411,6 +1969,7 @@ class MixCustom {
|
|
|
1411
1969
|
|
|
1412
1970
|
async create({ config = {}, options = {} } = {}) {
|
|
1413
1971
|
try {
|
|
1972
|
+
this.sanitizeCacheOptions(options);
|
|
1414
1973
|
if (Array.isArray(options.messages)) {
|
|
1415
1974
|
options.messages = this.convertMessages(options.messages, config);
|
|
1416
1975
|
}
|
|
@@ -1422,9 +1981,7 @@ class MixCustom {
|
|
|
1422
1981
|
console.log('\n[REQUEST DETAILS]');
|
|
1423
1982
|
|
|
1424
1983
|
console.log('\n[CONFIG]');
|
|
1425
|
-
|
|
1426
|
-
delete configToLog.debug;
|
|
1427
|
-
console.log(ModelMix.formatJSON(configToLog));
|
|
1984
|
+
console.log(ModelMix.formatJSON(configForDebug(config)));
|
|
1428
1985
|
|
|
1429
1986
|
console.log('\n[OPTIONS]');
|
|
1430
1987
|
console.log(ModelMix.formatJSON(request.options));
|
|
@@ -1444,11 +2001,11 @@ class MixCustom {
|
|
|
1444
2001
|
}));
|
|
1445
2002
|
}
|
|
1446
2003
|
} catch (error) {
|
|
1447
|
-
throw this.handleError(error
|
|
2004
|
+
throw this.handleError(error);
|
|
1448
2005
|
}
|
|
1449
2006
|
}
|
|
1450
2007
|
|
|
1451
|
-
handleError(error
|
|
2008
|
+
handleError(error) {
|
|
1452
2009
|
let errorMessage = 'An error occurred in MixCustom';
|
|
1453
2010
|
let statusCode = null;
|
|
1454
2011
|
let errorDetails = null;
|
|
@@ -1462,12 +2019,10 @@ class MixCustom {
|
|
|
1462
2019
|
}
|
|
1463
2020
|
|
|
1464
2021
|
const formattedError = {
|
|
1465
|
-
message: errorMessage,
|
|
2022
|
+
message: redactSecret(errorMessage, this.config.apiKey),
|
|
1466
2023
|
statusCode,
|
|
1467
|
-
details: errorDetails,
|
|
1468
|
-
stack: error.stack,
|
|
1469
|
-
config: config,
|
|
1470
|
-
options: options
|
|
2024
|
+
details: redactSecret(errorDetails, this.config.apiKey),
|
|
2025
|
+
stack: redactSecret(error.stack, this.config.apiKey)
|
|
1471
2026
|
};
|
|
1472
2027
|
|
|
1473
2028
|
return formattedError;
|
|
@@ -1586,19 +2141,15 @@ class MixCustom {
|
|
|
1586
2141
|
static extractTokens(data) {
|
|
1587
2142
|
// OpenAI/Groq/Together/Lambda/Cerebras/Fireworks format
|
|
1588
2143
|
if (data.usage) {
|
|
1589
|
-
return {
|
|
2144
|
+
return ModelMix.normalizeTokenUsage({
|
|
1590
2145
|
input: data.usage.prompt_tokens || 0,
|
|
1591
2146
|
output: data.usage.completion_tokens || 0,
|
|
1592
|
-
total: data.usage.total_tokens
|
|
1593
|
-
cached: ModelMix.extractCacheTokens(data.usage)
|
|
1594
|
-
|
|
2147
|
+
total: data.usage.total_tokens,
|
|
2148
|
+
cached: ModelMix.extractCacheTokens(data.usage),
|
|
2149
|
+
cacheWrite: ModelMix.extractCacheWriteTokens(data.usage)
|
|
2150
|
+
});
|
|
1595
2151
|
}
|
|
1596
|
-
return
|
|
1597
|
-
input: 0,
|
|
1598
|
-
output: 0,
|
|
1599
|
-
total: 0,
|
|
1600
|
-
cached: 0
|
|
1601
|
-
};
|
|
2152
|
+
return ModelMix.normalizeTokenUsage();
|
|
1602
2153
|
}
|
|
1603
2154
|
|
|
1604
2155
|
processResponse(response) {
|
|
@@ -1617,6 +2168,11 @@ class MixCustom {
|
|
|
1617
2168
|
}
|
|
1618
2169
|
|
|
1619
2170
|
class MixOpenAI extends MixCustom {
|
|
2171
|
+
sanitizeCacheOptions(options) {
|
|
2172
|
+
delete options.cache_control;
|
|
2173
|
+
delete options.prompt_cache_options;
|
|
2174
|
+
}
|
|
2175
|
+
|
|
1620
2176
|
getDefaultConfig(customConfig) {
|
|
1621
2177
|
|
|
1622
2178
|
if (!process.env.OPENAI_API_KEY) {
|
|
@@ -1690,22 +2246,26 @@ class MixOpenAI extends MixCustom {
|
|
|
1690
2246
|
continue;
|
|
1691
2247
|
}
|
|
1692
2248
|
|
|
2249
|
+
let convertedMessage = { ...message };
|
|
1693
2250
|
if (Array.isArray(message.content)) {
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
2251
|
+
convertedMessage = {
|
|
2252
|
+
...message,
|
|
2253
|
+
content: message.content.filter(content => content !== null && content !== undefined).map(content => {
|
|
2254
|
+
if (content && content.type === 'image') {
|
|
2255
|
+
const { media_type, data } = content.source;
|
|
2256
|
+
return {
|
|
2257
|
+
type: 'image_url',
|
|
2258
|
+
image_url: {
|
|
2259
|
+
url: `data:${media_type};base64,${data}`
|
|
2260
|
+
}
|
|
2261
|
+
};
|
|
2262
|
+
}
|
|
2263
|
+
return stripContentCacheMetadata(content);
|
|
2264
|
+
})
|
|
2265
|
+
};
|
|
1706
2266
|
}
|
|
1707
2267
|
|
|
1708
|
-
results.push(
|
|
2268
|
+
results.push(convertedMessage);
|
|
1709
2269
|
}
|
|
1710
2270
|
|
|
1711
2271
|
return results;
|
|
@@ -1765,10 +2325,14 @@ class MixOpenAIResponses extends MixOpenAI {
|
|
|
1765
2325
|
}
|
|
1766
2326
|
|
|
1767
2327
|
static buildResponsesRequest(options = {}, config = {}) {
|
|
1768
|
-
const
|
|
2328
|
+
const isGPT56 = typeof options.model === 'string' && options.model.startsWith('gpt-5.6');
|
|
2329
|
+
const input = MixOpenAIResponses.messagesToResponsesInput(options.messages, {
|
|
2330
|
+
translateNeutralCache: isGPT56
|
|
2331
|
+
});
|
|
1769
2332
|
if (config.system) {
|
|
1770
2333
|
input.unshift({ role: 'developer', content: [{ type: 'input_text', text: config.system }] });
|
|
1771
2334
|
}
|
|
2335
|
+
MixOpenAIResponses.validatePromptCaching(options, input);
|
|
1772
2336
|
const request = {
|
|
1773
2337
|
model: options.model,
|
|
1774
2338
|
input,
|
|
@@ -1812,10 +2376,52 @@ class MixOpenAIResponses extends MixOpenAI {
|
|
|
1812
2376
|
if (options.user !== undefined) request.user = options.user;
|
|
1813
2377
|
if (options.prompt_cache_key !== undefined) request.prompt_cache_key = options.prompt_cache_key;
|
|
1814
2378
|
if (options.prompt_cache_retention !== undefined) request.prompt_cache_retention = options.prompt_cache_retention;
|
|
2379
|
+
if (options.prompt_cache_options !== undefined) request.prompt_cache_options = options.prompt_cache_options;
|
|
1815
2380
|
|
|
1816
2381
|
return request;
|
|
1817
2382
|
}
|
|
1818
2383
|
|
|
2384
|
+
static validatePromptCaching(options, input) {
|
|
2385
|
+
const isGPT56 = typeof options.model === 'string' && options.model.startsWith('gpt-5.6');
|
|
2386
|
+
const cacheOptions = options.prompt_cache_options;
|
|
2387
|
+
const breakpoints = input.flatMap(message => Array.isArray(message.content)
|
|
2388
|
+
? message.content
|
|
2389
|
+
.filter(block => block?.prompt_cache_breakpoint !== undefined)
|
|
2390
|
+
.map(block => block.prompt_cache_breakpoint)
|
|
2391
|
+
: []);
|
|
2392
|
+
|
|
2393
|
+
if (isGPT56 && options.prompt_cache_retention !== undefined) {
|
|
2394
|
+
throw new Error('GPT-5.6 does not support prompt_cache_retention; use prompt_cache_options.ttl instead.');
|
|
2395
|
+
}
|
|
2396
|
+
if (!isGPT56 && cacheOptions !== undefined) {
|
|
2397
|
+
throw new Error('prompt_cache_options is only supported by GPT-5.6 models.');
|
|
2398
|
+
}
|
|
2399
|
+
if (!isGPT56 && breakpoints.length > 0) {
|
|
2400
|
+
throw new Error('prompt_cache_breakpoint is only supported by GPT-5.6 models.');
|
|
2401
|
+
}
|
|
2402
|
+
if (cacheOptions !== undefined) {
|
|
2403
|
+
if (!isPlainObject(cacheOptions)) {
|
|
2404
|
+
throw new TypeError('prompt_cache_options must be a plain non-null object.');
|
|
2405
|
+
}
|
|
2406
|
+
if (cacheOptions.mode !== undefined
|
|
2407
|
+
&& cacheOptions.mode !== 'implicit'
|
|
2408
|
+
&& cacheOptions.mode !== 'explicit') {
|
|
2409
|
+
throw new TypeError('prompt_cache_options.mode must be "implicit" or "explicit".');
|
|
2410
|
+
}
|
|
2411
|
+
if (cacheOptions.ttl !== undefined && cacheOptions.ttl !== '30m') {
|
|
2412
|
+
throw new TypeError('prompt_cache_options.ttl must be "30m".');
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
for (const breakpoint of breakpoints) {
|
|
2416
|
+
if (!isPlainObject(breakpoint)) {
|
|
2417
|
+
throw new TypeError('prompt_cache_breakpoint must be a plain non-null object.');
|
|
2418
|
+
}
|
|
2419
|
+
if (breakpoint.mode !== 'explicit') {
|
|
2420
|
+
throw new TypeError('prompt_cache_breakpoint mode must be "explicit".');
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
|
|
1819
2425
|
static processResponsesResponse(response) {
|
|
1820
2426
|
const message = MixOpenAIResponses.extractResponsesMessage(response.data);
|
|
1821
2427
|
return {
|
|
@@ -1829,19 +2435,15 @@ class MixOpenAIResponses extends MixOpenAI {
|
|
|
1829
2435
|
|
|
1830
2436
|
static extractResponsesTokens(data) {
|
|
1831
2437
|
if (data.usage) {
|
|
1832
|
-
return {
|
|
2438
|
+
return ModelMix.normalizeTokenUsage({
|
|
1833
2439
|
input: data.usage.input_tokens || 0,
|
|
1834
2440
|
output: data.usage.output_tokens || 0,
|
|
1835
|
-
total: data.usage.total_tokens
|
|
1836
|
-
cached: ModelMix.extractCacheTokens(data.usage)
|
|
1837
|
-
|
|
2441
|
+
total: data.usage.total_tokens,
|
|
2442
|
+
cached: ModelMix.extractCacheTokens(data.usage),
|
|
2443
|
+
cacheWrite: ModelMix.extractCacheWriteTokens(data.usage)
|
|
2444
|
+
});
|
|
1838
2445
|
}
|
|
1839
|
-
return
|
|
1840
|
-
input: 0,
|
|
1841
|
-
output: 0,
|
|
1842
|
-
total: 0,
|
|
1843
|
-
cached: 0
|
|
1844
|
-
};
|
|
2446
|
+
return ModelMix.normalizeTokenUsage();
|
|
1845
2447
|
}
|
|
1846
2448
|
|
|
1847
2449
|
static extractResponsesMessage(data) {
|
|
@@ -1855,27 +2457,70 @@ class MixOpenAIResponses extends MixOpenAI {
|
|
|
1855
2457
|
.trim();
|
|
1856
2458
|
}
|
|
1857
2459
|
|
|
1858
|
-
static messagesToResponsesInput(messages = []) {
|
|
2460
|
+
static messagesToResponsesInput(messages = [], { translateNeutralCache = false } = {}) {
|
|
1859
2461
|
const mapped = [];
|
|
1860
2462
|
|
|
1861
2463
|
for (const message of messages) {
|
|
1862
2464
|
if (!message || !message.role) continue;
|
|
1863
2465
|
if (message.tool_calls || message.role === 'tool') continue;
|
|
1864
2466
|
|
|
1865
|
-
|
|
2467
|
+
const content = [];
|
|
2468
|
+
const isAssistant = message.role === 'assistant';
|
|
2469
|
+
const textType = isAssistant ? 'output_text' : 'input_text';
|
|
1866
2470
|
if (typeof message.content === 'string') {
|
|
1867
|
-
text
|
|
2471
|
+
if (message.content) content.push({ type: textType, text: message.content });
|
|
1868
2472
|
} else if (Array.isArray(message.content)) {
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
2473
|
+
for (const item of message.content) {
|
|
2474
|
+
if (!item || typeof item !== 'object') continue;
|
|
2475
|
+
const neutralCache = item.cache !== undefined
|
|
2476
|
+
? normalizeContentCache(item.cache)
|
|
2477
|
+
: undefined;
|
|
2478
|
+
const promptCacheBreakpoint = item.prompt_cache_breakpoint !== undefined
|
|
2479
|
+
? item.prompt_cache_breakpoint
|
|
2480
|
+
: (translateNeutralCache && neutralCache?.breakpoint
|
|
2481
|
+
? { mode: 'explicit' }
|
|
2482
|
+
: undefined);
|
|
2483
|
+
const breakpoint = !isAssistant && promptCacheBreakpoint !== undefined
|
|
2484
|
+
? { prompt_cache_breakpoint: promptCacheBreakpoint }
|
|
2485
|
+
: {};
|
|
2486
|
+
|
|
2487
|
+
if ((item.type === 'text' || item.type === 'input_text' || item.type === 'output_text')
|
|
2488
|
+
&& typeof item.text === 'string') {
|
|
2489
|
+
content.push({ type: textType, text: item.text, ...breakpoint });
|
|
2490
|
+
continue;
|
|
2491
|
+
}
|
|
2492
|
+
if (item.type === 'image' && item.source) {
|
|
2493
|
+
let imageUrl;
|
|
2494
|
+
if (item.source.type === 'base64') {
|
|
2495
|
+
if (!item.source.media_type || typeof item.source.data !== 'string') {
|
|
2496
|
+
throw new TypeError('Responses base64 images require source.media_type and string source.data.');
|
|
2497
|
+
}
|
|
2498
|
+
imageUrl = `data:${item.source.media_type};base64,${item.source.data}`;
|
|
2499
|
+
} else if (item.source.type === 'url' && typeof item.source.data === 'string') {
|
|
2500
|
+
imageUrl = item.source.data;
|
|
2501
|
+
} else {
|
|
2502
|
+
throw new TypeError('Responses images must be processed to base64 or use a URL source.');
|
|
2503
|
+
}
|
|
2504
|
+
content.push({ type: 'input_image', image_url: imageUrl, ...breakpoint });
|
|
2505
|
+
continue;
|
|
2506
|
+
}
|
|
2507
|
+
if (item.type === 'image_url' && typeof item.image_url?.url === 'string') {
|
|
2508
|
+
content.push({ type: 'input_image', image_url: item.image_url.url, ...breakpoint });
|
|
2509
|
+
continue;
|
|
2510
|
+
}
|
|
2511
|
+
if (item.type === 'input_image' || item.type === 'input_file') {
|
|
2512
|
+
content.push({
|
|
2513
|
+
...stripContentCacheMetadata(item),
|
|
2514
|
+
...breakpoint
|
|
2515
|
+
});
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
1873
2518
|
}
|
|
1874
2519
|
|
|
1875
|
-
if (
|
|
2520
|
+
if (content.length === 0) continue;
|
|
1876
2521
|
mapped.push({
|
|
1877
2522
|
role: message.role,
|
|
1878
|
-
content
|
|
2523
|
+
content
|
|
1879
2524
|
});
|
|
1880
2525
|
}
|
|
1881
2526
|
|
|
@@ -1928,9 +2573,7 @@ class MixOpenAIWebSocket extends MixOpenAIResponses {
|
|
|
1928
2573
|
reject({
|
|
1929
2574
|
message: `Realtime WebSocket timed out after ${timeoutMs}ms`,
|
|
1930
2575
|
statusCode: null,
|
|
1931
|
-
details: null
|
|
1932
|
-
config: mergedConfig,
|
|
1933
|
-
options
|
|
2576
|
+
details: null
|
|
1934
2577
|
});
|
|
1935
2578
|
}, timeoutMs);
|
|
1936
2579
|
|
|
@@ -2024,9 +2667,7 @@ class MixOpenAIWebSocket extends MixOpenAIResponses {
|
|
|
2024
2667
|
reject({
|
|
2025
2668
|
message: event.error?.message || 'Realtime WebSocket error',
|
|
2026
2669
|
statusCode: null,
|
|
2027
|
-
details: event.error || event
|
|
2028
|
-
config: mergedConfig,
|
|
2029
|
-
options
|
|
2670
|
+
details: event.error || event
|
|
2030
2671
|
});
|
|
2031
2672
|
}
|
|
2032
2673
|
});
|
|
@@ -2039,9 +2680,7 @@ class MixOpenAIWebSocket extends MixOpenAIResponses {
|
|
|
2039
2680
|
message: error.message || 'Realtime WebSocket connection error',
|
|
2040
2681
|
statusCode: null,
|
|
2041
2682
|
details: null,
|
|
2042
|
-
stack: error.stack
|
|
2043
|
-
config: mergedConfig,
|
|
2044
|
-
options
|
|
2683
|
+
stack: error.stack
|
|
2045
2684
|
});
|
|
2046
2685
|
});
|
|
2047
2686
|
|
|
@@ -2052,9 +2691,7 @@ class MixOpenAIWebSocket extends MixOpenAIResponses {
|
|
|
2052
2691
|
reject({
|
|
2053
2692
|
message: 'Realtime WebSocket closed before response.done',
|
|
2054
2693
|
statusCode: null,
|
|
2055
|
-
details: null
|
|
2056
|
-
config: mergedConfig,
|
|
2057
|
-
options
|
|
2694
|
+
details: null
|
|
2058
2695
|
});
|
|
2059
2696
|
});
|
|
2060
2697
|
});
|
|
@@ -2155,6 +2792,23 @@ class MixKimi extends MixOpenAI {
|
|
|
2155
2792
|
|
|
2156
2793
|
class MixAnthropic extends MixCustom {
|
|
2157
2794
|
|
|
2795
|
+
sanitizeCacheOptions(options) {
|
|
2796
|
+
delete options.prompt_cache_key;
|
|
2797
|
+
delete options.prompt_cache_options;
|
|
2798
|
+
delete options.prompt_cache_retention;
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
static validateCacheControl(cacheControl) {
|
|
2802
|
+
if (!isPlainObject(cacheControl) || cacheControl.type !== 'ephemeral') {
|
|
2803
|
+
throw new TypeError('Anthropic cache_control must have type "ephemeral".');
|
|
2804
|
+
}
|
|
2805
|
+
if (cacheControl.ttl !== undefined
|
|
2806
|
+
&& cacheControl.ttl !== '5m'
|
|
2807
|
+
&& cacheControl.ttl !== '1h') {
|
|
2808
|
+
throw new TypeError('Anthropic cache_control.ttl must be "5m" or "1h".');
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
|
|
2158
2812
|
/**
|
|
2159
2813
|
* Opus 4.7+ and Claude 5 family reject sampling params (temperature/top_p/top_k).
|
|
2160
2814
|
* See: https://platform.claude.com/docs/en/about-claude/models/migration-guide
|
|
@@ -2200,10 +2854,20 @@ class MixAnthropic extends MixCustom {
|
|
|
2200
2854
|
delete options.top_k;
|
|
2201
2855
|
}
|
|
2202
2856
|
|
|
2857
|
+
const requestConfig = { ...config };
|
|
2858
|
+
if (hasNeutralCacheBreakpoint(options.messages)) {
|
|
2859
|
+
const contentCacheControl = options.cache_control ?? { type: 'ephemeral' };
|
|
2860
|
+
MixAnthropic.validateCacheControl(contentCacheControl);
|
|
2861
|
+
requestConfig._contentCacheControl = { ...contentCacheControl };
|
|
2862
|
+
delete options.cache_control;
|
|
2863
|
+
} else if (options.cache_control !== undefined) {
|
|
2864
|
+
MixAnthropic.validateCacheControl(options.cache_control);
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2203
2867
|
options.system = config.system;
|
|
2204
2868
|
|
|
2205
2869
|
try {
|
|
2206
|
-
return await super.create({ config, options });
|
|
2870
|
+
return await super.create({ config: requestConfig, options });
|
|
2207
2871
|
} catch (error) {
|
|
2208
2872
|
// Log the error details for debugging
|
|
2209
2873
|
if (error.response && error.response.data) {
|
|
@@ -2275,20 +2939,37 @@ class MixAnthropic extends MixCustom {
|
|
|
2275
2939
|
|
|
2276
2940
|
// Handle content conversion for other messages
|
|
2277
2941
|
if (message.content && Array.isArray(message.content)) {
|
|
2278
|
-
|
|
2942
|
+
const content = message.content.filter(content => content !== null && content !== undefined).map(content => {
|
|
2943
|
+
const neutralCache = content?.cache !== undefined
|
|
2944
|
+
? normalizeContentCache(content.cache)
|
|
2945
|
+
: undefined;
|
|
2946
|
+
if (neutralCache && content.cache_control !== undefined) {
|
|
2947
|
+
throw new TypeError('Use either cache or cache_control on an Anthropic content block, not both.');
|
|
2948
|
+
}
|
|
2949
|
+
let converted = content;
|
|
2279
2950
|
if (content && content.type === 'function') {
|
|
2280
|
-
|
|
2951
|
+
converted = {
|
|
2281
2952
|
type: 'tool_use',
|
|
2282
2953
|
id: content.id,
|
|
2283
2954
|
name: content.function.name,
|
|
2284
2955
|
input: JSON.parse(content.function.arguments)
|
|
2285
|
-
}
|
|
2956
|
+
};
|
|
2286
2957
|
}
|
|
2287
|
-
|
|
2958
|
+
const sanitized = stripContentCacheMetadata(converted);
|
|
2959
|
+
if (content.cache_control !== undefined) {
|
|
2960
|
+
MixAnthropic.validateCacheControl(content.cache_control);
|
|
2961
|
+
sanitized.cache_control = { ...content.cache_control };
|
|
2962
|
+
} else if (neutralCache?.breakpoint) {
|
|
2963
|
+
sanitized.cache_control = {
|
|
2964
|
+
...(config?._contentCacheControl || { type: 'ephemeral' })
|
|
2965
|
+
};
|
|
2966
|
+
}
|
|
2967
|
+
return sanitized;
|
|
2288
2968
|
});
|
|
2969
|
+
return { ...message, content };
|
|
2289
2970
|
}
|
|
2290
2971
|
|
|
2291
|
-
return message;
|
|
2972
|
+
return { ...message };
|
|
2292
2973
|
});
|
|
2293
2974
|
}
|
|
2294
2975
|
|
|
@@ -2370,19 +3051,26 @@ class MixAnthropic extends MixCustom {
|
|
|
2370
3051
|
static extractTokens(data) {
|
|
2371
3052
|
// Anthropic format
|
|
2372
3053
|
if (data.usage) {
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
3054
|
+
const cached = ModelMix.extractCacheTokens(data.usage);
|
|
3055
|
+
const cacheWrite5m = data.usage.cache_creation?.ephemeral_5m_input_tokens ?? 0;
|
|
3056
|
+
const cacheWrite1h = data.usage.cache_creation?.ephemeral_1h_input_tokens ?? 0;
|
|
3057
|
+
const cacheWrite = Math.max(
|
|
3058
|
+
ModelMix.extractCacheWriteTokens(data.usage),
|
|
3059
|
+
cacheWrite5m + cacheWrite1h
|
|
3060
|
+
);
|
|
3061
|
+
const input = (data.usage.input_tokens || 0) + cached + cacheWrite;
|
|
3062
|
+
const output = data.usage.output_tokens || 0;
|
|
3063
|
+
return ModelMix.normalizeTokenUsage({
|
|
3064
|
+
input,
|
|
3065
|
+
output,
|
|
3066
|
+
total: input + output,
|
|
3067
|
+
cached,
|
|
3068
|
+
cacheWrite,
|
|
3069
|
+
cacheWrite5m,
|
|
3070
|
+
cacheWrite1h
|
|
3071
|
+
});
|
|
2379
3072
|
}
|
|
2380
|
-
return
|
|
2381
|
-
input: 0,
|
|
2382
|
-
output: 0,
|
|
2383
|
-
total: 0,
|
|
2384
|
-
cached: 0
|
|
2385
|
-
};
|
|
3073
|
+
return ModelMix.normalizeTokenUsage();
|
|
2386
3074
|
}
|
|
2387
3075
|
|
|
2388
3076
|
processResponse(response) {
|
|
@@ -2566,6 +3254,13 @@ class MixGrok extends MixOpenAI {
|
|
|
2566
3254
|
...customConfig
|
|
2567
3255
|
});
|
|
2568
3256
|
}
|
|
3257
|
+
|
|
3258
|
+
async create({ config = {}, options = {} } = {}) {
|
|
3259
|
+
if (options.model === GROK420_REASONING || options.model === GROK420_NON_REASONING) {
|
|
3260
|
+
delete options.reasoning_effort;
|
|
3261
|
+
}
|
|
3262
|
+
return super.create({ config, options });
|
|
3263
|
+
}
|
|
2569
3264
|
}
|
|
2570
3265
|
|
|
2571
3266
|
class MixLambda extends MixCustom {
|
|
@@ -2740,6 +3435,7 @@ class MixGoogle extends MixCustom {
|
|
|
2740
3435
|
return super.getDefaultConfig({
|
|
2741
3436
|
url: 'https://generativelanguage.googleapis.com/v1beta/models',
|
|
2742
3437
|
apiKey: process.env.GEMINI_API_KEY,
|
|
3438
|
+
...customConfig
|
|
2743
3439
|
});
|
|
2744
3440
|
}
|
|
2745
3441
|
|
|
@@ -2909,9 +3605,7 @@ class MixGoogle extends MixCustom {
|
|
|
2909
3605
|
console.log('\n[REQUEST DETAILS - GOOGLE]');
|
|
2910
3606
|
|
|
2911
3607
|
console.log('\n[CONFIG]');
|
|
2912
|
-
|
|
2913
|
-
delete configToLog.debug;
|
|
2914
|
-
console.log(ModelMix.formatJSON(configToLog));
|
|
3608
|
+
console.log(ModelMix.formatJSON(configForDebug(config)));
|
|
2915
3609
|
|
|
2916
3610
|
console.log('\n[PAYLOAD]');
|
|
2917
3611
|
console.log(ModelMix.formatJSON(payload));
|
|
@@ -2927,7 +3621,7 @@ class MixGoogle extends MixCustom {
|
|
|
2927
3621
|
}));
|
|
2928
3622
|
}
|
|
2929
3623
|
} catch (error) {
|
|
2930
|
-
throw this.handleError(error
|
|
3624
|
+
throw this.handleError(error);
|
|
2931
3625
|
}
|
|
2932
3626
|
}
|
|
2933
3627
|
|
|
@@ -2965,19 +3659,15 @@ class MixGoogle extends MixCustom {
|
|
|
2965
3659
|
static extractTokens(data) {
|
|
2966
3660
|
// Google Gemini format
|
|
2967
3661
|
if (data.usageMetadata) {
|
|
2968
|
-
return {
|
|
3662
|
+
return ModelMix.normalizeTokenUsage({
|
|
2969
3663
|
input: data.usageMetadata.promptTokenCount || 0,
|
|
2970
3664
|
output: data.usageMetadata.candidatesTokenCount || 0,
|
|
2971
|
-
total: data.usageMetadata.totalTokenCount
|
|
2972
|
-
cached: ModelMix.extractCacheTokens(data.usageMetadata)
|
|
2973
|
-
|
|
3665
|
+
total: data.usageMetadata.totalTokenCount,
|
|
3666
|
+
cached: ModelMix.extractCacheTokens(data.usageMetadata),
|
|
3667
|
+
cacheWrite: ModelMix.extractCacheWriteTokens(data.usageMetadata)
|
|
3668
|
+
});
|
|
2974
3669
|
}
|
|
2975
|
-
return
|
|
2976
|
-
input: 0,
|
|
2977
|
-
output: 0,
|
|
2978
|
-
total: 0,
|
|
2979
|
-
cached: 0
|
|
2980
|
-
};
|
|
3670
|
+
return ModelMix.normalizeTokenUsage();
|
|
2981
3671
|
}
|
|
2982
3672
|
|
|
2983
3673
|
static stripUnsupportedSchemaProps(schema) {
|
|
@@ -3024,4 +3714,4 @@ class MixGoogle extends MixCustom {
|
|
|
3024
3714
|
}
|
|
3025
3715
|
}
|
|
3026
3716
|
|
|
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 };
|
|
3717
|
+
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 };
|