@smythos/sre 1.7.41 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/CHANGELOG +136 -64
  2. package/dist/index.js +65 -50
  3. package/dist/index.js.map +1 -1
  4. package/dist/types/Components/Async.class.d.ts +11 -5
  5. package/dist/types/index.d.ts +2 -0
  6. package/dist/types/subsystems/AgentManager/AgentData.service/connectors/SQLiteAgentDataConnector.class.d.ts +45 -0
  7. package/dist/types/subsystems/LLMManager/LLM.helper.d.ts +32 -1
  8. package/dist/types/subsystems/LLMManager/LLM.inference.d.ts +25 -2
  9. package/dist/types/subsystems/LLMManager/LLM.service/connectors/Anthropic.class.d.ts +22 -2
  10. package/dist/types/subsystems/LLMManager/LLM.service/connectors/Bedrock.class.d.ts +2 -2
  11. package/dist/types/subsystems/LLMManager/LLM.service/connectors/GoogleAI.class.d.ts +27 -2
  12. package/dist/types/subsystems/LLMManager/LLM.service/connectors/Groq.class.d.ts +22 -2
  13. package/dist/types/subsystems/LLMManager/LLM.service/connectors/Ollama.class.d.ts +22 -2
  14. package/dist/types/subsystems/LLMManager/LLM.service/connectors/Perplexity.class.d.ts +3 -3
  15. package/dist/types/subsystems/LLMManager/LLM.service/connectors/openai/OpenAIConnector.class.d.ts +23 -3
  16. package/dist/types/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/ChatCompletionsApiInterface.d.ts +2 -2
  17. package/dist/types/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/OpenAIApiInterface.d.ts +2 -2
  18. package/dist/types/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/ResponsesApiInterface.d.ts +2 -2
  19. package/dist/types/subsystems/LLMManager/LLM.service/connectors/xAI.class.d.ts +3 -3
  20. package/dist/types/subsystems/MemoryManager/LLMContext.d.ts +10 -3
  21. package/dist/types/subsystems/ObservabilityManager/Telemetry.service/connectors/OTel/OTel.class.d.ts +24 -0
  22. package/dist/types/subsystems/ObservabilityManager/Telemetry.service/connectors/OTel/OTel.redaction.helper.d.ts +49 -0
  23. package/dist/types/types/LLM.types.d.ts +30 -1
  24. package/package.json +4 -3
  25. package/src/Components/APICall/OAuth.helper.ts +16 -1
  26. package/src/Components/APIEndpoint.class.ts +11 -4
  27. package/src/Components/Async.class.ts +38 -5
  28. package/src/Components/GenAILLM.class.ts +13 -7
  29. package/src/Components/LLMAssistant.class.ts +3 -1
  30. package/src/Components/LogicAND.class.ts +13 -0
  31. package/src/Components/LogicAtLeast.class.ts +18 -0
  32. package/src/Components/LogicAtMost.class.ts +19 -0
  33. package/src/Components/LogicOR.class.ts +12 -2
  34. package/src/Components/LogicXOR.class.ts +11 -0
  35. package/src/constants.ts +1 -1
  36. package/src/helpers/Conversation.helper.ts +10 -8
  37. package/src/index.ts +2 -0
  38. package/src/index.ts.bak +2 -0
  39. package/src/subsystems/AgentManager/AgentData.service/connectors/SQLiteAgentDataConnector.class.ts +190 -0
  40. package/src/subsystems/AgentManager/AgentData.service/index.ts +2 -0
  41. package/src/subsystems/LLMManager/LLM.helper.ts +117 -1
  42. package/src/subsystems/LLMManager/LLM.inference.ts +136 -67
  43. package/src/subsystems/LLMManager/LLM.service/LLMConnector.ts +13 -6
  44. package/src/subsystems/LLMManager/LLM.service/connectors/Anthropic.class.ts +157 -33
  45. package/src/subsystems/LLMManager/LLM.service/connectors/Bedrock.class.ts +9 -8
  46. package/src/subsystems/LLMManager/LLM.service/connectors/GoogleAI.class.ts +121 -83
  47. package/src/subsystems/LLMManager/LLM.service/connectors/Groq.class.ts +125 -62
  48. package/src/subsystems/LLMManager/LLM.service/connectors/Ollama.class.ts +168 -76
  49. package/src/subsystems/LLMManager/LLM.service/connectors/Perplexity.class.ts +18 -8
  50. package/src/subsystems/LLMManager/LLM.service/connectors/VertexAI.class.ts +8 -4
  51. package/src/subsystems/LLMManager/LLM.service/connectors/openai/OpenAIConnector.class.ts +50 -8
  52. package/src/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/ChatCompletionsApiInterface.ts +30 -16
  53. package/src/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/OpenAIApiInterface.ts +2 -2
  54. package/src/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/ResponsesApiInterface.ts +29 -15
  55. package/src/subsystems/LLMManager/LLM.service/connectors/xAI.class.ts +10 -8
  56. package/src/subsystems/MemoryManager/LLMContext.ts +27 -8
  57. package/src/subsystems/ObservabilityManager/Telemetry.service/connectors/OTel/OTel.class.ts +467 -120
  58. package/src/subsystems/ObservabilityManager/Telemetry.service/connectors/OTel/OTel.redaction.helper.ts +203 -0
  59. package/src/types/LLM.types.ts +31 -1
  60. package/src/types/node-sqlite.d.ts +45 -0
