@smythos/sre 1.7.40 → 1.7.41
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/dist/index.js +49 -42
- package/dist/index.js.map +1 -1
- package/dist/types/Components/AgentPlugin.class.d.ts +1 -1
- package/dist/types/Components/RAG/DataSourceCleaner.class.d.ts +4 -4
- package/dist/types/Components/RAG/DataSourceComponent.class.d.ts +5 -1
- package/dist/types/config.d.ts +1 -0
- package/dist/types/helpers/Conversation.helper.d.ts +10 -13
- package/dist/types/helpers/TemplateString.helper.d.ts +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/subsystems/IO/VectorDB.service/VectorDBConnector.d.ts +1 -0
- package/dist/types/subsystems/LLMManager/LLM.helper.d.ts +19 -0
- package/dist/types/subsystems/LLMManager/LLM.service/connectors/GoogleAI.class.d.ts +15 -10
- package/dist/types/types/LLM.types.d.ts +23 -0
- package/package.json +1 -1
- package/src/Components/AgentPlugin.class.ts +20 -3
- package/src/Components/Classifier.class.ts +79 -16
- package/src/Components/ForEach.class.ts +34 -6
- package/src/Components/GenAILLM.class.ts +54 -23
- package/src/Components/LLMAssistant.class.ts +56 -21
- package/src/Components/RAG/DataSourceCleaner.class.ts +13 -11
- package/src/Components/RAG/DataSourceComponent.class.ts +39 -13
- package/src/Components/RAG/DataSourceIndexer.class.ts +18 -12
- package/src/Components/RAG/DataSourceLookup.class.ts +14 -10
- package/src/Components/ScrapflyWebScrape.class.ts +7 -0
- package/src/config.ts +1 -0
- package/src/helpers/Conversation.helper.ts +112 -26
- package/src/helpers/TemplateString.helper.ts +6 -5
- package/src/index.ts +1 -0
- package/src/index.ts.bak +1 -0
- package/src/subsystems/IO/VectorDB.service/VectorDBConnector.ts +1 -0
- package/src/subsystems/IO/VectorDB.service/connectors/PineconeVectorDB.class.ts +11 -0
- package/src/subsystems/IO/VectorDB.service/embed/index.ts +9 -11
- package/src/subsystems/LLMManager/LLM.helper.ts +25 -0
- package/src/subsystems/LLMManager/LLM.service/LLMConnector.ts +1 -1
- package/src/subsystems/LLMManager/LLM.service/connectors/GoogleAI.class.ts +190 -146
- package/src/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/utils.ts +1 -1
- package/src/subsystems/ObservabilityManager/Telemetry.service/connectors/OTel/OTel.class.ts +229 -12
- package/src/types/LLM.types.ts +24 -0
- package/src/utils/data.utils.ts +6 -4
- package/src/Components/DataSourceIndexer.class.ts +0 -295
|
@@ -63,11 +63,8 @@ const VALID_MIME_TYPES = [
|
|
|
63
63
|
type UsageMetadataWithThoughtsToken = GenerateContentResponseUsageMetadata & { thoughtsTokenCount?: number; cost?: number };
|
|
64
64
|
|
|
65
65
|
const IMAGE_GEN_FIXED_PRICING = {
|
|
66
|
-
'imagen-3.0-generate-001': 0.04, // Fixed cost per image
|
|
67
|
-
'imagen-4.0-generate-001': 0.04, // Fixed cost per image
|
|
68
66
|
'imagen-4': 0.04, // Standard Imagen 4
|
|
69
67
|
'imagen-4-ultra': 0.06, // Imagen 4 Ultra
|
|
70
|
-
'gemini-2.5-flash-image': 0.039,
|
|
71
68
|
};
|
|
72
69
|
|
|
73
70
|
export class GoogleAIConnector extends LLMConnector {
|
|
@@ -129,9 +126,19 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
129
126
|
let useTool = false;
|
|
130
127
|
|
|
131
128
|
if (toolCalls && toolCalls.length > 0) {
|
|
129
|
+
// Extract the thoughtSignature from the first tool call (Google AI only attaches it to the first one)
|
|
130
|
+
const sharedThoughtSignature = (toolCalls[0] as any).thoughtSignature;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Unique ID per streamRequest call to prevent tool ID collisions.
|
|
134
|
+
* Without unique IDs, each call would generate "tool-0", causing UI merge conflicts.
|
|
135
|
+
* Example: tool-ABC123-0, tool-DEF456-0, tool-GHI789-0 (instead of all "tool-0")
|
|
136
|
+
*/
|
|
137
|
+
const requestId = uid();
|
|
138
|
+
|
|
132
139
|
toolsData = toolCalls.map((toolCall, index) => ({
|
|
133
140
|
index,
|
|
134
|
-
id: `tool-${index}`,
|
|
141
|
+
id: `tool-${requestId}-${index}`,
|
|
135
142
|
type: 'function',
|
|
136
143
|
name: toolCall.functionCall?.name,
|
|
137
144
|
arguments:
|
|
@@ -139,7 +146,8 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
139
146
|
? toolCall.functionCall?.args
|
|
140
147
|
: JSON.stringify(toolCall.functionCall?.args ?? {}),
|
|
141
148
|
role: TLLMMessageRole.Assistant,
|
|
142
|
-
|
|
149
|
+
// All parallel tool calls share the same thoughtSignature from the first one
|
|
150
|
+
thoughtSignature: (toolCall as any).thoughtSignature || sharedThoughtSignature,
|
|
143
151
|
}));
|
|
144
152
|
useTool = true;
|
|
145
153
|
}
|
|
@@ -182,63 +190,92 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
182
190
|
|
|
183
191
|
let toolsData: ToolData[] = [];
|
|
184
192
|
let usage: UsageMetadataWithThoughtsToken | undefined;
|
|
193
|
+
let streamThoughtSignature: string | undefined; // Track signature across streaming chunks
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Unique ID per streamRequest call to prevent tool ID collisions.
|
|
197
|
+
* Without unique IDs, each call would generate "tool-0", causing UI merge conflicts.
|
|
198
|
+
* Example: tool-ABC123-0, tool-DEF456-0, tool-GHI789-0 (instead of all "tool-0")
|
|
199
|
+
*/
|
|
200
|
+
const requestId = uid();
|
|
201
|
+
|
|
202
|
+
// Defer async processing to next tick to ensure event listeners are attached first
|
|
203
|
+
// This prevents race condition where fast tool calls emit events before listeners are ready
|
|
204
|
+
setImmediate(() => {
|
|
205
|
+
(async () => {
|
|
206
|
+
try {
|
|
207
|
+
for await (const chunk of stream) {
|
|
208
|
+
emitter.emit(TLLMEvent.Data, chunk);
|
|
209
|
+
|
|
210
|
+
const parts = chunk.candidates?.[0]?.content?.parts || [];
|
|
211
|
+
// Extract text from parts, filtering out non-text parts and ensuring type safety
|
|
212
|
+
const textParts = parts
|
|
213
|
+
.map((part) => part?.text)
|
|
214
|
+
.filter((text): text is string => typeof text === 'string')
|
|
215
|
+
.join('');
|
|
216
|
+
if (textParts) {
|
|
217
|
+
emitter.emit(TLLMEvent.Content, textParts);
|
|
218
|
+
}
|
|
185
219
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
220
|
+
const toolCalls = chunk.candidates?.[0]?.content?.parts?.filter((part) => part.functionCall);
|
|
221
|
+
if (toolCalls && toolCalls.length > 0) {
|
|
222
|
+
// Capture thoughtSignature from the first tool call chunk if we haven't already
|
|
223
|
+
if (!streamThoughtSignature) {
|
|
224
|
+
streamThoughtSignature = (toolCalls[0] as any).thoughtSignature;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// For streaming, replace toolsData with the latest chunk (chunks contain cumulative tool calls)
|
|
228
|
+
// All tool calls in this request share the same requestId for uniqueness
|
|
229
|
+
toolsData = toolCalls.map((toolCall, index) => ({
|
|
230
|
+
index,
|
|
231
|
+
id: `tool-${requestId}-${index}`,
|
|
232
|
+
type: 'function' as const,
|
|
233
|
+
name: toolCall.functionCall?.name,
|
|
234
|
+
arguments:
|
|
235
|
+
typeof toolCall.functionCall?.args === 'string'
|
|
236
|
+
? toolCall.functionCall?.args
|
|
237
|
+
: JSON.stringify(toolCall.functionCall?.args ?? {}),
|
|
238
|
+
role: TLLMMessageRole.Assistant as any,
|
|
239
|
+
// All tool calls share the thoughtSignature from the first chunk
|
|
240
|
+
thoughtSignature: (toolCall as any).thoughtSignature || streamThoughtSignature,
|
|
241
|
+
}));
|
|
242
|
+
}
|
|
190
243
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
244
|
+
if (chunk.usageMetadata) {
|
|
245
|
+
usage = chunk.usageMetadata as UsageMetadataWithThoughtsToken;
|
|
246
|
+
}
|
|
194
247
|
}
|
|
195
248
|
|
|
196
|
-
|
|
197
|
-
if (
|
|
198
|
-
toolsData = toolCalls.map((toolCall, index) => ({
|
|
199
|
-
index,
|
|
200
|
-
id: `tool-${index}`,
|
|
201
|
-
type: 'function',
|
|
202
|
-
name: toolCall.functionCall?.name,
|
|
203
|
-
arguments:
|
|
204
|
-
typeof toolCall.functionCall?.args === 'string'
|
|
205
|
-
? toolCall.functionCall?.args
|
|
206
|
-
: JSON.stringify(toolCall.functionCall?.args ?? {}),
|
|
207
|
-
role: TLLMMessageRole.Assistant,
|
|
208
|
-
thoughtSignature: (toolCall as any).thoughtSignature, // Preserve Google AI's reasoning context
|
|
209
|
-
}));
|
|
249
|
+
// Emit ToolInfo once after all chunks are processed (similar to Anthropic's finalMessage pattern)
|
|
250
|
+
if (toolsData.length > 0) {
|
|
210
251
|
emitter.emit(TLLMEvent.ToolInfo, toolsData);
|
|
211
252
|
}
|
|
212
253
|
|
|
213
|
-
|
|
214
|
-
|
|
254
|
+
const finishReason = 'stop'; // GoogleAI doesn't provide finishReason in streaming
|
|
255
|
+
const reportedUsage: any[] = [];
|
|
256
|
+
|
|
257
|
+
if (usage) {
|
|
258
|
+
const reported = this.reportUsage(usage, {
|
|
259
|
+
modelEntryName: context.modelEntryName,
|
|
260
|
+
keySource: context.isUserKey ? APIKeySource.User : APIKeySource.Smyth,
|
|
261
|
+
agentId: context.agentId,
|
|
262
|
+
teamId: context.teamId,
|
|
263
|
+
});
|
|
264
|
+
reportedUsage.push(reported);
|
|
215
265
|
}
|
|
216
|
-
}
|
|
217
266
|
|
|
218
|
-
|
|
219
|
-
|
|
267
|
+
// Note: GoogleAI stream doesn't provide explicit finish reasons
|
|
268
|
+
// If we had a non-stop finish reason, we would emit Interrupted here
|
|
220
269
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
});
|
|
228
|
-
reportedUsage.push(reported);
|
|
270
|
+
setTimeout(() => {
|
|
271
|
+
emitter.emit(TLLMEvent.End, toolsData, reportedUsage, finishReason);
|
|
272
|
+
}, 100);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
logger.error(`streamRequest ${this.name}`, error, acRequest.candidate);
|
|
275
|
+
emitter.emit(TLLMEvent.Error, error);
|
|
229
276
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
// If we had a non-stop finish reason, we would emit Interrupted here
|
|
233
|
-
|
|
234
|
-
setTimeout(() => {
|
|
235
|
-
emitter.emit(TLLMEvent.End, toolsData, reportedUsage, finishReason);
|
|
236
|
-
}, 100);
|
|
237
|
-
} catch (error) {
|
|
238
|
-
logger.error(`streamRequest ${this.name}`, error, acRequest.candidate);
|
|
239
|
-
emitter.emit(TLLMEvent.Error, error);
|
|
240
|
-
}
|
|
241
|
-
})();
|
|
277
|
+
})();
|
|
278
|
+
});
|
|
242
279
|
|
|
243
280
|
return emitter;
|
|
244
281
|
} catch (error: any) {
|
|
@@ -294,12 +331,11 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
294
331
|
// https://ai.google.dev/gemini-api/docs/pricing#gemini-2.5-flash-image-preview
|
|
295
332
|
const usageMetadata = response?.usageMetadata as UsageMetadataWithThoughtsToken;
|
|
296
333
|
|
|
297
|
-
this.
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
context,
|
|
334
|
+
this.reportUsage(usageMetadata, {
|
|
335
|
+
modelEntryName: context.modelEntryName,
|
|
336
|
+
keySource: context.isUserKey ? APIKeySource.User : APIKeySource.Smyth,
|
|
337
|
+
agentId: context.agentId,
|
|
338
|
+
teamId: context.teamId,
|
|
303
339
|
});
|
|
304
340
|
|
|
305
341
|
if (imageData.length === 0) {
|
|
@@ -322,14 +358,23 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
322
358
|
// Report input tokens and image cost pricing based on the official pricing page:
|
|
323
359
|
// https://ai.google.dev/gemini-api/docs/pricing#gemini-2.5-flash-image-preview
|
|
324
360
|
const usageMetadata = response?.usageMetadata as UsageMetadataWithThoughtsToken;
|
|
325
|
-
|
|
326
|
-
|
|
361
|
+
|
|
362
|
+
const isImagen4 = modelName.startsWith('imagen-4');
|
|
363
|
+
|
|
364
|
+
if (isImagen4) {
|
|
365
|
+
this.reportImageCost({
|
|
327
366
|
cost: IMAGE_GEN_FIXED_PRICING[modelName],
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
367
|
+
numberOfImages: config.numberOfImages,
|
|
368
|
+
context,
|
|
369
|
+
});
|
|
370
|
+
} else {
|
|
371
|
+
this.reportUsage(usageMetadata, {
|
|
372
|
+
modelEntryName: context.modelEntryName,
|
|
373
|
+
keySource: context.isUserKey ? APIKeySource.User : APIKeySource.Smyth,
|
|
374
|
+
agentId: context.agentId,
|
|
375
|
+
teamId: context.teamId,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
333
378
|
|
|
334
379
|
return {
|
|
335
380
|
created: Math.floor(Date.now() / 1000),
|
|
@@ -356,7 +401,6 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
356
401
|
}
|
|
357
402
|
|
|
358
403
|
const ai = new GoogleGenAI({ apiKey });
|
|
359
|
-
const modelName = context.modelEntryName.replace(BUILT_IN_MODEL_PREFIX, '');
|
|
360
404
|
|
|
361
405
|
// Use the prepared body which already contains processed files and contents
|
|
362
406
|
const response = await ai.models.generateContent({
|
|
@@ -381,12 +425,11 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
381
425
|
// Report pricing for input tokens and image costs
|
|
382
426
|
const usageMetadata = response?.usageMetadata as UsageMetadataWithThoughtsToken;
|
|
383
427
|
|
|
384
|
-
this.
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
context,
|
|
428
|
+
this.reportUsage(usageMetadata, {
|
|
429
|
+
modelEntryName: context.modelEntryName,
|
|
430
|
+
keySource: context.isUserKey ? APIKeySource.User : APIKeySource.Smyth,
|
|
431
|
+
agentId: context.agentId,
|
|
432
|
+
teamId: context.teamId,
|
|
390
433
|
});
|
|
391
434
|
|
|
392
435
|
return {
|
|
@@ -528,7 +571,7 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
528
571
|
let inputTokens = usage?.promptTokenCount || 0;
|
|
529
572
|
|
|
530
573
|
// The pricing is the same for output and thinking tokens, so we can add them together.
|
|
531
|
-
|
|
574
|
+
let outputTokens = (usage?.candidatesTokenCount || 0) + (usage?.thoughtsTokenCount || 0);
|
|
532
575
|
|
|
533
576
|
// If cached input tokens are available, we need to subtract them from the input tokens.
|
|
534
577
|
let cachedInputTokens = usage?.cachedContentTokenCount || 0;
|
|
@@ -544,15 +587,11 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
544
587
|
'gemini-3-pro': 200_000,
|
|
545
588
|
};
|
|
546
589
|
|
|
547
|
-
let
|
|
548
|
-
let outTier = '';
|
|
549
|
-
let crTier = '';
|
|
590
|
+
let tier = '';
|
|
550
591
|
|
|
551
592
|
const modelWithTier = Object.keys(tierThresholds).find((model) => modelName.includes(model));
|
|
552
593
|
if (modelWithTier) {
|
|
553
|
-
|
|
554
|
-
outTier = outputTokens <= tierThresholds[modelWithTier] ? 'tier1' : 'tier2';
|
|
555
|
-
crTier = cachedInputTokens <= tierThresholds[modelWithTier] ? 'tier1' : 'tier2';
|
|
594
|
+
tier = inputTokens <= tierThresholds[modelWithTier] ? 'tier1' : 'tier2';
|
|
556
595
|
}
|
|
557
596
|
// #endregion
|
|
558
597
|
|
|
@@ -560,7 +599,7 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
560
599
|
// Since Gemini 2.5 Flash has a different pricing model for audio input tokens, we need to report audio input tokens separately.
|
|
561
600
|
let audioInputTokens = 0;
|
|
562
601
|
let cachedAudioInputTokens = 0;
|
|
563
|
-
const isFlashModel =
|
|
602
|
+
const isFlashModel = modelName.includes('flash');
|
|
564
603
|
|
|
565
604
|
if (isFlashModel) {
|
|
566
605
|
// There is no concept of different pricing for Flash models based on token tiers (e.g., less than or greater than 200k),
|
|
@@ -578,10 +617,20 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
578
617
|
}
|
|
579
618
|
// #endregion
|
|
580
619
|
|
|
620
|
+
// #region Calculate image tokens
|
|
621
|
+
const imageOutputTokens = usage?.candidatesTokensDetails?.find((detail) => detail.modality === 'IMAGE')?.tokenCount || 0;
|
|
622
|
+
|
|
623
|
+
// Gemini models does not return output text tokens right now for Image Generation, so we need to subtract the output image tokens from the output tokens to get the output text tokens.
|
|
624
|
+
if (imageOutputTokens) {
|
|
625
|
+
outputTokens = outputTokens - imageOutputTokens;
|
|
626
|
+
}
|
|
627
|
+
// #endregion Calculate image tokens
|
|
628
|
+
|
|
581
629
|
const usageData = {
|
|
582
630
|
sourceId: `llm:${modelName}`,
|
|
583
631
|
input_tokens: inputTokens,
|
|
584
632
|
output_tokens: outputTokens,
|
|
633
|
+
output_tokens_image: imageOutputTokens,
|
|
585
634
|
input_tokens_audio: audioInputTokens,
|
|
586
635
|
input_tokens_cache_read: cachedInputTokens,
|
|
587
636
|
input_tokens_cache_read_audio: cachedAudioInputTokens,
|
|
@@ -590,9 +639,7 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
590
639
|
keySource: metadata.keySource,
|
|
591
640
|
agentId: metadata.agentId,
|
|
592
641
|
teamId: metadata.teamId,
|
|
593
|
-
|
|
594
|
-
outTier,
|
|
595
|
-
crTier,
|
|
642
|
+
tier,
|
|
596
643
|
};
|
|
597
644
|
SystemEvents.emit('USAGE:LLM', usageData);
|
|
598
645
|
|
|
@@ -609,32 +656,12 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
609
656
|
return { textTokens, imageTokens };
|
|
610
657
|
}
|
|
611
658
|
|
|
612
|
-
protected
|
|
613
|
-
usage,
|
|
614
|
-
context,
|
|
615
|
-
numberOfImages = 1,
|
|
616
|
-
}: {
|
|
617
|
-
usage: { cost?: number; usageMetadata?: UsageMetadataWithThoughtsToken };
|
|
618
|
-
context: ILLMRequestContext;
|
|
619
|
-
numberOfImages?: number;
|
|
620
|
-
}) {
|
|
621
|
-
// Extract text and image tokens from rawUsage if available
|
|
622
|
-
let input_tokens_txt = 0;
|
|
623
|
-
let input_tokens_img = 0;
|
|
624
|
-
|
|
625
|
-
if (usage.usageMetadata) {
|
|
626
|
-
const { textTokens, imageTokens } = this.extractTokenCounts(usage.usageMetadata);
|
|
627
|
-
input_tokens_txt = textTokens;
|
|
628
|
-
input_tokens_img = imageTokens;
|
|
629
|
-
}
|
|
630
|
-
|
|
659
|
+
protected reportImageCost({ cost, context, numberOfImages = 1 }) {
|
|
631
660
|
const imageUsageData = {
|
|
632
661
|
sourceId: `api:imagegen.smyth`,
|
|
633
662
|
keySource: context.isUserKey ? APIKeySource.User : APIKeySource.Smyth,
|
|
634
663
|
|
|
635
|
-
cost:
|
|
636
|
-
input_tokens_txt,
|
|
637
|
-
input_tokens_img,
|
|
664
|
+
cost: cost * numberOfImages,
|
|
638
665
|
|
|
639
666
|
agentId: context.agentId,
|
|
640
667
|
teamId: context.teamId,
|
|
@@ -642,6 +669,39 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
642
669
|
SystemEvents.emit('USAGE:API', imageUsageData);
|
|
643
670
|
}
|
|
644
671
|
|
|
672
|
+
/**
|
|
673
|
+
* Normalizes function response values to ensure they conform to Google AI's STRUCT requirement.
|
|
674
|
+
* Gemini expects functionResponse.response to be a STRUCT (JSON object format), not a list or scalar.
|
|
675
|
+
*/
|
|
676
|
+
private normalizeFunctionResponse(value: unknown): any {
|
|
677
|
+
// Return objects as-is (but not arrays, which are also objects in JS)
|
|
678
|
+
if (value !== null && value !== undefined && typeof value === 'object' && !Array.isArray(value)) {
|
|
679
|
+
return value;
|
|
680
|
+
}
|
|
681
|
+
// Wrap all other types (arrays, scalars, null, undefined) in result key
|
|
682
|
+
return { result: value ?? null };
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Parses and normalizes function response values, handling string JSON and various data types.
|
|
687
|
+
*/
|
|
688
|
+
private parseFunctionResponse(response: unknown): any {
|
|
689
|
+
if (typeof response === 'string') {
|
|
690
|
+
try {
|
|
691
|
+
const parsed = JSON.parse(response);
|
|
692
|
+
// If parsed result is still a string, try parsing again (handles double-stringified JSON)
|
|
693
|
+
if (typeof parsed === 'string' && parsed !== response) {
|
|
694
|
+
return this.parseFunctionResponse(parsed);
|
|
695
|
+
}
|
|
696
|
+
return this.normalizeFunctionResponse(parsed);
|
|
697
|
+
} catch (error) {
|
|
698
|
+
// If parsing fails, wrap the string in an object to satisfy Google AI's Struct requirement
|
|
699
|
+
return { result: response };
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return this.normalizeFunctionResponse(response);
|
|
703
|
+
}
|
|
704
|
+
|
|
645
705
|
public formatToolsConfig({ toolDefinitions, toolChoice = 'auto' }) {
|
|
646
706
|
const tools = toolDefinitions.map((tool) => {
|
|
647
707
|
const { name, description, properties, requiredFields } = tool;
|
|
@@ -695,23 +755,10 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
695
755
|
return args ?? {};
|
|
696
756
|
};
|
|
697
757
|
|
|
698
|
-
|
|
699
|
-
if (typeof response === 'string') {
|
|
700
|
-
try {
|
|
701
|
-
const parsed = JSON.parse(response);
|
|
702
|
-
if (typeof parsed === 'string' && parsed !== response) {
|
|
703
|
-
return parseFunctionResponse(parsed);
|
|
704
|
-
}
|
|
705
|
-
return parsed;
|
|
706
|
-
} catch {
|
|
707
|
-
return response;
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
return response ?? {};
|
|
711
|
-
};
|
|
712
|
-
|
|
758
|
+
//#region Function call parts
|
|
713
759
|
if (messageBlock) {
|
|
714
760
|
const content: any[] = [];
|
|
761
|
+
let partFunctionCallIndex = 0; // Track function calls within this message block
|
|
715
762
|
|
|
716
763
|
if (Array.isArray(messageBlock.parts) && messageBlock.parts.length > 0) {
|
|
717
764
|
for (const part of messageBlock.parts) {
|
|
@@ -729,11 +776,12 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
729
776
|
args: parseFunctionArgs(part.functionCall.args),
|
|
730
777
|
},
|
|
731
778
|
};
|
|
732
|
-
//
|
|
733
|
-
if ((part as any).thoughtSignature) {
|
|
779
|
+
// Only the first function call part should have the thoughtSignature (Google AI requirement)
|
|
780
|
+
if (partFunctionCallIndex === 0 && (part as any).thoughtSignature) {
|
|
734
781
|
functionCallPart.thoughtSignature = (part as any).thoughtSignature;
|
|
735
782
|
}
|
|
736
783
|
content.push(functionCallPart);
|
|
784
|
+
partFunctionCallIndex++;
|
|
737
785
|
continue;
|
|
738
786
|
}
|
|
739
787
|
|
|
@@ -741,7 +789,7 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
741
789
|
content.push({
|
|
742
790
|
functionResponse: {
|
|
743
791
|
name: part.functionResponse.name,
|
|
744
|
-
response: parseFunctionResponse(part.functionResponse.response),
|
|
792
|
+
response: this.parseFunctionResponse(part.functionResponse.response),
|
|
745
793
|
},
|
|
746
794
|
});
|
|
747
795
|
continue;
|
|
@@ -761,15 +809,15 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
761
809
|
|
|
762
810
|
const hasFunctionCall = content.some((part) => part.functionCall);
|
|
763
811
|
if (!hasFunctionCall && toolsData.length > 0) {
|
|
764
|
-
toolsData.forEach((toolCall) => {
|
|
812
|
+
toolsData.forEach((toolCall, index) => {
|
|
765
813
|
const functionCallPart: any = {
|
|
766
814
|
functionCall: {
|
|
767
815
|
name: toolCall.name,
|
|
768
816
|
args: parseFunctionArgs(toolCall.arguments),
|
|
769
817
|
},
|
|
770
818
|
};
|
|
771
|
-
//
|
|
772
|
-
if (toolCall.thoughtSignature) {
|
|
819
|
+
// Only the first function call part should have the thoughtSignature (Google AI requirement)
|
|
820
|
+
if (index === 0 && toolCall.thoughtSignature) {
|
|
773
821
|
functionCallPart.thoughtSignature = toolCall.thoughtSignature;
|
|
774
822
|
}
|
|
775
823
|
content.push(functionCallPart);
|
|
@@ -788,13 +836,15 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
788
836
|
});
|
|
789
837
|
}
|
|
790
838
|
}
|
|
839
|
+
//#endregion Function call parts
|
|
791
840
|
|
|
841
|
+
//#region Function response parts
|
|
792
842
|
const functionResponseParts = toolsData
|
|
793
843
|
.filter((toolData) => toolData.result !== undefined)
|
|
794
844
|
.map((toolData) => ({
|
|
795
845
|
functionResponse: {
|
|
796
846
|
name: toolData.name,
|
|
797
|
-
response: parseFunctionResponse(toolData.result),
|
|
847
|
+
response: this.parseFunctionResponse(toolData.result),
|
|
798
848
|
},
|
|
799
849
|
}));
|
|
800
850
|
|
|
@@ -804,6 +854,7 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
804
854
|
parts: functionResponseParts,
|
|
805
855
|
});
|
|
806
856
|
}
|
|
857
|
+
//#endregion Function response parts
|
|
807
858
|
|
|
808
859
|
return messageBlocks;
|
|
809
860
|
}
|
|
@@ -826,18 +877,6 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
826
877
|
return args ?? {};
|
|
827
878
|
};
|
|
828
879
|
|
|
829
|
-
const parseFunctionResponse = (response: unknown) => {
|
|
830
|
-
if (typeof response === 'string') {
|
|
831
|
-
try {
|
|
832
|
-
return JSON.parse(response);
|
|
833
|
-
} catch {
|
|
834
|
-
return response;
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
return response;
|
|
839
|
-
};
|
|
840
|
-
|
|
841
880
|
const pushTextPart = (parts: any[], text?: string) => {
|
|
842
881
|
const value = typeof text === 'string' && text.trim() ? text : undefined;
|
|
843
882
|
if (value) {
|
|
@@ -846,6 +885,7 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
846
885
|
};
|
|
847
886
|
|
|
848
887
|
const normalizedParts: any[] = [];
|
|
888
|
+
let functionCallCount = 0; // Track function call parts for thoughtSignature handling
|
|
849
889
|
|
|
850
890
|
// Map roles to valid Google AI roles
|
|
851
891
|
switch (_message.role) {
|
|
@@ -879,16 +919,17 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
879
919
|
name: part.functionCall.name,
|
|
880
920
|
args: parseFunctionArgs(part.functionCall.args),
|
|
881
921
|
};
|
|
882
|
-
//
|
|
883
|
-
if ((part as any).thoughtSignature) {
|
|
922
|
+
// Only the first function call part should have the thoughtSignature (Google AI requirement)
|
|
923
|
+
if (functionCallCount === 0 && (part as any).thoughtSignature) {
|
|
884
924
|
normalizedPart.thoughtSignature = (part as any).thoughtSignature;
|
|
885
925
|
}
|
|
926
|
+
functionCallCount++;
|
|
886
927
|
}
|
|
887
928
|
|
|
888
929
|
if (part.functionResponse) {
|
|
889
930
|
normalizedPart.functionResponse = {
|
|
890
931
|
name: part.functionResponse.name,
|
|
891
|
-
response: parseFunctionResponse(part.functionResponse.response),
|
|
932
|
+
response: this.parseFunctionResponse(part.functionResponse.response),
|
|
892
933
|
};
|
|
893
934
|
}
|
|
894
935
|
|
|
@@ -917,17 +958,18 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
917
958
|
args: parseFunctionArgs(functionCallPart.args),
|
|
918
959
|
},
|
|
919
960
|
};
|
|
920
|
-
//
|
|
921
|
-
if ((contentPart as any).thoughtSignature) {
|
|
961
|
+
// Only the first function call part should have the thoughtSignature (Google AI requirement)
|
|
962
|
+
if (functionCallCount === 0 && (contentPart as any).thoughtSignature) {
|
|
922
963
|
normalizedFunctionCall.thoughtSignature = (contentPart as any).thoughtSignature;
|
|
923
964
|
}
|
|
924
965
|
normalizedParts.push(normalizedFunctionCall);
|
|
966
|
+
functionCallCount++;
|
|
925
967
|
} else if ('functionResponse' in contentPart && (contentPart as any).functionResponse) {
|
|
926
968
|
const functionResponsePart = (contentPart as any).functionResponse;
|
|
927
969
|
normalizedParts.push({
|
|
928
970
|
functionResponse: {
|
|
929
971
|
name: functionResponsePart.name,
|
|
930
|
-
response: parseFunctionResponse(functionResponsePart.response),
|
|
972
|
+
response: this.parseFunctionResponse(functionResponsePart.response),
|
|
931
973
|
},
|
|
932
974
|
});
|
|
933
975
|
} else {
|
|
@@ -956,6 +998,7 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
956
998
|
}
|
|
957
999
|
|
|
958
1000
|
if (Array.isArray(message?.tool_calls) && message.tool_calls.length > 0) {
|
|
1001
|
+
let functionCallIndex = 0;
|
|
959
1002
|
for (const toolCall of message.tool_calls) {
|
|
960
1003
|
if (!toolCall?.function?.name) continue;
|
|
961
1004
|
|
|
@@ -965,11 +1008,12 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
965
1008
|
args: parseFunctionArgs(toolCall.function.arguments),
|
|
966
1009
|
},
|
|
967
1010
|
};
|
|
968
|
-
//
|
|
969
|
-
if ((toolCall as any).thoughtSignature) {
|
|
1011
|
+
// Only the first function call part should have the thoughtSignature (Google AI requirement)
|
|
1012
|
+
if (functionCallIndex === 0 && (toolCall as any).thoughtSignature) {
|
|
970
1013
|
normalizedFunctionCall.thoughtSignature = (toolCall as any).thoughtSignature;
|
|
971
1014
|
}
|
|
972
1015
|
normalizedParts.push(normalizedFunctionCall);
|
|
1016
|
+
functionCallIndex++;
|
|
973
1017
|
}
|
|
974
1018
|
}
|
|
975
1019
|
|
|
@@ -7,5 +7,5 @@ import OpenAI from 'openai';
|
|
|
7
7
|
* Uses array includes for better maintainability when OpenAI adds new values.
|
|
8
8
|
*/
|
|
9
9
|
export function isValidOpenAIReasoningEffort(value: unknown): value is OpenAI.Responses.ResponseCreateParams['reasoning']['effort'] {
|
|
10
|
-
return ['minimal', 'low', 'medium', 'high'].includes(value as string);
|
|
10
|
+
return ['minimal', 'low', 'medium', 'high', 'xhigh'].includes(value as string);
|
|
11
11
|
}
|