@smythos/sre 1.7.18 → 1.7.20

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 (23) hide show
  1. package/dist/index.js +21 -21
  2. package/dist/index.js.map +1 -1
  3. package/dist/types/Components/RAG/DataSourceCleaner.class.d.ts +37 -0
  4. package/dist/types/Components/RAG/DataSourceComponent.class.d.ts +30 -0
  5. package/dist/types/Components/RAG/DataSourceIndexer.class.d.ts +14 -0
  6. package/dist/types/Components/RAG/DataSourceLookup.class.d.ts +36 -0
  7. package/dist/types/helpers/Conversation.helper.d.ts +3 -0
  8. package/dist/types/subsystems/LLMManager/LLM.inference.d.ts +10 -3
  9. package/dist/types/subsystems/LLMManager/LLM.service/connectors/GoogleAI.class.d.ts +4 -2
  10. package/dist/types/subsystems/Security/ManagedVault.service/connectors/SecretManagerManagedVault.d.ts +10 -0
  11. package/dist/types/subsystems/Security/Vault.service/connectors/SecretsManager.class.d.ts +5 -0
  12. package/dist/types/types/LLM.types.d.ts +2 -0
  13. package/dist/types/utils/array.utils.d.ts +4 -0
  14. package/package.json +1 -1
  15. package/src/helpers/AWSLambdaCode.helper.ts +40 -45
  16. package/src/helpers/Conversation.helper.ts +14 -10
  17. package/src/subsystems/LLMManager/LLM.inference.ts +63 -47
  18. package/src/subsystems/LLMManager/LLM.service/connectors/GoogleAI.class.ts +92 -19
  19. package/src/subsystems/LLMManager/ModelsProvider.service/connectors/JSONModelsProvider.class.ts +34 -27
  20. package/src/subsystems/ObservabilityManager/Telemetry.service/connectors/OTel/OTel.class.ts +6 -0
  21. package/src/subsystems/Security/ManagedVault.service/connectors/SecretManagerManagedVault.ts +111 -48
  22. package/src/subsystems/Security/Vault.service/connectors/SecretsManager.class.ts +92 -62
  23. package/src/types/LLM.types.ts +5 -0
@@ -13,14 +13,30 @@ import { TLLMChatResponse, TLLMMessageRole, TLLMModel, TLLMParams } from '@sre/t
13
13
  import { LLMConnector } from './LLM.service/LLMConnector';
14
14
  import { IModelsProviderRequest, ModelsProviderConnector } from './ModelsProvider.service/ModelsProviderConnector';
15
15
 
16
- const console = Logger('LLMInference');
16
+ const logger = Logger('LLMInference');
17
17
 
18
18
  type TPromptParams = { query?: string; contextWindow?: any[]; files?: any[]; params: TLLMParams; onFallback?: (data: any) => void };
19
19
 