@@ -0,0 +1,190 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { DatabaseSync } from 'node:sqlite';
4
+ import { AgentDataConnector } from '../AgentDataConnector';
5
+ import { JSONContentHelper } from '@sre/helpers/JsonContent.helper';
6
+ import { Logger } from '@sre/helpers/Log.helper';
7
+
8
+ const console = Logger('SQLiteAgentDataConnector');
9
+
10
+ export type SQLiteAgentDataSettings = {
11
+ databasePath: string;
12
+ tableName?: string;
13
+ };
14
+
15
+ type SQLiteRunResult = { changes?: number; lastInsertRowid?: number | bigint };
16
+
17
+ export interface SQLiteStatementAdapter<T = any> {
18
+ all(...params: any[]): T[];
19
+ get(...params: any[]): T | undefined;
20
+ run(...params: any[]): SQLiteRunResult;
21
+ }
22
+
23
+ export interface SQLiteDatabaseAdapter {
24
+ prepare<T = any>(sql: string): SQLiteStatementAdapter<T>;
25
+ exec(sql: string): void;
26
+ close(): void;
27
+ }
28
+
29
+ export type SQLiteAdapterFactory = (databasePath: string) => SQLiteDatabaseAdapter;
30
+
31
+ class NativeSQLiteAdapter implements SQLiteDatabaseAdapter {
32
+ private db: DatabaseSync;
33
+
34
+ constructor(databasePath: string) {
35
+ this.db = new DatabaseSync(databasePath);
36
+ }
37
+
38
+ public prepare<T = any>(sql: string) {
39
+ return this.db.prepare(sql) as unknown as SQLiteStatementAdapter<T>;
40
+ }
41
+
42
+ public exec(sql: string) {
43
+ this.db.exec(sql);
44
+ }
45
+
46
+ public close() {
47
+ this.db.close();
48
+ }
49
+ }
50
+
51
+ type AgentRow = {
52
+ id: number;
53
+ data: any;
54
+ aiAgentId: string;
55
+ };
56
+
57
+ export class SQLiteAgentDataConnector extends AgentDataConnector {
58
+ public name = 'SQLiteAgentDataConnector';
59
+ private adapter!: SQLiteDatabaseAdapter;
60
+ private readonly tableName: string;
61
+
62
+ constructor(protected _settings: SQLiteAgentDataSettings) {
63
+ super(_settings);
64
+ this.tableName = this.validateTableName(_settings?.tableName || 'AiAgentData');
65
+ }
66
+
67
+ public getAgentConfig(agentId: string): Partial<SQLiteAgentDataSettings> {
68
+ return {
69
+ databasePath: this._settings?.databasePath,
70
+ tableName: this.tableName,
71
+ };
72
+ }
73
+
74
+ public async start() {
75
+ super.start();
76
+ this.started = false;
77
+
78
+ if (!this._settings?.databasePath) {
79
+ throw new Error('SQLiteAgentDataConnector requires a databasePath setting');
80
+ }
81
+
82
+ this.ensureDatabaseDirectory(this._settings.databasePath);
83
+ this.adapter = new NativeSQLiteAdapter(this._settings.databasePath);
84
+ this.ensureSchema();
85
+
86
+ this.started = true;
87
+ }
88
+
89
+ public async stop() {
90
+ try {
91
+ this.adapter?.close();
92
+ } catch (error: any) {
93
+ console.warn('Error closing SQLite adapter', error?.message || error);
94
+ }
95
+ await super.stop();
96
+ }
97
+
98
+ public async getAgentData(agentId: string, version?: string) {
99
+ const ready = await this.ready();
100
+ if (!ready) {
101
+ throw new Error('Connector not ready');
102
+ }
103
+ const row = this.prepareStatement<AgentRow>(`SELECT id, data, aiAgentId FROM "${this.tableName}" WHERE aiAgentId = ? LIMIT 1`).get(agentId);
104
+
105
+ if (!row) {
106
+ throw new Error(`Agent with id ${agentId} not found`);
107
+ }
108
+
109
+ const parsed = this.parseData(row.data);
110
+ return {
111
+ data: parsed,
112
+ version: version || parsed?.version || '1.0',
113
+ };
114
+ }
115
+
116
+ public async getAgentIdByDomain(domain: string): Promise<string> {
117
+ return Promise.resolve('');
118
+ }
119
+
120
+ public async getAgentSettings(agentId: string, version?: string) {
121
+ const ready = await this.ready();
122
+ if (!ready) {
123
+ throw new Error('Connector not ready');
124
+ }
125
+ const agent = await this.getAgentData(agentId, version);
126
+ return agent?.data?.settings || {};
127
+ }
128
+
129
+ public async getAgentEmbodiments(agentId: string): Promise<any> {
130
+ const ready = await this.ready();
131
+ if (!ready) {
132
+ throw new Error('Connector not ready');
133
+ }
134
+ return [];
135
+ }
136
+
137
+ public async listTeamAgents(teamId: string, deployedOnly?: boolean, includeData?: boolean): Promise<any[]> {
138
+ const ready = await this.ready();
139
+ if (!ready) {
140
+ throw new Error('Connector not ready');
141
+ }
142
+ console.warn(`listTeamAgents is not implemented for SQLiteAgentDataConnector`);
143
+ return [];
144
+ }
145
+
146
+ public async isDeployed(agentId: string): Promise<boolean> {
147
+ const ready = await this.ready();
148
+ if (!ready) {
149
+ throw new Error('Connector not ready');
150
+ }
151
+ const record = await this.getAgentData(agentId).catch(() => null);
152
+ return !!record;
153
+ }
154
+
155
+ private ensureDatabaseDirectory(databasePath: string) {
156
+ if (databasePath === ':memory:') return;
157
+ const dbDir = path.dirname(path.resolve(databasePath));
158
+ if (!fs.existsSync(dbDir)) {
159
+ fs.mkdirSync(dbDir, { recursive: true });
160
+ }
161
+ }
162
+
163
+ private ensureSchema() {
164
+ const ddl = `
165
+ CREATE TABLE IF NOT EXISTS "${this.tableName}" (
166
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
167
+ data TEXT CHECK (json_valid(data)),
168
+ aiAgentId TEXT NOT NULL UNIQUE
169
+ );
170
+ `;
171
+ this.adapter.exec(ddl);
172
+ }
173
+
174
+ private validateTableName(tableName: string) {
175
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(tableName)) {
176
+ throw new Error(`Invalid SQLite table name: ${tableName}`);
177
+ }
178
+ return tableName;
179
+ }
180
+
181
+ private prepareStatement<T = any>(sql: string) {
182
+ return this.adapter.prepare<T>(sql);
183
+ }
184
+
185
+ private parseData(data: any) {
186
+ if (Buffer.isBuffer(data)) data = data.toString('utf-8');
187
+ if (typeof data === 'string') return JSONContentHelper.create(data).tryParse();
188
+ return data;
189
+ }
190
+ }
@@ -6,12 +6,14 @@ import { CLIAgentDataConnector } from './connectors/CLIAgentDataConnector.class'
6
6
  import { AgentDataConnector } from './AgentDataConnector';
