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