modelmix 5.0.6 → 5.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -7,30 +7,52 @@ const { inspect } = require('util');
7
7
  const log = require('lemonlog')('ModelMix');
8
8
  const Bottleneck = require('bottleneck');
9
9
  const path = require('path');
10
- const WebSocket = require('ws');
11
10
  const generateJsonSchema = require('./schema');
12
11
  const { Client } = require("@modelcontextprotocol/sdk/client/index.js");
13
12
  const { StdioClientTransport } = require("@modelcontextprotocol/sdk/client/stdio.js");
14
13
  const { MCPToolsManager } = require('./mcp-tools');
14
+ const { fetchBinaryResponse } = require('./http-client');
15
+ const { isPlainObject } = require('./lib/object-utils');
16
+ const { normalizeContentCache } = require('./lib/content-cache');
17
+ const tokenUsage = require('./lib/token-usage');
18
+ const { parseChainModels } = require('./lib/model-chain');
15
19
  const {
16
- stripContentTypeHeader,
17
- createMultipartFormData,
18
- buildRequestBodyAndHeaders
19
- } = require('./multipart');
20
- const {
21
- fetchJsonResponse,
22
- fetchBinaryResponse,
23
- fetchStreamResponse
24
- } = require('./http-client');
20
+ validateTemplateData,
21
+ validateTemplateDataKey,
22
+ preprocessChoiceDirectives,
23
+ createTemplateRenderContext
24
+ } = require('./lib/template-engine');
25
25
  const {
26
26
  normalizeEffort,
27
27
  applyUnifiedEffort,
28
28
  resolveProviderFamily,
29
- resolveGrok420ModelKey,
30
- GROK420_REASONING,
31
- GROK420_NON_REASONING
29
+ resolveGrok420ModelKey
32
30
  } = require('./effort');
33
31
 
32
+ let MixCustom;
33
+ let MixOpenAI;
34
+ let MixModeration;
35
+ let MixOpenAIResponses;
36
+ let MixOpenAIModeration;
37
+ let MixOpenAIWebSocket;
38
+ let MixOpenRouter;
39
+ let MixKimi;
40
+ let MixAnthropic;
41
+ let MixMiniMax;
42
+ let MixMiMo;
43
+ let MixPerplexity;
44
+ let MixOllama;
45
+ let MixGrok;
46
+ let MixLambda;
47
+ let MixLMStudio;
48
+ let MixGroq;
49
+ let MixTogether;
50
+ let MixCerebras;
51
+ let MixFireworks;
52
+ let MixNVIDIA;
53
+ let MixGoogle;
54
+ let ModerationMix;
55
+
34
56
  const DEFAULT_RETRYABLE_STATUS_CODES = [408, 425, 429, 500, 502, 503, 504, 529];
35
57
 