7
7
  import { LocalAgentDataConnector } from './connectors/LocalAgentDataConnector.class';
8
8
  import { NullAgentData } from './connectors/NullAgentData.class';
9
+ import { SQLiteAgentDataConnector } from './connectors/SQLiteAgentDataConnector.class';
9
10
  export class AgentDataService extends ConnectorServiceProvider {
10
11
  public register() {
11
12
  //FIXME : register an actual account connector, not the abstract one
12
13
  ConnectorService.register(TConnectorService.AgentData, 'AgentData', AgentDataConnector);
13
14
  ConnectorService.register(TConnectorService.AgentData, 'CLI', CLIAgentDataConnector);
14
15
  ConnectorService.register(TConnectorService.AgentData, 'Local', LocalAgentDataConnector);
16
+ ConnectorService.register(TConnectorService.AgentData, 'SQLite', SQLiteAgentDataConnector);
15
17
 
16
18
  ConnectorService.register(TConnectorService.AgentData, 'NullAgentData', NullAgentData);
17
19
  }
@@ -1,4 +1,4 @@
1
- import { type TLLMMessageBlock, TLLMMessageRole } from '@sre/types/LLM.types';
1
+ import { type TLLMMessageBlock, TLLMMessageRole, TLLMFinishReason } from '@sre/types/LLM.types';
2
2
 
3
3
  import axios from 'axios';
4
4
  import imageSize from 'image-size';
@@ -273,4 +273,120 @@ export class LLMHelper {
273
273
  // Examples: claude-opus-4-5, claude-sonnet-4-20250514, claude-4-opus
274
274
  return /claude-(?:\w+-)?4(?:-|$)/i.test(modelId);
275
275
  }
276
+
277
+ /**
278
+ * Normalizes provider-specific finish reason values to TLLMFinishReason enum.
279
+ * Handles provider-specific values from OpenAI, Anthropic, Google AI, and other providers.
280
+ *
281
+ * @param finishReason - The finish reason from the provider (can be string, null, or undefined)
282
+ * @returns Normalized TLLMFinishReason enum value
283
+ *
284
+ * @example
285
+ * const normalized = LLMHelper.normalizeFinishReason('end_turn');
286
+ * console.log(normalized); // TLLMFinishReason.Stop
287
+ *
288
+ * @example
289
+ * const normalized = LLMHelper.normalizeFinishReason('tool_use');
290
+ * console.log(normalized); // TLLMFinishReason.ToolCalls
291
+ *
292
+ * @example
293
+ * const normalized = LLMHelper.normalizeFinishReason('SAFETY');
294
+ * console.log(normalized); // TLLMFinishReason.ContentFilter
295
+ */
296
+ public static normalizeFinishReason(finishReason: string | null | undefined): TLLMFinishReason {
297
+ if (!finishReason) {
298
+ return TLLMFinishReason.Stop;
299
+ }
300
+
301
+ const normalized = finishReason.toLowerCase().trim();
302
+
303
+ // Map standard and provider-specific values
304
+ switch (normalized) {
305
+ // Natural stop
306
+ case 'stop':
307
+ case 'end_turn': // Anthropic - natural end of turn
308
+ case 'stop_sequence': // Anthropic - custom stop sequence matched
309
+ case 'pause_turn': // Anthropic - paused for long-running operation
310
+ return TLLMFinishReason.Stop;
311
+
312
+ // Token/length limits
313
+ case 'length':
314
+ case 'max_tokens': // Anthropic, Google AI
315
+ case 'incomplete': // OpenAI Responses API - response cut short due to max tokens or content filter
316
+ return TLLMFinishReason.Length;
317
+
318
+ case 'model_context_window_exceeded': // Anthropic - context window exceeded
319
+ return TLLMFinishReason.ContextWindowLength;
320
+
321
+ // Content filtering and safety
322
+ case 'content_filter':
323
+ case 'contentfilter':
324
+ case 'refusal': // Anthropic - refused due to safety/policy
325
+ case 'safety': // Google AI - flagged by safety filters
326
+ case 'recitation': // Google AI - copyrighted content recitation
327
+ case 'language': // Google AI - unsupported language
328
+ case 'blocklist': // Google AI - forbidden terms
329
+ case 'prohibited_content': // Google AI - prohibited content
330
+ case 'spii': // Google AI - sensitive personally identifiable information
331
+ return TLLMFinishReason.ContentFilter;
332
+
333
+ // Tool/function calls
334
+ case 'tool_calls':
335
+ case 'tool_use': // Anthropic - tool invocation
336
+ case 'function_call': // OpenAI deprecated
337
+ return TLLMFinishReason.ToolCalls;
338
+
339
+ // Abort
340
+ case 'abort':
341
+ return TLLMFinishReason.Abort;
342
+
343
+ // Errors
344
+ case 'error':
345
+ case 'malformed_function_call': // Google AI - invalid function call
346
+ return TLLMFinishReason.Error;
347
+
348
+ // Unknown/unmapped
349
+ default:
350
+ return TLLMFinishReason.Unknown;
351
+ }
352
+ }
353
+
354
+ /**
355
+ * Gets a user-friendly error message based on the finish reason.
356
+ *
357
+ * @param finishReason - The normalized finish reason enum value
358
+ * @returns User-friendly error message explaining why the response was interrupted
359
+ *
360
+ * @example
361
+ * const message = LLMHelper.getFinishReasonErrorMessage(TLLMFinishReason.Length);
362
+ * console.log(message); // "Empty response. This is usually due to output token limit reached..."
363
+ */
364
+ public static getFinishReasonErrorMessage(finishReason: TLLMFinishReason): string {
365
+ switch (finishReason) {
366
+ case TLLMFinishReason.Length:
367
+ return 'Empty response. This is usually due to output token limit reached. Please try again with a higher \'Maximum Output Tokens\'.';
368
+
369
+ case TLLMFinishReason.ContextWindowLength:
370
+ return 'Context window limit exceeded. Please shorten the input prompt or summarize the previous conversation to reduce token usage.';
371
+
372
+ case TLLMFinishReason.ContentFilter:
373
+ return 'The response was blocked by content filtering policies. Please modify your prompt and try again.';
374
+
375
+ case TLLMFinishReason.Abort:
376
+ return 'The request was aborted before completion.';
377
+
378
+ case TLLMFinishReason.Error:
379
+ return 'An error occurred while generating the response. Please try again.';
380
+
381
+ case TLLMFinishReason.ToolCalls:
382
+ return 'The model attempted to call a tool but the response was incomplete.';
383
+
384
+ case TLLMFinishReason.Unknown:
385
+ return 'The response was interrupted for an unknown reason. Please try again.';
386
+
387
+ case TLLMFinishReason.Stop:
388
+ default:
389
+ return 'The model stopped before completing the response.';
390
+ }
391
+ }
276
392
  }
