modelmix 5.0.6 → 5.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -112
- package/RLM_PLUGIN_SPEC.md +2 -2
- package/demo/fallback.js +2 -2
- package/demo/free.js +2 -3
- package/demo/gemini.js +2 -2
- package/demo/json.js +2 -2
- package/demo/mcp-simple.js +2 -2
- package/demo/mcp-tools.js +6 -6
- package/demo/mcp.js +1 -1
- package/demo/parallel-strategy.js +3 -3
- package/demo/parallel.js +2 -2
- package/demo/repl-powers.js +2 -3
- package/demo/rlm-basic.js +2 -2
- package/demo/rlm-fast.js +3 -3
- package/demo/rlm-simple.js +3 -3
- package/demo/short.js +1 -1
- package/demo/stream.js +1 -1
- package/demo/tokens.js +1 -1
- package/demo/verbose.js +5 -5
- package/effort.js +9 -3
- package/index.d.ts +0 -8
- package/index.js +499 -2861
- 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 +299 -0
- package/package.json +1 -1
- package/skills/modelmix/SKILL.md +10 -10
- package/test/effort.test.js +36 -0
- package/test/fallback.test.js +15 -3
- package/test/history.test.js +1 -1
- package/test/model-chain.test.js +12 -0
- package/test/moderation.test.js +1 -1
- package/test/public-api.test.js +54 -0
- package/test/tokens.test.js +20 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
const {
|
|
2
|
+
stripContentTypeHeader,
|
|
3
|
+
createMultipartFormData,
|
|
4
|
+
buildRequestBodyAndHeaders
|
|
5
|
+
} = require('../../multipart');
|
|
6
|
+
const {
|
|
7
|
+
fetchJsonResponse,
|
|
8
|
+
fetchStreamResponse
|
|
9
|
+
} = require('../../http-client');
|
|
10
|
+
const {
|
|
11
|
+
stripContentCacheMetadata
|
|
12
|
+
} = require('../content-cache');
|
|
13
|
+
const { configForDebug, redactSecret } = require('../provider-debug');
|
|
14
|
+
|
|
15
|
+
function createBaseProviders({ ModelMix }) {
|
|
16
|
+
class MixCustom {
|
|
17
|
+
constructor({ config = {}, options = {}, headers = {} } = {}) {
|
|
18
|
+
this.config = this.getDefaultConfig(config);
|
|
19
|
+
this.options = this.getDefaultOptions(options);
|
|
20
|
+
this.headers = this.getDefaultHeaders(headers);
|
|
21
|
+
this.streamCallback = null; // Define streamCallback here
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
getDefaultOptions(customOptions) {
|
|
25
|
+
return {
|
|
26
|
+
...customOptions
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
getDefaultConfig(customConfig) {
|
|
31
|
+
return {
|
|
32
|
+
url: '',
|
|
33
|
+
apiKey: '',
|
|
34
|
+
...customConfig
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getDefaultHeaders(customHeaders) {
|
|
39
|
+
return {
|
|
40
|
+
'accept': 'application/json',
|
|
41
|
+
'content-type': 'application/json',
|
|
42
|
+
'authorization': `Bearer ${this.config.apiKey}`,
|
|
43
|
+
...customHeaders
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
convertMessages(messages, config) {
|
|
48
|
+
return MixOpenAI.convertMessages(messages, config);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
sanitizeCacheOptions(options) {
|
|
52
|
+
delete options.cache_control;
|
|
53
|
+
delete options.prompt_cache_key;
|
|
54
|
+
delete options.prompt_cache_options;
|
|
55
|
+
delete options.prompt_cache_retention;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static stripContentTypeHeader(headers = {}) {
|
|
59
|
+
return stripContentTypeHeader(headers);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static createMultipartFormData({ fields = {}, files = [] } = {}) {
|
|
63
|
+
return createMultipartFormData({ fields, files });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
static buildRequestBodyAndHeaders(options, headers) {
|
|
67
|
+
return buildRequestBodyAndHeaders(options, headers);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async create({ config = {}, options = {} } = {}) {
|
|
71
|
+
try {
|
|
72
|
+
this.sanitizeCacheOptions(options);
|
|
73
|
+
if (Array.isArray(options.messages)) {
|
|
74
|
+
options.messages = this.convertMessages(options.messages, config);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const request = buildRequestBodyAndHeaders(options, this.headers);
|
|
78
|
+
|
|
79
|
+
// debug level 4 (verbose): Full request details
|
|
80
|
+
if (config.debug >= 4) {
|
|
81
|
+
console.log('\n[REQUEST DETAILS]');
|
|
82
|
+
|
|
83
|
+
console.log('\n[CONFIG]');
|
|
84
|
+
console.log(ModelMix.formatJSON(configForDebug(config)));
|
|
85
|
+
|
|
86
|
+
console.log('\n[OPTIONS]');
|
|
87
|
+
console.log(ModelMix.formatJSON(request.options));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (options.stream) {
|
|
91
|
+
return this.processStream(await fetchStreamResponse(this.config.url, {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers: request.headers,
|
|
94
|
+
body: request.body
|
|
95
|
+
}));
|
|
96
|
+
} else {
|
|
97
|
+
return this.processResponse(await fetchJsonResponse(this.config.url, {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
headers: request.headers,
|
|
100
|
+
body: request.body
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
} catch (error) {
|
|
104
|
+
throw this.handleError(error);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
handleError(error) {
|
|
109
|
+
let errorMessage = 'An error occurred in MixCustom';
|
|
110
|
+
let statusCode = null;
|
|
111
|
+
let errorDetails = null;
|
|
112
|
+
|
|
113
|
+
if (error?.isHttpError || error?.response || typeof error?.statusCode === 'number') {
|
|
114
|
+
statusCode = error.statusCode ?? error.response?.status ?? null;
|
|
115
|
+
errorMessage = error.message || `Request to ${this.config.url} failed with status code ${statusCode}`;
|
|
116
|
+
errorDetails = error.details ?? error.response?.data ?? null;
|
|
117
|
+
} else if (error?.message) {
|
|
118
|
+
errorMessage = error.message;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const formattedError = {
|
|
122
|
+
message: redactSecret(errorMessage, this.config.apiKey),
|
|
123
|
+
statusCode,
|
|
124
|
+
details: redactSecret(errorDetails, this.config.apiKey),
|
|
125
|
+
stack: redactSecret(error.stack, this.config.apiKey)
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
return formattedError;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
processStream(response) {
|
|
132
|
+
return new Promise((resolve, reject) => {
|
|
133
|
+
let raw = [];
|
|
134
|
+
let message = '';
|
|
135
|
+
let buffer = '';
|
|
136
|
+
|
|
137
|
+
response.data.on('data', chunk => {
|
|
138
|
+
buffer += chunk.toString();
|
|
139
|
+
|
|
140
|
+
let boundary;
|
|
141
|
+
while ((boundary = buffer.indexOf('\n')) !== -1) {
|
|
142
|
+
const dataStr = buffer.slice(0, boundary).trim();
|
|
143
|
+
buffer = buffer.slice(boundary + 1);
|
|
144
|
+
|
|
145
|
+
const firstBraceIndex = dataStr.indexOf('{');
|
|
146
|
+
if (dataStr === '[DONE]' || firstBraceIndex === -1) continue;
|
|
147
|
+
|
|
148
|
+
const jsonStr = dataStr.slice(firstBraceIndex);
|
|
149
|
+
try {
|
|
150
|
+
const data = JSON.parse(jsonStr);
|
|
151
|
+
if (this.streamCallback) {
|
|
152
|
+
const delta = this.extractDelta(data);
|
|
153
|
+
message += delta;
|
|
154
|
+
this.streamCallback({ response: data, message, delta });
|
|
155
|
+
raw.push(data);
|
|
156
|
+
}
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.error('Error parsing JSON:', error);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
response.data.on('end', () => resolve({
|
|
164
|
+
response: raw,
|
|
165
|
+
message: message.trim(),
|
|
166
|
+
toolCalls: [],
|
|
167
|
+
think: null,
|
|
168
|
+
tokens: raw.length > 0 ? MixCustom.extractTokens(raw[raw.length - 1]) : { input: 0, output: 0, total: 0, cached: 0 }
|
|
169
|
+
}));
|
|
170
|
+
response.data.on('error', reject);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
extractDelta(data) {
|
|
175
|
+
return data.choices[0].delta.content;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
static extractMessage(data) {
|
|
179
|
+
const choice = data?.choices?.[0] || {};
|
|
180
|
+
const messageObj = choice.message || {};
|
|
181
|
+
const finishReason = choice.finish_reason;
|
|
182
|
+
|
|
183
|
+
if (typeof messageObj.refusal === 'string' && messageObj.refusal.trim().length > 0) {
|
|
184
|
+
throw new Error(`OpenAI model refused to process this request: ${messageObj.refusal}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (finishReason === 'content_filter') {
|
|
188
|
+
throw new Error('OpenAI response was blocked by content_filter.');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let message = '';
|
|
192
|
+
if (typeof messageObj.content === 'string') {
|
|
193
|
+
message = messageObj.content.trim();
|
|
194
|
+
} else if (Array.isArray(messageObj.content)) {
|
|
195
|
+
const refusalPart = messageObj.content.find(part => part?.type === 'refusal' || (typeof part?.refusal === 'string' && part.refusal.trim().length > 0));
|
|
196
|
+
if (refusalPart) {
|
|
197
|
+
const refusalText = typeof refusalPart.refusal === 'string' ? refusalPart.refusal : 'No refusal text provided.';
|
|
198
|
+
throw new Error(`OpenAI model refused to process this request: ${refusalText}`);
|
|
199
|
+
}
|
|
200
|
+
message = messageObj.content
|
|
201
|
+
.filter(part => typeof part?.text === 'string')
|
|
202
|
+
.map(part => part.text)
|
|
203
|
+
.join('')
|
|
204
|
+
.trim();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const endTagIndex = message.indexOf('</think>');
|
|
208
|
+
if (message.startsWith('<think>') && endTagIndex !== -1) {
|
|
209
|
+
return message.substring(endTagIndex + 8).trim();
|
|
210
|
+
}
|
|
211
|
+
return message;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
static extractThink(data) {
|
|
215
|
+
|
|
216
|
+
if (data.choices[0].message?.reasoning_content) {
|
|
217
|
+
return data.choices[0].message.reasoning_content;
|
|
218
|
+
} else if (data.choices[0].message?.reasoning) {
|
|
219
|
+
return data.choices[0].message.reasoning;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const message = data.choices[0].message?.content?.trim() || '';
|
|
223
|
+
const endTagIndex = message.indexOf('</think>');
|
|
224
|
+
if (message.startsWith('<think>') && endTagIndex !== -1) {
|
|
225
|
+
return message.substring(7, endTagIndex).trim();
|
|
226
|
+
}
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
static extractToolCalls(data) {
|
|
231
|
+
return data.choices[0].message?.tool_calls?.map(call => ({
|
|
232
|
+
id: call.id,
|
|
233
|
+
type: 'function',
|
|
234
|
+
function: {
|
|
235
|
+
name: call.function.name,
|
|
236
|
+
arguments: call.function.arguments
|
|
237
|
+
}
|
|
238
|
+
})) || []
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
static extractTokens(data) {
|
|
242
|
+
// OpenAI/Groq/Together/Lambda/Cerebras/Fireworks format
|
|
243
|
+
if (data.usage) {
|
|
244
|
+
return ModelMix.normalizeTokenUsage({
|
|
245
|
+
input: data.usage.prompt_tokens || 0,
|
|
246
|
+
output: data.usage.completion_tokens || 0,
|
|
247
|
+
total: data.usage.total_tokens,
|
|
248
|
+
cached: ModelMix.extractCacheTokens(data.usage),
|
|
249
|
+
cacheWrite: ModelMix.extractCacheWriteTokens(data.usage)
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
return ModelMix.normalizeTokenUsage();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
processResponse(response) {
|
|
256
|
+
return {
|
|
257
|
+
message: MixCustom.extractMessage(response.data),
|
|
258
|
+
think: MixCustom.extractThink(response.data),
|
|
259
|
+
toolCalls: MixCustom.extractToolCalls(response.data),
|
|
260
|
+
tokens: MixCustom.extractTokens(response.data),
|
|
261
|
+
response: response.data
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
getOptionsTools(tools) {
|
|
266
|
+
return MixOpenAI.getOptionsTools(tools);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
class MixOpenAI extends MixCustom {
|
|
271
|
+
sanitizeCacheOptions(options) {
|
|
272
|
+
delete options.cache_control;
|
|
273
|
+
delete options.prompt_cache_options;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
getDefaultConfig(customConfig) {
|
|
277
|
+
|
|
278
|
+
if (!process.env.OPENAI_API_KEY) {
|
|
279
|
+
throw new Error('OpenAI API key not found. Please provide it in config or set OPENAI_API_KEY environment variable.');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return super.getDefaultConfig({
|
|
283
|
+
url: 'https://api.openai.com/v1/chat/completions',
|
|
284
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
285
|
+
...customConfig
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async create({ config = {}, options = {} } = {}) {
|
|
290
|
+
|
|
291
|
+
// Remove max_tokens and temperature for o1/o3 models
|
|
292
|
+
if (options.model?.startsWith('o')) {
|
|
293
|
+
delete options.max_tokens;
|
|
294
|
+
delete options.temperature;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Use max_completion_tokens and remove temperature for GPT-5 models
|
|
298
|
+
if (options.model?.includes('gpt-5')) {
|
|
299
|
+
if (options.max_tokens) {
|
|
300
|
+
options.max_completion_tokens = options.max_tokens;
|
|
301
|
+
delete options.max_tokens;
|
|
302
|
+
}
|
|
303
|
+
delete options.temperature;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return super.create({ config, options });
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
static convertMessages(messages, config) {
|
|
310
|
+
|
|
311
|
+
const content = config.system;
|
|
312
|
+
messages = [{ role: 'system', content }, ...messages || []];
|
|
313
|
+
|
|
314
|
+
const results = []
|
|
315
|
+
for (const message of messages) {
|
|
316
|
+
|
|
317
|
+
if (message.tool_calls) {
|
|
318
|
+
results.push({
|
|
319
|
+
role: 'assistant',
|
|
320
|
+
content: message.content ?? null,
|
|
321
|
+
...(message.reasoning_content && { reasoning_content: message.reasoning_content }),
|
|
322
|
+
tool_calls: message.tool_calls
|
|
323
|
+
})
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (message.role === 'tool') {
|
|
328
|
+
// Handle new format: tool_call_id directly on message
|
|
329
|
+
if (message.tool_call_id) {
|
|
330
|
+
results.push({
|
|
331
|
+
role: 'tool',
|
|
332
|
+
tool_call_id: message.tool_call_id,
|
|
333
|
+
content: message.content
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
// Handle old format: content is an array
|
|
337
|
+
else if (Array.isArray(message.content)) {
|
|
338
|
+
for (const content of message.content) {
|
|
339
|
+
results.push({
|
|
340
|
+
role: 'tool',
|
|
341
|
+
tool_call_id: content.tool_call_id,
|
|
342
|
+
content: content.content
|
|
343
|
+
})
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
let convertedMessage = { ...message };
|
|
350
|
+
if (Array.isArray(message.content)) {
|
|
351
|
+
convertedMessage = {
|
|
352
|
+
...message,
|
|
353
|
+
content: message.content.filter(content => content !== null && content !== undefined).map(content => {
|
|
354
|
+
if (content && content.type === 'image') {
|
|
355
|
+
const { media_type, data } = content.source;
|
|
356
|
+
return {
|
|
357
|
+
type: 'image_url',
|
|
358
|
+
image_url: {
|
|
359
|
+
url: `data:${media_type};base64,${data}`
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
return stripContentCacheMetadata(content);
|
|
364
|
+
})
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
results.push(convertedMessage);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return results;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
static getOptionsTools(tools) {
|
|
375
|
+
const options = {};
|
|
376
|
+
const toolsArray = [];
|
|
377
|
+
for (const tool in tools) {
|
|
378
|
+
for (const item of tools[tool]) {
|
|
379
|
+
toolsArray.push({
|
|
380
|
+
type: 'function',
|
|
381
|
+
function: {
|
|
382
|
+
name: item.name,
|
|
383
|
+
description: item.description,
|
|
384
|
+
parameters: item.inputSchema
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Solo incluir tools si el array no está vacío
|
|
391
|
+
if (toolsArray.length > 0) {
|
|
392
|
+
options.tools = toolsArray;
|
|
393
|
+
// options.tool_choice = "auto";
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return options;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
class MixModeration extends MixCustom {
|
|
401
|
+
getOptionsTools() {
|
|
402
|
+
return {};
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return { MixCustom, MixOpenAI, MixModeration };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
module.exports = createBaseProviders;
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
const { fetchJsonResponse } = require('../../http-client');
|
|
2
|
+
const { configForDebug } = require('../provider-debug');
|
|
3
|
+
|
|
4
|
+
function createGoogleProviders({ ModelMix, MixCustom }) {
|
|
5
|
+
class MixGoogle extends MixCustom {
|
|
6
|
+
getDefaultConfig(customConfig) {
|
|
7
|
+
return super.getDefaultConfig({
|
|
8
|
+
url: 'https://generativelanguage.googleapis.com/v1beta/models',
|
|
9
|
+
apiKey: process.env.GEMINI_API_KEY,
|
|
10
|
+
...customConfig
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
getDefaultHeaders(customHeaders) {
|
|
15
|
+
return {
|
|
16
|
+
'Content-Type': 'application/json',
|
|
17
|
+
...customHeaders
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static convertMessages(messages, config) {
|
|
22
|
+
return messages.map(message => {
|
|
23
|
+
|
|
24
|
+
// Handle assistant messages with tool_calls (content is null)
|
|
25
|
+
if (message.role === 'assistant' && message.tool_calls) {
|
|
26
|
+
return {
|
|
27
|
+
role: 'model',
|
|
28
|
+
parts: message.tool_calls.map(toolCall => {
|
|
29
|
+
const part = {
|
|
30
|
+
functionCall: {
|
|
31
|
+
name: toolCall.function.name,
|
|
32
|
+
args: JSON.parse(toolCall.function.arguments)
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
if (toolCall.thought_signature) {
|
|
36
|
+
part.thoughtSignature = toolCall.thought_signature;
|
|
37
|
+
}
|
|
38
|
+
return part;
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Handle new tool result format: tool_call_id and name directly on message
|
|
44
|
+
if (message.role === 'tool' && message.name) {
|
|
45
|
+
return {
|
|
46
|
+
role: 'user',
|
|
47
|
+
parts: [{
|
|
48
|
+
functionResponse: {
|
|
49
|
+
name: message.name,
|
|
50
|
+
response: {
|
|
51
|
+
output: message.content,
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
}]
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!Array.isArray(message.content)) return message;
|
|
59
|
+
const role = (message.role === 'assistant' || message.role === 'tool') ? 'model' : 'user'
|
|
60
|
+
|
|
61
|
+
if (message.role === 'tool') {
|
|
62
|
+
// Handle old format: content is an array of {name, content}
|
|
63
|
+
return {
|
|
64
|
+
role,
|
|
65
|
+
parts: message.content.map(content => ({
|
|
66
|
+
functionResponse: {
|
|
67
|
+
name: content.name,
|
|
68
|
+
response: {
|
|
69
|
+
output: content.content,
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
}))
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
role,
|
|
78
|
+
parts: message.content.map(content => {
|
|
79
|
+
if (content.type === 'text') {
|
|
80
|
+
return { text: content.text };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (content.type === 'image') {
|
|
84
|
+
return {
|
|
85
|
+
inline_data: {
|
|
86
|
+
mime_type: content.source.media_type,
|
|
87
|
+
data: content.source.data
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (content.type === 'function') {
|
|
93
|
+
return {
|
|
94
|
+
functionCall: {
|
|
95
|
+
name: content.function.name,
|
|
96
|
+
args: JSON.parse(content.function.arguments)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return content;
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Merge consecutive user messages containing only functionResponse parts
|
|
107
|
+
// Google requires all function responses for a turn in a single message
|
|
108
|
+
return converted.reduce((acc, msg) => {
|
|
109
|
+
if (acc.length > 0) {
|
|
110
|
+
const prev = acc[acc.length - 1];
|
|
111
|
+
if (prev.role === 'user' && msg.role === 'user' &&
|
|
112
|
+
prev.parts.every(p => p.functionResponse) &&
|
|
113
|
+
msg.parts.every(p => p.functionResponse)) {
|
|
114
|
+
prev.parts.push(...msg.parts);
|
|
115
|
+
return acc;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
acc.push(msg);
|
|
119
|
+
return acc;
|
|
120
|
+
}, []);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async create({ config = {}, options = {} } = {}) {
|
|
124
|
+
if (!this.config.apiKey) {
|
|
125
|
+
throw new Error('Gemini API key not found. Please provide it in config or set GEMINI_API_KEY environment variable.');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const generateContentApi = options.stream ? 'streamGenerateContent' : 'generateContent';
|
|
129
|
+
|
|
130
|
+
const fullUrl = `${this.config.url}/${options.model}:${generateContentApi}?key=${this.config.apiKey}`;
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
const content = config.system;
|
|
134
|
+
const systemInstruction = { parts: [{ text: content }] };
|
|
135
|
+
|
|
136
|
+
options.messages = MixGoogle.convertMessages(options.messages);
|
|
137
|
+
|
|
138
|
+
const generationConfig = {
|
|
139
|
+
maxOutputTokens: options.max_tokens,
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (options.top_p) {
|
|
143
|
+
generationConfig.topP = options.top_p;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Thinking / effort (from unified config.effort or native options)
|
|
147
|
+
if (options.thinkingConfig) {
|
|
148
|
+
generationConfig.thinkingConfig = options.thinkingConfig;
|
|
149
|
+
} else if (options.thinkingLevel != null || options.thinkingBudget != null) {
|
|
150
|
+
generationConfig.thinkingConfig = {};
|
|
151
|
+
if (options.thinkingLevel != null) {
|
|
152
|
+
generationConfig.thinkingConfig.thinkingLevel = options.thinkingLevel;
|
|
153
|
+
}
|
|
154
|
+
if (options.thinkingBudget != null) {
|
|
155
|
+
generationConfig.thinkingConfig.thinkingBudget = options.thinkingBudget;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Gemini does not support responseMimeType when function calling is used
|
|
160
|
+
const hasTools = options.tools && options.tools.length > 0 &&
|
|
161
|
+
options.tools.some(t => t.functionDeclarations && t.functionDeclarations.length > 0);
|
|
162
|
+
|
|
163
|
+
if (!hasTools) {
|
|
164
|
+
generationConfig.responseMimeType = "text/plain";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const payload = {
|
|
168
|
+
generationConfig,
|
|
169
|
+
systemInstruction,
|
|
170
|
+
contents: options.messages,
|
|
171
|
+
tools: options.tools
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
// debug level 4 (verbose): Full request details
|
|
176
|
+
if (config.debug >= 4) {
|
|
177
|
+
console.log('\n[REQUEST DETAILS - GOOGLE]');
|
|
178
|
+
|
|
179
|
+
console.log('\n[CONFIG]');
|
|
180
|
+
console.log(ModelMix.formatJSON(configForDebug(config)));
|
|
181
|
+
|
|
182
|
+
console.log('\n[PAYLOAD]');
|
|
183
|
+
console.log(ModelMix.formatJSON(payload));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (options.stream) {
|
|
187
|
+
throw new Error('Stream is not supported for Gemini');
|
|
188
|
+
} else {
|
|
189
|
+
return this.processResponse(await fetchJsonResponse(fullUrl, {
|
|
190
|
+
method: 'POST',
|
|
191
|
+
headers: this.headers,
|
|
192
|
+
body: JSON.stringify(payload)
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
} catch (error) {
|
|
196
|
+
throw this.handleError(error);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
processResponse(response) {
|
|
201
|
+
return {
|
|
202
|
+
message: MixGoogle.extractMessage(response.data),
|
|
203
|
+
think: null,
|
|
204
|
+
toolCalls: MixGoogle.extractToolCalls(response.data),
|
|
205
|
+
tokens: MixGoogle.extractTokens(response.data),
|
|
206
|
+
response: response.data
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
static extractToolCalls(data) {
|
|
211
|
+
return data.candidates?.[0]?.content?.parts?.map(part => {
|
|
212
|
+
if (part.functionCall) {
|
|
213
|
+
return {
|
|
214
|
+
id: part.functionCall.id,
|
|
215
|
+
type: 'function',
|
|
216
|
+
function: {
|
|
217
|
+
name: part.functionCall.name,
|
|
218
|
+
arguments: JSON.stringify(part.functionCall.args)
|
|
219
|
+
},
|
|
220
|
+
thought_signature: part.thoughtSignature || ""
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}).filter(item => item !== null) || [];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
static extractMessage(data) {
|
|
228
|
+
return data.candidates?.[0]?.content?.parts?.[0]?.text;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
static extractTokens(data) {
|
|
232
|
+
// Google Gemini format
|
|
233
|
+
if (data.usageMetadata) {
|
|
234
|
+
return ModelMix.normalizeTokenUsage({
|
|
235
|
+
input: data.usageMetadata.promptTokenCount || 0,
|
|
236
|
+
output: data.usageMetadata.candidatesTokenCount || 0,
|
|
237
|
+
thinking: data.usageMetadata.thoughtsTokenCount || 0,
|
|
238
|
+
total: data.usageMetadata.totalTokenCount,
|
|
239
|
+
cached: ModelMix.extractCacheTokens(data.usageMetadata),
|
|
240
|
+
cacheWrite: ModelMix.extractCacheWriteTokens(data.usageMetadata)
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
return ModelMix.normalizeTokenUsage();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
static stripUnsupportedSchemaProps(schema) {
|
|
247
|
+
if (!schema || typeof schema !== 'object') return schema;
|
|
248
|
+
const cleaned = { ...schema };
|
|
249
|
+
delete cleaned.default;
|
|
250
|
+
if (cleaned.properties) {
|
|
251
|
+
cleaned.properties = Object.fromEntries(
|
|
252
|
+
Object.entries(cleaned.properties).map(([key, value]) => [key, MixGoogle.stripUnsupportedSchemaProps(value)])
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
if (cleaned.items) {
|
|
256
|
+
cleaned.items = MixGoogle.stripUnsupportedSchemaProps(cleaned.items);
|
|
257
|
+
}
|
|
258
|
+
return cleaned;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
static getOptionsTools(tools) {
|
|
262
|
+
const functionDeclarations = [];
|
|
263
|
+
for (const tool in tools) {
|
|
264
|
+
for (const item of tools[tool]) {
|
|
265
|
+
functionDeclarations.push({
|
|
266
|
+
name: item.name,
|
|
267
|
+
description: item.description,
|
|
268
|
+
parameters: MixGoogle.stripUnsupportedSchemaProps(item.inputSchema)
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const options = {};
|
|
274
|
+
|
|
275
|
+
// Solo incluir tools si el array no está vacío
|
|
276
|
+
if (functionDeclarations.length > 0) {
|
|
277
|
+
options.tools = [{
|
|
278
|
+
functionDeclarations
|
|
279
|
+
}];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return options;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
getOptionsTools(tools) {
|
|
286
|
+
return MixGoogle.getOptionsTools(tools);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return { MixGoogle };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
module.exports = createGoogleProviders;
|