@smythos/sre 1.7.20 → 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.
Files changed (78) hide show
  1. package/dist/index.js +134 -89
  2. package/dist/index.js.map +1 -1
  3. package/dist/types/Components/AgentPlugin.class.d.ts +1 -1
  4. package/dist/types/Components/DataSourceIndexer.class.d.ts +4 -12
  5. package/dist/types/Components/GenAILLM.class.d.ts +5 -5
  6. package/dist/types/Components/RAG/DataSourceCleaner.class.d.ts +4 -4
  7. package/dist/types/Components/RAG/DataSourceComponent.class.d.ts +5 -1
  8. package/dist/types/Components/index.d.ts +3 -3
  9. package/dist/types/config.d.ts +1 -0
  10. package/dist/types/helpers/Conversation.helper.d.ts +10 -13
  11. package/dist/types/helpers/TemplateString.helper.d.ts +1 -1
  12. package/dist/types/index.d.ts +4 -3
  13. package/dist/types/subsystems/IO/VectorDB.service/VectorDBConnector.d.ts +1 -0
  14. package/dist/types/subsystems/IO/VectorDB.service/connectors/MilvusVectorDB.class.d.ts +1 -0
  15. package/dist/types/subsystems/IO/VectorDB.service/connectors/PineconeVectorDB.class.d.ts +11 -4
  16. package/dist/types/subsystems/IO/VectorDB.service/embed/index.d.ts +5 -0
  17. package/dist/types/subsystems/LLMManager/LLM.helper.d.ts +19 -0
  18. package/dist/types/subsystems/LLMManager/LLM.service/connectors/GoogleAI.class.d.ts +15 -10
  19. package/dist/types/subsystems/LLMManager/ModelsProvider.service/connectors/JSONModelsProvider.class.d.ts +35 -0
  20. package/dist/types/subsystems/Security/Account.service/AccountConnector.d.ts +2 -2
  21. package/dist/types/subsystems/Security/Vault.service/connectors/SecretsManager.class.d.ts +2 -3
  22. package/dist/types/types/LLM.types.d.ts +23 -0
  23. package/dist/types/types/VectorDB.types.d.ts +4 -0
  24. package/dist/types/utils/string.utils.d.ts +1 -0
  25. package/package.json +3 -3
  26. package/src/Components/APIEndpoint.class.ts +1 -6
  27. package/src/Components/AgentPlugin.class.ts +20 -3
  28. package/src/Components/Classifier.class.ts +79 -16
  29. package/src/Components/Component.class.ts +14 -1
  30. package/src/Components/ForEach.class.ts +34 -6
  31. package/src/Components/GenAILLM.class.ts +75 -34
  32. package/src/Components/LLMAssistant.class.ts +56 -21
  33. package/src/Components/RAG/DataSourceCleaner.class.ts +180 -0
  34. package/src/Components/RAG/DataSourceComponent.class.ts +137 -0
  35. package/src/Components/RAG/DataSourceIndexer.class.ts +260 -0
  36. package/src/Components/{DataSourceLookup.class.ts → RAG/DataSourceLookup.class.ts} +96 -3
  37. package/src/Components/ScrapflyWebScrape.class.ts +7 -0
  38. package/src/Components/ServerlessCode.class.ts +1 -4
  39. package/src/Components/index.ts +3 -3
  40. package/src/config.ts +1 -0
  41. package/src/helpers/Conversation.helper.ts +112 -26
  42. package/src/helpers/S3Cache.helper.ts +2 -1
  43. package/src/helpers/TemplateString.helper.ts +6 -5
  44. package/src/index.ts +213 -212
  45. package/src/index.ts.bak +213 -212
  46. package/src/subsystems/IO/NKV.service/connectors/NKVRedis.class.ts +3 -1
  47. package/src/subsystems/IO/VectorDB.service/VectorDBConnector.ts +1 -0
  48. package/src/subsystems/IO/VectorDB.service/connectors/MilvusVectorDB.class.ts +145 -19
  49. package/src/subsystems/IO/VectorDB.service/connectors/PineconeVectorDB.class.ts +67 -22
  50. package/src/subsystems/IO/VectorDB.service/embed/GoogleEmbedding.ts +1 -0
  51. package/src/subsystems/IO/VectorDB.service/embed/OpenAIEmbedding.ts +2 -1
  52. package/src/subsystems/IO/VectorDB.service/embed/index.ts +16 -0
  53. package/src/subsystems/LLMManager/LLM.helper.ts +25 -0
  54. package/src/subsystems/LLMManager/LLM.service/LLMConnector.ts +1 -1
  55. package/src/subsystems/LLMManager/LLM.service/connectors/Anthropic.class.ts +35 -10
  56. package/src/subsystems/LLMManager/LLM.service/connectors/Bedrock.class.ts +12 -4
  57. package/src/subsystems/LLMManager/LLM.service/connectors/Echo.class.ts +4 -4
  58. package/src/subsystems/LLMManager/LLM.service/connectors/GoogleAI.class.ts +192 -139
  59. package/src/subsystems/LLMManager/LLM.service/connectors/Groq.class.ts +17 -5
  60. package/src/subsystems/LLMManager/LLM.service/connectors/Ollama.class.ts +18 -3
  61. package/src/subsystems/LLMManager/LLM.service/connectors/Perplexity.class.ts +14 -5
  62. package/src/subsystems/LLMManager/LLM.service/connectors/VertexAI.class.ts +6 -4
  63. package/src/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/ChatCompletionsApiInterface.ts +5 -5
  64. package/src/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/ResponsesApiInterface.ts +8 -3
  65. package/src/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/utils.ts +1 -1
  66. package/src/subsystems/LLMManager/LLM.service/connectors/xAI.class.ts +9 -8
  67. package/src/subsystems/LLMManager/ModelsProvider.service/connectors/JSONModelsProvider.class.ts +92 -1
  68. package/src/subsystems/ObservabilityManager/Telemetry.service/connectors/OTel/OTel.class.ts +260 -17
  69. package/src/subsystems/Security/Account.service/AccountConnector.ts +3 -3
  70. package/src/subsystems/Security/Vault.service/connectors/SecretsManager.class.ts +8 -63
  71. package/src/types/LLM.types.ts +24 -0
  72. package/src/types/VectorDB.types.ts +4 -0
  73. package/src/utils/array.utils.ts +11 -0
  74. package/src/utils/base64.utils.ts +1 -1
  75. package/src/utils/data.utils.ts +6 -4
  76. package/src/utils/string.utils.ts +3 -192
  77. package/src/Components/DataSourceCleaner.class.ts +0 -92
  78. package/src/Components/DataSourceIndexer.class.ts +0 -181
