modelmix 4.7.2 → 5.0.0

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