@just-every/ensemble 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +245 -0
- package/dist/cost_tracker.d.ts +2 -0
- package/dist/cost_tracker.d.ts.map +1 -0
- package/dist/cost_tracker.js +2 -0
- package/dist/cost_tracker.js.map +1 -0
- package/dist/errors.d.ts +55 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +134 -0
- package/dist/errors.js.map +1 -0
- package/dist/external_models.d.ts +10 -0
- package/dist/external_models.d.ts.map +1 -0
- package/dist/external_models.js +36 -0
- package/dist/external_models.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +47 -0
- package/dist/index.js.map +1 -0
- package/dist/model_data.d.ts +63 -0
- package/dist/model_data.d.ts.map +1 -0
- package/dist/model_data.js +1070 -0
- package/dist/model_data.js.map +1 -0
- package/dist/model_providers/base_provider.d.ts +37 -0
- package/dist/model_providers/base_provider.d.ts.map +1 -0
- package/dist/model_providers/base_provider.js +146 -0
- package/dist/model_providers/base_provider.js.map +1 -0
- package/dist/model_providers/claude.d.ts +11 -0
- package/dist/model_providers/claude.d.ts.map +1 -0
- package/dist/model_providers/claude.js +788 -0
- package/dist/model_providers/claude.js.map +1 -0
- package/dist/model_providers/deepseek.d.ts +8 -0
- package/dist/model_providers/deepseek.d.ts.map +1 -0
- package/dist/model_providers/deepseek.js +136 -0
- package/dist/model_providers/deepseek.js.map +1 -0
- package/dist/model_providers/gemini.d.ts +11 -0
- package/dist/model_providers/gemini.d.ts.map +1 -0
- package/dist/model_providers/gemini.js +711 -0
- package/dist/model_providers/gemini.js.map +1 -0
- package/dist/model_providers/grok.d.ts +8 -0
- package/dist/model_providers/grok.d.ts.map +1 -0
- package/dist/model_providers/grok.js +22 -0
- package/dist/model_providers/grok.js.map +1 -0
- package/dist/model_providers/model_provider.d.ts +11 -0
- package/dist/model_providers/model_provider.d.ts.map +1 -0
- package/dist/model_providers/model_provider.js +170 -0
- package/dist/model_providers/model_provider.js.map +1 -0
- package/dist/model_providers/openai.d.ts +13 -0
- package/dist/model_providers/openai.d.ts.map +1 -0
- package/dist/model_providers/openai.js +822 -0
- package/dist/model_providers/openai.js.map +1 -0
- package/dist/model_providers/openai_chat.d.ts +14 -0
- package/dist/model_providers/openai_chat.d.ts.map +1 -0
- package/dist/model_providers/openai_chat.js +719 -0
- package/dist/model_providers/openai_chat.js.map +1 -0
- package/dist/model_providers/openrouter.d.ts +6 -0
- package/dist/model_providers/openrouter.d.ts.map +1 -0
- package/dist/model_providers/openrouter.js +18 -0
- package/dist/model_providers/openrouter.js.map +1 -0
- package/dist/model_providers/refactored_openai.d.ts +22 -0
- package/dist/model_providers/refactored_openai.d.ts.map +1 -0
- package/dist/model_providers/refactored_openai.js +310 -0
- package/dist/model_providers/refactored_openai.js.map +1 -0
- package/dist/model_providers/test_provider.d.ts +27 -0
- package/dist/model_providers/test_provider.d.ts.map +1 -0
- package/dist/model_providers/test_provider.js +185 -0
- package/dist/model_providers/test_provider.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/types/api_types.d.ts +249 -0
- package/dist/types/api_types.d.ts.map +1 -0
- package/dist/types/api_types.js +2 -0
- package/dist/types/api_types.js.map +1 -0
- package/dist/types/extended_types.d.ts +43 -0
- package/dist/types/extended_types.d.ts.map +1 -0
- package/dist/types/extended_types.js +2 -0
- package/dist/types/extended_types.js.map +1 -0
- package/dist/types.d.ts +301 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/async_queue.d.ts +14 -0
- package/dist/utils/async_queue.d.ts.map +1 -0
- package/dist/utils/async_queue.js +68 -0
- package/dist/utils/async_queue.js.map +1 -0
- package/dist/utils/cache.d.ts +60 -0
- package/dist/utils/cache.d.ts.map +1 -0
- package/dist/utils/cache.js +205 -0
- package/dist/utils/cache.js.map +1 -0
- package/dist/utils/communication.d.ts +3 -0
- package/dist/utils/communication.d.ts.map +1 -0
- package/dist/utils/communication.js +8 -0
- package/dist/utils/communication.js.map +1 -0
- package/dist/utils/cost_tracker.d.ts +26 -0
- package/dist/utils/cost_tracker.d.ts.map +1 -0
- package/dist/utils/cost_tracker.js +177 -0
- package/dist/utils/cost_tracker.js.map +1 -0
- package/dist/utils/delta_buffer.d.ts +14 -0
- package/dist/utils/delta_buffer.d.ts.map +1 -0
- package/dist/utils/delta_buffer.js +60 -0
- package/dist/utils/delta_buffer.js.map +1 -0
- package/dist/utils/image_to_text.d.ts +3 -0
- package/dist/utils/image_to_text.d.ts.map +1 -0
- package/dist/utils/image_to_text.js +81 -0
- package/dist/utils/image_to_text.js.map +1 -0
- package/dist/utils/image_utils.d.ts +18 -0
- package/dist/utils/image_utils.d.ts.map +1 -0
- package/dist/utils/image_utils.js +132 -0
- package/dist/utils/image_utils.js.map +1 -0
- package/dist/utils/llm_logger.d.ts +8 -0
- package/dist/utils/llm_logger.d.ts.map +1 -0
- package/dist/utils/llm_logger.js +24 -0
- package/dist/utils/llm_logger.js.map +1 -0
- package/dist/utils/quota_tracker.d.ts +22 -0
- package/dist/utils/quota_tracker.d.ts.map +1 -0
- package/dist/utils/quota_tracker.js +338 -0
- package/dist/utils/quota_tracker.js.map +1 -0
- package/dist/utils/stream_converter.d.ts +19 -0
- package/dist/utils/stream_converter.d.ts.map +1 -0
- package/dist/utils/stream_converter.js +172 -0
- package/dist/utils/stream_converter.js.map +1 -0
- package/dist/validation.d.ts +1789 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +289 -0
- package/dist/validation.js.map +1 -0
- package/dist/vitest.config.d.ts +3 -0
- package/dist/vitest.config.d.ts.map +1 -0
- package/dist/vitest.config.js +34 -0
- package/dist/vitest.config.js.map +1 -0
- package/package.json +86 -0
|
@@ -0,0 +1,711 @@
|
|
|
1
|
+
import { GoogleGenAI, Type, FunctionCallingConfigMode, } from '@google/genai';
|
|
2
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
+
import { costTracker } from '@just-every/ensemble/cost_tracker';
|
|
4
|
+
import { log_llm_error, log_llm_request, log_llm_response, } from '../utils/llm_logger.js';
|
|
5
|
+
import { isPaused } from '../utils/communication.js';
|
|
6
|
+
import { extractBase64Image, resizeAndTruncateForGemini, } from '../utils/image_utils.js';
|
|
7
|
+
import { bufferDelta, flushBufferedDeltas, } from '../utils/delta_buffer.js';
|
|
8
|
+
function convertParameterToGeminiFormat(param) {
|
|
9
|
+
let type = Type.STRING;
|
|
10
|
+
switch (param.type) {
|
|
11
|
+
case 'string':
|
|
12
|
+
type = Type.STRING;
|
|
13
|
+
break;
|
|
14
|
+
case 'number':
|
|
15
|
+
type = Type.NUMBER;
|
|
16
|
+
break;
|
|
17
|
+
case 'boolean':
|
|
18
|
+
type = Type.BOOLEAN;
|
|
19
|
+
break;
|
|
20
|
+
case 'object':
|
|
21
|
+
type = Type.OBJECT;
|
|
22
|
+
break;
|
|
23
|
+
case 'array':
|
|
24
|
+
type = Type.ARRAY;
|
|
25
|
+
break;
|
|
26
|
+
case 'null':
|
|
27
|
+
type = Type.STRING;
|
|
28
|
+
console.warn(`Mapping 'null' type to STRING`);
|
|
29
|
+
break;
|
|
30
|
+
default:
|
|
31
|
+
console.warn(`Unsupported parameter type '${param.type}'. Defaulting to STRING.`);
|
|
32
|
+
type = Type.STRING;
|
|
33
|
+
}
|
|
34
|
+
const result = { type, description: param.description };
|
|
35
|
+
if (type === Type.ARRAY) {
|
|
36
|
+
if (param.items) {
|
|
37
|
+
if (param.items.type === 'object') {
|
|
38
|
+
result.items = convertParameterToGeminiFormat(param.items);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
result.items = {
|
|
42
|
+
type: param.items.type === 'string' ? Type.STRING :
|
|
43
|
+
param.items.type === 'number' ? Type.NUMBER :
|
|
44
|
+
param.items.type === 'boolean' ? Type.BOOLEAN : Type.STRING
|
|
45
|
+
};
|
|
46
|
+
if (param.items.enum) {
|
|
47
|
+
result.items.enum = param.items.enum;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
result.items = { type: Type.STRING };
|
|
53
|
+
}
|
|
54
|
+
if (param.enum) {
|
|
55
|
+
result.items.enum = param.enum;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else if (type === Type.OBJECT) {
|
|
59
|
+
if (param.properties && typeof param.properties === 'object') {
|
|
60
|
+
result.properties = {};
|
|
61
|
+
for (const [propName, propSchema] of Object.entries(param.properties)) {
|
|
62
|
+
result.properties[propName] = convertParameterToGeminiFormat(propSchema);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
result.properties = {};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else if (param.enum) {
|
|
70
|
+
result.format = 'enum';
|
|
71
|
+
result.enum = param.enum;
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
function convertToGeminiFunctionDeclarations(tools) {
|
|
76
|
+
return tools
|
|
77
|
+
.map(tool => {
|
|
78
|
+
if (tool.definition.function.name === 'google_web_search') {
|
|
79
|
+
console.log('[Gemini] Enabling Google Search grounding');
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const properties = {};
|
|
83
|
+
const toolParams = tool.definition?.function?.parameters?.properties;
|
|
84
|
+
if (toolParams) {
|
|
85
|
+
for (const [name, param] of Object.entries(toolParams)) {
|
|
86
|
+
properties[name] = convertParameterToGeminiFormat(param);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
console.warn(`Tool ${tool.definition?.function?.name || 'Unnamed Tool'} has missing or invalid parameters definition.`);
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
name: tool.definition.function.name,
|
|
94
|
+
description: tool.definition.function.description,
|
|
95
|
+
parameters: {
|
|
96
|
+
type: Type.OBJECT,
|
|
97
|
+
properties,
|
|
98
|
+
required: Array.isArray(tool.definition?.function?.parameters?.required)
|
|
99
|
+
? tool.definition.function.parameters.required
|
|
100
|
+
: [],
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
})
|
|
104
|
+
.filter(Boolean);
|
|
105
|
+
}
|
|
106
|
+
function getImageMimeType(imageData) {
|
|
107
|
+
if (imageData.includes('data:image/jpeg'))
|
|
108
|
+
return 'image/jpeg';
|
|
109
|
+
if (imageData.includes('data:image/png'))
|
|
110
|
+
return 'image/png';
|
|
111
|
+
if (imageData.includes('data:image/gif'))
|
|
112
|
+
return 'image/gif';
|
|
113
|
+
if (imageData.includes('data:image/webp'))
|
|
114
|
+
return 'image/webp';
|
|
115
|
+
return 'image/jpeg';
|
|
116
|
+
}
|
|
117
|
+
function cleanBase64Data(imageData) {
|
|
118
|
+
return imageData.replace(/^data:image\/[a-z]+;base64,/, '');
|
|
119
|
+
}
|
|
120
|
+
function formatGroundingChunks(chunks) {
|
|
121
|
+
return chunks
|
|
122
|
+
.filter(c => c?.web?.uri)
|
|
123
|
+
.map((c, i) => `${i + 1}. ${c.web.title || 'Untitled'} – ${c.web.uri}`)
|
|
124
|
+
.join('\n');
|
|
125
|
+
}
|
|
126
|
+
async function convertToGeminiContents(messages) {
|
|
127
|
+
const contents = [];
|
|
128
|
+
for (const msg of messages) {
|
|
129
|
+
if (msg.type === 'function_call') {
|
|
130
|
+
let args = {};
|
|
131
|
+
try {
|
|
132
|
+
const parsedArgs = JSON.parse(msg.arguments || '{}');
|
|
133
|
+
args =
|
|
134
|
+
typeof parsedArgs === 'object' && parsedArgs !== null
|
|
135
|
+
? parsedArgs
|
|
136
|
+
: { value: parsedArgs };
|
|
137
|
+
}
|
|
138
|
+
catch (e) {
|
|
139
|
+
console.error(`Failed to parse function call arguments for ${msg.name}:`, msg.arguments, e);
|
|
140
|
+
args = {
|
|
141
|
+
error: 'Invalid JSON arguments provided',
|
|
142
|
+
raw_args: msg.arguments,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
contents.push({
|
|
146
|
+
role: 'model',
|
|
147
|
+
parts: [
|
|
148
|
+
{
|
|
149
|
+
functionCall: {
|
|
150
|
+
name: msg.name,
|
|
151
|
+
args,
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
else if (msg.type === 'function_call_output') {
|
|
158
|
+
if (typeof msg.output === 'string') {
|
|
159
|
+
const extracted = extractBase64Image(msg.output);
|
|
160
|
+
if (extracted.found && extracted.image_id !== null) {
|
|
161
|
+
const image_id = extracted.image_id;
|
|
162
|
+
const originalImageData = extracted.images[image_id];
|
|
163
|
+
const processedImageData = await resizeAndTruncateForGemini(originalImageData);
|
|
164
|
+
const mimeType = getImageMimeType(processedImageData);
|
|
165
|
+
const cleanedImageData = cleanBase64Data(processedImageData);
|
|
166
|
+
const parts = [];
|
|
167
|
+
parts.push({
|
|
168
|
+
functionResponse: {
|
|
169
|
+
name: msg.name,
|
|
170
|
+
response: {
|
|
171
|
+
content: extracted.replaceContent.trim() ||
|
|
172
|
+
`[image #${image_id}]`,
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
parts.push({
|
|
177
|
+
inlineData: {
|
|
178
|
+
mimeType: mimeType,
|
|
179
|
+
data: cleanedImageData,
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
contents.push({
|
|
183
|
+
role: 'user',
|
|
184
|
+
parts: parts,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
contents.push({
|
|
189
|
+
role: 'user',
|
|
190
|
+
parts: [
|
|
191
|
+
{
|
|
192
|
+
functionResponse: {
|
|
193
|
+
name: msg.name,
|
|
194
|
+
response: { content: msg.output || '' },
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
contents.push({
|
|
203
|
+
role: 'user',
|
|
204
|
+
parts: [
|
|
205
|
+
{
|
|
206
|
+
functionResponse: {
|
|
207
|
+
name: msg.name,
|
|
208
|
+
response: { content: msg.output || '' },
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
const role = msg.role === 'assistant' ? 'model' : 'user';
|
|
217
|
+
let textContent = '';
|
|
218
|
+
if (typeof msg.content === 'string') {
|
|
219
|
+
const extracted = extractBase64Image(msg.content);
|
|
220
|
+
if (extracted.found && extracted.image_id !== null) {
|
|
221
|
+
const image_id = extracted.image_id;
|
|
222
|
+
const originalImageData = extracted.images[image_id];
|
|
223
|
+
const processedImageData = await resizeAndTruncateForGemini(originalImageData);
|
|
224
|
+
const mimeType = getImageMimeType(processedImageData);
|
|
225
|
+
const cleanedImageData = cleanBase64Data(processedImageData);
|
|
226
|
+
const parts = [];
|
|
227
|
+
if (extracted.replaceContent.trim()) {
|
|
228
|
+
parts.push({
|
|
229
|
+
text: extracted.replaceContent.trim(),
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
parts.push({
|
|
233
|
+
inlineData: {
|
|
234
|
+
mimeType: mimeType,
|
|
235
|
+
data: cleanedImageData,
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
contents.push({
|
|
239
|
+
role,
|
|
240
|
+
parts: parts,
|
|
241
|
+
});
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
textContent = msg.content;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
else if (msg.content &&
|
|
249
|
+
typeof msg.content === 'object' &&
|
|
250
|
+
'text' in msg.content) {
|
|
251
|
+
textContent = msg.content.text;
|
|
252
|
+
}
|
|
253
|
+
if (textContent && textContent.trim() !== '') {
|
|
254
|
+
if (msg.type === 'thinking') {
|
|
255
|
+
textContent = 'Thinking: ' + textContent;
|
|
256
|
+
}
|
|
257
|
+
contents.push({
|
|
258
|
+
role,
|
|
259
|
+
parts: [{ text: textContent.trim() }],
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return contents;
|
|
265
|
+
}
|
|
266
|
+
const THINKING_BUDGET_CONFIGS = {
|
|
267
|
+
'-low': 0,
|
|
268
|
+
'-medium': 2048,
|
|
269
|
+
'-high': 12288,
|
|
270
|
+
'-max': 24576,
|
|
271
|
+
};
|
|
272
|
+
export class GeminiProvider {
|
|
273
|
+
client;
|
|
274
|
+
constructor(apiKey) {
|
|
275
|
+
const key = apiKey || process.env.GOOGLE_API_KEY;
|
|
276
|
+
if (!key) {
|
|
277
|
+
throw new Error('Failed to initialize Gemini client. GOOGLE_API_KEY is missing or not provided.');
|
|
278
|
+
}
|
|
279
|
+
this.client = new GoogleGenAI({
|
|
280
|
+
apiKey: key,
|
|
281
|
+
vertexai: false,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
async createEmbedding(modelId, input, opts) {
|
|
285
|
+
try {
|
|
286
|
+
let actualModelId = modelId.startsWith('gemini/')
|
|
287
|
+
? modelId.substring(7)
|
|
288
|
+
: modelId;
|
|
289
|
+
let thinkingConfig = null;
|
|
290
|
+
for (const [suffix, budget] of Object.entries(THINKING_BUDGET_CONFIGS)) {
|
|
291
|
+
if (actualModelId.endsWith(suffix)) {
|
|
292
|
+
thinkingConfig = { thinkingBudget: budget };
|
|
293
|
+
actualModelId = actualModelId.slice(0, -suffix.length);
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
console.log(`[Gemini] Generating embedding with model ${actualModelId}`);
|
|
298
|
+
const payload = {
|
|
299
|
+
model: actualModelId,
|
|
300
|
+
contents: input,
|
|
301
|
+
config: {
|
|
302
|
+
taskType: opts?.taskType ?? 'SEMANTIC_SIMILARITY',
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
if (thinkingConfig) {
|
|
306
|
+
payload.config.thinkingConfig = thinkingConfig;
|
|
307
|
+
}
|
|
308
|
+
const response = await this.client.models.embedContent(payload);
|
|
309
|
+
console.log('[Gemini] Embedding response structure:', JSON.stringify(response, (key, value) => key === 'values' &&
|
|
310
|
+
Array.isArray(value) &&
|
|
311
|
+
value.length > 10
|
|
312
|
+
? `[${value.length} items]`
|
|
313
|
+
: value, 2));
|
|
314
|
+
if (!response.embeddings || !Array.isArray(response.embeddings)) {
|
|
315
|
+
console.error('[Gemini] Unexpected embedding response structure:', response);
|
|
316
|
+
throw new Error('Invalid embedding response structure from Gemini API');
|
|
317
|
+
}
|
|
318
|
+
const estimatedTokens = typeof input === 'string'
|
|
319
|
+
? Math.ceil(input.length / 4)
|
|
320
|
+
: input.reduce((sum, text) => sum + Math.ceil(text.length / 4), 0);
|
|
321
|
+
let extractedValues = [];
|
|
322
|
+
let dimensions = 0;
|
|
323
|
+
if (response.embeddings.length > 0) {
|
|
324
|
+
if (response.embeddings[0].values) {
|
|
325
|
+
extractedValues = response.embeddings.map(e => e.values);
|
|
326
|
+
dimensions = extractedValues[0].length;
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
console.warn('[Gemini] Could not find expected "values" property in embeddings response');
|
|
330
|
+
extractedValues =
|
|
331
|
+
response.embeddings;
|
|
332
|
+
dimensions = Array.isArray(extractedValues[0])
|
|
333
|
+
? extractedValues[0].length
|
|
334
|
+
: 0;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
costTracker.addUsage({
|
|
338
|
+
model: modelId,
|
|
339
|
+
input_tokens: estimatedTokens,
|
|
340
|
+
output_tokens: 0,
|
|
341
|
+
metadata: {
|
|
342
|
+
dimensions,
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
if (Array.isArray(input) && input.length > 1) {
|
|
346
|
+
return extractedValues;
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
let result;
|
|
350
|
+
if (Array.isArray(extractedValues) &&
|
|
351
|
+
extractedValues.length >= 1) {
|
|
352
|
+
const firstValue = extractedValues[0];
|
|
353
|
+
if (Array.isArray(firstValue)) {
|
|
354
|
+
result = firstValue;
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
console.error('[Gemini] Unexpected format in embedding result:', firstValue);
|
|
358
|
+
result = [];
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
result = [];
|
|
363
|
+
}
|
|
364
|
+
let adjustedResult = result;
|
|
365
|
+
if (result.length !== 3072) {
|
|
366
|
+
console.warn(`Gemini embedding returned ${result.length} dimensions, adjusting to 3072...`);
|
|
367
|
+
if (result.length > 3072) {
|
|
368
|
+
adjustedResult = result.slice(0, 3072);
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
adjustedResult = [
|
|
372
|
+
...result,
|
|
373
|
+
...Array(3072 - result.length).fill(0),
|
|
374
|
+
];
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return adjustedResult;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
console.error('[Gemini] Error generating embedding:', error);
|
|
382
|
+
throw error;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
async *retryStreamOnIncompleteJson(requestFn, maxRetries = 2) {
|
|
386
|
+
let attempts = 0;
|
|
387
|
+
while (attempts <= maxRetries) {
|
|
388
|
+
try {
|
|
389
|
+
const stream = await requestFn();
|
|
390
|
+
for await (const chunk of stream) {
|
|
391
|
+
yield chunk;
|
|
392
|
+
}
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
catch (error) {
|
|
396
|
+
attempts++;
|
|
397
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
398
|
+
if (errorMsg.includes('Incomplete JSON segment') &&
|
|
399
|
+
attempts <= maxRetries) {
|
|
400
|
+
console.warn(`[Gemini] Incomplete JSON segment error, retrying (${attempts}/${maxRetries})...`);
|
|
401
|
+
await new Promise(resolve => setTimeout(resolve, 1000 * attempts));
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
throw error;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
async *createResponseStream(model, messages, agent) {
|
|
409
|
+
const tools = agent
|
|
410
|
+
? await agent.getTools()
|
|
411
|
+
: [];
|
|
412
|
+
const settings = agent?.modelSettings;
|
|
413
|
+
let contentBuffer = '';
|
|
414
|
+
const messageId = uuidv4();
|
|
415
|
+
let eventOrder = 0;
|
|
416
|
+
const deltaBuffers = new Map();
|
|
417
|
+
let hasYieldedToolStart = false;
|
|
418
|
+
const shownGrounding = new Set();
|
|
419
|
+
let requestId = undefined;
|
|
420
|
+
const chunks = [];
|
|
421
|
+
try {
|
|
422
|
+
const contents = await convertToGeminiContents(messages);
|
|
423
|
+
if (contents.length === 0) {
|
|
424
|
+
throw new Error('No valid content found in messages after conversion.');
|
|
425
|
+
}
|
|
426
|
+
const lastContent = contents[contents.length - 1];
|
|
427
|
+
if (lastContent.role !== 'user') {
|
|
428
|
+
console.warn("Last message in history is not from 'user'. Gemini might not respond as expected.");
|
|
429
|
+
}
|
|
430
|
+
let thinkingBudget = null;
|
|
431
|
+
for (const [suffix, budget] of Object.entries(THINKING_BUDGET_CONFIGS)) {
|
|
432
|
+
if (model.endsWith(suffix)) {
|
|
433
|
+
thinkingBudget = budget;
|
|
434
|
+
model = model.slice(0, -suffix.length);
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
const config = {
|
|
439
|
+
thinkingConfig: {
|
|
440
|
+
includeThoughts: true,
|
|
441
|
+
},
|
|
442
|
+
};
|
|
443
|
+
if (thinkingBudget) {
|
|
444
|
+
config.thinkingConfig.thinkingBudget = thinkingBudget;
|
|
445
|
+
}
|
|
446
|
+
if (settings?.stop_sequence) {
|
|
447
|
+
config.stopSequences = [settings.stop_sequence];
|
|
448
|
+
}
|
|
449
|
+
if (settings?.temperature) {
|
|
450
|
+
config.temperature = settings.temperature;
|
|
451
|
+
}
|
|
452
|
+
if (settings?.max_tokens) {
|
|
453
|
+
config.maxOutputTokens = settings.max_tokens;
|
|
454
|
+
}
|
|
455
|
+
if (settings?.top_p) {
|
|
456
|
+
config.topP = settings.top_p;
|
|
457
|
+
}
|
|
458
|
+
if (settings?.top_k) {
|
|
459
|
+
config.topK = settings.top_k;
|
|
460
|
+
}
|
|
461
|
+
if (settings?.json_schema) {
|
|
462
|
+
config.responseMimeType = 'application/json';
|
|
463
|
+
config.responseSchema = settings.json_schema.schema;
|
|
464
|
+
if (config.responseSchema) {
|
|
465
|
+
const removeAdditionalProperties = (obj) => {
|
|
466
|
+
if (!obj || typeof obj !== 'object') {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if ('additionalProperties' in obj) {
|
|
470
|
+
delete obj.additionalProperties;
|
|
471
|
+
}
|
|
472
|
+
if (obj.properties &&
|
|
473
|
+
typeof obj.properties === 'object') {
|
|
474
|
+
Object.values(obj.properties).forEach(prop => {
|
|
475
|
+
removeAdditionalProperties(prop);
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
if (obj.items) {
|
|
479
|
+
removeAdditionalProperties(obj.items);
|
|
480
|
+
}
|
|
481
|
+
['oneOf', 'anyOf', 'allOf'].forEach(key => {
|
|
482
|
+
if (obj[key] && Array.isArray(obj[key])) {
|
|
483
|
+
obj[key].forEach((subSchema) => {
|
|
484
|
+
removeAdditionalProperties(subSchema);
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
};
|
|
489
|
+
removeAdditionalProperties(config.responseSchema);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
let hasGoogleWebSearch = false;
|
|
493
|
+
if (tools && tools.length > 0) {
|
|
494
|
+
hasGoogleWebSearch = tools.some(tool => tool.definition.function.name === 'google_web_search');
|
|
495
|
+
const functionDeclarations = convertToGeminiFunctionDeclarations(tools);
|
|
496
|
+
let allowedFunctionNames = [];
|
|
497
|
+
if (functionDeclarations.length > 0) {
|
|
498
|
+
config.tools = [{ functionDeclarations }];
|
|
499
|
+
if (settings?.tool_choice) {
|
|
500
|
+
let toolChoice;
|
|
501
|
+
if (typeof settings.tool_choice === 'object' &&
|
|
502
|
+
settings.tool_choice?.type === 'function' &&
|
|
503
|
+
settings.tool_choice?.function?.name) {
|
|
504
|
+
toolChoice = FunctionCallingConfigMode.ANY;
|
|
505
|
+
allowedFunctionNames = [
|
|
506
|
+
settings.tool_choice.function.name,
|
|
507
|
+
];
|
|
508
|
+
}
|
|
509
|
+
else if (settings.tool_choice === 'required') {
|
|
510
|
+
toolChoice = FunctionCallingConfigMode.ANY;
|
|
511
|
+
}
|
|
512
|
+
else if (settings.tool_choice === 'auto') {
|
|
513
|
+
toolChoice = FunctionCallingConfigMode.AUTO;
|
|
514
|
+
}
|
|
515
|
+
else if (settings.tool_choice === 'none') {
|
|
516
|
+
toolChoice = FunctionCallingConfigMode.NONE;
|
|
517
|
+
}
|
|
518
|
+
if (toolChoice) {
|
|
519
|
+
config.toolConfig = {
|
|
520
|
+
functionCallingConfig: {
|
|
521
|
+
mode: toolChoice,
|
|
522
|
+
},
|
|
523
|
+
};
|
|
524
|
+
if (allowedFunctionNames.length > 0) {
|
|
525
|
+
config.toolConfig.functionCallingConfig.allowedFunctionNames =
|
|
526
|
+
allowedFunctionNames;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
else if (!hasGoogleWebSearch) {
|
|
532
|
+
console.warn('Tools were provided but resulted in empty declarations after conversion.');
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (hasGoogleWebSearch) {
|
|
536
|
+
console.log('[Gemini] Enabling Google Search grounding');
|
|
537
|
+
config.tools = [{ googleSearch: {} }];
|
|
538
|
+
config.toolConfig = {
|
|
539
|
+
functionCallingConfig: {
|
|
540
|
+
mode: FunctionCallingConfigMode.ANY,
|
|
541
|
+
allowedFunctionNames: ['googleSearch'],
|
|
542
|
+
},
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
const requestParams = {
|
|
546
|
+
model,
|
|
547
|
+
contents,
|
|
548
|
+
config,
|
|
549
|
+
};
|
|
550
|
+
requestId = log_llm_request(agent.agent_id, 'google', model, requestParams);
|
|
551
|
+
const getStreamFn = () => this.client.models.generateContentStream(requestParams);
|
|
552
|
+
const response = this.retryStreamOnIncompleteJson(getStreamFn);
|
|
553
|
+
let usageMetadata;
|
|
554
|
+
for await (const chunk of response) {
|
|
555
|
+
chunks.push(chunk);
|
|
556
|
+
if (isPaused()) {
|
|
557
|
+
console.log(`[Gemini] System paused during stream for model ${model}. Aborting processing.`);
|
|
558
|
+
yield {
|
|
559
|
+
type: 'message_delta',
|
|
560
|
+
content: '\n⏸️ Stream paused by user.',
|
|
561
|
+
message_id: messageId,
|
|
562
|
+
order: 999,
|
|
563
|
+
};
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
if (chunk.functionCalls && chunk.functionCalls.length > 0) {
|
|
567
|
+
const toolCallsToEmit = [];
|
|
568
|
+
for (const fc of chunk.functionCalls) {
|
|
569
|
+
if (fc && fc.name) {
|
|
570
|
+
const callId = `call_${uuidv4()}`;
|
|
571
|
+
toolCallsToEmit.push({
|
|
572
|
+
id: callId,
|
|
573
|
+
type: 'function',
|
|
574
|
+
function: {
|
|
575
|
+
name: fc.name,
|
|
576
|
+
arguments: JSON.stringify(fc.args || {}),
|
|
577
|
+
},
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
if (toolCallsToEmit.length > 0 && !hasYieldedToolStart) {
|
|
582
|
+
yield {
|
|
583
|
+
type: 'tool_start',
|
|
584
|
+
tool_calls: toolCallsToEmit,
|
|
585
|
+
};
|
|
586
|
+
hasYieldedToolStart = true;
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
if (chunk.text) {
|
|
591
|
+
contentBuffer += chunk.text;
|
|
592
|
+
for (const ev of bufferDelta(deltaBuffers, messageId, chunk.text, content => ({
|
|
593
|
+
type: 'message_delta',
|
|
594
|
+
content,
|
|
595
|
+
message_id: messageId,
|
|
596
|
+
order: eventOrder++,
|
|
597
|
+
}))) {
|
|
598
|
+
yield ev;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
const gChunks = chunk.candidates?.[0]?.groundingMetadata?.groundingChunks;
|
|
602
|
+
if (Array.isArray(gChunks)) {
|
|
603
|
+
const newChunks = gChunks.filter(c => c?.web?.uri && !shownGrounding.has(c.web.uri));
|
|
604
|
+
if (newChunks.length) {
|
|
605
|
+
newChunks.forEach(c => shownGrounding.add(c.web.uri));
|
|
606
|
+
const formatted = formatGroundingChunks(newChunks);
|
|
607
|
+
yield {
|
|
608
|
+
type: 'message_delta',
|
|
609
|
+
content: '\n\nSearch Results:\n' + formatted + '\n',
|
|
610
|
+
message_id: messageId,
|
|
611
|
+
order: eventOrder++,
|
|
612
|
+
};
|
|
613
|
+
contentBuffer +=
|
|
614
|
+
'\n\nSearch Results:\n' + formatted + '\n';
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
if (chunk.candidates?.[0]?.content?.parts) {
|
|
618
|
+
for (const part of chunk.candidates[0].content.parts) {
|
|
619
|
+
if (part.inlineData?.data) {
|
|
620
|
+
yield {
|
|
621
|
+
type: 'file_complete',
|
|
622
|
+
data_format: 'base64',
|
|
623
|
+
data: part.inlineData.data,
|
|
624
|
+
mime_type: part.inlineData.mimeType || 'image/png',
|
|
625
|
+
message_id: uuidv4(),
|
|
626
|
+
order: eventOrder++,
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
if (chunk.usageMetadata) {
|
|
632
|
+
usageMetadata = chunk.usageMetadata;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
for (const ev of flushBufferedDeltas(deltaBuffers, (_id, content) => ({
|
|
636
|
+
type: 'message_delta',
|
|
637
|
+
content,
|
|
638
|
+
message_id: messageId,
|
|
639
|
+
order: eventOrder++,
|
|
640
|
+
}))) {
|
|
641
|
+
yield ev;
|
|
642
|
+
}
|
|
643
|
+
if (usageMetadata) {
|
|
644
|
+
costTracker.addUsage({
|
|
645
|
+
model,
|
|
646
|
+
input_tokens: usageMetadata.promptTokenCount || 0,
|
|
647
|
+
output_tokens: usageMetadata.candidatesTokenCount || 0,
|
|
648
|
+
cached_tokens: usageMetadata.cachedContentTokenCount || 0,
|
|
649
|
+
metadata: {
|
|
650
|
+
total_tokens: usageMetadata.totalTokenCount || 0,
|
|
651
|
+
reasoning_tokens: usageMetadata.thoughtsTokenCount || 0,
|
|
652
|
+
tool_tokens: usageMetadata.toolUsePromptTokenCount || 0,
|
|
653
|
+
},
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
else {
|
|
657
|
+
console.error('No usage metadata found in the response. This may affect token tracking.');
|
|
658
|
+
costTracker.addUsage({
|
|
659
|
+
model,
|
|
660
|
+
input_tokens: 0,
|
|
661
|
+
output_tokens: 0,
|
|
662
|
+
cached_tokens: 0,
|
|
663
|
+
metadata: {
|
|
664
|
+
total_tokens: 0,
|
|
665
|
+
source: 'estimated',
|
|
666
|
+
},
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
if (!hasYieldedToolStart && contentBuffer) {
|
|
670
|
+
yield {
|
|
671
|
+
type: 'message_complete',
|
|
672
|
+
content: contentBuffer,
|
|
673
|
+
message_id: messageId,
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
catch (error) {
|
|
678
|
+
log_llm_error(requestId, error);
|
|
679
|
+
const errorMessage = error instanceof Error
|
|
680
|
+
? error.stack || error.message
|
|
681
|
+
: String(error);
|
|
682
|
+
if (errorMessage.includes('Incomplete JSON segment')) {
|
|
683
|
+
console.error('[Gemini] Stream terminated with incomplete JSON. This may indicate network issues or timeouts.');
|
|
684
|
+
}
|
|
685
|
+
console.error('\n=== Gemini error ===');
|
|
686
|
+
console.dir(error, { depth: null });
|
|
687
|
+
console.error('\n=== JSON dump of error ===');
|
|
688
|
+
console.error(JSON.stringify(error, Object.getOwnPropertyNames(error), 2));
|
|
689
|
+
console.error('\n=== Manual property walk ===');
|
|
690
|
+
for (const key of Reflect.ownKeys(error)) {
|
|
691
|
+
console.error(`${String(key)}:`, error[key]);
|
|
692
|
+
}
|
|
693
|
+
yield {
|
|
694
|
+
type: 'error',
|
|
695
|
+
error: `Gemini error ${model}: ${errorMessage}`,
|
|
696
|
+
};
|
|
697
|
+
if (!hasYieldedToolStart && contentBuffer) {
|
|
698
|
+
yield {
|
|
699
|
+
type: 'message_complete',
|
|
700
|
+
content: contentBuffer,
|
|
701
|
+
message_id: messageId,
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
finally {
|
|
706
|
+
log_llm_response(requestId, chunks);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
export const geminiProvider = new GeminiProvider();
|
|
711
|
+
//# sourceMappingURL=gemini.js.map
|