@@ -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
- thoughtSignature: (toolCall as any).thoughtSignature, // Preserve Google AI's reasoning context
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,54 +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
- (async () => {
187
- try {
188
- for await (const chunk of stream) {
189
- const chunkText = chunk.text ?? '';
190
- if (chunkText) {
191
- emitter.emit('content', chunkText);
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
+ }
243
+
244
+ if (chunk.usageMetadata) {
245
+ usage = chunk.usageMetadata as UsageMetadataWithThoughtsToken;
246
+ }
192
247
  }
193
248
 
194
- const toolCalls = chunk.candidates?.[0]?.content?.parts?.filter((part) => part.functionCall);
195
- if (toolCalls && toolCalls.length > 0) {
196
- toolsData = toolCalls.map((toolCall, index) => ({
197
- index,
198
- id: `tool-${index}`,
199
- type: 'function',
200
- name: toolCall.functionCall?.name,
201
- arguments:
202
- typeof toolCall.functionCall?.args === 'string'
203
- ? toolCall.functionCall?.args
204
- : JSON.stringify(toolCall.functionCall?.args ?? {}),
205
- role: TLLMMessageRole.Assistant,
206
- thoughtSignature: (toolCall as any).thoughtSignature, // Preserve Google AI's reasoning context
207
- }));
249
+ // Emit ToolInfo once after all chunks are processed (similar to Anthropic's finalMessage pattern)
250
+ if (toolsData.length > 0) {
208
251
  emitter.emit(TLLMEvent.ToolInfo, toolsData);
209
252
  }
210
253
 
211
- if (chunk.usageMetadata) {
212
- usage = chunk.usageMetadata as UsageMetadataWithThoughtsToken;
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);
213
265
  }
214
- }
215
266
 
216
- if (usage) {
217
- this.reportUsage(usage, {
218
- modelEntryName: context.modelEntryName,
219
- keySource: context.isUserKey ? APIKeySource.User : APIKeySource.Smyth,
220
- agentId: context.agentId,
221
- teamId: context.teamId,
222
- });
223
- }
267
+ // Note: GoogleAI stream doesn't provide explicit finish reasons
268
+ // If we had a non-stop finish reason, we would emit Interrupted here
224
269
 
225
- setTimeout(() => {
226
- emitter.emit('end', toolsData);
227
- }, 100);
228
- } catch (error) {
229
- logger.error(`streamRequest ${this.name}`, error, acRequest.candidate);
230
- emitter.emit('error', error);
231
- }
232
- })();
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);
276
+ }
277
+ })();
278
+ });
233
279
 