@@ -8,8 +8,9 @@ import { BinaryInput } from '@sre/helpers/BinaryInput.helper';
8
8
  import { Logger } from '@sre/helpers/Log.helper';
9
9
  import { AccessCandidate } from '@sre/Security/AccessControl/AccessCandidate.class';
10
10
  import { IAgent } from '@sre/types/Agent.types';
11
- import { TLLMChatResponse, TLLMMessageRole, TLLMModel, TLLMParams } from '@sre/types/LLM.types';
11
+ import { TLLMChatResponse, TLLMMessageRole, TLLMModel, TLLMParams, TLLMEvent, TLLMFinishReason } from '@sre/types/LLM.types';
12
12
 
13
+ import { LLMHelper } from './LLM.helper';
13
14
  import { LLMConnector } from './LLM.service/LLMConnector';
14
15
  import { IModelsProviderRequest, ModelsProviderConnector } from './ModelsProvider.service/ModelsProviderConnector';
15
16
 
@@ -96,29 +97,33 @@ export class LLMInference {
96
97
 
97
98
  const result = this._llmConnector.postProcess(response?.content);
98
99
  if (result.error) {
99
- // If the model stopped before completing the response, this is usually due to output token limit reached.
100
- if (response.finishReason !== 'stop') {
101
- throw new Error('The model stopped before completing the response, this is usually due to output token limit reached.');
100
+ // If the model stopped before completing the response normally, provide specific error message
101
+ if (response.finishReason !== TLLMFinishReason.Stop) {
102
+ const errorMessage = LLMHelper.getFinishReasonErrorMessage(response.finishReason);
103
+ throw new Error(errorMessage);
102
104
  }
103
105
 
104
- // If the model stopped due to other reasons, throw the error
106
+ // If the model stopped normally but there's a postProcess error, throw the postProcess error
105
107
  throw new Error(result.error);
106
108
  }
107
109
  return result;
108
110
  } catch (error: any) {
109
111
  // Attempt fallback for custom models (only if not already in fallback)
110
112
  if (!isInFallback) {
111
- try {
112
- const fallbackParams = await this.getSafeFallbackParams(params);
113
- const fallbackResult = await this.executeFallback('prompt', { query, contextWindow, files, params: fallbackParams, onFallback });
114
-
115
- // If fallback succeeded, return the result
116
- if (fallbackResult !== null) {
117
- return fallbackResult;
113
+ const isCustomModel = await this._modelProviderReq.isUserCustomLLM(this._model);
114
+ if (isCustomModel) {
115
+ try {
116
+ const fallbackParams = await this.getSafeFallbackParams(params);
117
+ const fallbackResult = await this.executeFallback('prompt', { query, contextWindow, files, params: fallbackParams, onFallback });
118
+
119
+ // If fallback succeeded, return the result
120
+ if (fallbackResult !== null) {
121
+ return fallbackResult;
122
+ }
123
+ } catch (fallbackError) {
124
+ // If fallback also failed, log it but continue to throw original error
125
+ logger.warn('Fallback also failed:', fallbackError);
118
126
  }
119
- } catch (fallbackError) {
120
- // If fallback also failed, log it but continue to throw original error
121
- logger.warn('Fallback also failed:', fallbackError);
122
127
  }
123
128
  }
124
129
 
@@ -147,41 +152,20 @@ export class LLMInference {
147
152
  onFallback({ model: this._model });
148
153
  }
149
154
 
150
- try {
151
- return await this._llmConnector.user(AccessCandidate.agent(params.agentId)).streamRequest(params);
152
- } catch (error) {
153
- // Attempt fallback for custom models (only if not already in fallback)
154
- if (!isInFallback) {
155
- try {
156
- const fallbackParams = await this.getSafeFallbackParams(params);
157
- const fallbackResult = await this.executeFallback('promptStream', {
158
- query,
159
- contextWindow,
160
- files,
161
- params: fallbackParams,
162
- onFallback,
163
- });
164
-
165
- // If fallback succeeded, return the result
166
- if (fallbackResult !== null) {
167
- return fallbackResult;
168
- }
169
- } catch (fallbackError) {
170
- // If fallback also failed, log it but continue to return error emitter
171
- logger.warn('Fallback also failed:', fallbackError);
172
- }
173
- }
155
+ // Connectors now always return emitters (they don't throw errors)
156
+ const primaryEmitter = await this._llmConnector.user(AccessCandidate.agent(params.agentId)).streamRequest(params);
174
157
 
175
- // If fallback was not attempted or failed, return error emitter
176
- logger.error('Error in streamRequest:', error);
158
+ // Only wrap with fallback capability if this is a custom model (not already in fallback)
159
+ // For regular models, return the emitter directly - errors flow naturally to the caller
160
+ if (!isInFallback) {
161
+ const isCustomModel = await this._modelProviderReq.isUserCustomLLM(this._model);
177
162
 
178
- const dummyEmitter = new EventEmitter();
179
- process.nextTick(() => {
180
- dummyEmitter.emit('error', error);
181
- dummyEmitter.emit('end');
182
- });
183
- return dummyEmitter;
163
+ if (isCustomModel) {
164
+ return this.wrapWithFallback(primaryEmitter, { query, contextWindow, files, params, onFallback });
165
+ }
184
166
  }
167
+
168
+ return primaryEmitter;
185
169
  }
186
170
 
187
171
  /**
@@ -232,19 +216,20 @@ export class LLMInference {
232
216
 
233
217
  /**
234
218
  * Executes fallback logic for custom models when the primary model fails.
235
- * This method checks if a fallback model is configured and invokes the appropriate LLM method.
219
+ * Checks if a fallback model is configured and switches to it.
236
220
  * Prevents infinite loops by passing a flag to indicate we're in a fallback attempt.
237
221
  *
222
+ * **Important**: This method should only be called for custom models (already verified by caller).
223
+ *
238
224
  * @param methodName - The name of the method being called ('prompt' or 'promptStream')
239
225
  * @param args - The original arguments passed to the method
240
- * @returns The result from the fallback execution, or null if fallback should not be attempted
226
+ * @returns The result from the fallback execution, or null if no fallback is configured
241
227
  */
242
228
  private async executeFallback(methodName: 'prompt' | 'promptStream', args: TPromptParams): Promise<any> {
243
- const isCustomModel = await this._modelProviderReq.isUserCustomLLM(this._model);
244
229
  const fallbackModel = await this._modelProviderReq.getFallbackLLM(this._model);
245
230
 
246
- // Only execute fallback if it's a custom model with a configured fallback
247
- if (!isCustomModel || !fallbackModel) {
231
+ // Only execute fallback if a fallback model is configured
232
+ if (!fallbackModel) {
248
233
  return null;
249
234
  }
250
235
 
@@ -266,6 +251,93 @@ export class LLMInference {
266
251
  }
267
252
  }
268
253
 
254
+ /**
255
+ * Wraps an emitter with fallback capability using a proxy pattern.
256
+ * This creates a transparent proxy that forwards all events from the source emitter.
257
+ * On error, it attempts to switch to a fallback model and seamlessly redirects events.
258
+ *
259
+ * **Important**: This method is only called for custom models that have fallback configured.
260
+ * Regular models return their emitters directly without wrapping, so errors flow naturally.
261
+ *
262
+ * **Design Pattern**: Proxy/Decorator with listener-based event forwarding
263
+ * **Coupling**: Minimal - reads event types from TLLMEvent enum (single source of truth)
264
+ * **Reliability**: Uses listeners (not emit interception) to avoid timing issues with async emits
265
+ *
266
+ * Note: We use the TLLMEvent enum as the source of truth for all event types.
267
+ * This provides a good balance between decoupling and reliability. The enum already
268
+ * defines all possible LLM events, and connectors emit these standard events.
269
+ *
270
+ * @param sourceEmitter - The custom model's event emitter
271
+ * @param args - The original prompt arguments for fallback execution
272
+ * @returns A proxy emitter that transparently handles primary/fallback switching
273
+ */
274
+ private wrapWithFallback(sourceEmitter: EventEmitter, args: TPromptParams): EventEmitter {
275
+ const proxyEmitter = new EventEmitter();
276
+ let fallbackAttempted = false;
277
+
278
+ /**
279
+ * Attaches forwarding listeners for all event types in TLLMEvent.
280
+ * Uses listeners instead of emit() interception to avoid timing issues with setImmediate.
281
+ *
282
+ * @param source - The emitter to forward events from
283
+ * @param skipErrors - If true, skips forwarding error events (handled separately)
284
+ */
285
+ const forwardAllEvents = (source: EventEmitter, skipErrors: boolean) => {
286
+ // Get all event types from TLLMEvent enum
287
+ const eventTypes = Object.values(TLLMEvent);
288
+
289
+ for (const eventType of eventTypes) {
290
+ // Skip error events if we're intercepting them
291
+ if (skipErrors && eventType === TLLMEvent.Error) {
292
+ continue;
293
+ }
294
+
295
+ // Attach listener to forward this event type
296
+ source.on(eventType, (...eventArgs: any[]) => {
297
+ proxyEmitter.emit(eventType, ...eventArgs);
298
+ });
299
+ }
300
+ };
301
+
302
+ // Handle error events for fallback logic
303
+ const handleError = async (error: Error) => {
304
+ if (fallbackAttempted) return;
305
+ fallbackAttempted = true;
306
+
307
+ // Stop forwarding from primary emitter
308
+ sourceEmitter.removeAllListeners();
309
+
310
+ try {
311
+ const fallbackParams = await this.getSafeFallbackParams(args.params);
312
+ const fallbackEmitter = await this.executeFallback('promptStream', {
313
+ ...args,
314
+ params: fallbackParams,
315
+ });
316
+
317
+ if (fallbackEmitter) {
318
+ // Forward all events from fallback emitter (including errors)
319
+ forwardAllEvents(fallbackEmitter, false);
320
+ logger.info('Successfully switched to fallback stream');
321
+ return;
322
+ }
323
+ } catch (fallbackError) {
324
+ logger.warn('Fallback attempt failed:', fallbackError);
325
+ }
326
+
327
+ // If we get here, fallback failed or was not available - emit error on proxy
328
+ proxyEmitter.emit(TLLMEvent.Error, error);
329
+ proxyEmitter.emit(TLLMEvent.End, [], [], TLLMFinishReason.Error);
330
+ };
331
+
332
+ // Attach error handler FIRST to intercept errors
333
+ sourceEmitter.once(TLLMEvent.Error, handleError);
334
+
335
+ // Forward all non-error events from primary emitter
336
+ forwardAllEvents(sourceEmitter, true);
337
+
338
+ return proxyEmitter;
339
+ }
340
+
269
341
  public async imageGenRequest({ query, files, params }: TPromptParams) {
270
342
  params.prompt = query;
271
343
  return this._llmConnector.user(AccessCandidate.agent(params.agentId)).imageGenRequest(params);
@@ -280,24 +352,21 @@ export class LLMInference {
280
352
  //@deprecated
281
353
  public async streamRequest(params: any, agent: string | IAgent) {
282
354
  const agentId = isAgent(agent) ? (agent as IAgent).id : agent;
283
- try {
284
- if (!params.messages || !params.messages?.length) {
285
- throw new Error('Input messages are required.');
286
- }
287
-
288
- const model = params.model || this._model;
289
-
290
- return await this._llmConnector.user(AccessCandidate.agent(agentId)).streamRequest({ ...params, model });
291
- } catch (error) {
292
- logger.error('Error in streamRequest:', error);
293
-
294
- const dummyEmitter = new EventEmitter();
355
+ if (!params.messages || !params.messages?.length) {
356
+ // Return an emitter with error/end events for validation errors
357
+ const errorEmitter = new EventEmitter();
358
+ const validationError = new Error('Input messages are required.');
295
359
  process.nextTick(() => {
296
- dummyEmitter.emit('error', error);
297
- dummyEmitter.emit('end');
360
+ errorEmitter.emit(TLLMEvent.Error, validationError);
361
+ errorEmitter.emit(TLLMEvent.End, [], [], TLLMFinishReason.Error);
298
362
  });
299
- return dummyEmitter;
363
+ return errorEmitter;
300
364
  }
365
+
366
+ const model = params.model || this._model;
367
+
368
+ // Connectors now always return emitters (they don't throw errors)
369
+ return await this._llmConnector.user(AccessCandidate.agent(agentId)).streamRequest({ ...params, model });
301
370
  }
302
371
 
303
372
  //@deprecated
@@ -112,6 +112,7 @@ export abstract class LLMConnector extends Connector {
112
112
  const response = await this.request({
113
113
  acRequest: candidate.readRequest,
114
114
  body: preparedParams.body,
115
+ abortSignal: preparedParams.abortSignal,
115
116
  context: {
116
117
  modelEntryName: preparedParams.modelEntryName,
117
118
  agentId: preparedParams.agentId,
@@ -137,6 +138,7 @@ export abstract class LLMConnector extends Connector {
137
138
  const requestParams = {
138
139
  acRequest: candidate.readRequest,
139
140
  body: preparedParams.body,
141
+ abortSignal: preparedParams.abortSignal,
140
142
  context: {
141
143
  modelEntryName: preparedParams.modelEntryName,
142
144
  agentId: preparedParams.agentId,
@@ -262,15 +264,18 @@ export abstract class LLMConnector extends Connector {
262
264
 
263
265
  private async prepareParams(candidate: AccessCandidate, params: TLLMConnectorParams): Promise<TLLMPreparedParams> {
264
266
  const modelsProvider: ModelsProviderConnector = ConnectorService.getModelsProviderConnector();
265
- // Assign file from the original parameters to avoid overwriting the original constructor
266
- const files = params?.files;
267
- delete params?.files; // need to remove files to avoid any issues during JSON.stringify() especially when we have large files
267
+ // Extract files and abortSignal from the original parameters to avoid overwriting the original constructor
268
+ const { files, abortSignal, ...restParams } = params;
268
269
 
269
- const clonedParams = JSON.parse(JSON.stringify(params)); // Avoid mutation of the original params
270
+ const clonedParams = JSON.parse(JSON.stringify(restParams)); // Avoid mutation of the original params
270
271
 
271
272
  // Format the parameters to ensure proper type of values
272
273
  const _params: TLLMPreparedParams = this.formatParamValues(clonedParams);
273
274
 
275
+ // Re-attach non-serializable properties ignored before cloning
276
+ _params.abortSignal = abortSignal;
277
+ _params.files = files;
278
+
274
279
  const model = _params.model;
275
280
  const teamId = await this.getTeamId(candidate);
276
281
 
@@ -307,8 +312,6 @@ export abstract class LLMConnector extends Connector {
307
312
  }
308
313
 
309
314
  _params.model = await modelProviderCandidate.getModelId(model);
310
- // Attach the files again after formatting the parameters
311
- _params.files = files;
312
315
 
313
316
  const features = modelInfo?.features || [];
314
317
 
@@ -327,6 +330,9 @@ export abstract class LLMConnector extends Connector {
327
330
  xai: await this.prepareXAIToolsInfo(_params),
328
331
  };
329
332
 
333
+ // Filter out default and system-specific outputs (e.g., _debug, _error) to isolate custom outputs for structured response
334
+ _params.structuredOutputs = _params?.outputs?.filter((output) => !output.default && !['_debug', '_error'].includes(output.name)) || [];
335
+
330
336
  // The input adapter transforms the standardized parameters into the specific format required by the target LLM provider
331
337
  _params.agentId = candidate.id;
332
338
  const body = await this.reqBodyAdapter(_params);
@@ -461,6 +467,7 @@ export abstract class LLMConnector extends Connector {
461
467
  }
462
468
 
463
469
  //FIXME: to revisit by Alaa-eddine
470
+ // TODO: This part is a bit confusing. We send “consistent” messages to the LLM, but they still aren’t truly consistent. For example, we send { role: 'system', content: 'You are a helpful assistant.' }, which isn’t compatible with Google AI. However, we still need to mark it as `system` because we later convert it to `systemInstruction`. We should revisit the architecture later and make the flow simpler and more straightforward.
464
471
  if (key === 'messages') {
465
472
  _value = this.getConsistentMessages(_value);
466
473
  }