@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.
Files changed (40) hide show
  1. package/dist/index.js +49 -42
  2. package/dist/index.js.map +1 -1
  3. package/dist/types/Components/AgentPlugin.class.d.ts +1 -1
  4. package/dist/types/Components/RAG/DataSourceCleaner.class.d.ts +4 -4
  5. package/dist/types/Components/RAG/DataSourceComponent.class.d.ts +5 -1
  6. package/dist/types/config.d.ts +1 -0
  7. package/dist/types/helpers/Conversation.helper.d.ts +10 -13
  8. package/dist/types/helpers/TemplateString.helper.d.ts +1 -1
  9. package/dist/types/index.d.ts +1 -0
  10. package/dist/types/subsystems/IO/VectorDB.service/VectorDBConnector.d.ts +1 -0
  11. package/dist/types/subsystems/LLMManager/LLM.helper.d.ts +19 -0
  12. package/dist/types/subsystems/LLMManager/LLM.service/connectors/GoogleAI.class.d.ts +15 -10
  13. package/dist/types/types/LLM.types.d.ts +23 -0
  14. package/package.json +1 -1
  15. package/src/Components/AgentPlugin.class.ts +20 -3
  16. package/src/Components/Classifier.class.ts +79 -16
  17. package/src/Components/ForEach.class.ts +34 -6
  18. package/src/Components/GenAILLM.class.ts +54 -23
  19. package/src/Components/LLMAssistant.class.ts +56 -21
  20. package/src/Components/RAG/DataSourceCleaner.class.ts +13 -11
  21. package/src/Components/RAG/DataSourceComponent.class.ts +39 -13
  22. package/src/Components/RAG/DataSourceIndexer.class.ts +18 -12
  23. package/src/Components/RAG/DataSourceLookup.class.ts +14 -10
  24. package/src/Components/ScrapflyWebScrape.class.ts +7 -0
  25. package/src/config.ts +1 -0
  26. package/src/helpers/Conversation.helper.ts +112 -26
  27. package/src/helpers/TemplateString.helper.ts +6 -5
  28. package/src/index.ts +1 -0
  29. package/src/index.ts.bak +1 -0
  30. package/src/subsystems/IO/VectorDB.service/VectorDBConnector.ts +1 -0
  31. package/src/subsystems/IO/VectorDB.service/connectors/PineconeVectorDB.class.ts +11 -0
  32. package/src/subsystems/IO/VectorDB.service/embed/index.ts +9 -11
  33. package/src/subsystems/LLMManager/LLM.helper.ts +25 -0
  34. package/src/subsystems/LLMManager/LLM.service/LLMConnector.ts +1 -1
  35. package/src/subsystems/LLMManager/LLM.service/connectors/GoogleAI.class.ts +190 -146
  36. package/src/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/utils.ts +1 -1
  37. package/src/subsystems/ObservabilityManager/Telemetry.service/connectors/OTel/OTel.class.ts +229 -12
  38. package/src/types/LLM.types.ts +24 -0
  39. package/src/utils/data.utils.ts +6 -4
  40. 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
- 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,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
- (async () => {
187
- try {
188
- for await (const chunk of stream) {
189
- emitter.emit(TLLMEvent.Data, chunk);
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
- const chunkText = chunk.text ?? '';
192
- if (chunkText) {
193
- emitter.emit(TLLMEvent.Content, chunkText);
244
+ if (chunk.usageMetadata) {
245
+ usage = chunk.usageMetadata as UsageMetadataWithThoughtsToken;
246
+ }
194
247
  }
195
248
 
196
- const toolCalls = chunk.candidates?.[0]?.content?.parts?.filter((part) => part.functionCall);
197
- if (toolCalls && toolCalls.length > 0) {
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
- if (chunk.usageMetadata) {
214
- 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);
215
265
  }
216
- }
217
266
 
218
- const finishReason = 'stop'; // GoogleAI doesn't provide finishReason in streaming
219
- const reportedUsage: any[] = [];
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
- if (usage) {
222
- const reported = this.reportUsage(usage, {
223
- modelEntryName: context.modelEntryName,
224
- keySource: context.isUserKey ? APIKeySource.User : APIKeySource.Smyth,
225
- agentId: context.agentId,
226
- teamId: context.teamId,
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
- // Note: GoogleAI stream doesn't provide explicit finish reasons
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.reportImageUsage({
298
- usage: {
299
- cost: IMAGE_GEN_FIXED_PRICING[modelName],
300
- usageMetadata,
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
- this.reportImageUsage({
326
- usage: {
361
+
362
+ const isImagen4 = modelName.startsWith('imagen-4');
363
+
364
+ if (isImagen4) {
365
+ this.reportImageCost({
327
366
  cost: IMAGE_GEN_FIXED_PRICING[modelName],
328
- usageMetadata,
329
- },
330
- numberOfImages: config.numberOfImages,
331
- context,
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.reportImageUsage({
385
- usage: {
386
- cost: IMAGE_GEN_FIXED_PRICING[modelName],
387
- usageMetadata,
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
- const outputTokens = (usage?.candidatesTokenCount || 0) + (usage?.thoughtsTokenCount || 0);
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 inTier = '';
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
- inTier = inputTokens <= tierThresholds[modelWithTier] ? 'tier1' : 'tier2';
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 = ['gemini-2.5-flash'].includes(modelName);
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
- inTier,
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 reportImageUsage({
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: usage.cost * numberOfImages,
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
- const parseFunctionResponse = (response: unknown): any => {
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
- // Preserve thoughtSignature if present for Google AI reasoning context
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
- // Preserve thoughtSignature if present for Google AI reasoning context
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
- // Preserve thoughtSignature if present for Google AI reasoning context
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
- // Preserve thoughtSignature if present for Google AI reasoning context
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
- // Preserve thoughtSignature if present for Google AI reasoning context
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
  }