234
280
  return emitter;
235
281
  } catch (error: any) {
@@ -285,12 +331,11 @@ export class GoogleAIConnector extends LLMConnector {
285
331
  // https://ai.google.dev/gemini-api/docs/pricing#gemini-2.5-flash-image-preview
286
332
  const usageMetadata = response?.usageMetadata as UsageMetadataWithThoughtsToken;
287
333
 
288
- this.reportImageUsage({
289
- usage: {
290
- cost: IMAGE_GEN_FIXED_PRICING[modelName],
291
- usageMetadata,
292
- },
293
- 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,
294
339
  });
295
340
 
296
341
  if (imageData.length === 0) {
@@ -313,14 +358,23 @@ export class GoogleAIConnector extends LLMConnector {
313
358
  // Report input tokens and image cost pricing based on the official pricing page:
314
359
  // https://ai.google.dev/gemini-api/docs/pricing#gemini-2.5-flash-image-preview
315
360
  const usageMetadata = response?.usageMetadata as UsageMetadataWithThoughtsToken;
316
- this.reportImageUsage({
317
- usage: {
361
+
362
+ const isImagen4 = modelName.startsWith('imagen-4');
363
+
364
+ if (isImagen4) {
365
+ this.reportImageCost({
318
366
  cost: IMAGE_GEN_FIXED_PRICING[modelName],
319
- usageMetadata,
320
- },
321
- numberOfImages: config.numberOfImages,
322
- context,
323
- });
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
+ }
324
378
 
325
379
  return {
326
380
  created: Math.floor(Date.now() / 1000),
@@ -347,7 +401,6 @@ export class GoogleAIConnector extends LLMConnector {
347
401
  }
348
402
 
349
403
  const ai = new GoogleGenAI({ apiKey });
350
- const modelName = context.modelEntryName.replace(BUILT_IN_MODEL_PREFIX, '');
351
404
 
352
405
  // Use the prepared body which already contains processed files and contents
353
406
  const response = await ai.models.generateContent({
@@ -372,12 +425,11 @@ export class GoogleAIConnector extends LLMConnector {
372
425
  // Report pricing for input tokens and image costs
373
426
  const usageMetadata = response?.usageMetadata as UsageMetadataWithThoughtsToken;
374
427
 
375
- this.reportImageUsage({
376
- usage: {
377
- cost: IMAGE_GEN_FIXED_PRICING[modelName],
378
- usageMetadata,
379
- },
380
- 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,
381
433
  });
382
434
 
383
435
  return {
@@ -519,7 +571,7 @@ export class GoogleAIConnector extends LLMConnector {
519
571
  let inputTokens = usage?.promptTokenCount || 0;
520
572
 
521
573
  // The pricing is the same for output and thinking tokens, so we can add them together.
522
- const outputTokens = (usage?.candidatesTokenCount || 0) + (usage?.thoughtsTokenCount || 0);
574
+ let outputTokens = (usage?.candidatesTokenCount || 0) + (usage?.thoughtsTokenCount || 0);
523
575
 
524
576
  // If cached input tokens are available, we need to subtract them from the input tokens.
525
577
  let cachedInputTokens = usage?.cachedContentTokenCount || 0;
@@ -535,15 +587,11 @@ export class GoogleAIConnector extends LLMConnector {
535
587
  'gemini-3-pro': 200_000,
536
588
  };
537
589
 
538
- let inTier = '';
539
- let outTier = '';
540
- let crTier = '';
590
+ let tier = '';
541
591
 
542
592
  const modelWithTier = Object.keys(tierThresholds).find((model) => modelName.includes(model));
543
593
  if (modelWithTier) {
544
- inTier = inputTokens <= tierThresholds[modelWithTier] ? 'tier1' : 'tier2';
545
- outTier = outputTokens <= tierThresholds[modelWithTier] ? 'tier1' : 'tier2';
546
- crTier = cachedInputTokens <= tierThresholds[modelWithTier] ? 'tier1' : 'tier2';
594
+ tier = inputTokens <= tierThresholds[modelWithTier] ? 'tier1' : 'tier2';
547
595
  }
548
596
  // #endregion
549
597
 
@@ -551,7 +599,7 @@ export class GoogleAIConnector extends LLMConnector {
551
599
  // Since Gemini 2.5 Flash has a different pricing model for audio input tokens, we need to report audio input tokens separately.
552
600
  let audioInputTokens = 0;
553
601
  let cachedAudioInputTokens = 0;
554
- const isFlashModel = ['gemini-2.5-flash'].includes(modelName);
602
+ const isFlashModel = modelName.includes('flash');
555
603
 
556
604
  if (isFlashModel) {
557
605
  // There is no concept of different pricing for Flash models based on token tiers (e.g., less than or greater than 200k),
@@ -569,10 +617,20 @@ export class GoogleAIConnector extends LLMConnector {
569
617
  }
570
618
  // #endregion
571
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
+
572
629
  const usageData = {
573
630
  sourceId: `llm:${modelName}`,
574
631
  input_tokens: inputTokens,
575
632
  output_tokens: outputTokens,
633
+ output_tokens_image: imageOutputTokens,
576
634
  input_tokens_audio: audioInputTokens,
577
635
  input_tokens_cache_read: cachedInputTokens,
578
636
  input_tokens_cache_read_audio: cachedAudioInputTokens,
@@ -581,9 +639,7 @@ export class GoogleAIConnector extends LLMConnector {
581
639
  keySource: metadata.keySource,
582
640
  agentId: metadata.agentId,
583
641
  teamId: metadata.teamId,
584
- inTier,
585
- outTier,
586
- crTier,
642
+ tier,
587
643
  };
588
644
  SystemEvents.emit('USAGE:LLM', usageData);
589
645
 
@@ -600,32 +656,12 @@ export class GoogleAIConnector extends LLMConnector {
600
656
  return { textTokens, imageTokens };
601
657
  }
602
658
 
603
- protected reportImageUsage({
604
- usage,
605
- context,
606
- numberOfImages = 1,
607
- }: {
608
- usage: { cost?: number; usageMetadata?: UsageMetadataWithThoughtsToken };
609
- context: ILLMRequestContext;
610
- numberOfImages?: number;
611
- }) {
612
- // Extract text and image tokens from rawUsage if available
613
- let input_tokens_txt = 0;
614
- let input_tokens_img = 0;
615
-
616
- if (usage.usageMetadata) {
617
- const { textTokens, imageTokens } = this.extractTokenCounts(usage.usageMetadata);
618
- input_tokens_txt = textTokens;
619
- input_tokens_img = imageTokens;
620
- }
621
-
659
+ protected reportImageCost({ cost, context, numberOfImages = 1 }) {
622
660
  const imageUsageData = {
623
661
  sourceId: `api:imagegen.smyth`,
624
662
  keySource: context.isUserKey ? APIKeySource.User : APIKeySource.Smyth,
625
663
 
626
- cost: usage.cost * numberOfImages,
627
- input_tokens_txt,
628
- input_tokens_img,
664
+ cost: cost * numberOfImages,
629
665
 
630
666
  agentId: context.agentId,
631
667
  teamId: context.teamId,
@@ -633,6 +669,39 @@ export class GoogleAIConnector extends LLMConnector {
633
669
  SystemEvents.emit('USAGE:API', imageUsageData);
634
670
  }
635
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
+
636
705
  public formatToolsConfig({ toolDefinitions, toolChoice = 'auto' }) {
637
706
  const tools = toolDefinitions.map((tool) => {
638
707
  const { name, description, properties, requiredFields } = tool;
@@ -686,23 +755,10 @@ export class GoogleAIConnector extends LLMConnector {
686
755
  return args ?? {};
687
756
  };
688
757
 
689
- const parseFunctionResponse = (response: unknown): any => {
690
- if (typeof response === 'string') {
691
- try {
692
- const parsed = JSON.parse(response);
693
- if (typeof parsed === 'string' && parsed !== response) {
694
- return parseFunctionResponse(parsed);
695
- }
696
- return parsed;
697
- } catch {
698
- return response;
699
- }
700
- }
701
- return response ?? {};
702
- };
703
-
758
+ //#region Function call parts
704
759
  if (messageBlock) {
705
760
  const content: any[] = [];
761
+ let partFunctionCallIndex = 0; // Track function calls within this message block
706
762
 
707
763
  if (Array.isArray(messageBlock.parts) && messageBlock.parts.length > 0) {
708
764
  for (const part of messageBlock.parts) {
@@ -720,11 +776,12 @@ export class GoogleAIConnector extends LLMConnector {
720
776
  args: parseFunctionArgs(part.functionCall.args),
721
777
  },
722
778
  };
723
- // Preserve thoughtSignature if present for Google AI reasoning context
724
- 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) {
725
781
  functionCallPart.thoughtSignature = (part as any).thoughtSignature;
726
782
  }
727
783
  content.push(functionCallPart);
784
+ partFunctionCallIndex++;
728
785
  continue;
729
786
  }
730
787
 
@@ -732,7 +789,7 @@ export class GoogleAIConnector extends LLMConnector {
732
789
  content.push({
733
790
  functionResponse: {
734
791
  name: part.functionResponse.name,
735
- response: parseFunctionResponse(part.functionResponse.response),
792
+ response: this.parseFunctionResponse(part.functionResponse.response),
736
793
  },
737
794
  });
738
795
  continue;
@@ -752,15 +809,15 @@ export class GoogleAIConnector extends LLMConnector {
752
809
 
753
810
  const hasFunctionCall = content.some((part) => part.functionCall);
754
811
  if (!hasFunctionCall && toolsData.length > 0) {
755
- toolsData.forEach((toolCall) => {
812
+ toolsData.forEach((toolCall, index) => {
756
813
  const functionCallPart: any = {
757
814
  functionCall: {
758
815
  name: toolCall.name,
759
816
  args: parseFunctionArgs(toolCall.arguments),
760
817
  },
761
818
  };
762
- // Preserve thoughtSignature if present for Google AI reasoning context
763
- if (toolCall.thoughtSignature) {
819
+ // Only the first function call part should have the thoughtSignature (Google AI requirement)
820
+ if (index === 0 && toolCall.thoughtSignature) {
764
821
  functionCallPart.thoughtSignature = toolCall.thoughtSignature;
765
822
  }
766
823
  content.push(functionCallPart);
@@ -779,13 +836,15 @@ export class GoogleAIConnector extends LLMConnector {
779
836
  });
780
837
  }
781
838
  }
839
+ //#endregion Function call parts
782
840
 
841
+ //#region Function response parts
783
842
  const functionResponseParts = toolsData
784
843
  .filter((toolData) => toolData.result !== undefined)
785
844
  .map((toolData) => ({
786
845
  functionResponse: {
787
846
  name: toolData.name,
788
- response: parseFunctionResponse(toolData.result),
847
+ response: this.parseFunctionResponse(toolData.result),
789
848
  },
790
849
  }));
791
850
 
@@ -795,6 +854,7 @@ export class GoogleAIConnector extends LLMConnector {
795
854
  parts: functionResponseParts,
796
855
  });
797
856
  }
857
+ //#endregion Function response parts
798
858
 
799
859
  return messageBlocks;
800
860
  }
@@ -817,18 +877,6 @@ export class GoogleAIConnector extends LLMConnector {
817
877
  return args ?? {};
818
878
  };
819
879
 
820
- const parseFunctionResponse = (response: unknown) => {
821
- if (typeof response === 'string') {
822
- try {
823
- return JSON.parse(response);
824
- } catch {
825
- return response;
826
- }
827
- }
828
-
829
- return response;
830
- };
831
-
832
880
  const pushTextPart = (parts: any[], text?: string) => {
833
881
  const value = typeof text === 'string' && text.trim() ? text : undefined;
834
882
  if (value) {
@@ -837,6 +885,7 @@ export class GoogleAIConnector extends LLMConnector {
837
885
  };
838
886
 
839
887
  const normalizedParts: any[] = [];
888
+ let functionCallCount = 0; // Track function call parts for thoughtSignature handling
840
889
 
841
890
  // Map roles to valid Google AI roles
842
891
  switch (_message.role) {
@@ -870,16 +919,17 @@ export class GoogleAIConnector extends LLMConnector {
870
919
  name: part.functionCall.name,
871
920
  args: parseFunctionArgs(part.functionCall.args),
872
921
  };
873
- // Preserve thoughtSignature if present for Google AI reasoning context
874
- 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) {
875
924
  normalizedPart.thoughtSignature = (part as any).thoughtSignature;
876
925
  }
926
+ functionCallCount++;
877
927
  }
878
928
 
879
929
  if (part.functionResponse) {
880
930
  normalizedPart.functionResponse = {
881
931
  name: part.functionResponse.name,
882
- response: parseFunctionResponse(part.functionResponse.response),
932
+ response: this.parseFunctionResponse(part.functionResponse.response),
883
933
  };
884
934
  }
885
935
 
@@ -908,17 +958,18 @@ export class GoogleAIConnector extends LLMConnector {
908
958
  args: parseFunctionArgs(functionCallPart.args),
909
959
  },
910
960
  };
911
- // Preserve thoughtSignature if present for Google AI reasoning context
912
- 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) {
913
963
  normalizedFunctionCall.thoughtSignature = (contentPart as any).thoughtSignature;
914
964
  }
915
965
  normalizedParts.push(normalizedFunctionCall);
966
+ functionCallCount++;
916
967
  } else if ('functionResponse' in contentPart && (contentPart as any).functionResponse) {
917
968
  const functionResponsePart = (contentPart as any).functionResponse;
918
969
  normalizedParts.push({
919
970
  functionResponse: {
920
971
  name: functionResponsePart.name,
921
- response: parseFunctionResponse(functionResponsePart.response),
972
+ response: this.parseFunctionResponse(functionResponsePart.response),
922
973
  },
923
974
  });
924
975
  } else {
@@ -947,6 +998,7 @@ export class GoogleAIConnector extends LLMConnector {
947
998
  }
948
999
 
949
1000
  if (Array.isArray(message?.tool_calls) && message.tool_calls.length > 0) {
1001
+ let functionCallIndex = 0;
950
1002
  for (const toolCall of message.tool_calls) {
951
1003
  if (!toolCall?.function?.name) continue;
952
1004
 
@@ -956,11 +1008,12 @@ export class GoogleAIConnector extends LLMConnector {
956
1008
  args: parseFunctionArgs(toolCall.function.arguments),
957
1009
  },
958
1010
  };
959
- // Preserve thoughtSignature if present for Google AI reasoning context
960
- 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) {
961
1013
  normalizedFunctionCall.thoughtSignature = (toolCall as any).thoughtSignature;
962
1014
  }
963
1015
  normalizedParts.push(normalizedFunctionCall);
1016
+ functionCallIndex++;
964
1017
  }
965
1018
  }
966
1019
 
@@ -108,6 +108,7 @@ export class GroqConnector extends LLMConnector {
108
108
  const stream = await groq.chat.completions.create({ ...body, stream: true, stream_options: { include_usage: true } });
109
109
 
110
110
  let toolsData: ToolData[] = [];
111
+ let finishReason = 'stop';
111
112
 
112
113
  (async () => {
113
114
  for await (const chunk of stream as any) {
@@ -117,10 +118,10 @@ export class GroqConnector extends LLMConnector {
117
118
  if (usage) {
118
119
  usage_data.push(usage);
119
120
  }
120
- emitter.emit('data', delta);
121
+ emitter.emit(TLLMEvent.Data, delta);
121
122
 
122
123
  if (delta?.content) {
123
- emitter.emit('content', delta.content);
124
+ emitter.emit(TLLMEvent.Content, delta.content);
124
125
  }
125
126
 
126
127
  if (delta?.tool_calls) {
@@ -139,24 +140,35 @@ export class GroqConnector extends LLMConnector {
139
140
  }
140
141
  });
141
142
  }
143
+
144
+ // Capture finish reason
145
+ if (chunk.choices[0]?.finish_reason) {
146
+ finishReason = chunk.choices[0].finish_reason;
147
+ }
142
148
  }
143
149
 
144
150
  if (toolsData.length > 0) {
145
151
  emitter.emit(TLLMEvent.ToolInfo, toolsData);
146
152
  }
147
153
 
154
+ const reportedUsage: any[] = [];
148
155
  usage_data.forEach((usage) => {
149
- // probably we can acc them and send them as one event
150
- this.reportUsage(usage, {
156
+ const reported = this.reportUsage(usage, {
151
157
  modelEntryName: context.modelEntryName,
152
158
  keySource: context.isUserKey ? APIKeySource.User : APIKeySource.Smyth,
153
159
  agentId: context.agentId,
154
160
  teamId: context.teamId,
155
161
  });
162
+ reportedUsage.push(reported);
156
163
  });
157
164
 
165
+ // Emit interrupted event if finishReason is not 'stop'
166
+ if (finishReason !== 'stop') {
167
+ emitter.emit(TLLMEvent.Interrupted, finishReason);
168
+ }
169
+
158
170
  setTimeout(() => {
159
- emitter.emit('end', toolsData);
171
+ emitter.emit(TLLMEvent.End, toolsData, reportedUsage, finishReason);
160
172
  }, 100);
161
173
  })();
162
174