36
58
  function getErrorStatusCode(error) {
@@ -41,12 +63,6 @@ function sleep(ms) {
41
63
  return new Promise(resolve => setTimeout(resolve, ms));
42
64
  }
43
65
 
44
- function isPlainObject(value) {
45
- if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
46
- const prototype = Object.getPrototypeOf(value);
47
- return prototype === Object.prototype || prototype === null;
48
- }
49
-
50
66
  function clonePluginValue(value, seen = new WeakMap()) {
51
67
  if (value === null || typeof value !== 'object') return value;
52
68
  if (Buffer.isBuffer(value)) return Buffer.from(value);
@@ -75,382 +91,7 @@ function validatePluginResult(result, pluginName) {
75
91
  return result;
76
92
  }
77
93
 
78
- function normalizeContentCache(cache) {
79
- if (cache !== undefined) {
80
- if (!isPlainObject(cache) || cache.breakpoint !== true) {
81
- throw new TypeError('cache must be { breakpoint: true }.');
82
- }
83
- return { breakpoint: true };
84
- }
85
- return undefined;
86
- }
87
-
88
- function stripContentCacheMetadata(content) {
89
- if (!content || typeof content !== 'object') return content;
90
- const sanitized = { ...content };
91
- delete sanitized.cache;
92
- delete sanitized.cache_control;
93
- delete sanitized.prompt_cache_breakpoint;
94
- return sanitized;
95
- }
96
-
97
- function hasNeutralCacheBreakpoint(messages = []) {
98
- return messages.some(message => Array.isArray(message?.content)
99
- && message.content.some(block => block?.cache?.breakpoint === true));
100
- }
101
-
102
- function validateTemplateData(value) {
103
- if (!isPlainObject(value)) {
104
- throw new TypeError('Template data must be a plain non-null object.');
105
- }
106
- if (Object.prototype.hasOwnProperty.call(value, '$mix')) {
107
- throw new TypeError('Template data key "$mix" is reserved.');
108
- }
109
- }
110
-
111
- function validateTemplateDataKey(key) {
112
- if (typeof key !== 'string' || key.length === 0) {
113
- throw new TypeError('Template data key must be a non-empty string.');
114
- }
115
- if (key === '$mix') {
116
- throw new TypeError('Template data key "$mix" is reserved.');
117
- }
118
- }
119
-
120
- function templateLocation({ filename, label }, lineNumber) {
121
- return `${filename || label} at line ${lineNumber}`;
122
- }
123
-
124
- function preprocessChoiceDirectives(source, { filename = null, label = 'template' } = {}) {
125
- const parts = source.split(/(\r\n|\n|\r)/);
126
- const blocks = [];
127
-
128
- for (let index = 0; index < parts.length; index += 2) {
129
- const line = parts[index];
130
- const trimmed = line.trim();
131
- const lineNumber = (index / 2) + 1;
132
- const location = templateLocation({ filename, label }, lineNumber);
133
- const newline = parts[index + 1] || '';
134
-
135
- if (/^<%\s*choice\s*%>$/.test(trimmed)) {
136
- const parent = blocks[blocks.length - 1];
137
- if (parent && parent.optionCount === 0) {
138
- throw new Error(`A nested choice must be inside an option (${location}).`);
139
- }
140
- blocks.push({ lineNumber, optionCount: 0, weighted: null });
141
- parts[index] = '<% $mix.choice(option => { -%>';
142
- continue;
143
- }
144
-
145
- const optionMatch = trimmed.match(/^<%\s*option(?:\s+(.+?))?\s*%>$/);
146
- if (optionMatch) {
147
- const block = blocks[blocks.length - 1];
148
- if (!block) {
149
- throw new Error(`Option directive must be inside a choice (${location}).`);
150
- }
151
-
152
- const weightText = optionMatch[1];
153
- const weighted = weightText !== undefined;
154
- if (block.weighted !== null && block.weighted !== weighted) {
155
- throw new Error(`Choice options must either all have weights or all omit them (${location}).`);
156
- }
157
-
158
- let argument = '';
159
- if (weighted) {
160
- const weight = Number(weightText);
161
- if (!Number.isFinite(weight) || weight <= 0) {
162
- throw new Error(`Choice weight must be a positive finite number (${location}).`);
163
- }
164
- argument = `${weight}, `;
165
- }
166
-
167
- block.weighted = weighted;
168
- parts[index] = `<% ${block.optionCount > 0 ? '}); ' : ''}option(${argument}() => { -%>`;
169
- block.optionCount += 1;
170
- continue;
171
- }
172
-
173
- if (/^<%\s*\/choice\s*%>$/.test(trimmed)) {
174
- const block = blocks.pop();
175
- if (!block) {
176
- throw new Error(`Closing choice directive has no matching opening directive (${location}).`);
177
- }
178
- if (block.optionCount === 0) {
179
- throw new Error(`Choice must contain at least one option (${location}).`);
180
- }
181
- parts[index] = '<% }); }); -%>';
182
- continue;
183
- }
184
-
185
- if (/^<%\s*(?:choice|option|\/choice)(?:\s|%>)/.test(trimmed)) {
186
- throw new Error(`Invalid choice directive (${location}).`);
187
- }
188
-
189
- const block = blocks[blocks.length - 1];
190
- if (block && block.optionCount === 0) {
191
- if (trimmed) {
192
- throw new Error(`Choice content must be inside an option (${location}).`);
193
- }
194
- parts[index] = '<%# -%>';
195
- }
196
-
197
- if (newline) parts[index + 1] = newline;
198
- }
199
-
200
- if (blocks.length > 0) {
201
- const block = blocks[blocks.length - 1];
202
- throw new Error(`Unclosed choice directive (${templateLocation({ filename, label }, block.lineNumber)}).`);
203
- }
204
-
205
- return parts.join('');
206
- }
207
-
208
- function createTemplateRenderContext(random = Math.random) {
209
- const choice = defineOptions => {
210
- if (typeof defineOptions !== 'function') {
211
- throw new TypeError('$mix.choice expects an option definition callback.');
212
- }
213
-
214
- const options = [];
215
- let weighted = null;
216
- const option = (weightOrRender, renderOption) => {
217
- const hasWeight = renderOption !== undefined;
218
- const weight = hasWeight ? weightOrRender : 1;
219
- const render = hasWeight ? renderOption : weightOrRender;
220
-
221
- if (weighted !== null && weighted !== hasWeight) {
222
- throw new TypeError('$mix.choice options cannot mix weighted and unweighted forms.');
223
- }
224
- if (!Number.isFinite(weight) || weight <= 0) {
225
- throw new TypeError('$mix.choice weights must be positive finite numbers.');
226
- }
227
- if (typeof render !== 'function') {
228
- throw new TypeError('$mix.choice options require a render callback.');
229
- }
230
-
231
- weighted = hasWeight;
232
- options.push({ weight, render });
233
- };
234
-
235
- defineOptions(option);
236
- if (options.length === 0) {
237
- throw new Error('$mix.choice requires at least one option.');
238
- }
239
-
240
- const totalWeight = options.reduce((sum, current) => sum + current.weight, 0);
241
- if (!Number.isFinite(totalWeight)) {
242
- throw new TypeError('$mix.choice total weight must be finite.');
243
- }
244
-
245
- let target = random() * totalWeight;
246
- for (const current of options) {
247
- target -= current.weight;
248
- if (target < 0) return current.render();
249
- }
250
- return options[options.length - 1].render();
251
- };
252
-
253
- return {
254
- helpers: Object.freeze({ choice }),
255
- renderedTemplateData: new Map(),
256
- renderedMessages: new Map(),
257
- renderedSystems: new Map()
258
- };
259
- }
260
-
261
- function configForDebug(config) {
262
- const safeConfig = { ...config };
263
- delete safeConfig.apiKey;
264
- delete safeConfig.debug;
265
- return safeConfig;
266
- }
267
-
268
- function redactSecret(value, secret, seen = new WeakSet()) {
269
- if (!secret) return value;
270
- if (typeof value === 'string') return value.split(secret).join('[REDACTED]');
271
- if (!value || typeof value !== 'object') return value;
272
- if (seen.has(value)) return '[Circular]';
273
-
274
- seen.add(value);
275
- if (Array.isArray(value)) {
276
- return value.map(item => redactSecret(item, secret, seen));
277
- }
278
- return Object.fromEntries(
279
- Object.entries(value).map(([key, item]) => [key, redactSecret(item, secret, seen)])
280
- );
281
- }
282
-
283
- // Pricing per 1M tokens in USD
284
- // Based on provider pricing pages linked in README
285
- const GPT56_LONG_CONTEXT_PRICING = Object.freeze({
286
- inputThreshold: 272_000,
287
- inputMultiplier: 2,
288
- outputMultiplier: 1.5
289
- });
290
-
291
- const GROK46_LONG_CONTEXT_PRICING = Object.freeze({
292
- inputThreshold: 200_000,
293
- inputMultiplier: 2,
294
- outputMultiplier: 2,
295
- inclusive: true
296
- });
297
-
298
- function usesLongContextRates(pricing, inputTokens) {
299
- const longContext = pricing.longContext;
300
- if (!longContext) return false;
301
- return longContext.inclusive
302
- ? inputTokens >= longContext.inputThreshold
303
- : inputTokens > longContext.inputThreshold;
304
- }
305
-
306
- const MODEL_PRICING = {
307
- // OpenAI
308
- 'gpt-realtime-mini': { input: 0.60, cachedInput: 0.06, output: 2.40 },
309
- 'gpt-realtime': { input: 4.00, cachedInput: 0.40, output: 16.00 },
310
- 'gpt-5.6-sol': { input: 5.00, cachedInput: 0.50, cacheWrite: 6.25, output: 30.00, longContext: GPT56_LONG_CONTEXT_PRICING },
311
- 'gpt-5.6-terra': { input: 2.00, cachedInput: 0.20, cacheWrite: 2.50, output: 12.00, longContext: GPT56_LONG_CONTEXT_PRICING },
312
- 'gpt-5.6-luna': { input: 0.20, cachedInput: 0.02, cacheWrite: 0.25, output: 1.20, longContext: GPT56_LONG_CONTEXT_PRICING },
313
- 'gpt-5.5-pro': { input: 30.00, output: 180.00 },
314
- 'gpt-5.5': { input: 5.00, cachedInput: 0.50, output: 30.00 },
315
- 'gpt-5.4': { input: 2.50, cachedInput: 0.25, output: 15.00 },
316
- 'gpt-5.4-pro': { input: 30.00, output: 180.00 },
317
- 'gpt-5.4-mini': { input: 0.75, cachedInput: 0.075, output: 4.50 },
318
- 'gpt-5.4-nano': { input: 0.20, cachedInput: 0.02, output: 1.25 },
319
- 'gpt-5.3-codex': { input: 1.75, cachedInput: 0.175, output: 14.00 },
320
- 'gpt-5.2': { input: 1.75, cachedInput: 0.175, output: 14.00 },
321
- 'gpt-5.2-chat-latest': { input: 1.75, cachedInput: 0.175, output: 14.00 },
322
- 'gpt-5.1': { input: 1.25, cachedInput: 0.125, output: 10.00 },
323
- 'gpt-5': { input: 1.25, cachedInput: 0.125, output: 10.00 },
324
- 'gpt-5-mini': { input: 0.25, cachedInput: 0.025, output: 2.00 },
325
- 'gpt-5-nano': { input: 0.05, cachedInput: 0.005, output: 0.40 },
326
- 'gpt-4.1': { input: 2.00, cachedInput: 0.50, output: 8.00 },
327
- 'gpt-4.1-mini': { input: 0.40, cachedInput: 0.10, output: 1.60 },
328
- 'gpt-4.1-nano': { input: 0.10, cachedInput: 0.025, output: 0.40 },
329
- // gptOss (Together/Groq/Cerebras/OpenRouter)
330
- 'openai/gpt-oss-120b': { input: 0.15, output: 0.60 },
331
- 'gpt-oss-120b': { input: 0.15, output: 0.60 },
332
- 'openai/gpt-oss-120b:free': { input: 0, output: 0 },
333
- // Anthropic
334
- 'claude-fable-5': { input: 10.00, cachedInput: 1.00, cacheWrite: 12.50, cacheWrite1h: 20.00, output: 50.00 },
335
- 'claude-opus-5': { input: 5.00, cachedInput: 0.50, cacheWrite: 6.25, cacheWrite1h: 10.00, output: 25.00 },
336
- 'claude-sonnet-5': { input: 3.00, cachedInput: 0.30, cacheWrite: 3.75, cacheWrite1h: 6.00, output: 15.00 },
337
- 'claude-opus-4-8': { input: 5.00, cachedInput: 0.50, cacheWrite: 6.25, cacheWrite1h: 10.00, output: 25.00 },
338
- 'claude-opus-4-7': { input: 5.00, cachedInput: 0.50, cacheWrite: 6.25, cacheWrite1h: 10.00, output: 25.00 },
339
- 'claude-opus-4-6': { input: 5.00, cachedInput: 0.50, cacheWrite: 6.25, cacheWrite1h: 10.00, output: 25.00 },
340
- 'claude-sonnet-4-6': { input: 3.00, cachedInput: 0.30, cacheWrite: 3.75, cacheWrite1h: 6.00, output: 15.00 },
341
- 'claude-sonnet-4-5-20250929': { input: 3.00, cachedInput: 0.30, cacheWrite: 3.75, cacheWrite1h: 6.00, output: 15.00 },
342
- 'claude-haiku-4-5-20251001': { input: 1.00, cachedInput: 0.10, cacheWrite: 1.25, cacheWrite1h: 2.00, output: 5.00 },
343
- // Google
344
- 'gemini-3.1-pro-preview': { input: 2.00, output: 12.00 },
345
- 'gemini-3-pro-preview': { input: 2.00, output: 12.00 },
346
- 'gemini-3-flash-preview': { input: 0.50, output: 3.00 },
347
- 'gemini-3.7-flash': { input: 0.75, cachedInput: 0.075, output: 3.75 },
348
- 'gemini-3.6-flash': { input: 0.75, cachedInput: 0.075, output: 3.75 },
349
- 'gemini-3.5-flash': { input: 0.75, output: 4.50 },
350
- 'gemini-3.5-flash-lite': { input: 0.30, output: 2.50 },
351
- 'gemini-2.5-pro': { input: 1.25, output: 10.00 },
352
- 'gemini-2.5-flash': { input: 0.30, output: 2.50 },
353
- 'gemini-3.1-flash-lite-preview': { input: 0.25, output: 1.50 },
354
- // Grok
355
- 'grok-4.6': { input: 2.00, cachedInput: 0.50, output: 6.00, longContext: GROK46_LONG_CONTEXT_PRICING },
356
- 'grok-4.5': { input: 2.00, output: 6.00 },
357
- 'grok-4.3': { input: 1.25, output: 2.50 },
358
- 'grok-4.20-multi-agent-0309': { input: 1.25, output: 2.50 },
359
- 'grok-4.20-0309': { input: 1.25, output: 2.50 },
360
- 'grok-4.20-0309-reasoning': { input: 1.25, output: 2.50 },
361
- 'grok-4.20-0309-non-reasoning': { input: 1.25, output: 2.50 },
362
- // Fireworks
363
- 'accounts/fireworks/models/deepseek-v4-flash': { input: 0.14, output: 0.28 },
364
- 'accounts/fireworks/models/deepseek-v4-pro': { input: 1.74, output: 3.48 },
365
- 'accounts/fireworks/models/deepseek-v4-pro-0813': { input: 1.32, cachedInput: 0.044, output: 3.96 },
366
- 'deepseek-ai/DeepSeek-V4-Flash': { input: 0.14, output: 0.28 },
367
- 'deepseek-ai/DeepSeek-V4-Pro': { input: 2.10, output: 4.40 },
368
- 'deepseek/deepseek-v4-flash': { input: 0.09, output: 0.18 },
369
- 'accounts/fireworks/models/glm-4p7': { input: 0.55, output: 2.19 },
370
- 'accounts/fireworks/models/glm-5p1': { input: 1.05, output: 3.50 },
371
- 'zai-org/GLM-5.2': { input: 1.40, output: 4.40 },
372
- 'accounts/fireworks/models/kimi-k2p5': { input: 0.50, output: 2.80 },
373
- 'qwen/qwen3.5-397b-a17b': { input: 0.385, output: 2.45 },
374
- 'accounts/fireworks/models/qwen3p6-plus': { input: 0.50, output: 3.00 },
375
- 'Qwen/Qwen3.6-Plus': { input: 0.50, output: 3.00 },
376
- 'accounts/fireworks/models/qwen3p7-plus': { input: 0.40, output: 1.60 },
377
- 'qwen/qwen3.7-plus': { input: 0.32, output: 1.28 },
378
- 'accounts/fireworks/models/qwen3p8-2p4t-a95b': { input: 2.00, cachedInput: 0.25, output: 6.00 },
379
- 'qwen/qwen3.8-max': { input: 2.00, output: 6.00 },
380
- // MiniMax
381
- 'MiniMax-M2.5': { input: 0.30, output: 1.20 },
382
- 'MiniMax-M2.7': { input: 0.30, output: 1.20 },
383
- 'MiniMax-M3': { input: 0.30, output: 1.20 },
384
- 'minimax/minimax-m2.7': { input: 0.30, output: 1.20 },
385
- 'minimax/minimax-m3': { input: 0.30, output: 1.20 },
386
- 'MiniMaxAI/MiniMax-M3': { input: 0.30, output: 1.20 },
387
- // Perplexity
388
- 'sonar': { input: 1.00, output: 1.00 },
389
- 'sonar-pro': { input: 3.00, output: 15.00 },
390
- // Hermes 4 (OpenRouter)
391
- 'nousresearch/hermes-4-70b': { input: 0.13, output: 0.40 },
392
- 'nousresearch/hermes-4-405b': { input: 1.00, output: 3.00 },
393
- // Hermes 3 (Lambda/OpenRouter)
394
- 'Hermes-3-Llama-3.1-405B-FP8': { input: 0.80, output: 0.80 },
395
- 'nousresearch/hermes-3-llama-3.1-405b:free': { input: 0, output: 0 },
396
- // Qwen3 (Together/Cerebras)
397
- 'Qwen/Qwen3-235B-A22B-fp8-tput': { input: 0.20, output: 0.60 },
398
- 'qwen-3-32b': { input: 0.20, output: 0.60 },
399
- // Kimi K2.5 (Together/Fireworks/OpenRouter)
400
- 'moonshotai/Kimi-K2.5': { input: 0.50, output: 2.80 },
401
- 'moonshotai/kimi-k2.5': { input: 0.50, output: 2.80 },
402
- // Kimi K3
403
- 'kimi-k3': { input: 3.00, output: 15.00 },
404
- 'moonshotai/kimi-k3': { input: 3.00, output: 15.00 },
405
- // GLM 4.7 (OpenRouter/Cerebras)
406
- 'z-ai/glm-4.7': { input: 0.55, output: 2.19 },
407
- 'zai-glm-4.7': { input: 0.55, output: 2.19 },
408
- };
409
-
410
- const CHAIN_MODEL_SHORTCUTS = new Set([
411
- 'gpt41', 'gpt41mini', 'gpt41nano', 'gpt5', 'gpt5mini', 'gpt5nano',
412
- 'gpt51', 'gpt52', 'gpt54', 'gpt54mini', 'gpt54nano', 'gpt54pro',
413
- 'gpt55', 'gpt55pro', 'gpt56sol', 'gpt56terra', 'gpt56luna',
414
- 'gptRealtime', 'gptRealtimeMini', 'gpt53codex', 'gpt53chat', 'gptOss',
415
- 'fable50', 'fable5', 'opus50', 'opus5', 'opus48', 'opus47', 'opus46',
416
- 'sonnet50', 'sonnet5', 'sonnet46', 'sonnet45', 'haiku45',
417
- 'gemini25flash', 'gemini31pro', 'gemini3pro', 'gemini3flash',
418
- 'gemini37flash', 'gemini36flash', 'gemini35flash', 'gemini35flashLite',
419
- 'gemini31flashLite', 'gemini25pro', 'sonarPro', 'sonar',
420
- 'grok46', 'grok45', 'grok43', 'grok420multiAgent', 'grok420',
421
- 'qwen3', 'qwen35397b', 'qwen36plus', 'qwen37plus', 'qwen38max',
422
- 'hermes470b', 'hermes4405b', 'hermes3',
423
- 'kimiK26', 'kimiK27Code', 'kimiK3', 'kimiK25',
424
- 'minimaxM25', 'minimaxM27', 'minimaxM3', 'mimo25', 'mimo25pro',
425
- 'deepseekV4Pro', 'deepseekV4Flash', 'GLM51', 'GLM52'
426
- ]);
427
-
428
- function parseChainModels(modelSpecs) {
429
- if (modelSpecs.length === 0) {
430
- throw new TypeError('chain() requires at least one model shortcut string.');
431
- }
432
-
433
- return modelSpecs.map((modelSpec, index) => {
434
- if (typeof modelSpec !== 'string') {
435
- throw new TypeError(`Invalid chain model at index ${index}: expected a model shortcut string.`);
436
- }
437
-
438
- const match = /^([A-Za-z_$][A-Za-z0-9_$]*)(?:@(-?\d+))?$/.exec(modelSpec);
439
- if (!match) {
440
- throw new TypeError(`Invalid chain model "${modelSpec}": expected "shortcut" or "shortcut@effort".`);
441
- }
442
-
443
- const shortcut = match[1];
444
- if (!CHAIN_MODEL_SHORTCUTS.has(shortcut)) {
445
- throw new Error(`Unknown model shortcut "${shortcut}" in chain().`);
446
- }
447
94
 
448
- return {
449
- shortcut,
450
- effort: match[2] === undefined ? undefined : normalizeEffort(Number(match[2]))
451
- };
452
- });
453
- }
454
95
 
455
96
  class ModelMix {
456
97
 
@@ -708,166 +349,28 @@ class ModelMix {
708
349
  return str.length > maxLen ? str.substring(0, maxLen) + '...' : str;
709
350
  }
710
351
 
711
- static normalizeTokenUsage({ input = 0, output = 0, thinking = 0, total, cached = 0, cacheWrite = 0, cacheWrite5m = 0, cacheWrite1h = 0 } = {}) {
712
- const tokenCount = value => Number.isFinite(value) ? Math.max(0, value) : 0;
713
- const normalizedInput = tokenCount(input);
714
- const normalizedOutput = tokenCount(output);
715
- const normalizedThinking = tokenCount(thinking);
716
- const normalizedCached = tokenCount(cached);
717
- const normalizedCacheWrite5m = tokenCount(cacheWrite5m);
718
- const normalizedCacheWrite1h = tokenCount(cacheWrite1h);
719
- const normalizedCacheWrite = Math.max(
720
- tokenCount(cacheWrite),
721
- normalizedCacheWrite5m + normalizedCacheWrite1h
722
- );
723
- const normalizedTotal = Number.isFinite(total)
724
- ? Math.max(0, total)
725
- : normalizedInput + normalizedOutput + normalizedThinking;
726
- const uncachedInput = Math.max(0, normalizedInput - normalizedCached - normalizedCacheWrite);
727
- const cacheHitRate = normalizedInput > 0
728
- ? Number((normalizedCached / normalizedInput).toFixed(4))
729
- : 0;
730
-
731
- return {
732
- input: normalizedInput,
733
- output: normalizedOutput,
734
- thinking: normalizedThinking,
735
- total: normalizedTotal,
736
- cached: normalizedCached,
737
- cacheWrite: normalizedCacheWrite,
738
- cacheWrite5m: normalizedCacheWrite5m,
739
- cacheWrite1h: normalizedCacheWrite1h,
740
- uncachedInput,
741
- cacheHitRate,
742
- cacheSavings: 0,
743
- cacheWritePremium: 0,
744
- breakEvenHits: 0,
745
- cost: 0,
746
- costBreakdown: {
747
- uncachedInput: 0,
748
- cachedInput: 0,
749
- cacheWrite: 0,
750
- cacheWrite5m: 0,
751
- cacheWrite1h: 0,
752
- output: 0,
753
- total: 0
754
- }
755
- };
352
+ static normalizeTokenUsage(usage = {}) {
353
+ return tokenUsage.normalizeTokenUsage(usage);
756
354
  }
757
355
 
758
356
  static calculateCostBreakdown(modelKey, tokens) {
759
- const pricing = MODEL_PRICING[modelKey];
760
- if (!pricing) return ModelMix.normalizeTokenUsage().costBreakdown;
761
-
762
- const normalized = ModelMix.normalizeTokenUsage(tokens);
763
- const longContext = pricing.longContext;
764
- const useLongContextRates = usesLongContextRates(pricing, normalized.input);
765
- const inputMultiplier = useLongContextRates ? longContext.inputMultiplier : 1;
766
- const outputMultiplier = useLongContextRates ? longContext.outputMultiplier : 1;
767
- const {
768
- input: inputPerMillion,
769
- cachedInput: cachedInputPerMillion = inputPerMillion,
770
- cacheWrite: cacheWritePerMillion = inputPerMillion,
771
- cacheWrite1h: cacheWrite1hPerMillion = cacheWritePerMillion,
772
- output: outputPerMillion
773
- } = pricing;
774
- const roundCost = value => Number(value.toFixed(12));
775
- const genericCacheWrite = Math.max(
776
- 0,
777
- normalized.cacheWrite - normalized.cacheWrite5m - normalized.cacheWrite1h
778
- );
779
- const cacheWrite5mCost = roundCost(
780
- normalized.cacheWrite5m * cacheWritePerMillion * inputMultiplier / 1_000_000
781
- );
782
- const cacheWrite1hCost = roundCost(
783
- normalized.cacheWrite1h * cacheWrite1hPerMillion * inputMultiplier / 1_000_000
784
- );
785
- const genericCacheWriteCost = roundCost(
786
- genericCacheWrite * cacheWritePerMillion * inputMultiplier / 1_000_000
787
- );
788
- const breakdown = {
789
- uncachedInput: roundCost(normalized.uncachedInput * inputPerMillion * inputMultiplier / 1_000_000),
790
- cachedInput: roundCost(normalized.cached * cachedInputPerMillion * inputMultiplier / 1_000_000),
791
- cacheWrite: roundCost(genericCacheWriteCost + cacheWrite5mCost + cacheWrite1hCost),
792
- cacheWrite5m: cacheWrite5mCost,
793
- cacheWrite1h: cacheWrite1hCost,
794
- output: roundCost(
795
- (normalized.output + normalized.thinking) * outputPerMillion * outputMultiplier / 1_000_000
796
- )
797
- };
798
- breakdown.total = roundCost(
799
- breakdown.uncachedInput
800
- + breakdown.cachedInput
801
- + breakdown.cacheWrite
802
- + breakdown.output
803
- );
804
- return breakdown;
357
+ return tokenUsage.calculateCostBreakdown(modelKey, tokens);
805
358
  }
806
359
 
807
360
  static calculateCacheMetrics(modelKey, tokens) {
808
- const pricing = MODEL_PRICING[modelKey];
809
- const emptyMetrics = {
810
- cacheSavings: 0,
811
- cacheWritePremium: 0,
812
- breakEvenHits: 0
813
- };
814
- if (!pricing) return emptyMetrics;
815
-
816
- const normalized = ModelMix.normalizeTokenUsage(tokens);
817
- const longContext = pricing.longContext;
818
- const useLongContextRates = usesLongContextRates(pricing, normalized.input);
819
- const inputMultiplier = useLongContextRates ? longContext.inputMultiplier : 1;
820
- const cachedInputPerMillion = pricing.cachedInput ?? pricing.input;
821
- const cacheWritePerMillion = pricing.cacheWrite ?? pricing.input;
822
- const cacheWrite1hPerMillion = pricing.cacheWrite1h ?? cacheWritePerMillion;
823
- const readSavingsPerMillion = Math.max(0, pricing.input - cachedInputPerMillion) * inputMultiplier;
824
- const writePremiumPerMillion = Math.max(0, cacheWritePerMillion - pricing.input) * inputMultiplier;
825
- const write1hPremiumPerMillion = Math.max(0, cacheWrite1hPerMillion - pricing.input) * inputMultiplier;
826
- const roundCost = value => Number(value.toFixed(12));
827
- const cacheSavings = roundCost(normalized.cached * readSavingsPerMillion / 1_000_000);
828
- const genericCacheWrite = Math.max(
829
- 0,
830
- normalized.cacheWrite - normalized.cacheWrite5m - normalized.cacheWrite1h
831
- );
832
- const cacheWritePremium = roundCost(
833
- (
834
- (genericCacheWrite + normalized.cacheWrite5m) * writePremiumPerMillion
835
- + normalized.cacheWrite1h * write1hPremiumPerMillion
836
- ) / 1_000_000
837
- );
838
- const fullHitSavings = normalized.cacheWrite * readSavingsPerMillion / 1_000_000;
839
-
840
- return {
841
- cacheSavings,
842
- cacheWritePremium,
843
- breakEvenHits: fullHitSavings > 0
844
- ? Number((cacheWritePremium / fullHitSavings).toFixed(4))
845
- : 0
846
- };
361
+ return tokenUsage.calculateCacheMetrics(modelKey, tokens);
847
362
  }
848
363
 
849
364
  static calculateCost(modelKey, tokens) {
850
- if (!MODEL_PRICING[modelKey]) return null;
851
- return ModelMix.calculateCostBreakdown(modelKey, tokens).total;
365
+ return tokenUsage.calculateCost(modelKey, tokens);
852
366
  }
853
367
 
854
368
  static extractCacheTokens(usage = {}) {
855
- return usage.input_tokens_details?.cached_tokens
856
- ?? usage.prompt_tokens_details?.cached_tokens
857
- ?? usage.cache_read_input_tokens
858
- ?? usage.cachedContentTokenCount
859
- ?? usage.cached_content_token_count
860
- ?? 0;
369
+ return tokenUsage.extractCacheTokens(usage);
861
370
  }
862
371
 
863
372
  static extractCacheWriteTokens(usage = {}) {
864
- return usage.input_tokens_details?.cache_write_tokens
865
- ?? usage.prompt_tokens_details?.cache_write_tokens
866
- ?? usage.cache_creation_input_tokens
867
- ?? usage.cache_write_input_tokens
868
- ?? usage.cacheWriteTokenCount
869
- ?? usage.cache_write_token_count
870
- ?? 0;
373
+ return tokenUsage.extractCacheWriteTokens(usage);
871
374
  }
872
375
 
873
376
  static formatInputSummary(messages, system, debug = 2) {
@@ -918,7 +421,8 @@ class ModelMix {
918
421
 
919
422
  attach(key, provider) {
920
423
 
921
- if (this.models.some(model => model.key === key)) {
424
+ if (this.models.some(model => model.key === key
425
+ && model.provider.constructor === provider.constructor)) {
922
426
  return this;
923
427
  }
924
428
 
@@ -930,15 +434,6 @@ class ModelMix {
930
434
  return this;
931
435
  }
932
436
 
933
- gpt41({ options = {}, config = {} } = {}) {
934
- return this.attach('gpt-4.1', new MixOpenAI({ options, config }));
935
- }
936
- gpt41mini({ options = {}, config = {} } = {}) {
937
- return this.attach('gpt-4.1-mini', new MixOpenAI({ options, config }));
938
- }
939
- gpt41nano({ options = {}, config = {} } = {}) {
940
- return this.attach('gpt-4.1-nano', new MixOpenAI({ options, config }));
941
- }
942
437
  gpt5({ options = {}, config = {} } = {}) {
943
438
  return this.attach('gpt-5', new MixOpenAI({ options, config }));
944
439
  }
@@ -998,7 +493,7 @@ class ModelMix {
998
493
  if (mix.together) this.attach('openai/gpt-oss-120b', new MixTogether({ options, config }));
999
494
  if (mix.cerebras) this.attach('gpt-oss-120b', new MixCerebras({ options, config }));
1000
495
  if (mix.groq) this.attach('openai/gpt-oss-120b', new MixGroq({ options, config }));
1001
- if (mix.openrouter) this.attach('openai/gpt-oss-120b:free', new MixOpenRouter({ options, config }));
496
+ if (mix.openrouter) this.attach('openai/gpt-oss-120b', new MixOpenRouter({ options, config }));
1002
497
  return this;
1003
498
  }
1004
499
  fable50({ options = {}, config = {} } = {}) {
@@ -1037,18 +532,9 @@ class ModelMix {
1037
532
  haiku45({ options = {}, config = {} } = {}) {
1038
533
  return this.attach('claude-haiku-4-5-20251001', new MixAnthropic({ options, config }));
1039
534
  }
1040
- gemini25flash({ options = {}, config = {} } = {}) {
1041
- return this.attach('gemini-2.5-flash', new MixGoogle({ options, config }));
1042
- }
1043
535
  gemini31pro({ options = {}, config = {} } = {}) {
1044
536
  return this.attach('gemini-3.1-pro-preview', new MixGoogle({ options, config }));
1045
537
  }
1046
- gemini3pro({ options = {}, config = {} } = {}) {
1047
- return this.attach('gemini-3-pro-preview', new MixGoogle({ options, config }));
1048
- }
1049
- gemini3flash({ options = {}, config = {} } = {}) {
1050
- return this.attach('gemini-3-flash-preview', new MixGoogle({ options, config }));
1051
- }
1052
538
  gemini37flash({ options = {}, config = {} } = {}) {
1053
539
  return this.attach('gemini-3.7-flash', new MixGoogle({ options, config }));
1054
540
  }
@@ -1064,9 +550,6 @@ class ModelMix {
1064
550
  gemini31flashLite({ options = {}, config = {} } = {}) {
1065
551
  return this.attach('gemini-3.1-flash-lite-preview', new MixGoogle({ options, config }));
1066
552
  }
1067
- gemini25pro({ options = {}, config = {} } = {}) {
1068
- return this.attach('gemini-2.5-pro', new MixGoogle({ options, config }));
1069
- }
1070
553
  sonarPro({ options = {}, config = {} } = {}) {
1071
554
  return this.attach('sonar-pro', new MixPerplexity({ options, config }));
1072
555
  }
@@ -1171,12 +654,6 @@ class ModelMix {
1171
654
  }
1172
655
 
1173
656
 
1174
- minimaxM25({ options = {}, config = {}, mix = { minimax: true } } = {}) {
1175
- mix = { ...this.mix, ...mix };
1176
- if (mix.minimax) this.attach('MiniMax-M2.5', new MixMiniMax({ options, config }));
1177
- return this;
1178
- }
1179
-
1180
657
  minimaxM27({ options = {}, config = {}, mix = { openrouter: true, minimax: true } } = {}) {
1181
658
  mix = { ...this.mix, ...mix };
1182
659
  if (mix.nvidia) this.attach('minimaxai/minimax-m2.7', new MixNVIDIA({ options, config }));
@@ -1703,426 +1180,470 @@ class ModelMix {
1703
1180
  return this.systemTemplate;
1704
1181
  }
1705
1182
 
1706
- async execute({
1707
- config = {},
1708
- options = {},
1709
- systemSuffix = '',
1710
- outputMode = 'raw',
1711
- _templateContext = null,
1712
- _pluginRequest = null,
1713
- _executionMetadata = null,
1714
- _pluginsApplied = false
1715
- } = {}) {
1716
- const isRootExecution = _templateContext === null;
1717
- const templateContext = _templateContext || createTemplateRenderContext(() => this._choiceRandom());
1718
-
1719
- if (!_pluginsApplied && this.plugins.length > 0) {
1720
- const preparedMessages = await this.prepareMessages(templateContext);
1721
- if (preparedMessages.length === 0) {
1722
- throw new Error("No user messages have been added. Use addText(prompt), addTextFromFile(filePath), addImage(filePath), or addImageFromUrl(url) to add a prompt.");
1723
- }
1724
- const requestConfig = {
1725
- ...this.config,
1726
- ...config,
1727
- retry: {
1728
- ...(this.config.retry || {}),
1729
- ...(config.retry || {})
1730
- }
1731
- };
1732
- const systemTemplate = this._resolveSystemTemplate(config, {});
1733
- const systemCacheKey = JSON.stringify([systemTemplate.filename, systemTemplate.source]);
1734
- if (!templateContext.renderedSystems.has(systemCacheKey)) {
1735
- templateContext.renderedSystems.set(
1736
- systemCacheKey,
1737
- this._renderTemplate(systemTemplate.source, {
1738
- filename: systemTemplate.filename,
1739
- label: 'system template'
1740
- }, templateContext)
1741
- );
1183
+ _mergeRequestConfig(config = {}) {
1184
+ return {
1185
+ ...this.config,
1186
+ ...config,
1187
+ retry: {
1188
+ ...(this.config.retry || {}),
1189
+ ...(config.retry || {})
1742
1190
  }
1743
- const request = {
1744
- system: templateContext.renderedSystems.get(systemCacheKey) + systemSuffix,
1745
- messages: clonePluginValue(preparedMessages),
1746
- options: clonePluginValue({ ...this.options, ...options }),
1747
- config: clonePluginValue(requestConfig),
1748
- outputMode
1749
- };
1750
- const executionMetadata = _executionMetadata || {
1751
- executionId: randomUUID(),
1752
- parentExecutionId: null,
1753
- depth: 0
1754
- };
1755
- let providerInvoked = false;
1756
-
1757
- const dispatch = async index => {
1758
- if (index === this.plugins.length) {
1759
- providerInvoked = true;
1760
- return this.execute({
1761
- config,
1762
- options,
1763
- systemSuffix,
1764
- outputMode,
1765
- _templateContext: templateContext,
1766
- _pluginRequest: request,
1767
- _executionMetadata: executionMetadata,
1768
- _pluginsApplied: true
1769
- });
1770
- }
1771
-
1772
- const plugin = this.plugins[index];
1773
- let nextCalled = false;
1774
- const next = () => {
1775
- if (nextCalled) {
1776
- throw new Error(`Plugin "${plugin.name}" called next() multiple times.`);
1777
- }
1778
- nextCalled = true;
1779
- return dispatch(index + 1);
1780
- };
1781
- const context = {
1782
- request,
1783
- execution: Object.freeze({ ...executionMetadata }),
1784
- invoke: input => this._invokeChild(input, executionMetadata)
1785
- };
1786
- const result = await plugin.execute(context, next);
1787
- return validatePluginResult(result, plugin.name);
1788
- };
1191
+ };
1192
+ }
1789
1193
 
1790
- const result = await dispatch(0);
1791
- this.lastRaw = result;
1792
- if (!providerInvoked) {
1793
- if (this.config.max_history === 0) {
1794
- this.messages = [];
1795
- } else if (result.message) {
1796
- this._addText(result.message, { role: 'assistant' });
1797
- }
1798
- }
1799
- if (isRootExecution) this._commitTemplateRenderContext(templateContext);
1800
- return result;
1194
+ _requirePreparedMessages(messages) {
1195
+ if (messages.length === 0) {
1196
+ throw new Error("No user messages have been added. Use addText(prompt), addTextFromFile(filePath), addImage(filePath), or addImageFromUrl(url) to add a prompt.");
1801
1197
  }
1198
+ }
1802
1199
 
1803
- if (!this.models || this.models.length === 0) {
1804
- throw new Error("No models specified. Use methods like .gpt5(), .sonnet46() first.");
1200
+ _renderSystem(config, providerConfig, systemSuffix, templateContext) {
1201
+ const systemTemplate = this._resolveSystemTemplate(config, providerConfig);
1202
+ const systemCacheKey = JSON.stringify([systemTemplate.filename, systemTemplate.source]);
1203
+ if (!templateContext.renderedSystems.has(systemCacheKey)) {
1204
+ templateContext.renderedSystems.set(
1205
+ systemCacheKey,
1206
+ this._renderTemplate(systemTemplate.source, {
1207
+ filename: systemTemplate.filename,
1208
+ label: 'system template'
1209
+ }, templateContext)
1210
+ );
1805
1211
  }
1212
+ return templateContext.renderedSystems.get(systemCacheKey) + systemSuffix;
1213
+ }
1806
1214
 
1807
- const execution = this.limiter.schedule(async () => {
1808
- const preparedMessages = _pluginRequest
1809
- ? _pluginRequest.messages
1810
- : await this.prepareMessages(templateContext);
1215
+ async _executePlugins({
1216
+ config,
1217
+ options,
1218
+ systemSuffix,
1219
+ outputMode,
1220
+ templateContext,
1221
+ executionMetadata,
1222
+ isRootExecution
1223
+ }) {
1224
+ const preparedMessages = await this.prepareMessages(templateContext);
1225
+ this._requirePreparedMessages(preparedMessages);
1811
1226
 
1812
- if (preparedMessages.length === 0) {
1813
- throw new Error("No user messages have been added. Use addText(prompt), addTextFromFile(filePath), addImage(filePath), or addImageFromUrl(url) to add a prompt.");
1227
+ const request = {
1228
+ system: this._renderSystem(config, {}, systemSuffix, templateContext),
1229
+ messages: clonePluginValue(preparedMessages),
1230
+ options: clonePluginValue({ ...this.options, ...options }),
1231
+ config: clonePluginValue(this._mergeRequestConfig(config)),
1232
+ outputMode
1233
+ };
1234
+ const metadata = executionMetadata || {
1235
+ executionId: randomUUID(),
1236
+ parentExecutionId: null,
1237
+ depth: 0
1238
+ };
1239
+ let providerInvoked = false;
1240
+
1241
+ const dispatch = async index => {
1242
+ if (index === this.plugins.length) {
1243
+ providerInvoked = true;
1244
+ return this.execute({
1245
+ config,
1246
+ options,
1247
+ systemSuffix,
1248
+ outputMode,
1249
+ _templateContext: templateContext,
1250
+ _pluginRequest: request,
1251
+ _executionMetadata: metadata,
1252
+ _pluginsApplied: true
1253
+ });
1814
1254
  }
1815
1255
 
1816
- // Merge config to get final roundRobin value and retry settings
1817
- const finalConfig = _pluginRequest
1818
- ? _pluginRequest.config
1819
- : {
1820
- ...this.config,
1821
- ...config,
1822
- retry: {
1823
- ...(this.config.retry || {}),
1824
- ...(config.retry || {})
1825
- }
1826
- };
1256
+ const plugin = this.plugins[index];
1257
+ let nextCalled = false;
1258
+ const next = () => {
1259
+ if (nextCalled) {
1260
+ throw new Error(`Plugin "${plugin.name}" called next() multiple times.`);
1261
+ }
1262
+ nextCalled = true;
1263
+ return dispatch(index + 1);
1264
+ };
1265
+ const context = {
1266
+ request,
1267
+ execution: Object.freeze({ ...metadata }),
1268
+ invoke: input => this._invokeChild(input, metadata)
1269
+ };
1270
+ const result = await plugin.execute(context, next);
1271
+ return validatePluginResult(result, plugin.name);
1272
+ };
1827
1273
 
1828
- // Try all models in order (first is primary, rest are fallbacks)
1829
- const modelsToTry = this.models.map((model, index) => ({ model, index }));
1274
+ const result = await dispatch(0);
1275
+ this.lastRaw = result;
1276
+ if (!providerInvoked) {
1277
+ if (this.config.max_history === 0) {
1278
+ this.messages = [];
1279
+ } else if (result.message) {
1280
+ this._addText(result.message, { role: 'assistant' });
1281
+ }
1282
+ }
1283
+ if (isRootExecution) this._commitTemplateRenderContext(templateContext);
1284
+ return result;
1285
+ }
1830
1286
 
1831
- // Round robin: rotate models array AFTER using current for next request
1832
- if (finalConfig.roundRobin && this.models.length > 1) {
1833
- const firstModel = this.models.shift();
1834
- this.models.push(firstModel);
1287
+ _createProviderAttempt({
1288
+ currentModel,
1289
+ preparedMessages,
1290
+ config,
1291
+ options,
1292
+ finalConfig,
1293
+ pluginRequest,
1294
+ systemSuffix,
1295
+ templateContext
1296
+ }) {
1297
+ const provider = currentModel.provider;
1298
+ const currentOptions = {
1299
+ ...this.options,
1300
+ messages: preparedMessages,
1301
+ ...provider.options,
1302
+ ...provider.getOptionsTools(this.tools),
1303
+ ...options,
1304
+ ...(pluginRequest?.options || {}),
1305
+ model: currentModel.key
1306
+ };
1307
+ const currentConfig = pluginRequest
1308
+ ? {
1309
+ ...provider.config,
1310
+ ...pluginRequest.config,
1311
+ retry: {
1312
+ ...(provider.config?.retry || {}),
1313
+ ...(pluginRequest.config.retry || {})
1314
+ }
1835
1315
  }
1316
+ : {
1317
+ ...finalConfig,
1318
+ ...provider.config,
1319
+ ...config,
1320
+ retry: {
1321
+ ...(finalConfig.retry || {}),
1322
+ ...(provider.config?.retry || {}),
1323
+ ...(config.retry || {})
1324
+ }
1325
+ };
1836
1326
 
1837
- let lastError = null;
1327
+ currentConfig.system = pluginRequest
1328
+ ? pluginRequest.system
1329
+ : this._renderSystem(config, provider.config, systemSuffix, templateContext);
1838
1330
 
1839
- for (let i = 0; i < modelsToTry.length; i++) {
1331
+ const resolvedModelKey = resolveGrok420ModelKey(
1332
+ currentModel.key,
1333
+ currentConfig.effort,
1334
+ currentOptions
1335
+ );
1336
+ currentOptions.model = resolvedModelKey;
1337
+ applyUnifiedEffort(
1338
+ currentOptions,
1339
+ currentConfig,
1340
+ resolveProviderFamily(provider),
1341
+ resolvedModelKey
1342
+ );
1840
1343
 
1841
- const { model: currentModel, index: originalIndex } = modelsToTry[i];
1842
- const currentModelKey = currentModel.key;
1843
- const providerInstance = currentModel.provider;
1844
- const optionsTools = providerInstance.getOptionsTools(this.tools);
1344
+ return { provider, currentOptions, currentConfig, resolvedModelKey };
1345
+ }
1845
1346
 
1846
- // Create clean copies for each provider to avoid contamination
1847
- const currentOptions = {
1848
- ...this.options,
1849
- messages: preparedMessages,
1850
- ...providerInstance.options,
1851
- ...optionsTools,
1852
- ...options,
1853
- ...(_pluginRequest?.options || {}),
1854
- model: currentModelKey
1855
- };
1347
+ _logProviderAttempt({ attempt, originalIndex, provider, currentConfig, resolvedModelKey, preparedMessages }) {
1348
+ if (currentConfig.debug < 1) return;
1856
1349
 
1857
- const currentConfig = _pluginRequest
1858
- ? {
1859
- ...providerInstance.config,
1860
- ..._pluginRequest.config,
1861
- retry: {
1862
- ...(providerInstance.config?.retry || {}),
1863
- ...(_pluginRequest.config.retry || {})
1864
- }
1865
- }
1866
- : {
1867
- ...finalConfig,
1868
- ...providerInstance.config,
1869
- ...config,
1870
- retry: {
1871
- ...(finalConfig.retry || {}),
1872
- ...(providerInstance.config?.retry || {}),
1873
- ...(config.retry || {})
1874
- }
1875
- };
1876
- if (_pluginRequest) {
1877
- currentConfig.system = _pluginRequest.system;
1878
- } else {
1879
- const systemTemplate = this._resolveSystemTemplate(config, providerInstance.config);
1880
- const systemCacheKey = JSON.stringify([systemTemplate.filename, systemTemplate.source]);
1881
- if (!templateContext.renderedSystems.has(systemCacheKey)) {
1882
- templateContext.renderedSystems.set(
1883
- systemCacheKey,
1884
- this._renderTemplate(systemTemplate.source, {
1885
- filename: systemTemplate.filename,
1886
- label: 'system template'
1887
- }, templateContext)
1888
- );
1889
- }
1890
- currentConfig.system = templateContext.renderedSystems.get(systemCacheKey) + systemSuffix;
1891
- }
1350
+ const isPrimary = attempt === 0;
1351
+ const prefix = isPrimary ? '→' : '↻';
1352
+ const suffix = isPrimary
1353
+ ? (currentConfig.roundRobin ? ` (round-robin #${originalIndex + 1})` : '')
1354
+ : ' (fallback)';
1355
+ const providerName = provider.constructor.name.replace(/^Mix/, '').toLowerCase();
1356
+ const effort = currentConfig.effort === undefined ? '' : `@${currentConfig.effort}`;
1357
+ const header = `\n${prefix} [${providerName}:${resolvedModelKey}${effort}] #${originalIndex + 1}${suffix}`;
1892
1358
 
1893
- // Grok 4.20 alias reasoning / non-reasoning from unified effort
1894
- const resolvedModelKey = resolveGrok420ModelKey(
1895
- currentModelKey,
1896
- currentConfig.effort,
1897
- currentOptions
1898
- );
1899
- currentOptions.model = resolvedModelKey;
1359
+ if (currentConfig.debug >= 2) {
1360
+ console.log(`${header}\n${ModelMix.formatInputSummary(preparedMessages, currentConfig.system, currentConfig.debug)}`);
1361
+ } else {
1362
+ console.log(header);
1363
+ }
1364
+ }
1365
+
1366
+ async _invokeProviderWithRetry(provider, currentOptions, currentConfig, resolvedModelKey) {
1367
+ if (currentOptions.stream && this.streamCallback) {
1368
+ provider.streamCallback = this.streamCallback;
1369
+ }
1370
+
1371
+ const retryConfig = currentConfig.retry || {};
1372
+ const retries = retryConfig.enabled ? Math.max(0, retryConfig.retries || 0) : 0;
1373
+ const baseDelayMs = Math.max(0, retryConfig.baseDelayMs || 0);
1374
+ const maxDelayMs = Math.max(baseDelayMs, retryConfig.maxDelayMs || baseDelayMs);
1375
+ const retryableStatusCodes = new Set(
1376
+ Array.isArray(retryConfig.retryableStatusCodes) && retryConfig.retryableStatusCodes.length > 0
1377
+ ? retryConfig.retryableStatusCodes
1378
+ : DEFAULT_RETRYABLE_STATUS_CODES
1379
+ );
1900
1380
 
1901
- // Unified effort → native provider fields (skipped if native already set)
1902
- const providerFamily = resolveProviderFamily(providerInstance);
1903
- applyUnifiedEffort(currentOptions, currentConfig, providerFamily, resolvedModelKey);
1381
+ let attempt = 0;
1382
+ while (true) {
1383
+ const startTime = Date.now();
1384
+ try {
1385
+ const result = await provider.create({ options: currentOptions, config: currentConfig });
1386
+ return { result, elapsedMs: Date.now() - startTime };
1387
+ } catch (error) {
1388
+ const statusCode = getErrorStatusCode(error);
1389
+ if (attempt >= retries || !retryableStatusCodes.has(statusCode)) throw error;
1904
1390
 
1905
1391
  if (currentConfig.debug >= 1) {
1906
- const isPrimary = i === 0;
1907
- const prefix = isPrimary ? '→' : '↻';
1908
- const suffix = isPrimary
1909
- ? (currentConfig.roundRobin ? ` (round-robin #${originalIndex + 1})` : '')
1910
- : ' (fallback)';
1911
- // Extract provider name from class name (e.g., "MixOpenRouter" -> "openrouter")
1912
- const providerName = providerInstance.constructor.name.replace(/^Mix/, '').toLowerCase();
1913
- const header = `\n${prefix} [${providerName}:${resolvedModelKey}] #${originalIndex + 1}${suffix}`;
1914
-
1915
- if (currentConfig.debug >= 2) {
1916
- console.log(`${header}\n${ModelMix.formatInputSummary(preparedMessages, currentConfig.system, currentConfig.debug)}`);
1917
- } else {
1918
- console.log(header);
1919
- }
1392
+ console.log(`↺ Retrying [${resolvedModelKey}] due to status ${statusCode} (${attempt + 2}/${retries + 1})`);
1920
1393
  }
1394
+ const delay = Math.min(baseDelayMs * Math.pow(2, attempt), maxDelayMs);
1395
+ await sleep(delay);
1396
+ attempt += 1;
1397
+ }
1398
+ }
1399
+ }
1921
1400
 
1922
- try {
1923
- if (currentOptions.stream && this.streamCallback) {
1924
- providerInstance.streamCallback = this.streamCallback;
1925
- }
1926
-
1927
- const retryConfig = currentConfig.retry || {};
1928
- const retries = retryConfig.enabled ? Math.max(0, retryConfig.retries || 0) : 0;
1929
- const baseDelayMs = Math.max(0, retryConfig.baseDelayMs || 0);
1930
- const maxDelayMs = Math.max(baseDelayMs, retryConfig.maxDelayMs || baseDelayMs);
1931
- const retryableStatusCodes = new Set(
1932
- Array.isArray(retryConfig.retryableStatusCodes) && retryConfig.retryableStatusCodes.length > 0
1933
- ? retryConfig.retryableStatusCodes
1934
- : DEFAULT_RETRYABLE_STATUS_CODES
1935
- );
1936
-
1937
- let attempt = 0;
1938
- let result;
1939
- let startTime = 0;
1940
-
1941
- while (true) {
1942
- try {
1943
- startTime = Date.now();
1944
- result = await providerInstance.create({ options: currentOptions, config: currentConfig });
1945
- break;
1946
- } catch (attemptError) {
1947
- const statusCode = getErrorStatusCode(attemptError);
1948
- const isRetryable = retryableStatusCodes.has(statusCode);
1949
- const canRetry = attempt < retries && isRetryable;
1950
-
1951
- if (!canRetry) {
1952
- throw attemptError;
1953
- }
1954
-
1955
- if (currentConfig.debug >= 1) {
1956
- const nextAttempt = attempt + 2;
1957
- const totalAttempts = retries + 1;
1958
- console.log(`↺ Retrying [${resolvedModelKey}] due to status ${statusCode} (${nextAttempt}/${totalAttempts})`);
1959
- }
1960
-
1961
- const delay = Math.min(baseDelayMs * Math.pow(2, attempt), maxDelayMs);
1962
- await sleep(delay);
1963
- attempt += 1;
1964
- }
1965
- }
1401
+ _enrichResultTokens(result, resolvedModelKey, elapsedMs) {
1402
+ if (!result.tokens) return;
1966
1403
 
1967
- const elapsedMs = Date.now() - startTime;
1968
-
1969
- if (result.tokens) {
1970
- const normalizedTokens = ModelMix.normalizeTokenUsage(result.tokens);
1971
- const costBreakdown = ModelMix.calculateCostBreakdown(resolvedModelKey, normalizedTokens);
1972
- const cacheMetrics = ModelMix.calculateCacheMetrics(resolvedModelKey, normalizedTokens);
1973
- result.tokens = {
1974
- ...result.tokens,
1975
- ...normalizedTokens,
1976
- ...cacheMetrics,
1977
- cost: MODEL_PRICING[resolvedModelKey] ? costBreakdown.total : 0,
1978
- costBreakdown
1979
- };
1980
- const elapsedSec = elapsedMs / 1000;
1981
- result.tokens.speed = elapsedSec > 0 ? Math.round(result.tokens.output / elapsedSec) : 0;
1982
- }
1404
+ const normalizedTokens = ModelMix.normalizeTokenUsage(result.tokens);
1405
+ const costBreakdown = ModelMix.calculateCostBreakdown(resolvedModelKey, normalizedTokens);
1406
+ const cacheMetrics = ModelMix.calculateCacheMetrics(resolvedModelKey, normalizedTokens);
1407
+ result.tokens = {
1408
+ ...result.tokens,
1409
+ ...normalizedTokens,
1410
+ ...cacheMetrics,
1411
+ cost: tokenUsage.hasModelPricing(resolvedModelKey) ? costBreakdown.total : 0,
1412
+ costBreakdown
1413
+ };
1414
+ const elapsedSec = elapsedMs / 1000;
1415
+ result.tokens.speed = elapsedSec > 0 ? Math.round(result.tokens.output / elapsedSec) : 0;
1416
+ }
1417
+
1418
+ async _continueToolCalls(result, pluginRequest, execution) {
1419
+ const toolMessages = pluginRequest
1420
+ ? clonePluginValue(pluginRequest.messages)
1421
+ : this.messages;
1422
+ if (result.assistantMessage) {
1423
+ toolMessages.push(result.assistantMessage);
1424
+ } else if (result.message) {
1425
+ if (result.signature) {
1426
+ toolMessages.push({
1427
+ role: 'assistant',
1428
+ content: [{
1429
+ type: 'thinking',
1430
+ thinking: result.think ?? '',
1431
+ signature: result.signature
1432
+ }]
1433
+ });
1434
+ } else {
1435
+ toolMessages.push({
1436
+ role: 'assistant',
1437
+ content: [{ type: 'text', text: result.message }]
1438
+ });
1439
+ }
1440
+ }
1983
1441
 
1984
- if (result.toolCalls && result.toolCalls.length > 0) {
1985
- const toolMessages = _pluginRequest
1986
- ? clonePluginValue(_pluginRequest.messages)
1987
- : this.messages;
1988
- if (result.assistantMessage) {
1989
- toolMessages.push(result.assistantMessage);
1990
- } else if (result.message) {
1991
- if (result.signature) {
1992
- toolMessages.push({
1993
- role: "assistant", content: [{
1994
- type: "thinking",
1995
- // Empty string is valid (Anthropic display: "omitted").
1996
- thinking: result.think ?? '',
1997
- signature: result.signature
1998
- }]
1999
- });
2000
- } else {
2001
- toolMessages.push({
2002
- role: 'assistant',
2003
- content: [{ type: 'text', text: result.message }]
2004
- });
2005
- }
2006
- }
1442
+ if (!result.assistantMessage) {
1443
+ toolMessages.push({ role: 'assistant', content: null, tool_calls: result.toolCalls });
1444
+ }
1445
+ const toolResults = await this.processToolCalls(result.toolCalls);
1446
+ for (const toolResult of toolResults) {
1447
+ toolMessages.push({
1448
+ role: 'tool',
1449
+ tool_call_id: toolResult.tool_call_id,
1450
+ name: toolResult.name,
1451
+ content: toolResult.content
1452
+ });
1453
+ }
1454
+ this.messages = toolMessages;
2007
1455
 
2008
- if (!result.assistantMessage) {
2009
- toolMessages.push({ role: "assistant", content: null, tool_calls: result.toolCalls });
2010
- }
1456
+ return this.execute({
1457
+ ...execution,
1458
+ _pluginRequest: pluginRequest
1459
+ ? { ...pluginRequest, messages: toolMessages }
1460
+ : null
1461
+ });
1462
+ }
2011
1463
 
2012
- const toolResults = await this.processToolCalls(result.toolCalls);
2013
- for (const toolResult of toolResults) {
2014
- toolMessages.push({
2015
- role: 'tool',
2016
- tool_call_id: toolResult.tool_call_id,
2017
- name: toolResult.name,
2018
- content: toolResult.content
2019
- });
2020
- }
2021
- this.messages = toolMessages;
2022
-
2023
- const nextPluginRequest = _pluginRequest
2024
- ? {
2025
- ..._pluginRequest,
2026
- messages: toolMessages
2027
- }
2028
- : null;
2029
- return this.execute({
2030
- options,
2031
- config,
2032
- systemSuffix,
2033
- outputMode,
2034
- _templateContext: templateContext,
2035
- _pluginRequest: nextPluginRequest,
2036
- _executionMetadata,
2037
- _pluginsApplied
2038
- });
2039
- }
1464
+ _logProviderSuccess(result, currentConfig) {
1465
+ if (currentConfig.debug === 1) console.log('✓ Success');
2040
1466
 
2041
- // debug level 1: Just success indicator
2042
- if (currentConfig.debug === 1) {
2043
- console.log(`✓ Success`);
2044
- }
1467
+ if (currentConfig.debug >= 2) {
1468
+ const tokenInfo = result.tokens
1469
+ ? ` ${result.tokens.input} → ${result.tokens.output} tok`
1470
+ + (result.tokens.cached ? ` (cached:${result.tokens.cached})` : '')
1471
+ + (result.tokens.speed ? ` | ${result.tokens.speed} t/s` : '')
1472
+ + (result.tokens.cost != null ? ` $${result.tokens.cost.toFixed(4)}` : '')
1473
+ : '';
1474
+ console.log(`✓${tokenInfo}\n${ModelMix.formatOutputSummary(result, currentConfig.debug).trim()}`);
1475
+ }
2045
1476
 
2046
- // debug level 2: Readable summary of output
2047
- if (currentConfig.debug >= 2) {
2048
- const tokenInfo = result.tokens
2049
- ? ` ${result.tokens.input} → ${result.tokens.output} tok`
2050
- + (result.tokens.cached ? ` (cached:${result.tokens.cached})` : '')
2051
- + (result.tokens.speed ? ` | ${result.tokens.speed} t/s` : '')
2052
- + (result.tokens.cost != null ? ` $${result.tokens.cost.toFixed(4)}` : '')
2053
- : '';
2054
- console.log(`✓${tokenInfo}\n${ModelMix.formatOutputSummary(result, currentConfig.debug).trim()}`);
2055
- }
1477
+ if (currentConfig.debug >= 4) {
1478
+ if (result.response) {
1479
+ console.log('\n[RAW RESPONSE]');
1480
+ console.log(ModelMix.formatJSON(result.response));
1481
+ }
1482
+ if (result.message) {
1483
+ console.log('\n[FULL MESSAGE]');
1484
+ console.log(ModelMix.formatMessage(result.message));
1485
+ }
1486
+ if (result.think) {
1487
+ console.log('\n[FULL THINKING]');
1488
+ console.log(result.think);
1489
+ }
1490
+ }
2056
1491
 
2057
- // debug level 4 (verbose): Full response details
2058
- if (currentConfig.debug >= 4) {
2059
- if (result.response) {
2060
- console.log('\n[RAW RESPONSE]');
2061
- console.log(ModelMix.formatJSON(result.response));
2062
- }
1492
+ if (currentConfig.debug >= 1) console.log('');
1493
+ }
2063
1494
 
2064
- if (result.message) {
2065
- console.log('\n[FULL MESSAGE]');
2066
- console.log(ModelMix.formatMessage(result.message));
2067
- }
1495
+ _recordProviderResult(result) {
1496
+ this.lastRaw = result;
1497
+ if (this.config.max_history === 0) {
1498
+ this.messages = [];
1499
+ } else if (result.message) {
1500
+ if (result.assistantMessage) {
1501
+ this.messages.push(result.assistantMessage);
1502
+ } else if (result.signature) {
1503
+ this.messages.push({
1504
+ role: 'assistant',
1505
+ content: [{
1506
+ type: 'thinking',
1507
+ thinking: result.think ?? '',
1508
+ signature: result.signature
1509
+ }, {
1510
+ type: 'text',
1511
+ text: result.message
1512
+ }]
1513
+ });
1514
+ } else {
1515
+ this._addText(result.message, { role: 'assistant' });
1516
+ }
1517
+ }
1518
+ }
2068
1519
 
2069
- if (result.think) {
2070
- console.log('\n[FULL THINKING]');
2071
- console.log(result.think);
2072
- }
2073
- }
1520
+ _logProviderFailure(error, currentModelKey, attempt, modelsToTry) {
1521
+ log.warn(`Model ${currentModelKey} failed (Attempt #${attempt + 1}/${modelsToTry.length}).`);
1522
+ if (error.message) log.warn(`Error: ${error.message}`);
1523
+ if (error.statusCode) log.warn(`Status Code: ${error.statusCode}`);
1524
+ if (error.details) log.warn(`Details:\n${ModelMix.formatJSON(error.details)}`);
2074
1525
 
2075
- if (currentConfig.debug >= 1) console.log('');
2076
-
2077
- this.lastRaw = result;
2078
-
2079
- // Manage conversation history based on max_history setting
2080
- if (this.config.max_history === 0) {
2081
- // Stateless: clear messages so next call starts fresh
2082
- this.messages = [];
2083
- } else if (result.message) {
2084
- // Persist assistant response for multi-turn conversations
2085
- if (result.assistantMessage) {
2086
- this.messages.push(result.assistantMessage);
2087
- } else if (result.signature) {
2088
- this.messages.push({
2089
- role: "assistant", content: [{
2090
- type: "thinking",
2091
- // Empty string is valid (Anthropic display: "omitted").
2092
- thinking: result.think ?? '',
2093
- signature: result.signature
2094
- }, {
2095
- type: "text",
2096
- text: result.message
2097
- }]
2098
- });
2099
- } else {
2100
- this._addText(result.message, { role: "assistant" });
2101
- }
2102
- }
1526
+ if (attempt === modelsToTry.length - 1) {
1527
+ console.error(`All ${modelsToTry.length} model(s) failed. Throwing last error from ${currentModelKey}.`);
1528
+ throw error;
1529
+ }
1530
+ log.info(`-> Proceeding to next model: ${modelsToTry[attempt + 1].model.key}`);
1531
+ }
1532
+
1533
+ async _executeProviderChain({
1534
+ config,
1535
+ options,
1536
+ systemSuffix,
1537
+ outputMode,
1538
+ templateContext,
1539
+ pluginRequest,
1540
+ executionMetadata,
1541
+ pluginsApplied
1542
+ }) {
1543
+ const preparedMessages = pluginRequest
1544
+ ? pluginRequest.messages
1545
+ : await this.prepareMessages(templateContext);
1546
+ this._requirePreparedMessages(preparedMessages);
1547
+
1548
+ const finalConfig = pluginRequest ? pluginRequest.config : this._mergeRequestConfig(config);
1549
+ const modelsToTry = this.models.map((model, index) => ({ model, index }));
1550
+ if (finalConfig.roundRobin && this.models.length > 1) {
1551
+ this.models.push(this.models.shift());
1552
+ }
1553
+
1554
+ let lastError = null;
1555
+ for (let attempt = 0; attempt < modelsToTry.length; attempt++) {
1556
+ const { model: currentModel, index: originalIndex } = modelsToTry[attempt];
1557
+ const providerAttempt = this._createProviderAttempt({
1558
+ currentModel,
1559
+ preparedMessages,
1560
+ config,
1561
+ options,
1562
+ finalConfig,
1563
+ pluginRequest,
1564
+ systemSuffix,
1565
+ templateContext
1566
+ });
1567
+ this._logProviderAttempt({
1568
+ attempt,
1569
+ originalIndex,
1570
+ preparedMessages,
1571
+ ...providerAttempt
1572
+ });
2103
1573
 
2104
- return result;
1574
+ try {
1575
+ const { result, elapsedMs } = await this._invokeProviderWithRetry(
1576
+ providerAttempt.provider,
1577
+ providerAttempt.currentOptions,
1578
+ providerAttempt.currentConfig,
1579
+ providerAttempt.resolvedModelKey
1580
+ );
1581
+ this._enrichResultTokens(result, providerAttempt.resolvedModelKey, elapsedMs);
2105
1582
 
2106
- } catch (error) {
2107
- lastError = error;
2108
- log.warn(`Model ${currentModelKey} failed (Attempt #${i + 1}/${modelsToTry.length}).`);
2109
- if (error.message) log.warn(`Error: ${error.message}`);
2110
- if (error.statusCode) log.warn(`Status Code: ${error.statusCode}`);
2111
- if (error.details) log.warn(`Details:\n${ModelMix.formatJSON(error.details)}`);
2112
-
2113
- if (i === modelsToTry.length - 1) {
2114
- console.error(`All ${modelsToTry.length} model(s) failed. Throwing last error from ${currentModelKey}.`);
2115
- throw lastError;
2116
- } else {
2117
- const nextModelKey = modelsToTry[i + 1].model.key;
2118
- log.info(`-> Proceeding to next model: ${nextModelKey}`);
2119
- }
1583
+ if (result.toolCalls && result.toolCalls.length > 0) {
1584
+ return this._continueToolCalls(result, pluginRequest, {
1585
+ options,
1586
+ config,
1587
+ systemSuffix,
1588
+ outputMode,
1589
+ _templateContext: templateContext,
1590
+ _executionMetadata: executionMetadata,
1591
+ _pluginsApplied: pluginsApplied
1592
+ });
2120
1593
  }
1594
+
1595
+ this._logProviderSuccess(result, providerAttempt.currentConfig);
1596
+ this._recordProviderResult(result);
1597
+ return result;
1598
+ } catch (error) {
1599
+ lastError = error;
1600
+ this._logProviderFailure(error, currentModel.key, attempt, modelsToTry);
2121
1601
  }
1602
+ }
2122
1603
 
2123
- log.error("Fallback logic completed without success or throwing the final error.");
2124
- throw lastError || new Error("Failed to get response from any model, and no specific error was caught.");
2125
- });
1604
+ log.error('Fallback logic completed without success or throwing the final error.');
1605
+ throw lastError || new Error('Failed to get response from any model, and no specific error was caught.');
1606
+ }
1607
+
1608
+ async execute({
1609
+ config = {},
1610
+ options = {},
1611
+ systemSuffix = '',
1612
+ outputMode = 'raw',
1613
+ _templateContext = null,
1614
+ _pluginRequest = null,
1615
+ _executionMetadata = null,
1616
+ _pluginsApplied = false
1617
+ } = {}) {
1618
+ const isRootExecution = _templateContext === null;
1619
+ const templateContext = _templateContext || createTemplateRenderContext(() => this._choiceRandom());
1620
+
1621
+ if (!_pluginsApplied && this.plugins.length > 0) {
1622
+ return this._executePlugins({
1623
+ config,
1624
+ options,
1625
+ systemSuffix,
1626
+ outputMode,
1627
+ templateContext,
1628
+ executionMetadata: _executionMetadata,
1629
+ isRootExecution
1630
+ });
1631
+ }
1632
+
1633
+ if (!this.models || this.models.length === 0) {
1634
+ throw new Error('No models specified. Use methods like .gpt5(), .sonnet46() first.');
1635
+ }
1636
+
1637
+ const execution = this.limiter.schedule(() => this._executeProviderChain({
1638
+ config,
1639
+ options,
1640
+ systemSuffix,
1641
+ outputMode,
1642
+ templateContext,
1643
+ pluginRequest: _pluginRequest,
1644
+ executionMetadata: _executionMetadata,
1645
+ pluginsApplied: _pluginsApplied
1646
+ }));
2126
1647
 
2127
1648
  if (!isRootExecution) return execution;
2128
1649
 
@@ -2130,7 +1651,6 @@ class ModelMix {
2130
1651
  this._commitTemplateRenderContext(templateContext);
2131
1652
  return result;
2132
1653
  }
2133
-
2134
1654
  async processToolCalls(toolCalls) {
2135
1655
  const result = []
2136
1656
 
@@ -2294,1915 +1814,33 @@ class ModelMix {
2294
1814
  }
2295
1815
  }
2296
1816
 
2297
- class MixCustom {
2298
- constructor({ config = {}, options = {}, headers = {} } = {}) {
2299
- this.config = this.getDefaultConfig(config);
2300
- this.options = this.getDefaultOptions(options);
2301
- this.headers = this.getDefaultHeaders(headers);
2302
- this.streamCallback = null; // Define streamCallback here
2303
- }
2304
-
2305
- getDefaultOptions(customOptions) {
2306
- return {
2307
- ...customOptions
2308
- };
2309
- }
2310
-
2311
- getDefaultConfig(customConfig) {
2312
- return {
2313
- url: '',
2314
- apiKey: '',
2315
- ...customConfig
2316
- };
2317
- }
2318
-
2319
- getDefaultHeaders(customHeaders) {
2320
- return {
2321
- 'accept': 'application/json',
2322
- 'content-type': 'application/json',
2323
- 'authorization': `Bearer ${this.config.apiKey}`,
2324
- ...customHeaders
2325
- };
2326
- }
2327
-
2328
- convertMessages(messages, config) {
2329
- return MixOpenAI.convertMessages(messages, config);
2330
- }
2331
-
2332
- sanitizeCacheOptions(options) {
2333
- delete options.cache_control;
2334
- delete options.prompt_cache_key;
2335
- delete options.prompt_cache_options;
2336
- delete options.prompt_cache_retention;
2337
- }
2338
-
2339
- static stripContentTypeHeader(headers = {}) {
2340
- return stripContentTypeHeader(headers);
2341
- }
2342
-
2343
- static createMultipartFormData({ fields = {}, files = [] } = {}) {
2344
- return createMultipartFormData({ fields, files });
2345
- }
2346
-
2347
- static buildRequestBodyAndHeaders(options, headers) {
2348
- return buildRequestBodyAndHeaders(options, headers);
2349
- }
2350
-
2351
- async create({ config = {}, options = {} } = {}) {
2352
- try {
2353
- this.sanitizeCacheOptions(options);
2354
- if (Array.isArray(options.messages)) {
2355
- options.messages = this.convertMessages(options.messages, config);
2356
- }
2357
-
2358
- const request = buildRequestBodyAndHeaders(options, this.headers);
2359
-
2360
- // debug level 4 (verbose): Full request details
2361
- if (config.debug >= 4) {
2362
- console.log('\n[REQUEST DETAILS]');
2363
-
2364
- console.log('\n[CONFIG]');
2365
- console.log(ModelMix.formatJSON(configForDebug(config)));
2366
-
2367
- console.log('\n[OPTIONS]');
2368
- console.log(ModelMix.formatJSON(request.options));
2369
- }
2370
-
2371
- if (options.stream) {
2372
- return this.processStream(await fetchStreamResponse(this.config.url, {
2373
- method: 'POST',
2374
- headers: request.headers,
2375
- body: request.body
2376
- }));
2377
- } else {
2378
- return this.processResponse(await fetchJsonResponse(this.config.url, {
2379
- method: 'POST',
2380
- headers: request.headers,
2381
- body: request.body
2382
- }));
2383
- }
2384
- } catch (error) {
2385
- throw this.handleError(error);
2386
- }
2387
- }
2388
-
2389
- handleError(error) {
2390
- let errorMessage = 'An error occurred in MixCustom';
2391
- let statusCode = null;
2392
- let errorDetails = null;
2393
-
2394
- if (error?.isHttpError || error?.response || typeof error?.statusCode === 'number') {
2395
- statusCode = error.statusCode ?? error.response?.status ?? null;
2396
- errorMessage = error.message || `Request to ${this.config.url} failed with status code ${statusCode}`;
2397
- errorDetails = error.details ?? error.response?.data ?? null;
2398
- } else if (error?.message) {
2399
- errorMessage = error.message;
2400
- }
2401
-
2402
- const formattedError = {
2403
- message: redactSecret(errorMessage, this.config.apiKey),
2404
- statusCode,
2405
- details: redactSecret(errorDetails, this.config.apiKey),
2406
- stack: redactSecret(error.stack, this.config.apiKey)
2407
- };
2408
-
2409
- return formattedError;
2410
- }
2411
-
2412
- processStream(response) {
2413
- return new Promise((resolve, reject) => {
2414
- let raw = [];
2415
- let message = '';
2416
- let buffer = '';
2417
-
2418
- response.data.on('data', chunk => {
2419
- buffer += chunk.toString();
2420
-
2421
- let boundary;
2422
- while ((boundary = buffer.indexOf('\n')) !== -1) {
2423
- const dataStr = buffer.slice(0, boundary).trim();
2424
- buffer = buffer.slice(boundary + 1);
2425
-
2426
- const firstBraceIndex = dataStr.indexOf('{');
2427
- if (dataStr === '[DONE]' || firstBraceIndex === -1) continue;
2428
-
2429
- const jsonStr = dataStr.slice(firstBraceIndex);
2430
- try {
2431
- const data = JSON.parse(jsonStr);
2432
- if (this.streamCallback) {
2433
- const delta = this.extractDelta(data);
2434
- message += delta;
2435
- this.streamCallback({ response: data, message, delta });
2436
- raw.push(data);
2437
- }
2438
- } catch (error) {
2439
- console.error('Error parsing JSON:', error);
2440
- }
2441
- }
2442
- });
2443
-
2444
- response.data.on('end', () => resolve({
2445
- response: raw,
2446
- message: message.trim(),
2447
- toolCalls: [],
2448
- think: null,
2449
- tokens: raw.length > 0 ? MixCustom.extractTokens(raw[raw.length - 1]) : { input: 0, output: 0, total: 0, cached: 0 }
2450
- }));
2451
- response.data.on('error', reject);
2452
- });
2453
- }
2454
-
2455
- extractDelta(data) {
2456
- return data.choices[0].delta.content;
2457
- }
2458
-
2459
- static extractMessage(data) {
2460
- const choice = data?.choices?.[0] || {};
2461
- const messageObj = choice.message || {};
2462
- const finishReason = choice.finish_reason;
2463
-
2464
- if (typeof messageObj.refusal === 'string' && messageObj.refusal.trim().length > 0) {
2465
- throw new Error(`OpenAI model refused to process this request: ${messageObj.refusal}`);
2466
- }
2467
-
2468
- if (finishReason === 'content_filter') {
2469
- throw new Error('OpenAI response was blocked by content_filter.');
2470
- }
2471
-
2472
- let message = '';
2473
- if (typeof messageObj.content === 'string') {
2474
- message = messageObj.content.trim();
2475
- } else if (Array.isArray(messageObj.content)) {
2476
- const refusalPart = messageObj.content.find(part => part?.type === 'refusal' || (typeof part?.refusal === 'string' && part.refusal.trim().length > 0));
2477
- if (refusalPart) {
2478
- const refusalText = typeof refusalPart.refusal === 'string' ? refusalPart.refusal : 'No refusal text provided.';
2479
- throw new Error(`OpenAI model refused to process this request: ${refusalText}`);
2480
- }
2481
- message = messageObj.content
2482
- .filter(part => typeof part?.text === 'string')
2483
- .map(part => part.text)
2484
- .join('')
2485
- .trim();
2486
- }
2487
-
2488
- const endTagIndex = message.indexOf('</think>');
2489
- if (message.startsWith('<think>') && endTagIndex !== -1) {
2490
- return message.substring(endTagIndex + 8).trim();
2491
- }
2492
- return message;
2493
- }
2494
-
2495
- static extractThink(data) {
2496
-
2497
- if (data.choices[0].message?.reasoning_content) {
2498
- return data.choices[0].message.reasoning_content;
2499
- } else if (data.choices[0].message?.reasoning) {
2500
- return data.choices[0].message.reasoning;
2501
- }
2502
-
2503
- const message = data.choices[0].message?.content?.trim() || '';
2504
- const endTagIndex = message.indexOf('</think>');
2505
- if (message.startsWith('<think>') && endTagIndex !== -1) {
2506
- return message.substring(7, endTagIndex).trim();
2507
- }
2508
- return null;
2509
- }
2510
-
2511
- static extractToolCalls(data) {
2512
- return data.choices[0].message?.tool_calls?.map(call => ({
2513
- id: call.id,
2514
- type: 'function',
2515
- function: {
2516
- name: call.function.name,
2517
- arguments: call.function.arguments
2518
- }
2519
- })) || []
2520
- }
2521
-
2522
- static extractTokens(data) {
2523
- // OpenAI/Groq/Together/Lambda/Cerebras/Fireworks format
2524
- if (data.usage) {
2525
- return ModelMix.normalizeTokenUsage({
2526
- input: data.usage.prompt_tokens || 0,
2527
- output: data.usage.completion_tokens || 0,
2528
- total: data.usage.total_tokens,
2529
- cached: ModelMix.extractCacheTokens(data.usage),
2530
- cacheWrite: ModelMix.extractCacheWriteTokens(data.usage)
2531
- });
2532
- }
2533
- return ModelMix.normalizeTokenUsage();
2534
- }
2535
-
2536
- processResponse(response) {
2537
- return {
2538
- message: MixCustom.extractMessage(response.data),
2539
- think: MixCustom.extractThink(response.data),
2540
- toolCalls: MixCustom.extractToolCalls(response.data),
2541
- tokens: MixCustom.extractTokens(response.data),
2542
- response: response.data
2543
- }
2544
- }
2545
-
2546
- getOptionsTools(tools) {
2547
- return MixOpenAI.getOptionsTools(tools);
2548
- }
2549
- }
2550
-
2551
- class MixOpenAI extends MixCustom {
2552
- sanitizeCacheOptions(options) {
2553
- delete options.cache_control;
2554
- delete options.prompt_cache_options;
2555
- }
2556
-
2557
- getDefaultConfig(customConfig) {
2558
-
2559
- if (!process.env.OPENAI_API_KEY) {
2560
- throw new Error('OpenAI API key not found. Please provide it in config or set OPENAI_API_KEY environment variable.');
2561
- }
2562
-
2563
- return super.getDefaultConfig({
2564
- url: 'https://api.openai.com/v1/chat/completions',
2565
- apiKey: process.env.OPENAI_API_KEY,
2566
- ...customConfig
2567
- });
2568
- }
2569
-
2570
- async create({ config = {}, options = {} } = {}) {
2571
-
2572
- // Remove max_tokens and temperature for o1/o3 models
2573
- if (options.model?.startsWith('o')) {
2574
- delete options.max_tokens;
2575
- delete options.temperature;
2576
- }
2577
-
2578
- // Use max_completion_tokens and remove temperature for GPT-5 models
2579
- if (options.model?.includes('gpt-5')) {
2580
- if (options.max_tokens) {
2581
- options.max_completion_tokens = options.max_tokens;
2582
- delete options.max_tokens;
2583
- }
2584
- delete options.temperature;
2585
- }
2586
-
2587
- return super.create({ config, options });
2588
- }
2589
-
2590
- static convertMessages(messages, config) {
2591
-
2592
- const content = config.system;
2593
- messages = [{ role: 'system', content }, ...messages || []];
2594
-
2595
- const results = []
2596
- for (const message of messages) {
2597
-
2598
- if (message.tool_calls) {
2599
- results.push({
2600
- role: 'assistant',
2601
- content: message.content ?? null,
2602
- ...(message.reasoning_content && { reasoning_content: message.reasoning_content }),
2603
- tool_calls: message.tool_calls
2604
- })
2605
- continue;
2606
- }
2607
-
2608
- if (message.role === 'tool') {
2609
- // Handle new format: tool_call_id directly on message
2610
- if (message.tool_call_id) {
2611
- results.push({
2612
- role: 'tool',
2613
- tool_call_id: message.tool_call_id,
2614
- content: message.content
2615
- });
2616
- }
2617
- // Handle old format: content is an array
2618
- else if (Array.isArray(message.content)) {
2619
- for (const content of message.content) {
2620
- results.push({
2621
- role: 'tool',
2622
- tool_call_id: content.tool_call_id,
2623
- content: content.content
2624
- })
2625
- }
2626
- }
2627
- continue;
2628
- }
2629
-
2630
- let convertedMessage = { ...message };
2631
- if (Array.isArray(message.content)) {
2632
- convertedMessage = {
2633
- ...message,
2634
- content: message.content.filter(content => content !== null && content !== undefined).map(content => {
2635
- if (content && content.type === 'image') {
2636
- const { media_type, data } = content.source;
2637
- return {
2638
- type: 'image_url',
2639
- image_url: {
2640
- url: `data:${media_type};base64,${data}`
2641
- }
2642
- };
2643
- }
2644
- return stripContentCacheMetadata(content);
2645
- })
2646
- };
2647
- }
2648
-
2649
- results.push(convertedMessage);
2650
- }
2651
-
2652
- return results;
2653
- }
2654
-
2655
- static getOptionsTools(tools) {
2656
- const options = {};
2657
- const toolsArray = [];
2658
- for (const tool in tools) {
2659
- for (const item of tools[tool]) {
2660
- toolsArray.push({
2661
- type: 'function',
2662
- function: {
2663
- name: item.name,
2664
- description: item.description,
2665
- parameters: item.inputSchema
2666
- }
2667
- });
2668
- }
2669
- }
2670
-
2671
- // Solo incluir tools si el array no está vacío
2672
- if (toolsArray.length > 0) {
2673
- options.tools = toolsArray;
2674
- // options.tool_choice = "auto";
2675
- }
2676
-
2677
- return options;
2678
- }
2679
- }
2680
-
2681
- class MixModeration extends MixCustom {
2682
- getOptionsTools() {
2683
- return {};
2684
- }
2685
- }
2686
-
2687
- class MixOpenAIResponses extends MixOpenAI {
2688
- async create({ config = {}, options = {} } = {}) {
2689
-
2690
- // Keep GPT/o-model option normalization behavior
2691
- if (options.model?.startsWith('o')) {
2692
- delete options.max_tokens;
2693
- delete options.temperature;
2694
- }
2695
- if (options.model?.includes('gpt-5')) {
2696
- if (options.max_tokens) {
2697
- options.max_completion_tokens = options.max_tokens;
2698
- delete options.max_tokens;
2699
- }
2700
- delete options.temperature;
2701
- }
2702
-
2703
- const responsesUrl = this.config.url.replace('/chat/completions', '/responses');
2704
- const request = MixOpenAIResponses.buildResponsesRequest(options, config);
2705
- const response = await fetchJsonResponse(responsesUrl, {
2706
- method: 'POST',
2707
- headers: this.headers,
2708
- body: JSON.stringify(request)
2709
- });
2710
-
2711
- return MixOpenAIResponses.processResponsesResponse(response);
2712
- }
2713
-
2714
- static buildResponsesRequest(options = {}, config = {}) {
2715
- const isGPT56 = typeof options.model === 'string' && options.model.startsWith('gpt-5.6');
2716
- const input = MixOpenAIResponses.messagesToResponsesInput(options.messages, {
2717
- translateNeutralCache: isGPT56
2718
- });
2719
- if (config.system) {
2720
- input.unshift({ role: 'developer', content: [{ type: 'input_text', text: config.system }] });
2721
- }
2722
- MixOpenAIResponses.validatePromptCaching(options, input);
2723
- const request = {
2724
- model: options.model,
2725
- input,
2726
- stream: false
2727
- };
2728
-
2729
- if (options.reasoning_effort) request.reasoning = { effort: options.reasoning_effort };
2730
- if (options.verbosity) request.text = { verbosity: options.verbosity };
2731
-
2732
- if (options.response_format) {
2733
- const rf = options.response_format;
2734
- let format;
2735
- if (rf.type === 'json_schema' && rf.json_schema) {
2736
- format = {
2737
- type: 'json_schema',
2738
- name: rf.json_schema.name || 'response',
2739
- strict: true,
2740
- schema: rf.json_schema.schema
2741
- };
2742
- } else if (rf.type) {
2743
- format = { type: rf.type };
2744
- }
2745
- if (format) {
2746
- request.text = { ...request.text, format };
2747
- }
2748
- }
2749
-
2750
- if (typeof options.max_completion_tokens === 'number') {
2751
- request.max_output_tokens = options.max_completion_tokens;
2752
- } else if (typeof options.max_tokens === 'number') {
2753
- request.max_output_tokens = options.max_tokens;
2754
- }
2755
-
2756
- if (typeof options.temperature === 'number') request.temperature = options.temperature;
2757
- if (typeof options.top_p === 'number') request.top_p = options.top_p;
2758
- if (typeof options.presence_penalty === 'number') request.presence_penalty = options.presence_penalty;
2759
- if (typeof options.frequency_penalty === 'number') request.frequency_penalty = options.frequency_penalty;
2760
- if (options.stop !== undefined) request.stop = options.stop;
2761
- if (typeof options.n === 'number') request.n = options.n;
2762
- if (options.logit_bias !== undefined) request.logit_bias = options.logit_bias;
2763
- if (options.user !== undefined) request.user = options.user;
2764
- if (options.prompt_cache_key !== undefined) request.prompt_cache_key = options.prompt_cache_key;
2765
- if (options.prompt_cache_retention !== undefined) request.prompt_cache_retention = options.prompt_cache_retention;
2766
- if (options.prompt_cache_options !== undefined) request.prompt_cache_options = options.prompt_cache_options;
2767
-
2768
- return request;
2769
- }
2770
-
2771
- static validatePromptCaching(options, input) {
2772
- const isGPT56 = typeof options.model === 'string' && options.model.startsWith('gpt-5.6');
2773
- const cacheOptions = options.prompt_cache_options;
2774
- const breakpoints = input.flatMap(message => Array.isArray(message.content)
2775
- ? message.content
2776
- .filter(block => block?.prompt_cache_breakpoint !== undefined)
2777
- .map(block => block.prompt_cache_breakpoint)
2778
- : []);
2779
-
2780
- if (isGPT56 && options.prompt_cache_retention !== undefined) {
2781
- throw new Error('GPT-5.6 does not support prompt_cache_retention; use prompt_cache_options.ttl instead.');
2782
- }
2783
- if (!isGPT56 && cacheOptions !== undefined) {
2784
- throw new Error('prompt_cache_options is only supported by GPT-5.6 models.');
2785
- }
2786
- if (!isGPT56 && breakpoints.length > 0) {
2787
- throw new Error('prompt_cache_breakpoint is only supported by GPT-5.6 models.');
2788
- }
2789
- if (cacheOptions !== undefined) {
2790
- if (!isPlainObject(cacheOptions)) {
2791
- throw new TypeError('prompt_cache_options must be a plain non-null object.');
2792
- }
2793
- if (cacheOptions.mode !== undefined
2794
- && cacheOptions.mode !== 'implicit'
2795
- && cacheOptions.mode !== 'explicit') {
2796
- throw new TypeError('prompt_cache_options.mode must be "implicit" or "explicit".');
2797
- }
2798
- if (cacheOptions.ttl !== undefined && cacheOptions.ttl !== '30m') {
2799
- throw new TypeError('prompt_cache_options.ttl must be "30m".');
2800
- }
2801
- }
2802
- for (const breakpoint of breakpoints) {
2803
- if (!isPlainObject(breakpoint)) {
2804
- throw new TypeError('prompt_cache_breakpoint must be a plain non-null object.');
2805
- }
2806
- if (breakpoint.mode !== 'explicit') {
2807
- throw new TypeError('prompt_cache_breakpoint mode must be "explicit".');
2808
- }
2809
- }
2810
- }
2811
-
2812
- static processResponsesResponse(response) {
2813
- const message = MixOpenAIResponses.extractResponsesMessage(response.data);
2814
- return {
2815
- message,
2816
- think: null,
2817
- toolCalls: [],
2818
- tokens: MixOpenAIResponses.extractResponsesTokens(response.data),
2819
- response: response.data
2820
- };
2821
- }
2822
-
2823
- static extractResponsesTokens(data) {
2824
- if (data.usage) {
2825
- return ModelMix.normalizeTokenUsage({
2826
- input: data.usage.input_tokens || 0,
2827
- output: data.usage.output_tokens || 0,
2828
- total: data.usage.total_tokens,
2829
- cached: ModelMix.extractCacheTokens(data.usage),
2830
- cacheWrite: ModelMix.extractCacheWriteTokens(data.usage)
2831
- });
2832
- }
2833
- return ModelMix.normalizeTokenUsage();
2834
- }
2835
-
2836
- static extractResponsesMessage(data) {
2837
- if (!Array.isArray(data.output)) return '';
2838
- return data.output
2839
- .filter(item => item.type === 'message')
2840
- .flatMap(item => Array.isArray(item.content) ? item.content : [])
2841
- .filter(content => content.type === 'output_text' && typeof content.text === 'string')
2842
- .map(content => content.text)
2843
- .join('\n')
2844
- .trim();
2845
- }
2846
-
2847
- static messagesToResponsesInput(messages = [], { translateNeutralCache = false } = {}) {
2848
- const mapped = [];
2849
-
2850
- for (const message of messages) {
2851
- if (!message || !message.role) continue;
2852
- if (message.tool_calls || message.role === 'tool') continue;
2853
-
2854
- const content = [];
2855
- const isAssistant = message.role === 'assistant';
2856
- const textType = isAssistant ? 'output_text' : 'input_text';
2857
- if (typeof message.content === 'string') {
2858
- if (message.content) content.push({ type: textType, text: message.content });
2859
- } else if (Array.isArray(message.content)) {
2860
- for (const item of message.content) {
2861
- if (!item || typeof item !== 'object') continue;
2862
- const neutralCache = item.cache !== undefined
2863
- ? normalizeContentCache(item.cache)
2864
- : undefined;
2865
- const promptCacheBreakpoint = item.prompt_cache_breakpoint !== undefined
2866
- ? item.prompt_cache_breakpoint
2867
- : (translateNeutralCache && neutralCache?.breakpoint
2868
- ? { mode: 'explicit' }
2869
- : undefined);
2870
- const breakpoint = !isAssistant && promptCacheBreakpoint !== undefined
2871
- ? { prompt_cache_breakpoint: promptCacheBreakpoint }
2872
- : {};
2873
-
2874
- if ((item.type === 'text' || item.type === 'input_text' || item.type === 'output_text')
2875
- && typeof item.text === 'string') {
2876
- content.push({ type: textType, text: item.text, ...breakpoint });
2877
- continue;
2878
- }
2879
- if (item.type === 'image' && item.source) {
2880
- let imageUrl;
2881
- if (item.source.type === 'base64') {
2882
- if (!item.source.media_type || typeof item.source.data !== 'string') {
2883
- throw new TypeError('Responses base64 images require source.media_type and string source.data.');
2884
- }
2885
- imageUrl = `data:${item.source.media_type};base64,${item.source.data}`;
2886
- } else if (item.source.type === 'url' && typeof item.source.data === 'string') {
2887
- imageUrl = item.source.data;
2888
- } else {
2889
- throw new TypeError('Responses images must be processed to base64 or use a URL source.');
2890
- }
2891
- content.push({ type: 'input_image', image_url: imageUrl, ...breakpoint });
2892
- continue;
2893
- }
2894
- if (item.type === 'image_url' && typeof item.image_url?.url === 'string') {
2895
- content.push({ type: 'input_image', image_url: item.image_url.url, ...breakpoint });
2896
- continue;
2897
- }
2898
- if (item.type === 'input_image' || item.type === 'input_file') {
2899
- content.push({
2900
- ...stripContentCacheMetadata(item),
2901
- ...breakpoint
2902
- });
2903
- }
2904
- }
2905
- }
2906
-
2907
- if (content.length === 0) continue;
2908
- mapped.push({
2909
- role: message.role,
2910
- content
2911
- });
2912
- }
2913
-
2914
- return mapped;
2915
- }
2916
- }
2917
-
2918
- class MixOpenAIModeration extends MixModeration {
2919
- getDefaultConfig(customConfig) {
2920
- const apiKey = customConfig.apiKey || process.env.OPENAI_API_KEY;
2921
- if (!apiKey) {
2922
- throw new Error('OpenAI API key not found. Please provide it in config or set OPENAI_API_KEY environment variable.');
2923
- }
2924
-
2925
- return super.getDefaultConfig({
2926
- url: 'https://api.openai.com/v1/moderations',
2927
- apiKey,
2928
- ...customConfig
2929
- });
2930
- }
2931
-
2932
- async create({ config = {}, options = {} } = {}) {
2933
- if (options.stream) {
2934
- throw new Error('Stream is not supported for OpenAI moderation');
2935
- }
2936
-
2937
- const input = MixOpenAIModeration.messagesToModerationInput(options.messages);
2938
- const response = await fetchJsonResponse(this.config.url, {
2939
- method: 'POST',
2940
- headers: this.headers,
2941
- body: JSON.stringify({ model: options.model, input })
2942
- });
2943
-
2944
- return {
2945
- moderation: response.data.results,
2946
- tokens: ModelMix.normalizeTokenUsage(),
2947
- response: response.data
2948
- };
2949
- }
2950
-
2951
- static messagesToModerationInput(messages = []) {
2952
- const input = [];
2953
-
2954
- for (const message of messages) {
2955
- if (typeof message.content === 'string') {
2956
- input.push({ type: 'text', text: message.content });
2957
- continue;
2958
- }
2959
- if (!Array.isArray(message.content)) continue;
2960
-
2961
- for (const content of message.content) {
2962
- if (content?.type === 'text') {
2963
- input.push({ type: 'text', text: content.text });
2964
- } else if (content?.type === 'image') {
2965
- const { media_type: mediaType, data } = content.source || {};
2966
- if (!mediaType || !data) {
2967
- throw new Error('OpenAI moderation images must be prepared as base64 data URLs');
2968
- }
2969
- input.push({
2970
- type: 'image_url',
2971
- image_url: { url: `data:${mediaType};base64,${data}` }
2972
- });
2973
- }
2974
- }
2975
- }
2976
-
2977
- return input;
2978
- }
2979
- }
2980
-
2981
- class ModerationMix extends ModelMix {
2982
- static new(setup = {}) {
2983
- return new ModerationMix(setup);
2984
- }
2985
-
2986
- new({ options = {}, config = {} } = {}) {
2987
- return new ModerationMix({
2988
- options: { ...this.options, ...options },
2989
- config: { ...this.config, ...config }
2990
- });
2991
- }
2992
-
2993
- attach(key, provider) {
2994
- if (!(provider instanceof MixModeration)) {
2995
- throw new Error('ModerationMix only accepts moderation providers.');
2996
- }
2997
- return super.attach(key, provider);
2998
- }
2999
-
3000
- openai({ options = {}, config = {} } = {}) {
3001
- return this.attach('omni-moderation-latest', new MixOpenAIModeration({ options, config }));
3002
- }
3003
-
3004
- async message() {
3005
- throw new Error('ModerationMix does not generate messages. Use raw() and read result.moderation.');
3006
- }
3007
-
3008
- async json() {
3009
- throw new Error('ModerationMix does not generate JSON. Use raw() and read result.moderation.');
3010
- }
3011
-
3012
- async block() {
3013
- throw new Error('ModerationMix does not generate blocks. Use raw() and read result.moderation.');
3014
- }
3015
-
3016
- async stream() {
3017
- throw new Error('ModerationMix does not support streaming. Use raw().');
3018
- }
3019
- }
3020
-
3021
- class MixOpenAIWebSocket extends MixOpenAIResponses {
3022
- getDefaultConfig(customConfig) {
3023
- return super.getDefaultConfig({
3024
- realtimeUrl: 'wss://api.openai.com/v1/realtime',
3025
- websocketTimeoutMs: 120000,
3026
- ...customConfig
3027
- });
3028
- }
3029
-
3030
- async create({ config = {}, options = {} } = {}) {
3031
- if (options.model?.startsWith('o')) {
3032
- delete options.max_tokens;
3033
- delete options.temperature;
3034
- }
3035
- if (options.model?.includes('gpt-5')) {
3036
- if (options.max_tokens) {
3037
- options.max_completion_tokens = options.max_tokens;
3038
- delete options.max_tokens;
3039
- }
3040
- delete options.temperature;
3041
- }
3042
-
3043
- const mergedConfig = { ...this.config, ...config };
3044
- const realtimeUrl = `${mergedConfig.realtimeUrl}?model=${encodeURIComponent(options.model)}`;
3045
- const timeoutMs = mergedConfig.websocketTimeoutMs || 120000;
3046
-
3047
- return await new Promise((resolve, reject) => {
3048
- const ws = new WebSocket(realtimeUrl, {
3049
- headers: {
3050
- authorization: `Bearer ${mergedConfig.apiKey}`
3051
- }
3052
- });
3053
-
3054
- const events = [];
3055
- let message = '';
3056
- let settled = false;
3057
- let finalResponse = null;
3058
-
3059
- const timeout = setTimeout(() => {
3060
- if (settled) return;
3061
- settled = true;
3062
- ws.close();
3063
- reject({
3064
- message: `Realtime WebSocket timed out after ${timeoutMs}ms`,
3065
- statusCode: null,
3066
- details: null
3067
- });
3068
- }, timeoutMs);
3069
-
3070
- const cleanUp = () => clearTimeout(timeout);
3071
-
3072
- ws.on('open', () => {
3073
- const session = {
3074
- type: 'realtime',
3075
- output_modalities: ['text']
3076
- };
3077
-
3078
- if (mergedConfig.system) session.instructions = mergedConfig.system;
3079
- if (Array.isArray(options.tools) && options.tools.length > 0) {
3080
- session.tools = options.tools;
3081
- }
3082
-
3083
- ws.send(JSON.stringify({ type: 'session.update', session }));
3084
-
3085
- const items = MixOpenAIWebSocket.messagesToConversationItems(options.messages);
3086
- for (const item of items) {
3087
- ws.send(JSON.stringify({
3088
- type: 'conversation.item.create',
3089
- item
3090
- }));
3091
- }
3092
-
3093
- const responseConfig = { output_modalities: ['text'] };
3094
- if (typeof options.max_completion_tokens === 'number') {
3095
- responseConfig.max_output_tokens = Math.min(options.max_completion_tokens, 4096);
3096
- } else if (typeof options.max_tokens === 'number') {
3097
- responseConfig.max_output_tokens = Math.min(options.max_tokens, 4096);
3098
- }
3099
- if (Array.isArray(options.tools) && options.tools.length > 0) responseConfig.tools = options.tools;
3100
-
3101
- ws.send(JSON.stringify({
3102
- type: 'response.create',
3103
- response: responseConfig
3104
- }));
3105
- });
3106
-
3107
- ws.on('message', raw => {
3108
- let event;
3109
- try {
3110
- event = JSON.parse(raw.toString());
3111
- } catch {
3112
- return;
3113
- }
3114
-
3115
- events.push(event);
3116
-
3117
- const isTextDeltaEvent = event.type === 'response.text.delta' || event.type === 'response.output_text.delta';
3118
- if (isTextDeltaEvent) {
3119
- const delta = MixOpenAIWebSocket.extractDelta(event);
3120
- if (delta) {
3121
- message += delta;
3122
- if (this.streamCallback) {
3123
- this.streamCallback({ response: event, message, delta });
3124
- }
3125
- }
3126
- return;
3127
- }
3128
-
3129
- if (event.type === 'response.done') {
3130
- finalResponse = event.response || null;
3131
- if (!message && finalResponse) {
3132
- message = MixOpenAIResponses.extractResponsesMessage(finalResponse);
3133
- }
3134
-
3135
- if (!settled) {
3136
- settled = true;
3137
- cleanUp();
3138
- ws.close();
3139
- resolve({
3140
- message: message.trim(),
3141
- think: null,
3142
- toolCalls: [],
3143
- tokens: MixOpenAIResponses.extractResponsesTokens(finalResponse || {}),
3144
- response: {
3145
- response: finalResponse,
3146
- events
3147
- }
3148
- });
3149
- }
3150
- return;
3151
- }
3152
-
3153
- if (event.type === 'error' && !settled) {
3154
- settled = true;
3155
- cleanUp();
3156
- ws.close();
3157
- reject({
3158
- message: event.error?.message || 'Realtime WebSocket error',
3159
- statusCode: null,
3160
- details: event.error || event
3161
- });
3162
- }
3163
- });
3164
-
3165
- ws.on('error', error => {
3166
- if (settled) return;
3167
- settled = true;
3168
- cleanUp();
3169
- reject({
3170
- message: error.message || 'Realtime WebSocket connection error',
3171
- statusCode: null,
3172
- details: null,
3173
- stack: error.stack
3174
- });
3175
- });
3176
-
3177
- ws.on('close', () => {
3178
- if (settled) return;
3179
- settled = true;
3180
- cleanUp();
3181
- reject({
3182
- message: 'Realtime WebSocket closed before response.done',
3183
- statusCode: null,
3184
- details: null
3185
- });
3186
- });
3187
- });
3188
- }
3189
-
3190
- static messagesToConversationItems(messages = []) {
3191
- const items = [];
3192
-
3193
- for (const message of messages) {
3194
- if (!message || !message.role) continue;
3195
- if (message.role === 'tool' || message.tool_calls) continue;
3196
-
3197
- const role = message.role === 'assistant' ? 'assistant' : (message.role === 'system' ? 'system' : 'user');
3198
- const content = [];
3199
-
3200
- if (typeof message.content === 'string') {
3201
- content.push({
3202
- type: role === 'assistant' ? 'text' : 'input_text',
3203
- text: message.content
3204
- });
3205
- } else if (Array.isArray(message.content)) {
3206
- for (const item of message.content) {
3207
- if (!item || item.type !== 'text' || typeof item.text !== 'string') continue;
3208
- content.push({
3209
- type: role === 'assistant' ? 'text' : 'input_text',
3210
- text: item.text
3211
- });
3212
- }
3213
- }
3214
-
3215
- if (content.length === 0) continue;
3216
- items.push({ type: 'message', role, content });
3217
- }
3218
-
3219
- return items;
3220
- }
3221
-
3222
- static extractDelta(event) {
3223
- if (typeof event.delta === 'string') return event.delta;
3224
- return '';
3225
- }
3226
- }
3227
-
3228
- class MixOpenRouter extends MixOpenAI {
3229
- getDefaultConfig(customConfig) {
3230
-
3231
- if (!process.env.OPENROUTER_API_KEY) {
3232
- throw new Error('OpenRouter API key not found. Please provide it in config or set OPENROUTER_API_KEY environment variable.');
3233
- }
3234
-
3235
- return MixCustom.prototype.getDefaultConfig.call(this, {
3236
- url: 'https://openrouter.ai/api/v1/chat/completions',
3237
- apiKey: process.env.OPENROUTER_API_KEY,
3238
- ...customConfig
3239
- });
3240
- }
3241
- }
3242
-
3243
- class MixKimi extends MixOpenAI {
3244
- getDefaultConfig(customConfig) {
3245
- if (!process.env.MOONSHOT_API_KEY) {
3246
- throw new Error('Moonshot API key not found. Please provide it in config or set MOONSHOT_API_KEY environment variable.');
3247
- }
3248
-
3249
- return MixCustom.prototype.getDefaultConfig.call(this, {
3250
- url: 'https://api.moonshot.ai/v1/chat/completions',
3251
- apiKey: process.env.MOONSHOT_API_KEY,
3252
- ...customConfig
3253
- });
3254
- }
3255
-
3256
- async create({ config = {}, options = {} } = {}) {
3257
- if (Object.hasOwn(options, 'max_tokens')) {
3258
- options.max_completion_tokens = options.max_tokens;
3259
- delete options.max_tokens;
3260
- }
3261
-
3262
- delete options.temperature;
3263
- delete options.top_p;
3264
- delete options.n;
3265
- delete options.presence_penalty;
3266
- delete options.frequency_penalty;
3267
-
3268
- return super.create({ config, options });
3269
- }
3270
-
3271
- extractDelta(data) {
3272
- return data?.choices?.[0]?.delta?.content || '';
3273
- }
3274
-
3275
- processResponse(response) {
3276
- return {
3277
- ...super.processResponse(response),
3278
- assistantMessage: response.data?.choices?.[0]?.message
3279
- };
3280
- }
3281
- }
3282
-
3283
- class MixAnthropic extends MixCustom {
3284
-
3285
- sanitizeCacheOptions(options) {
3286
- delete options.prompt_cache_key;
3287
- delete options.prompt_cache_options;
3288
- delete options.prompt_cache_retention;
3289
- }
3290
-
3291
- static validateCacheControl(cacheControl) {
3292
- if (!isPlainObject(cacheControl) || cacheControl.type !== 'ephemeral') {
3293
- throw new TypeError('Anthropic cache_control must have type "ephemeral".');
3294
- }
3295
- if (cacheControl.ttl !== undefined
3296
- && cacheControl.ttl !== '5m'
3297
- && cacheControl.ttl !== '1h') {
3298
- throw new TypeError('Anthropic cache_control.ttl must be "5m" or "1h".');
3299
- }
3300
- }
3301
-
3302
- /**
3303
- * Opus 4.7+ and Claude 5 family reject sampling params (temperature/top_p/top_k).
3304
- * See: https://platform.claude.com/docs/en/about-claude/models/migration-guide
3305
- */
3306
- static rejectsSamplingParams(model = '') {
3307
- const id = String(model).toLowerCase();
3308
- if (!id.includes('claude')) return false;
3309
- if (id.includes('mythos') || id.includes('fable')) return true;
3310
-
3311
- const opus = id.match(/claude-opus-(\d+)(?:-(\d+))?/);
3312
- if (opus) {
3313
- const major = Number(opus[1]);
3314
- const minor = opus[2] !== undefined ? Number(opus[2]) : 0;
3315
- return major > 4 || (major === 4 && minor >= 7);
3316
- }
3317
-
3318
- const sonnet = id.match(/claude-sonnet-(\d+)/);
3319
- if (sonnet) return Number(sonnet[1]) >= 5;
3320
-
3321
- return false;
3322
- }
3323
-
3324
- getDefaultConfig(customConfig) {
3325
-
3326
- if (!process.env.ANTHROPIC_API_KEY) {
3327
- throw new Error('Anthropic API key not found. Please provide it in config or set ANTHROPIC_API_KEY environment variable.');
3328
- }
3329
-
3330
- return super.getDefaultConfig({
3331
- url: 'https://api.anthropic.com/v1/messages',
3332
- apiKey: process.env.ANTHROPIC_API_KEY,
3333
- ...customConfig
3334
- });
3335
- }
3336
-
3337
- async create({ config = {}, options = {} } = {}) {
3338
-
3339
- delete options.response_format;
3340
-
3341
- if (MixAnthropic.rejectsSamplingParams(options.model)) {
3342
- delete options.temperature;
3343
- delete options.top_p;
3344
- delete options.top_k;
3345
- }
3346
-
3347
- const requestConfig = { ...config };
3348
- if (hasNeutralCacheBreakpoint(options.messages)) {
3349
- const contentCacheControl = options.cache_control ?? { type: 'ephemeral' };
3350
- MixAnthropic.validateCacheControl(contentCacheControl);
3351
- requestConfig._contentCacheControl = { ...contentCacheControl };
3352
- delete options.cache_control;
3353
- } else if (options.cache_control !== undefined) {
3354
- MixAnthropic.validateCacheControl(options.cache_control);
3355
- }
3356
-
3357
- options.system = config.system;
3358
-
3359
- try {
3360
- return await super.create({ config: requestConfig, options });
3361
- } catch (error) {
3362
- // Log the error details for debugging
3363
- if (error.response && error.response.data) {
3364
- log.error('Anthropic API Error:\n', error.response.data);
3365
- }
3366
- throw error;
3367
- }
3368
- }
3369
-
3370
- convertMessages(messages, config) {
3371
- return MixAnthropic.convertMessages(messages, config);
3372
- }
3373
-
3374
- static convertMessages(messages, config) {
3375
- // Filter out orphaned tool results for Anthropic
3376
- const filteredMessages = [];
3377
- for (let i = 0; i < messages.length; i++) {
3378
- if (messages[i].role === 'tool') {
3379
- // Preceding assistant may use OpenAI tool_calls or Anthropic tool_use blocks.
3380
- let foundToolCall = false;
3381
- for (let j = i - 1; j >= 0; j--) {
3382
- if (ModelMix.hasToolInteraction(messages[j]) && messages[j].role === 'assistant') {
3383
- foundToolCall = true;
3384
- break;
3385
- }
3386
- }
3387
- if (!foundToolCall) {
3388
- // Skip orphaned tool results
3389
- continue;
3390
- }
3391
- }
3392
- filteredMessages.push(messages[i]);
3393
- }
3394
-
3395
- return filteredMessages.map(message => {
3396
- if (message.role === 'tool') {
3397
- // Handle new format: tool_call_id directly on message
3398
- if (message.tool_call_id) {
3399
- return {
3400
- role: "user",
3401
- content: [{
3402
- type: "tool_result",
3403
- tool_use_id: message.tool_call_id,
3404
- content: message.content
3405
- }]
3406
- }
3407
- }
3408
- // Handle old format: content is an array
3409
- return {
3410
- role: "user",
3411
- content: message.content.map(content => ({
3412
- type: "tool_result",
3413
- tool_use_id: content.tool_call_id,
3414
- content: content.content
3415
- }))
3416
- }
3417
- }
3418
-
3419
- // Handle messages with tool_calls (assistant messages that call tools)
3420
- if (message.tool_calls) {
3421
- const content = message.tool_calls.map(call => ({
3422
- type: 'tool_use',
3423
- id: call.id,
3424
- name: call.function.name,
3425
- input: JSON.parse(call.function.arguments)
3426
- }));
3427
- return { role: 'assistant', content };
3428
- }
3429
-
3430
- // Handle content conversion for other messages
3431
- if (message.content && Array.isArray(message.content)) {
3432
- const content = message.content.filter(content => content !== null && content !== undefined).map(content => {
3433
- const neutralCache = content?.cache !== undefined
3434
- ? normalizeContentCache(content.cache)
3435
- : undefined;
3436
- if (neutralCache && content.cache_control !== undefined) {
3437
- throw new TypeError('Use either cache or cache_control on an Anthropic content block, not both.');
3438
- }
3439
- let converted = content;
3440
- if (content && content.type === 'function') {
3441
- converted = {
3442
- type: 'tool_use',
3443
- id: content.id,
3444
- name: content.function.name,
3445
- input: JSON.parse(content.function.arguments)
3446
- };
3447
- }
3448
- const sanitized = stripContentCacheMetadata(converted);
3449
- if (content.cache_control !== undefined) {
3450
- MixAnthropic.validateCacheControl(content.cache_control);
3451
- sanitized.cache_control = { ...content.cache_control };
3452
- } else if (neutralCache?.breakpoint) {
3453
- sanitized.cache_control = {
3454
- ...(config?._contentCacheControl || { type: 'ephemeral' })
3455
- };
3456
- }
3457
- return sanitized;
3458
- });
3459
- return { ...message, content };
3460
- }
3461
-
3462
- return { ...message };
3463
- });
3464
- }
3465
-
3466
- getDefaultHeaders(customHeaders) {
3467
- return super.getDefaultHeaders({
3468
- 'x-api-key': this.config.apiKey,
3469
- 'anthropic-version': '2023-06-01',
3470
- ...customHeaders
3471
- });
3472
- }
3473
-
3474
- extractDelta(data) {
3475
- if (data.delta && data.delta.text) return data.delta.text;
3476
- return '';
3477
- }
3478
-
3479
- static extractToolCalls(data) {
3480
-
3481
- return data.content.map(item => {
3482
- if (item.type === 'tool_use') {
3483
- return {
3484
- id: item.id,
3485
- type: 'function',
3486
- function: {
3487
- name: item.name,
3488
- arguments: JSON.stringify(item.input)
3489
- }
3490
- };
3491
- }
3492
- return null;
3493
- }).filter(item => item !== null);
3494
- }
3495
-
3496
- static extractMessage(data) {
3497
- const content = Array.isArray(data?.content) ? data.content : [];
3498
- const stopReason = data?.stop_reason;
3499
-
3500
- // Anthropic can return text in different positions depending on thinking/tool blocks.
3501
- const textBlock = content.find(block => typeof block?.text === 'string' && block.text.trim().length > 0);
3502
- if (textBlock) {
3503
- return textBlock.text;
3504
- }
3505
-
3506
- // A tool_use turn can legitimately contain no text blocks.
3507
- if (stopReason === 'tool_use') {
3508
- return '';
3509
- }
3510
-
3511
- // Empty/non-text content is often due to safety refusal or token limits.
3512
- const contentTypes = content.map(block => block?.type || 'unknown').join(', ') || 'none';
3513
-
3514
- if (stopReason === 'refusal') {
3515
- throw new Error('Anthropic refused to process this request (content policy). Try different wording or a fallback model.');
3516
- }
3517
- if (!content.length) {
3518
- throw new Error(`Anthropic returned empty content (stop_reason: ${stopReason ?? 'unknown'}).`);
3519
- }
3520
- throw new Error(`Anthropic content blocks are missing .text (stop_reason: ${stopReason ?? 'unknown'}, content_types: ${contentTypes}).`);
3521
- }
3522
-
3523
- static extractThinkingBlock(data) {
3524
- const content = Array.isArray(data?.content) ? data.content : [];
3525
- return content.find(block => block?.type === 'thinking') || null;
3526
- }
3527
-
3528
- static extractThink(data) {
3529
- const block = MixAnthropic.extractThinkingBlock(data);
3530
- // Preserve empty string: display "omitted" returns thinking: "" with a signature.
3531
- return typeof block?.thinking === 'string' ? block.thinking : null;
3532
- }
3533
-
3534
- static extractSignature(data) {
3535
- const block = MixAnthropic.extractThinkingBlock(data);
3536
- return typeof block?.signature === 'string' && block.signature
3537
- ? block.signature
3538
- : null;
3539
- }
3540
-
3541
- static extractTokens(data) {
3542
- // Anthropic format
3543
- if (data.usage) {
3544
- const cached = ModelMix.extractCacheTokens(data.usage);
3545
- const cacheWrite5m = data.usage.cache_creation?.ephemeral_5m_input_tokens ?? 0;
3546
- const cacheWrite1h = data.usage.cache_creation?.ephemeral_1h_input_tokens ?? 0;
3547
- const cacheWrite = Math.max(
3548
- ModelMix.extractCacheWriteTokens(data.usage),
3549
- cacheWrite5m + cacheWrite1h
3550
- );
3551
- const input = (data.usage.input_tokens || 0) + cached + cacheWrite;
3552
- const output = data.usage.output_tokens || 0;
3553
- return ModelMix.normalizeTokenUsage({
3554
- input,
3555
- output,
3556
- total: input + output,
3557
- cached,
3558
- cacheWrite,
3559
- cacheWrite5m,
3560
- cacheWrite1h
3561
- });
3562
- }
3563
- return ModelMix.normalizeTokenUsage();
3564
- }
3565
-
3566
- processResponse(response) {
3567
- const data = response.data;
3568
- return {
3569
- message: MixAnthropic.extractMessage(data),
3570
- think: MixAnthropic.extractThink(data),
3571
- toolCalls: MixAnthropic.extractToolCalls(data),
3572
- tokens: MixAnthropic.extractTokens(data),
3573
- response: data,
3574
- signature: MixAnthropic.extractSignature(data),
3575
- // Replay Anthropic content blocks verbatim (including empty thinking).
3576
- assistantMessage: Array.isArray(data?.content)
3577
- ? { role: 'assistant', content: data.content }
3578
- : undefined
3579
- }
3580
- }
3581
-
3582
- getOptionsTools(tools) {
3583
- return MixAnthropic.getOptionsTools(tools);
3584
- }
3585
-
3586
- static getOptionsTools(tools) {
3587
- const options = {};
3588
- const toolsArray = [];
3589
- for (const tool in tools) {
3590
- for (const item of tools[tool]) {
3591
- toolsArray.push({
3592
- name: item.name,
3593
- description: item.description,
3594
- input_schema: item.inputSchema
3595
- });
3596
- }
3597
- }
3598
-
3599
- // Solo incluir tools si el array no está vacío
3600
- if (toolsArray.length > 0) {
3601
- options.tools = toolsArray;
3602
- }
3603
-
3604
- return options;
3605
- }
3606
- }
3607
-
3608
- class MixMiniMax extends MixOpenAI {
3609
- getDefaultConfig(customConfig) {
3610
-
3611
- if (!process.env.MINIMAX_API_KEY) {
3612
- throw new Error('MiniMax API key not found. Please provide it in config or set MINIMAX_API_KEY environment variable.');
3613
- }
3614
-
3615
- return MixCustom.prototype.getDefaultConfig.call(this, {
3616
- url: 'https://api.minimax.io/v1/chat/completions',
3617
- apiKey: process.env.MINIMAX_API_KEY,
3618
- ...customConfig
3619
- });
3620
- }
3621
-
3622
- extractDelta(data) {
3623
- // MiniMax might send different formats during streaming
3624
- if (data.choices && data.choices[0] && data.choices[0].delta && data.choices[0].delta.content) {
3625
- return data.choices[0].delta.content;
3626
- }
3627
- return '';
3628
- }
3629
- }
3630
-
3631
- class MixMiMo extends MixOpenAI {
3632
- getDefaultConfig(customConfig) {
3633
- if (!process.env.MIMO_API_KEY) {
3634
- throw new Error('MiMo API key not found. Please provide it in config or set MIMO_API_KEY environment variable.');
3635
- }
3636
-
3637
- return MixCustom.prototype.getDefaultConfig.call(this, {
3638
- url: 'https://api.xiaomimimo.com/v1/chat/completions',
3639
- apiKey: process.env.MIMO_API_KEY,
3640
- ...customConfig
3641
- });
3642
- }
3643
-
3644
- getDefaultHeaders(customHeaders) {
3645
- return {
3646
- 'accept': 'application/json',
3647
- 'content-type': 'application/json',
3648
- 'api-key': this.config.apiKey,
3649
- ...customHeaders
3650
- };
3651
- }
3652
- }
3653
-
3654
- class MixPerplexity extends MixCustom {
3655
- getDefaultConfig(customConfig) {
3656
-
3657
- if (!process.env.PPLX_API_KEY) {
3658
- throw new Error('Perplexity API key not found. Please provide it in config or set PPLX_API_KEY environment variable.');
3659
- }
3660
-
3661
- return super.getDefaultConfig({
3662
- url: 'https://api.perplexity.ai/chat/completions',
3663
- apiKey: process.env.PPLX_API_KEY,
3664
- ...customConfig
3665
- });
3666
- }
3667
-
3668
- async create({ config = {}, options = {} } = {}) {
3669
-
3670
- if (config.schema) {
3671
- options.response_format = {
3672
- type: 'json_schema',
3673
- json_schema: { schema: config.schema }
3674
- };
3675
- }
3676
-
3677
- return super.create({ config, options });
3678
- }
3679
- }
3680
-
3681
- class MixOllama extends MixCustom {
3682
-
3683
- getDefaultConfig(customConfig) {
3684
- return super.getDefaultConfig({
3685
- url: 'http://localhost:11434/api/chat',
3686
- ...customConfig
3687
- });
3688
- }
3689
-
3690
- getDefaultOptions(customOptions) {
3691
- return {
3692
- options: customOptions,
3693
- };
3694
- }
3695
-
3696
- extractDelta(data) {
3697
- if (data.message && data.message.content) return data.message.content;
3698
- return '';
3699
- }
3700
-
3701
- extractMessage(data) {
3702
- return data.message.content.trim();
3703
- }
3704
-
3705
- convertMessages(messages, config) {
3706
- return MixOllama.convertMessages(messages, config);
3707
- }
3708
-
3709
- static convertMessages(messages, config) {
3710
- const content = config.system;
3711
- messages = [{ role: 'system', content }, ...messages || []];
3712
-
3713
- return messages.map(entry => {
3714
- let content = '';
3715
- let images = [];
3716
-
3717
- entry.content.forEach(item => {
3718
- if (item.type === 'text') {
3719
- content += item.text + ' ';
3720
- } else if (item.type === 'image') {
3721
- images.push(item.source.data);
3722
- }
3723
- });
3724
-
3725
- return {
3726
- role: entry.role,
3727
- content: content.trim(),
3728
- images: images
3729
- };
3730
- });
3731
- }
3732
- }
3733
-
3734
- class MixGrok extends MixOpenAI {
3735
- getDefaultConfig(customConfig) {
3736
-
3737
- if (!process.env.XAI_API_KEY) {
3738
- throw new Error('Grok API key not found. Please provide it in config or set XAI_API_KEY environment variable.');
3739
- }
3740
-
3741
- return super.getDefaultConfig({
3742
- url: 'https://api.x.ai/v1/chat/completions',
3743
- apiKey: process.env.XAI_API_KEY,
3744
- ...customConfig
3745
- });
3746
- }
3747
-
3748
- async create({ config = {}, options = {} } = {}) {
3749
- if (options.model === GROK420_REASONING || options.model === GROK420_NON_REASONING) {
3750
- delete options.reasoning_effort;
3751
- }
3752
- return super.create({ config, options });
3753
- }
3754
- }
3755
-
3756
- class MixLambda extends MixCustom {
3757
- getDefaultConfig(customConfig) {
3758
-
3759
- if (!process.env.LAMBDA_API_KEY) {
3760
- throw new Error('Lambda API key not found. Please provide it in config or set LAMBDA_API_KEY environment variable.');
3761
- }
3762
-
3763
- return super.getDefaultConfig({
3764
- url: 'https://api.lambda.ai/v1/chat/completions',
3765
- apiKey: process.env.LAMBDA_API_KEY,
3766
- ...customConfig
3767
- });
3768
- }
3769
- }
3770
-
3771
- class MixLMStudio extends MixCustom {
3772
- getDefaultConfig(customConfig) {
3773
- return super.getDefaultConfig({
3774
- url: 'http://localhost:1234/v1/chat/completions',
3775
- ...customConfig
3776
- });
3777
- }
3778
-
3779
- create({ config = {}, options = {} } = {}) {
3780
- if (config.schema) {
3781
- options.response_format = {
3782
- type: 'json_schema',
3783
- json_schema: { schema: config.schema }
3784
- };
3785
- }
3786
- return super.create({ config, options });
3787
- }
3788
-
3789
- static extractThink(data) {
3790
- const message = data.choices[0].message?.content?.trim() || '';
3791
-
3792
- // Check for LMStudio special tags
3793
- const startTag = '<|channel|>analysis<|message|>';
3794
- const endTag = '<|end|><|start|>assistant<|channel|>final<|message|>';
3795
-
3796
- const startIndex = message.indexOf(startTag);
3797
- const endIndex = message.indexOf(endTag);
3798
-
3799
- if (startIndex !== -1 && endIndex !== -1) {
3800
- // Extract content between the special tags
3801
- const thinkContent = message.substring(startIndex + startTag.length, endIndex).trim();
3802
- return thinkContent;
3803
- }
3804
-
3805
- // Fall back to default extraction method
3806
- return MixCustom.extractThink(data);
3807
- }
3808
-
3809
- static extractMessage(data) {
3810
- const message = data.choices[0].message?.content?.trim() || '';
3811
-
3812
- // Check for LMStudio special tags and extract final message
3813
- const endTag = '<|end|><|start|>assistant<|channel|>final<|message|>';
3814
- const endIndex = message.indexOf(endTag);
3815
-
3816
- if (endIndex !== -1) {
3817
- // Return only the content after the final message tag
3818
- return message.substring(endIndex + endTag.length).trim();
3819
- }
3820
-
3821
- // Fall back to default extraction method
3822
- return MixCustom.extractMessage(data);
3823
- }
3824
-
3825
- processResponse(response) {
3826
- return {
3827
- message: MixLMStudio.extractMessage(response.data),
3828
- think: MixLMStudio.extractThink(response.data),
3829
- toolCalls: MixCustom.extractToolCalls(response.data),
3830
- tokens: MixCustom.extractTokens(response.data),
3831
- response: response.data
3832
- };
3833
- }
3834
- }
3835
-
3836
- class MixGroq extends MixCustom {
3837
- getDefaultConfig(customConfig) {
3838
-
3839
- if (!process.env.GROQ_API_KEY) {
3840
- throw new Error('Groq API key not found. Please provide it in config or set GROQ_API_KEY environment variable.');
3841
- }
3842
-
3843
- return super.getDefaultConfig({
3844
- url: 'https://api.groq.com/openai/v1/chat/completions',
3845
- apiKey: process.env.GROQ_API_KEY,
3846
- ...customConfig
3847
- });
3848
- }
3849
- }
3850
-
3851
- class MixTogether extends MixCustom {
3852
- getDefaultConfig(customConfig) {
3853
-
3854
- if (!process.env.TOGETHER_API_KEY) {
3855
- throw new Error('Together API key not found. Please provide it in config or set TOGETHER_API_KEY environment variable.');
3856
- }
3857
-
3858
- return super.getDefaultConfig({
3859
- url: 'https://api.together.xyz/v1/chat/completions',
3860
- apiKey: process.env.TOGETHER_API_KEY,
3861
- ...customConfig
3862
- });
3863
- }
3864
-
3865
- getDefaultOptions(customOptions) {
3866
- return {
3867
- stop: ["<|eot_id|>", "<|eom_id|>"],
3868
- ...customOptions
3869
- };
3870
- }
3871
- }
3872
-
3873
- class MixCerebras extends MixCustom {
3874
- getDefaultConfig(customConfig) {
3875
-
3876
- if (!process.env.CEREBRAS_API_KEY) {
3877
- throw new Error('Together API key not found. Please provide it in config or set CEREBRAS_API_KEY environment variable.');
3878
- }
3879
-
3880
- return super.getDefaultConfig({
3881
- url: 'https://api.cerebras.ai/v1/chat/completions',
3882
- apiKey: process.env.CEREBRAS_API_KEY,
3883
- ...customConfig
3884
- });
3885
- }
3886
-
3887
- create({ config = {}, options = {} } = {}) {
3888
- delete options.response_format;
3889
- return super.create({ config, options });
3890
- }
3891
- }
3892
-
3893
- class MixFireworks extends MixCustom {
3894
- getDefaultConfig(customConfig) {
3895
-
3896
- if (!process.env.FIREWORKS_API_KEY) {
3897
- throw new Error('Fireworks API key not found. Please provide it in config or set FIREWORKS_API_KEY environment variable.');
3898
- }
3899
-
3900
- return super.getDefaultConfig({
3901
- url: 'https://api.fireworks.ai/inference/v1/chat/completions',
3902
- apiKey: process.env.FIREWORKS_API_KEY,
3903
- ...customConfig
3904
- });
3905
- }
3906
- }
3907
-
3908
- class MixNVIDIA extends MixCustom {
3909
- getDefaultConfig(customConfig) {
3910
-
3911
- if (!process.env.NVIDIA_API_KEY) {
3912
- throw new Error('NVIDIA API key not found. Please provide it in config or set NVIDIA_API_KEY environment variable.');
3913
- }
3914
-
3915
- return super.getDefaultConfig({
3916
- url: 'https://integrate.api.nvidia.com/v1/chat/completions',
3917
- apiKey: process.env.NVIDIA_API_KEY,
3918
- ...customConfig
3919
- });
3920
- }
3921
- }
3922
-
3923
- class MixGoogle extends MixCustom {
3924
- getDefaultConfig(customConfig) {
3925
- return super.getDefaultConfig({
3926
- url: 'https://generativelanguage.googleapis.com/v1beta/models',
3927
- apiKey: process.env.GEMINI_API_KEY,
3928
- ...customConfig
3929
- });
3930
- }
3931
-
3932
- getDefaultHeaders(customHeaders) {
3933
- return {
3934
- 'Content-Type': 'application/json',
3935
- ...customHeaders
3936
- };
3937
- }
3938
-
3939
- static convertMessages(messages, config) {
3940
- return messages.map(message => {
3941
-
3942
- // Handle assistant messages with tool_calls (content is null)
3943
- if (message.role === 'assistant' && message.tool_calls) {
3944
- return {
3945
- role: 'model',
3946
- parts: message.tool_calls.map(toolCall => {
3947
- const part = {
3948
- functionCall: {
3949
- name: toolCall.function.name,
3950
- args: JSON.parse(toolCall.function.arguments)
3951
- }
3952
- };
3953
- if (toolCall.thought_signature) {
3954
- part.thoughtSignature = toolCall.thought_signature;
3955
- }
3956
- return part;
3957
- })
3958
- }
3959
- }
3960
-
3961
- // Handle new tool result format: tool_call_id and name directly on message
3962
- if (message.role === 'tool' && message.name) {
3963
- return {
3964
- role: 'user',
3965
- parts: [{
3966
- functionResponse: {
3967
- name: message.name,
3968
- response: {
3969
- output: message.content,
3970
- },
3971
- }
3972
- }]
3973
- }
3974
- }
3975
-
3976
- if (!Array.isArray(message.content)) return message;
3977
- const role = (message.role === 'assistant' || message.role === 'tool') ? 'model' : 'user'
3978
-
3979
- if (message.role === 'tool') {
3980
- // Handle old format: content is an array of {name, content}
3981
- return {
3982
- role,
3983
- parts: message.content.map(content => ({
3984
- functionResponse: {
3985
- name: content.name,
3986
- response: {
3987
- output: content.content,
3988
- },
3989
- }
3990
- }))
3991
- }
3992
- }
3993
-
3994
- return {
3995
- role,
3996
- parts: message.content.map(content => {
3997
- if (content.type === 'text') {
3998
- return { text: content.text };
3999
- }
4000
-
4001
- if (content.type === 'image') {
4002
- return {
4003
- inline_data: {
4004
- mime_type: content.source.media_type,
4005
- data: content.source.data
4006
- }
4007
- }
4008
- }
4009
-
4010
- if (content.type === 'function') {
4011
- return {
4012
- functionCall: {
4013
- name: content.function.name,
4014
- args: JSON.parse(content.function.arguments)
4015
- }
4016
- }
4017
- }
4018
-
4019
- return content;
4020
- })
4021
- }
4022
- });
4023
-
4024
- // Merge consecutive user messages containing only functionResponse parts
4025
- // Google requires all function responses for a turn in a single message
4026
- return converted.reduce((acc, msg) => {
4027
- if (acc.length > 0) {
4028
- const prev = acc[acc.length - 1];
4029
- if (prev.role === 'user' && msg.role === 'user' &&
4030
- prev.parts.every(p => p.functionResponse) &&
4031
- msg.parts.every(p => p.functionResponse)) {
4032
- prev.parts.push(...msg.parts);
4033
- return acc;
4034
- }
4035
- }
4036
- acc.push(msg);
4037
- return acc;
4038
- }, []);
4039
- }
4040
-
4041
- async create({ config = {}, options = {} } = {}) {
4042
- if (!this.config.apiKey) {
4043
- throw new Error('Gemini API key not found. Please provide it in config or set GEMINI_API_KEY environment variable.');
4044
- }
4045
-
4046
- const generateContentApi = options.stream ? 'streamGenerateContent' : 'generateContent';
4047
-
4048
- const fullUrl = `${this.config.url}/${options.model}:${generateContentApi}?key=${this.config.apiKey}`;
4049
-
4050
-
4051
- const content = config.system;
4052
- const systemInstruction = { parts: [{ text: content }] };
4053
-
4054
- options.messages = MixGoogle.convertMessages(options.messages);
4055
-
4056
- const generationConfig = {
4057
- maxOutputTokens: options.max_tokens,
4058
- }
4059
-
4060
- if (options.top_p) {
4061
- generationConfig.topP = options.top_p;
4062
- }
4063
-
4064
- // Thinking / effort (from unified config.effort or native options)
4065
- if (options.thinkingConfig) {
4066
- generationConfig.thinkingConfig = options.thinkingConfig;
4067
- } else if (options.thinkingLevel != null || options.thinkingBudget != null) {
4068
- generationConfig.thinkingConfig = {};
4069
- if (options.thinkingLevel != null) {
4070
- generationConfig.thinkingConfig.thinkingLevel = options.thinkingLevel;
4071
- }
4072
- if (options.thinkingBudget != null) {
4073
- generationConfig.thinkingConfig.thinkingBudget = options.thinkingBudget;
4074
- }
4075
- }
4076
-
4077
- // Gemini does not support responseMimeType when function calling is used
4078
- const hasTools = options.tools && options.tools.length > 0 &&
4079
- options.tools.some(t => t.functionDeclarations && t.functionDeclarations.length > 0);
4080
-
4081
- if (!hasTools) {
4082
- generationConfig.responseMimeType = "text/plain";
4083
- }
4084
-
4085
- const payload = {
4086
- generationConfig,
4087
- systemInstruction,
4088
- contents: options.messages,
4089
- tools: options.tools
4090
- };
4091
-
4092
- try {
4093
- // debug level 4 (verbose): Full request details
4094
- if (config.debug >= 4) {
4095
- console.log('\n[REQUEST DETAILS - GOOGLE]');
4096
-
4097
- console.log('\n[CONFIG]');
4098
- console.log(ModelMix.formatJSON(configForDebug(config)));
4099
-
4100
- console.log('\n[PAYLOAD]');
4101
- console.log(ModelMix.formatJSON(payload));
4102
- }
4103
-
4104
- if (options.stream) {
4105
- throw new Error('Stream is not supported for Gemini');
4106
- } else {
4107
- return this.processResponse(await fetchJsonResponse(fullUrl, {
4108
- method: 'POST',
4109
- headers: this.headers,
4110
- body: JSON.stringify(payload)
4111
- }));
4112
- }
4113
- } catch (error) {
4114
- throw this.handleError(error);
4115
- }
4116
- }
4117
-
4118
- processResponse(response) {
4119
- return {
4120
- message: MixGoogle.extractMessage(response.data),
4121
- think: null,
4122
- toolCalls: MixGoogle.extractToolCalls(response.data),
4123
- tokens: MixGoogle.extractTokens(response.data),
4124
- response: response.data
4125
- }
4126
- }
4127
-
4128
- static extractToolCalls(data) {
4129
- return data.candidates?.[0]?.content?.parts?.map(part => {
4130
- if (part.functionCall) {
4131
- return {
4132
- id: part.functionCall.id,
4133
- type: 'function',
4134
- function: {
4135
- name: part.functionCall.name,
4136
- arguments: JSON.stringify(part.functionCall.args)
4137
- },
4138
- thought_signature: part.thoughtSignature || ""
4139
- };
4140
- }
4141
- return null;
4142
- }).filter(item => item !== null) || [];
4143
- }
4144
-
4145
- static extractMessage(data) {
4146
- return data.candidates?.[0]?.content?.parts?.[0]?.text;
4147
- }
4148
-
4149
- static extractTokens(data) {
4150
- // Google Gemini format
4151
- if (data.usageMetadata) {
4152
- return ModelMix.normalizeTokenUsage({
4153
- input: data.usageMetadata.promptTokenCount || 0,
4154
- output: data.usageMetadata.candidatesTokenCount || 0,
4155
- thinking: data.usageMetadata.thoughtsTokenCount || 0,
4156
- total: data.usageMetadata.totalTokenCount,
4157
- cached: ModelMix.extractCacheTokens(data.usageMetadata),
4158
- cacheWrite: ModelMix.extractCacheWriteTokens(data.usageMetadata)
4159
- });
4160
- }
4161
- return ModelMix.normalizeTokenUsage();
4162
- }
4163
-
4164
- static stripUnsupportedSchemaProps(schema) {
4165
- if (!schema || typeof schema !== 'object') return schema;
4166
- const cleaned = { ...schema };
4167
- delete cleaned.default;
4168
- if (cleaned.properties) {
4169
- cleaned.properties = Object.fromEntries(
4170
- Object.entries(cleaned.properties).map(([key, value]) => [key, MixGoogle.stripUnsupportedSchemaProps(value)])
4171
- );
4172
- }
4173
- if (cleaned.items) {
4174
- cleaned.items = MixGoogle.stripUnsupportedSchemaProps(cleaned.items);
4175
- }
4176
- return cleaned;
4177
- }
4178
-
4179
- static getOptionsTools(tools) {
4180
- const functionDeclarations = [];
4181
- for (const tool in tools) {
4182
- for (const item of tools[tool]) {
4183
- functionDeclarations.push({
4184
- name: item.name,
4185
- description: item.description,
4186
- parameters: MixGoogle.stripUnsupportedSchemaProps(item.inputSchema)
4187
- });
4188
- }
4189
- }
4190
-
4191
- const options = {};
4192
-
4193
- // Solo incluir tools si el array no está vacío
4194
- if (functionDeclarations.length > 0) {
4195
- options.tools = [{
4196
- functionDeclarations
4197
- }];
4198
- }
4199
-
4200
- return options;
4201
- }
4202
-
4203
- getOptionsTools(tools) {
4204
- return MixGoogle.getOptionsTools(tools);
4205
- }
4206
- }
1817
+ ({
1818
+ MixCustom,
1819
+ MixOpenAI,
1820
+ MixModeration,
1821
+ MixOpenAIResponses,
1822
+ MixOpenAIModeration,
1823
+ MixOpenAIWebSocket,
1824
+ MixOpenRouter,
1825
+ MixKimi,
1826
+ MixAnthropic,
1827
+ MixMiniMax,
1828
+ MixMiMo,
1829
+ MixPerplexity,
1830
+ MixOllama,
1831
+ MixGrok,
1832
+ MixLambda,
1833
+ MixLMStudio,
1834
+ MixGroq,
1835
+ MixTogether,
1836
+ MixCerebras,
1837
+ MixFireworks,
1838
+ MixNVIDIA,
1839
+ MixGoogle,
1840
+ ModerationMix
1841
+ } = require('./lib/providers')({
1842
+ ModelMix,
1843
+ log
1844
+ }));
4207
1845
 
4208
1846
  module.exports = { MixCustom, ModelMix, ModerationMix, MixModeration, MixAnthropic, MixKimi, MixMiniMax, MixMiMo, MixOpenAI, MixOpenAIResponses, MixOpenAIModeration, MixOpenAIWebSocket, MixOpenRouter, MixPerplexity, MixOllama, MixLMStudio, MixGroq, MixTogether, MixGrok, MixCerebras, MixGoogle, MixFireworks, MixNVIDIA, normalizeEffort, applyUnifiedEffort, resolveProviderFamily };