20
20
  export class LLMInference {
21
- private model: string | TLLMModel;
22
- private llmConnector: LLMConnector;
23
- private modelProviderReq: IModelsProviderRequest;
21
+ private _model: string | TLLMModel;
22
+ public get model() {
23
+ return this._model;
24
+ }
25
+ public get modelId() {
26
+ return typeof this._model === 'string' ? this._model : this._model?.modelId;
27
+ }
28
+ private _llmConnector: LLMConnector;
29
+ public get llmConnector() {
30
+ return this._llmConnector;
31
+ }
32
+ private _modelProviderReq: IModelsProviderRequest;
33
+ public get modelProviderReq() {
34
+ return this._modelProviderReq;
35
+ }
36
+ private _llmProviderName: string;
37
+ public get llmProviderName() {
38
+ return this._llmProviderName;
39
+ }
24
40
  public teamId?: string;
25
41
 
26
42
  public static async getInstance(model: string | TLLMModel, candidate: AccessCandidate) {
@@ -34,18 +50,18 @@ export class LLMInference {
34
50
  const llmInference = new LLMInference();
35
51
  llmInference.teamId = teamId;
36
52
 
37
- llmInference.modelProviderReq = modelsProvider.requester(candidate);
53
+ llmInference._modelProviderReq = modelsProvider.requester(candidate);
38
54
 
39
- const llmProvider = await llmInference.modelProviderReq.getProvider(model);
40
- if (llmProvider) {
41
- llmInference.llmConnector = ConnectorService.getLLMConnector(llmProvider);
55
+ llmInference._llmProviderName = await llmInference._modelProviderReq.getProvider(model);
56
+ if (llmInference._llmProviderName) {
57
+ llmInference._llmConnector = ConnectorService.getLLMConnector(llmInference._llmProviderName);
42
58
  }
43
59
 
44
- if (!llmInference.llmConnector) {
45
- console.error(`Model ${model} unavailable for team ${teamId}`);
60
+ if (!llmInference._llmConnector) {
61
+ logger.warn(`Model ${model} unavailable for team ${teamId}`);
46
62
  }
47
63
 
48
- llmInference.model = model;
64
+ llmInference._model = model;
49
65
 
50
66
  return llmInference;
51
67
  }
@@ -53,32 +69,32 @@ export class LLMInference {
53
69
  public static user(candidate: AccessCandidate): any {}
54
70
 
55
71
  public get connector(): LLMConnector {
56
- return this.llmConnector;
72
+ return this._llmConnector;
57
73
  }
58
74
 
59
75
  public async prompt({ query, contextWindow, files, params, onFallback = () => {} }: TPromptParams, isInFallback: boolean = false) {
60
76
  let messages = contextWindow || [];
61
77
 
62
78
  if (query) {
63
- const content = this.llmConnector.enhancePrompt(query, params);
79
+ const content = this._llmConnector.enhancePrompt(query, params);
64
80
  messages.push({ role: TLLMMessageRole.User, content });
65
81
  }
66
82
 
67
83
  // Reset the model, since the fallback model may change — especially when using user custom models.
68
- params.model = this.model;
84
+ params.model = this._model;
69
85
 
70
86
  params.messages = messages;
71
87
  params.files = files;
72
88
 
73
89
  // If a fallback model is used, trigger the onFallback callback to notify the caller.
74
90
  if (isInFallback && typeof onFallback === 'function') {
75
- onFallback({ model: this.model });
91
+ onFallback({ model: this._model });
76
92
  }
77
93
 
78
94
  try {
79
- let response: TLLMChatResponse = await this.llmConnector.requester(AccessCandidate.agent(params.agentId)).request(params);
95
+ let response: TLLMChatResponse = await this._llmConnector.requester(AccessCandidate.agent(params.agentId)).request(params);
80
96
 
81
- const result = this.llmConnector.postProcess(response?.content);
97
+ const result = this._llmConnector.postProcess(response?.content);
82
98
  if (result.error) {
83
99
  // If the model stopped before completing the response, this is usually due to output token limit reached.
84
100
  if (response.finishReason !== 'stop') {
@@ -102,12 +118,12 @@ export class LLMInference {
102
118
  }
103
119
  } catch (fallbackError) {
104
120
  // If fallback also failed, log it but continue to throw original error
105
- console.warn('Fallback also failed:', fallbackError);
121
+ logger.warn('Fallback also failed:', fallbackError);
106
122
  }
107
123
  }
108
124
 
109
125
  // If fallback was not attempted or failed, throw the original error
110
- console.error('Error in chatRequest: ', error);
126
+ logger.error('Error in chatRequest: ', error);
111
127
  throw error;
112
128
  }
113
129
  }
@@ -116,23 +132,23 @@ export class LLMInference {
116
132
  let messages = contextWindow || [];
117
133
 
118
134
  if (query) {
119
- const content = this.llmConnector.enhancePrompt(query, params);
135
+ const content = this._llmConnector.enhancePrompt(query, params);
120
136
  messages.push({ role: TLLMMessageRole.User, content });
121
137
  }
122
138
 
123
139
  // Reset the model, since the fallback model may change — especially when using user custom models.
124
- params.model = this.model;
140
+ params.model = this._model;
125
141
 
126
142
  params.messages = messages;
127
143
  params.files = files;
128
144
 
129
145
  // If a fallback model is used, trigger the onFallback callback to notify the caller.
130
146
  if (isInFallback && typeof onFallback === 'function') {
131
- onFallback({ model: this.model });
147
+ onFallback({ model: this._model });
132
148
  }
133
149
 
134
150
  try {
135
- return await this.llmConnector.user(AccessCandidate.agent(params.agentId)).streamRequest(params);
151
+ return await this._llmConnector.user(AccessCandidate.agent(params.agentId)).streamRequest(params);
136
152
  } catch (error) {
137
153
  // Attempt fallback for custom models (only if not already in fallback)
138
154
  if (!isInFallback) {
@@ -152,12 +168,12 @@ export class LLMInference {
152
168
  }
153
169
  } catch (fallbackError) {
154
170
  // If fallback also failed, log it but continue to return error emitter
155
- console.warn('Fallback also failed:', fallbackError);
171
+ logger.warn('Fallback also failed:', fallbackError);
156
172
  }
157
173
  }
158
174
 
159
175
  // If fallback was not attempted or failed, return error emitter
160
- console.error('Error in streamRequest:', error);
176
+ logger.error('Error in streamRequest:', error);
161
177
 
162
178
  const dummyEmitter = new EventEmitter();
163
179
  process.nextTick(() => {
@@ -224,22 +240,22 @@ export class LLMInference {
224
240
  * @returns The result from the fallback execution, or null if fallback should not be attempted
225
241
  */
226
242
  private async executeFallback(methodName: 'prompt' | 'promptStream', args: TPromptParams): Promise<any> {
227
- const isCustomModel = await this.modelProviderReq.isUserCustomLLM(this.model);
228
- const fallbackModel = await this.modelProviderReq.getFallbackLLM(this.model);
243
+ const isCustomModel = await this._modelProviderReq.isUserCustomLLM(this._model);
244
+ const fallbackModel = await this._modelProviderReq.getFallbackLLM(this._model);
229
245
 
230
246
  // Only execute fallback if it's a custom model with a configured fallback
231
247
  if (!isCustomModel || !fallbackModel) {
232
248
  return null;
233
249
  }
234
250
 
235
- console.info(`Attempting fallback from ${this.model} to ${fallbackModel}`);
251
+ logger.info(`Attempting fallback from ${this._model} to ${fallbackModel}`);
236
252
 
237
253
  // Mutate the model and connector to use fallback
238
- this.model = fallbackModel;
254
+ this._model = fallbackModel;
239
255
 
240
- const llmProvider = await this.modelProviderReq.getProvider(fallbackModel);
256
+ const llmProvider = await this._modelProviderReq.getProvider(fallbackModel);
241
257
  if (llmProvider) {
242
- this.llmConnector = ConnectorService.getLLMConnector(llmProvider);
258
+ this._llmConnector = ConnectorService.getLLMConnector(llmProvider);
243
259
  }
244
260
 
245
261
  // Call the appropriate method with isInFallback=true to prevent further fallbacks
@@ -252,13 +268,13 @@ export class LLMInference {
252
268
 
253
269
  public async imageGenRequest({ query, files, params }: TPromptParams) {
254
270
  params.prompt = query;
255
- return this.llmConnector.user(AccessCandidate.agent(params.agentId)).imageGenRequest(params);
271
+ return this._llmConnector.user(AccessCandidate.agent(params.agentId)).imageGenRequest(params);
256
272
  }
257
273
 
258
274
  public async imageEditRequest({ query, files, params }: TPromptParams) {
259
275
  params.prompt = query;
260
276
  params.files = files;
261
- return this.llmConnector.user(AccessCandidate.agent(params.agentId)).imageEditRequest(params);
277
+ return this._llmConnector.user(AccessCandidate.agent(params.agentId)).imageEditRequest(params);
262
278
  }
263
279
 
264
280
  //@deprecated
@@ -269,11 +285,11 @@ export class LLMInference {
269
285
  throw new Error('Input messages are required.');
270
286
  }
271
287
 
272
- const model = params.model || this.model;
288
+ const model = params.model || this._model;
273
289
 
274
- return await this.llmConnector.user(AccessCandidate.agent(agentId)).streamRequest({ ...params, model });
290
+ return await this._llmConnector.user(AccessCandidate.agent(agentId)).streamRequest({ ...params, model });
275
291
  } catch (error) {
276
- console.error('Error in streamRequest:', error);
292
+ logger.error('Error in streamRequest:', error);
277
293
 
278
294
  const dummyEmitter = new EventEmitter();
279
295
  process.nextTick(() => {
@@ -306,11 +322,11 @@ export class LLMInference {
306
322
  //FIXME we need to update the connector multimediaStreamRequest in order to ignore prompt param if not provided
307
323
  const userMessage = Array.isArray(params.messages) ? params.messages.pop() : {};
308
324
  const prompt = userMessage?.content || '';
309
- const model = params.model || this.model;
325
+ const model = params.model || this._model;
310
326
 
311
- return await this.llmConnector.user(AccessCandidate.agent(agentId)).multimodalStreamRequest(prompt, { ...params, model });
327
+ return await this._llmConnector.user(AccessCandidate.agent(agentId)).multimodalStreamRequest(prompt, { ...params, model });
312
328
  } catch (error: any) {
313
- console.error('Error in multimodalRequest: ', error);
329
+ logger.error('Error in multimodalRequest: ', error);
314
330
 
315
331
  throw error;
316
332
  }
@@ -337,12 +353,12 @@ export class LLMInference {
337
353
  params.files = _files;
338
354
 
339
355
  try {
340
- prompt = this.llmConnector.enhancePrompt(prompt, config);
341
- const model = params.model || this.model;
356
+ prompt = this._llmConnector.enhancePrompt(prompt, config);
357
+ const model = params.model || this._model;
342
358
 
343
- return await this.llmConnector.user(AccessCandidate.agent(agentId)).multimodalStreamRequest(prompt, { ...params, model });
359
+ return await this._llmConnector.user(AccessCandidate.agent(agentId)).multimodalStreamRequest(prompt, { ...params, model });
344
360
  } catch (error: any) {
345
- console.error('Error in multimodalRequest: ', error);
361
+ logger.error('Error in multimodalRequest: ', error);
346
362
 
347
363
  throw error;
348
364
  }
@@ -376,7 +392,7 @@ export class LLMInference {
376
392
 
377
393
  //#region get max model context
378
394
 
379
- const modelInfo = await this.modelProviderReq.getModelInfo(this.model, true);
395
+ const modelInfo = await this._modelProviderReq.getModelInfo(this._model, true);
380
396
  let maxModelContext = modelInfo?.tokens;
381
397
  let maxModelOutputTokens = modelInfo?.completionTokens || modelInfo?.tokens;
382
398
  // const isStandardLLM = LLMRegistry.isStandardLLM(this.model);
@@ -399,10 +415,10 @@ export class LLMInference {
399
415
  }
400
416
 
401
417
  if (maxInputContext <= 0) {
402
- console.warn('Max input context is 0, returning empty context window, This usually indicates a wrong model configuration');
418
+ logger.warn('Max input context is 0, returning empty context window, This usually indicates a wrong model configuration');
403
419
  }
404
420
 
405
- console.debug(
421
+ logger.debug(
406
422
  `Context Window Configuration: Max Input Tokens: ${maxInputContext}, Max Output Tokens: ${maxOutputContext}, Max Model Tokens: ${maxModelContext}`
407
423
  );
408
424
  const systemMessage = { role: 'system', content: systemPrompt };
@@ -473,7 +489,7 @@ function countTokens(content: any, model: 'gpt-4o' | 'gpt-4o-mini' = 'gpt-4o') {
473
489
  const tokens = encodeChat([{ role: 'user', content: _stringifiedContent } as ChatMessage], model);
474
490
  return tokens.length;
475
491
  } catch (error) {
476
- console.warn('Error in countTokens: ', error);
492
+ logger.warn('Error in countTokens: ', error);
477
493
  return 0;
478
494
  }
479
495
  }
@@ -139,6 +139,7 @@ export class GoogleAIConnector extends LLMConnector {
139
139
  ? toolCall.functionCall?.args
140
140
  : JSON.stringify(toolCall.functionCall?.args ?? {}),
141
141
  role: TLLMMessageRole.Assistant,
142
+ thoughtSignature: (toolCall as any).thoughtSignature, // Preserve Google AI's reasoning context
142
143
  }));
143
144
  useTool = true;
144
145
  }
@@ -202,6 +203,7 @@ export class GoogleAIConnector extends LLMConnector {
202
203
  ? toolCall.functionCall?.args
203
204
  : JSON.stringify(toolCall.functionCall?.args ?? {}),
204
205
  role: TLLMMessageRole.Assistant,
206
+ thoughtSignature: (toolCall as any).thoughtSignature, // Preserve Google AI's reasoning context
205
207
  }));
206
208
  emitter.emit(TLLMEvent.ToolInfo, toolsData);
207
209
  }
@@ -426,6 +428,13 @@ export class GoogleAIConnector extends LLMConnector {
426
428
  if (params.stopSequences?.length) config.stopSequences = params.stopSequences;
427
429
  if (responseMimeType) config.responseMimeType = responseMimeType;
428
430
 
431
+ // #region Gemini 3 specific fields
432
+ const isGemini3Model = params.modelEntryName?.includes('gemini-3');
433
+
434
+ if (isGemini3Model) {
435
+ if (params?.reasoningEffort) config.thinkingConfig = { thinkingLevel: params.reasoningEffort };
436
+ }
437
+
429
438
  if (systemInstruction) body.systemInstruction = systemInstruction;
430
439
  if (Object.keys(config).length > 0) {
431
440
  body.generationConfig = config;
@@ -505,36 +514,76 @@ export class GoogleAIConnector extends LLMConnector {
505
514
  ) {
506
515
  // SmythOS (built-in) models have a prefix, so we need to remove it to get the model name
507
516
  const modelName = metadata.modelEntryName.replace(BUILT_IN_MODEL_PREFIX, '');
508
- let tier = '';
517
+
518
+ // Initially, all input tokens – such as text, audio, image, video, document, etc. – were included in promptTokenCount.
519
+ let inputTokens = usage?.promptTokenCount || 0;
520
+
521
+ // 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);
523
+
524
+ // If cached input tokens are available, we need to subtract them from the input tokens.
525
+ let cachedInputTokens = usage?.cachedContentTokenCount || 0;
526
+
527
+ if (cachedInputTokens) {
528
+ inputTokens = inputTokens - cachedInputTokens;
529
+ }
530
+
531
+ // #region Find matching model and set tier based on threshold
509
532
  const tierThresholds = {
510
533
  'gemini-1.5-pro': 128_000,
511
534
  'gemini-2.5-pro': 200_000,
535
+ 'gemini-3-pro': 200_000,
512
536
  };
513
537
 
514
- const textInputTokens =
515
- usage?.['promptTokensDetails']?.find((detail) => detail.modality === 'TEXT')?.tokenCount || usage?.promptTokenCount || 0;
516
- const audioInputTokens = usage?.['promptTokensDetails']?.find((detail) => detail.modality === 'AUDIO')?.tokenCount || 0;
538
+ let inTier = '';
539
+ let outTier = '';
540
+ let crTier = '';
517
541
 
518
- // Find matching model and set tier based on threshold
519
542
  const modelWithTier = Object.keys(tierThresholds).find((model) => modelName.includes(model));
520
543
  if (modelWithTier) {
521
- tier = textInputTokens < tierThresholds[modelWithTier] ? 'tier1' : 'tier2';
544
+ inTier = inputTokens <= tierThresholds[modelWithTier] ? 'tier1' : 'tier2';
545
+ outTier = outputTokens <= tierThresholds[modelWithTier] ? 'tier1' : 'tier2';
546
+ crTier = cachedInputTokens <= tierThresholds[modelWithTier] ? 'tier1' : 'tier2';
522
547
  }
548
+ // #endregion
523
549
 
550
+ // #region Calculate audio input tokens
551
+ // Since Gemini 2.5 Flash has a different pricing model for audio input tokens, we need to report audio input tokens separately.
552
+ let audioInputTokens = 0;
553
+ let cachedAudioInputTokens = 0;
554
+ const isFlashModel = ['gemini-2.5-flash'].includes(modelName);
555
+
556
+ if (isFlashModel) {
557
+ // There is no concept of different pricing for Flash models based on token tiers (e.g., less than or greater than 200k),
558
+ // so we don't need to provide tier information for audio input tokens.
559
+ audioInputTokens = usage?.promptTokensDetails?.find((detail) => detail.modality === 'AUDIO')?.tokenCount || 0;
560
+
561
+ // subtract the audio cached input tokens from the audio input tokens and total cached input tokens.
562
+ cachedAudioInputTokens = usage?.cacheTokensDetails?.find((detail) => detail.modality === 'AUDIO')?.tokenCount || 0;
563
+ if (cachedAudioInputTokens) {
564
+ audioInputTokens = audioInputTokens - cachedAudioInputTokens;
565
+ cachedInputTokens = cachedInputTokens - cachedAudioInputTokens;
566
+ }
567
+
568
+ inputTokens = inputTokens - audioInputTokens;
569
+ }
524
570
  // #endregion
525
571
 
526
572
  const usageData = {
527
573
  sourceId: `llm:${modelName}`,
528
- input_tokens: textInputTokens,
529
- output_tokens: usage?.candidatesTokenCount || 0,
574
+ input_tokens: inputTokens,
575
+ output_tokens: outputTokens,
530
576
  input_tokens_audio: audioInputTokens,
531
- input_tokens_cache_read: usage?.cachedContentTokenCount || 0,
577
+ input_tokens_cache_read: cachedInputTokens,
578
+ input_tokens_cache_read_audio: cachedAudioInputTokens,
532
579
  input_tokens_cache_write: 0,
533
- reasoning_tokens: usage?.thoughtsTokenCount,
580
+ // reasoning_tokens: usage?.thoughtsTokenCount, // * reasoning tokens are included in the output tokens.
534
581
  keySource: metadata.keySource,
535
582
  agentId: metadata.agentId,
536
583
  teamId: metadata.teamId,
537
- tier,
584
+ inTier,
585
+ outTier,
586
+ crTier,
538
587
  };
539
588
  SystemEvents.emit('USAGE:LLM', usageData);
540
589
 
@@ -665,12 +714,17 @@ export class GoogleAIConnector extends LLMConnector {
665
714
  }
666
715
 
667
716
  if (part.functionCall) {
668
- content.push({
717
+ const functionCallPart: any = {
669
718
  functionCall: {
670
719
  name: part.functionCall.name,
671
720
  args: parseFunctionArgs(part.functionCall.args),
672
721
  },
673
- });
722
+ };
723
+ // Preserve thoughtSignature if present for Google AI reasoning context
724
+ if ((part as any).thoughtSignature) {
725
+ functionCallPart.thoughtSignature = (part as any).thoughtSignature;
726
+ }
727
+ content.push(functionCallPart);
674
728
  continue;
675
729
  }
676
730
 
@@ -699,12 +753,17 @@ export class GoogleAIConnector extends LLMConnector {
699
753
  const hasFunctionCall = content.some((part) => part.functionCall);
700
754
  if (!hasFunctionCall && toolsData.length > 0) {
701
755
  toolsData.forEach((toolCall) => {
702
- content.push({
756
+ const functionCallPart: any = {
703
757
  functionCall: {
704
758
  name: toolCall.name,
705
759
  args: parseFunctionArgs(toolCall.arguments),
706
760
  },
707
- });
761
+ };
762
+ // Preserve thoughtSignature if present for Google AI reasoning context
763
+ if (toolCall.thoughtSignature) {
764
+ functionCallPart.thoughtSignature = toolCall.thoughtSignature;
765
+ }
766
+ content.push(functionCallPart);
708
767
  });
709
768
  }
710
769
 
@@ -811,6 +870,10 @@ export class GoogleAIConnector extends LLMConnector {
811
870
  name: part.functionCall.name,
812
871
  args: parseFunctionArgs(part.functionCall.args),
813
872
  };
873
+ // Preserve thoughtSignature if present for Google AI reasoning context
874
+ if ((part as any).thoughtSignature) {
875
+ normalizedPart.thoughtSignature = (part as any).thoughtSignature;
876
+ }
814
877
  }
815
878
 
816
879
  if (part.functionResponse) {
@@ -839,12 +902,17 @@ export class GoogleAIConnector extends LLMConnector {
839
902
  pushTextPart(normalizedParts, contentPart.text);
840
903
  } else if ('functionCall' in contentPart && (contentPart as any).functionCall) {
841
904
  const functionCallPart = (contentPart as any).functionCall;
842
- normalizedParts.push({
905
+ const normalizedFunctionCall: any = {
843
906
  functionCall: {
844
907
  name: functionCallPart.name,
845
908
  args: parseFunctionArgs(functionCallPart.args),
846
909
  },
847
- });
910
+ };
911
+ // Preserve thoughtSignature if present for Google AI reasoning context
912
+ if ((contentPart as any).thoughtSignature) {
913
+ normalizedFunctionCall.thoughtSignature = (contentPart as any).thoughtSignature;
914
+ }
915
+ normalizedParts.push(normalizedFunctionCall);
848
916
  } else if ('functionResponse' in contentPart && (contentPart as any).functionResponse) {
849
917
  const functionResponsePart = (contentPart as any).functionResponse;
850
918
  normalizedParts.push({
@@ -882,12 +950,17 @@ export class GoogleAIConnector extends LLMConnector {
882
950
  for (const toolCall of message.tool_calls) {
883
951
  if (!toolCall?.function?.name) continue;
884
952
 
885
- normalizedParts.push({
953
+ const normalizedFunctionCall: any = {
886
954
  functionCall: {
887
955
  name: toolCall.function.name,
888
956
  args: parseFunctionArgs(toolCall.function.arguments),
889
957
  },
890
- });
958
+ };
959
+ // Preserve thoughtSignature if present for Google AI reasoning context
960
+ if ((toolCall as any).thoughtSignature) {
961
+ normalizedFunctionCall.thoughtSignature = (toolCall as any).thoughtSignature;
962
+ }
963
+ normalizedParts.push(normalizedFunctionCall);
891
964
  }
892
965
  }
893
966
 
@@ -16,7 +16,7 @@ import fsSync from 'fs';
16
16
  import path from 'path';
17
17
  import { findSmythPath } from '@sre/helpers/Sysconfig.helper';
18
18
 
19
- const console = Logger('SmythModelsProvider');
19
+ const logger = Logger('SmythModelsProvider');
20
20
 
21
21
  type SmythModelsProviderConfig = {
22
22
  /**
@@ -58,7 +58,7 @@ export class JSONModelsProvider extends ModelsProviderConnector {
58
58
  this._settings.mode = 'merge'; //Force merge mode if using models from .smyth folder
59
59
  this.initDirWatcher(modelsFolder); //this.started will be set to true when the watcher is ready
60
60
  } else {
61
- console.warn('No models folder found ... falling back to built-in models only');
61
+ logger.warn('No models folder found ... falling back to built-in models only');
62
62
  this.started = true;
63
63
  }
64
64
  }
@@ -71,7 +71,7 @@ export class JSONModelsProvider extends ModelsProviderConnector {
71
71
  const _modelsFolder = findSmythPath('models');
72
72
 
73
73
  if (fsSync.existsSync(_modelsFolder)) {
74
- console.warn('Using default models folder : ', _modelsFolder);
74
+ logger.warn('Using default models folder : ', _modelsFolder);
75
75
  return _modelsFolder;
76
76
  }
77
77
 
@@ -106,7 +106,7 @@ export class JSONModelsProvider extends ModelsProviderConnector {
106
106
 
107
107
  private async reindexModels(dir: string) {
108
108
  try {
109
- console.debug(`Reindexing models from directory: ${dir}`);
109
+ logger.debug(`Reindexing models from directory: ${dir}`);
110
110
 
111
111
  // Scan directory for models and get them as an object
112
112
  const scannedModels = await this.scanDirectoryForModels(dir);
@@ -121,9 +121,9 @@ export class JSONModelsProvider extends ModelsProviderConnector {
121
121
 
122
122
  JSONModelsProvider.localCache.clear();
123
123
 
124
- console.debug(`Successfully reindexed models. Total models: ${Object.keys(this.models).length}`);
124
+ logger.debug(`Successfully reindexed models. Total models: ${Object.keys(this.models).length}`);
125
125
  } catch (error) {
126
- console.error(`Error reindexing models from directory "${dir}":`, error);
126
+ logger.error(`Error reindexing models from directory "${dir}":`, error);
127
127
  }
128
128
  }
129
129
 
@@ -141,15 +141,21 @@ export class JSONModelsProvider extends ModelsProviderConnector {
141
141
  const subDirModels = await this.scanDirectoryForModels(fullPath);
142
142
  Object.assign(scannedModels, subDirModels);
143
143
  } else if (entry.isFile() && entry.name.endsWith('.json')) {
144
- // Process JSON files and merge results
145
- const fileContent = await fs.readFile(fullPath, 'utf-8');
146
- const modelData = JSON.parse(fileContent);
147
- const validModels = await this.getValidModels(modelData);
148
- Object.assign(scannedModels, validModels);
144
+ try {
145
+ // Process JSON files and merge results
146
+
147
+ const fileContent = await fs.readFile(fullPath, 'utf-8');
148
+ const modelData = JSON.parse(fileContent);
149
+ const validModels = await this.getValidModels(modelData);
150
+ Object.assign(scannedModels, validModels);
151
+ } catch (error) {
152
+ console.error(`Error parsing model data from file "${fullPath}"`);
153
+ logger.warn(`Error parsing model data from file "${fullPath}":`, error.message);
154
+ }
149
155
  }
150
156
  }
151
157
  } catch (error) {
152
- console.warn(`Error scanning directory "${dir}":`, error);
158
+ logger.warn(`Error scanning directory "${dir}":`, error);
153
159
  }
154
160
 
155
161
  return scannedModels;
@@ -164,9 +170,9 @@ export class JSONModelsProvider extends ModelsProviderConnector {
164
170
  // Single model case
165
171
  if (this.isValidSingleModel(modelData)) {
166
172
  validModels[modelData.modelId] = modelData as TLLMModel;
167
- console.debug(`Loaded model: ${modelData.modelId}`);
173
+ logger.debug(`Loaded model: ${modelData.modelId}`);
168
174
  } else {
169
- console.warn(`Invalid model format`, modelData);
175
+ logger.warn(`Invalid model format`, modelData);
170
176
  }
171
177
  } else if (typeof modelData === 'object' && !Array.isArray(modelData)) {
172
178
  // Object of models case
@@ -178,19 +184,19 @@ export class JSONModelsProvider extends ModelsProviderConnector {
178
184
  //console.debug(`Loaded model: ${modelId}`);
179
185
  models += `${modelId} `;
180
186
  } else {
181
- console.warn(`Invalid model format for model "${modelId}"`);
187
+ logger.warn(`Invalid model format for model "${modelId}"`);
182
188
  }
183
189
  } catch (error) {
184
- console.warn(`Error processing model "${modelId}":`, error);
190
+ logger.warn(`Error processing model "${modelId}":`, error);
185
191
  // Continue processing other models instead of failing the whole file
186
192
  }
187
193
  }
188
- console.debug(`Loaded models: ${models}`);
194
+ logger.debug(`Loaded models: ${models}`);
189
195
  } else {
190
- console.warn(`Invalid format (not a model or object of models)`);
196
+ logger.warn(`Invalid format (not a model or object of models)`);
191
197
  }
192
198
  } catch (error) {
193
- console.warn(`Error loading model:`, error);
199
+ logger.warn(`Error loading model:`, error);
194
200
  }
195
201
 
196
202
  return validModels;
@@ -212,7 +218,7 @@ export class JSONModelsProvider extends ModelsProviderConnector {
212
218
  const stats = fsSync.statSync(dir);
213
219
 
214
220
  if (!stats.isDirectory() && !stats.isFile()) {
215
- console.warn(`Path "${dir}" is neither a file nor a directory ... skipping models watcher and falling back to built-in models only`);
221
+ logger.warn(`Path "${dir}" is neither a file nor a directory ... skipping models watcher and falling back to built-in models only`);
216
222
  this.started = true;
217
223
  return;
218
224
  }
@@ -230,17 +236,18 @@ export class JSONModelsProvider extends ModelsProviderConnector {
230
236
  if (this._settings?.mode === 'merge') this.models = { ...this.models, ...modelData };
231
237
  else this.models = modelData;
232
238
  } catch (error) {
233
- console.error(`Error parsing model data from file "${dir}":`, error);
239
+ console.error(`Error parsing model data from file "${dir}":`);
240
+ logger.warn(`Error parsing model data from file "${dir}":`, error.message);
234
241
  }
235
242
  this.started = true;
236
243
  return;
237
244
  }
238
245
 
239
- console.warn(`Path "${dir}" is neither a file nor a directory`);
246
+ logger.warn(`Path "${dir}" is neither a file nor a directory`);
240
247
  return;
241
248
  }
242
249
  } catch (error) {
243
- console.warn(`Path "${dir}" does not exist or cannot be accessed:`, error.message);
250
+ logger.warn(`Path "${dir}" does not exist or cannot be accessed:`, error.message);
244
251
  return;
245
252
  }
246
253
 
@@ -262,19 +269,19 @@ export class JSONModelsProvider extends ModelsProviderConnector {
262
269
 
263
270
  watcher
264
271
  .on('add', (path) => {
265
- console.debug(`File ${path} has been added`);
272
+ logger.debug(`File ${path} has been added`);
266
273
  debouncedReindex();
267
274
  })
268
275
  .on('change', (path) => {
269
- console.debug(`File ${path} has been changed`);
276
+ logger.debug(`File ${path} has been changed`);
270
277
  debouncedReindex();
271
278
  })
272
279
  .on('unlink', (path) => {
273
- console.debug(`File ${path} has been removed`);
280
+ logger.debug(`File ${path} has been removed`);
274
281
  debouncedReindex();
275
282
  })
276
283
  .on('ready', async () => {
277
- console.debug(`Watcher ready. Performing initial scan of ${dir}`);
284
+ logger.debug(`Watcher ready. Performing initial scan of ${dir}`);
278
285
  // Do initial scan once when watcher is ready
279
286
  await this.reindexModels(dir);
280
287
  this.started = true;