@smythos/sre 1.7.42 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG +448 -66
- package/dist/index.js +65 -50
- package/dist/index.js.map +1 -1
- package/dist/types/Components/Async.class.d.ts +11 -5
- package/dist/types/index.d.ts +2 -0
- package/dist/types/subsystems/AgentManager/AgentData.service/connectors/SQLiteAgentDataConnector.class.d.ts +45 -0
- package/dist/types/subsystems/LLMManager/LLM.helper.d.ts +32 -1
- package/dist/types/subsystems/LLMManager/LLM.inference.d.ts +25 -2
- package/dist/types/subsystems/LLMManager/LLM.service/connectors/Anthropic.class.d.ts +22 -2
- package/dist/types/subsystems/LLMManager/LLM.service/connectors/Bedrock.class.d.ts +2 -2
- package/dist/types/subsystems/LLMManager/LLM.service/connectors/GoogleAI.class.d.ts +27 -2
- package/dist/types/subsystems/LLMManager/LLM.service/connectors/Groq.class.d.ts +22 -2
- package/dist/types/subsystems/LLMManager/LLM.service/connectors/Ollama.class.d.ts +22 -2
- package/dist/types/subsystems/LLMManager/LLM.service/connectors/Perplexity.class.d.ts +3 -3
- package/dist/types/subsystems/LLMManager/LLM.service/connectors/openai/OpenAIConnector.class.d.ts +23 -3
- package/dist/types/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/ChatCompletionsApiInterface.d.ts +2 -2
- package/dist/types/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/OpenAIApiInterface.d.ts +2 -2
- package/dist/types/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/ResponsesApiInterface.d.ts +2 -2
- package/dist/types/subsystems/LLMManager/LLM.service/connectors/xAI.class.d.ts +3 -3
- package/dist/types/subsystems/MemoryManager/LLMContext.d.ts +10 -3
- package/dist/types/subsystems/ObservabilityManager/Telemetry.service/connectors/OTel/OTel.class.d.ts +24 -0
- package/dist/types/subsystems/ObservabilityManager/Telemetry.service/connectors/OTel/OTel.redaction.helper.d.ts +49 -0
- package/dist/types/types/LLM.types.d.ts +30 -1
- package/package.json +4 -3
- package/src/Components/APICall/OAuth.helper.ts +16 -1
- package/src/Components/APIEndpoint.class.ts +11 -4
- package/src/Components/Async.class.ts +38 -5
- package/src/Components/GenAILLM.class.ts +13 -7
- package/src/Components/ImageGenerator.class.ts +32 -13
- package/src/Components/LLMAssistant.class.ts +3 -1
- package/src/Components/LogicAND.class.ts +13 -0
- package/src/Components/LogicAtLeast.class.ts +18 -0
- package/src/Components/LogicAtMost.class.ts +19 -0
- package/src/Components/LogicOR.class.ts +12 -2
- package/src/Components/LogicXOR.class.ts +11 -0
- package/src/constants.ts +1 -1
- package/src/helpers/Conversation.helper.ts +10 -8
- package/src/index.ts +2 -0
- package/src/index.ts.bak +2 -0
- package/src/subsystems/AgentManager/AgentData.service/connectors/SQLiteAgentDataConnector.class.ts +190 -0
- package/src/subsystems/AgentManager/AgentData.service/index.ts +2 -0
- package/src/subsystems/LLMManager/LLM.helper.ts +117 -1
- package/src/subsystems/LLMManager/LLM.inference.ts +136 -67
- package/src/subsystems/LLMManager/LLM.service/LLMConnector.ts +22 -6
- package/src/subsystems/LLMManager/LLM.service/connectors/Anthropic.class.ts +157 -33
- package/src/subsystems/LLMManager/LLM.service/connectors/Bedrock.class.ts +9 -8
- package/src/subsystems/LLMManager/LLM.service/connectors/GoogleAI.class.ts +124 -90
- package/src/subsystems/LLMManager/LLM.service/connectors/Groq.class.ts +125 -62
- package/src/subsystems/LLMManager/LLM.service/connectors/Ollama.class.ts +168 -76
- package/src/subsystems/LLMManager/LLM.service/connectors/Perplexity.class.ts +18 -8
- package/src/subsystems/LLMManager/LLM.service/connectors/VertexAI.class.ts +8 -4
- package/src/subsystems/LLMManager/LLM.service/connectors/openai/OpenAIConnector.class.ts +50 -8
- package/src/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/ChatCompletionsApiInterface.ts +30 -16
- package/src/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/OpenAIApiInterface.ts +2 -2
- package/src/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/ResponsesApiInterface.ts +29 -15
- package/src/subsystems/LLMManager/LLM.service/connectors/xAI.class.ts +10 -8
- package/src/subsystems/MemoryManager/LLMContext.ts +27 -8
- package/src/subsystems/ObservabilityManager/Telemetry.service/connectors/OTel/OTel.class.ts +313 -85
- package/src/subsystems/ObservabilityManager/Telemetry.service/connectors/OTel/OTel.redaction.helper.ts +203 -0
- package/src/types/LLM.types.ts +31 -1
- package/src/types/node-sqlite.d.ts +45 -0
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
ILLMRequestContext,
|
|
28
28
|
TLLMPreparedParams,
|
|
29
29
|
LLMInterface,
|
|
30
|
+
TLLMFinishReason,
|
|
30
31
|
} from '@sre/types/LLM.types';
|
|
31
32
|
import { LLMHelper } from '@sre/LLMManager/LLM.helper';
|
|
32
33
|
|
|
@@ -39,18 +40,6 @@ import { hookAsync } from '@sre/Core/HookService';
|
|
|
39
40
|
|
|
40
41
|
const logger = Logger('GoogleAIConnector');
|
|
41
42
|
|
|
42
|
-
const MODELS_SUPPORT_SYSTEM_INSTRUCTION = [
|
|
43
|
-
'gemini-1.5-pro-exp-0801',
|
|
44
|
-
'gemini-1.5-pro-latest',
|
|
45
|
-
'gemini-1.5-pro-latest',
|
|
46
|
-
'gemini-1.5-pro',
|
|
47
|
-
'gemini-1.5-pro-001',
|
|
48
|
-
'gemini-1.5-flash-latest',
|
|
49
|
-
'gemini-1.5-flash-001',
|
|
50
|
-
'gemini-1.5-flash',
|
|
51
|
-
];
|
|
52
|
-
const MODELS_SUPPORT_JSON_RESPONSE = MODELS_SUPPORT_SYSTEM_INSTRUCTION;
|
|
53
|
-
|
|
54
43
|
// Supported file MIME types for Google AI's Gemini models
|
|
55
44
|
const VALID_MIME_TYPES = [
|
|
56
45
|
...SUPPORTED_MIME_TYPES_MAP.GoogleAI.image,
|
|
@@ -84,7 +73,7 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
84
73
|
}
|
|
85
74
|
|
|
86
75
|
@hookAsync('LLMConnector.request')
|
|
87
|
-
protected async request({ acRequest, body, context }: ILLMRequestFuncParams): Promise<TLLMChatResponse> {
|
|
76
|
+
protected async request({ acRequest, body, context, abortSignal }: ILLMRequestFuncParams): Promise<TLLMChatResponse> {
|
|
88
77
|
try {
|
|
89
78
|
logger.debug(`request ${this.name}`, acRequest.candidate);
|
|
90
79
|
|
|
@@ -94,6 +83,7 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
94
83
|
generationConfig: body.generationConfig,
|
|
95
84
|
systemInstruction: body.systemInstruction,
|
|
96
85
|
promptConfig,
|
|
86
|
+
abortSignal,
|
|
97
87
|
});
|
|
98
88
|
|
|
99
89
|
const genAI = await this.getClient(context);
|
|
@@ -108,7 +98,7 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
108
98
|
|
|
109
99
|
const response = await genAI.models.generateContent(requestPayload as any);
|
|
110
100
|
const content = response.text ?? '';
|
|
111
|
-
const finishReason = (response.candidates?.[0]?.finishReason ||
|
|
101
|
+
const finishReason = LLMHelper.normalizeFinishReason(response.candidates?.[0]?.finishReason || TLLMFinishReason.Stop);
|
|
112
102
|
const usage = response.usageMetadata as UsageMetadataWithThoughtsToken | undefined;
|
|
113
103
|
|
|
114
104
|
if (usage) {
|
|
@@ -166,8 +156,28 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
166
156
|
}
|
|
167
157
|
}
|
|
168
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Stream request implementation.
|
|
161
|
+
*
|
|
162
|
+
* **Error Handling Pattern:**
|
|
163
|
+
* - Always returns emitters, never throws errors - ensures consistent error handling
|
|
164
|
+
* - Uses setImmediate for event emission - prevents race conditions where events fire before listeners attach
|
|
165
|
+
* - Emits End after terminal events (Error, Abort) - ensures cleanup code always runs
|
|
166
|
+
*
|
|
167
|
+
* **Why setImmediate?**
|
|
168
|
+
* Since streamRequest is async, callers must await to get the emitter, creating a timing gap.
|
|
169
|
+
* setImmediate defers event emission to the next event loop tick, ensuring events fire AFTER
|
|
170
|
+
* listeners are attached. This prevents race conditions where synchronous event emission
|
|
171
|
+
* would occur before listeners can be registered.
|
|
172
|
+
*
|
|
173
|
+
* @param acRequest - Access request for authorization
|
|
174
|
+
* @param body - Request body parameters
|
|
175
|
+
* @param context - LLM request context
|
|
176
|
+
* @param abortSignal - AbortSignal for cancellation
|
|
177
|
+
* @returns EventEmitter that emits TLLMEvent events (Data, Content, Error, Abort, End, etc.)
|
|
178
|
+
*/
|
|
169
179
|
@hookAsync('LLMConnector.streamRequest')
|
|
170
|
-
protected async streamRequest({ acRequest, body, context }: ILLMRequestFuncParams): Promise<EventEmitter> {
|
|
180
|
+
protected async streamRequest({ acRequest, body, context, abortSignal }: ILLMRequestFuncParams): Promise<EventEmitter> {
|
|
171
181
|
logger.debug(`streamRequest ${this.name}`, acRequest.candidate);
|
|
172
182
|
const emitter = new EventEmitter();
|
|
173
183
|
|
|
@@ -177,6 +187,7 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
177
187
|
generationConfig: body.generationConfig,
|
|
178
188
|
systemInstruction: body.systemInstruction,
|
|
179
189
|
promptConfig,
|
|
190
|
+
abortSignal,
|
|
180
191
|
});
|
|
181
192
|
|
|
182
193
|
const genAI = await this.getClient(context);
|
|
@@ -251,7 +262,7 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
251
262
|
emitter.emit(TLLMEvent.ToolInfo, toolsData);
|
|
252
263
|
}
|
|
253
264
|
|
|
254
|
-
const finishReason =
|
|
265
|
+
const finishReason: TLLMFinishReason = TLLMFinishReason.Stop; // GoogleAI doesn't provide finishReason in streaming
|
|
255
266
|
const reportedUsage: any[] = [];
|
|
256
267
|
|
|
257
268
|
if (usage) {
|
|
@@ -271,16 +282,47 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
271
282
|
emitter.emit(TLLMEvent.End, toolsData, reportedUsage, finishReason);
|
|
272
283
|
}, 100);
|
|
273
284
|
} catch (error) {
|
|
274
|
-
|
|
275
|
-
|
|
285
|
+
const isAbort = (error as any)?.name === 'AbortError' || abortSignal?.aborted;
|
|
286
|
+
if (isAbort) {
|
|
287
|
+
logger.debug(`streamRequest ${this.name} aborted`, error, acRequest.candidate);
|
|
288
|
+
// Always use DOMException with name 'AbortError' per Web API standards for consistency
|
|
289
|
+
const abortError = new DOMException('Request aborted', 'AbortError');
|
|
290
|
+
setImmediate(() => {
|
|
291
|
+
emitter.emit(TLLMEvent.Abort, abortError);
|
|
292
|
+
emitter.emit(TLLMEvent.End, [], [], TLLMFinishReason.Abort);
|
|
293
|
+
});
|
|
294
|
+
} else {
|
|
295
|
+
logger.error(`streamRequest ${this.name}`, error, acRequest.candidate);
|
|
296
|
+
setImmediate(() => {
|
|
297
|
+
emitter.emit(TLLMEvent.Error, error);
|
|
298
|
+
emitter.emit(TLLMEvent.End, [], [], TLLMFinishReason.Error);
|
|
299
|
+
});
|
|
300
|
+
}
|
|
276
301
|
}
|
|
277
302
|
})();
|
|
278
303
|
});
|
|
279
304
|
|
|
280
305
|
return emitter;
|
|
281
306
|
} catch (error: any) {
|
|
307
|
+
const isAbort = error?.name === 'AbortError' || abortSignal?.aborted;
|
|
308
|
+
|
|
309
|
+
if (isAbort) {
|
|
310
|
+
// Always use DOMException with name 'AbortError' per Web API standards for consistency
|
|
311
|
+
const abortError = new DOMException('Request aborted', 'AbortError');
|
|
312
|
+
logger.debug(`streamRequest ${this.name} aborted`, abortError, acRequest.candidate);
|
|
313
|
+
setImmediate(() => {
|
|
314
|
+
emitter.emit(TLLMEvent.Abort, abortError);
|
|
315
|
+
emitter.emit(TLLMEvent.End, [], [], TLLMFinishReason.Abort);
|
|
316
|
+
});
|
|
317
|
+
return emitter;
|
|
318
|
+
}
|
|
319
|
+
|
|
282
320
|
logger.error(`streamRequest ${this.name}`, error, acRequest.candidate);
|
|
283
|
-
|
|
321
|
+
setImmediate(() => {
|
|
322
|
+
emitter.emit(TLLMEvent.Error, error);
|
|
323
|
+
emitter.emit(TLLMEvent.End, [], [], TLLMFinishReason.Error);
|
|
324
|
+
});
|
|
325
|
+
return emitter;
|
|
284
326
|
}
|
|
285
327
|
}
|
|
286
328
|
// #region Image Generation, will be moved to a different subsystem/service
|
|
@@ -340,7 +382,7 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
340
382
|
|
|
341
383
|
if (imageData.length === 0) {
|
|
342
384
|
throw new Error(
|
|
343
|
-
'Please enter a valid prompt — for example: "Create a picture of a nano banana dish in a fancy restaurant with a Gemini theme."'
|
|
385
|
+
'Please enter a valid prompt — for example: "Create a picture of a nano banana dish in a fancy restaurant with a Gemini theme."',
|
|
344
386
|
);
|
|
345
387
|
}
|
|
346
388
|
|
|
@@ -452,6 +494,18 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
452
494
|
}
|
|
453
495
|
}
|
|
454
496
|
|
|
497
|
+
// Extract system messages before preparing messages
|
|
498
|
+
// All modern Gemini models (2.0+, 2.5, 3.0) support native system instruction
|
|
499
|
+
let systemInstruction = '';
|
|
500
|
+
const originalMessages = params?.messages || [];
|
|
501
|
+
|
|
502
|
+
if (LLMHelper.hasSystemMessage(originalMessages)) {
|
|
503
|
+
const { systemMessage, otherMessages } = LLMHelper.separateSystemMessages(originalMessages);
|
|
504
|
+
systemInstruction = this.extractMessageContent(systemMessage as TLLMMessageBlock);
|
|
505
|
+
// Pass only non-system messages to prepareMessages
|
|
506
|
+
params = { ...params, messages: otherMessages };
|
|
507
|
+
}
|
|
508
|
+
|
|
455
509
|
const messages = await this.prepareMessages(params);
|
|
456
510
|
|
|
457
511
|
const body: TGoogleAIRequestBody = {
|
|
@@ -461,14 +515,11 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
461
515
|
|
|
462
516
|
const responseFormat = params?.responseFormat || '';
|
|
463
517
|
let responseMimeType = '';
|
|
464
|
-
let systemInstruction = '';
|
|
465
518
|
|
|
466
519
|
if (responseFormat === 'json') {
|
|
467
520
|
systemInstruction += JSON_RESPONSE_INSTRUCTION;
|
|
468
521
|
|
|
469
|
-
|
|
470
|
-
responseMimeType = 'application/json';
|
|
471
|
-
}
|
|
522
|
+
responseMimeType = 'application/json';
|
|
472
523
|
}
|
|
473
524
|
|
|
474
525
|
const config: Record<string, any> = {};
|
|
@@ -528,10 +579,12 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
528
579
|
generationConfig,
|
|
529
580
|
systemInstruction,
|
|
530
581
|
promptConfig,
|
|
582
|
+
abortSignal,
|
|
531
583
|
}: {
|
|
532
584
|
generationConfig?: TGoogleAIRequestBody['generationConfig'];
|
|
533
585
|
systemInstruction?: TGoogleAIRequestBody['systemInstruction'];
|
|
534
586
|
promptConfig?: Record<string, any>;
|
|
587
|
+
abortSignal?: AbortSignal;
|
|
535
588
|
}): Record<string, any> | undefined {
|
|
536
589
|
const config: Record<string, any> = {};
|
|
537
590
|
|
|
@@ -557,12 +610,16 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
557
610
|
config.systemInstruction = systemInstruction;
|
|
558
611
|
}
|
|
559
612
|
|
|
613
|
+
if (abortSignal) {
|
|
614
|
+
config.abortSignal = abortSignal;
|
|
615
|
+
}
|
|
616
|
+
|
|
560
617
|
return Object.keys(config).length > 0 ? config : undefined;
|
|
561
618
|
}
|
|
562
619
|
|
|
563
620
|
protected reportUsage(
|
|
564
621
|
usage: UsageMetadataWithThoughtsToken,
|
|
565
|
-
metadata: { modelEntryName: string; keySource: APIKeySource; agentId: string; teamId: string }
|
|
622
|
+
metadata: { modelEntryName: string; keySource: APIKeySource; agentId: string; teamId: string },
|
|
566
623
|
) {
|
|
567
624
|
// SmythOS (built-in) models have a prefix, so we need to remove it to get the model name
|
|
568
625
|
const modelName = metadata.modelEntryName.replace(BUILT_IN_MODEL_PREFIX, '');
|
|
@@ -581,17 +638,13 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
581
638
|
}
|
|
582
639
|
|
|
583
640
|
// #region Find matching model and set tier based on threshold
|
|
584
|
-
const
|
|
585
|
-
|
|
586
|
-
'gemini-2.5-pro': 200_000,
|
|
587
|
-
'gemini-3-pro': 200_000,
|
|
588
|
-
};
|
|
641
|
+
const isProModel = modelName.includes('pro');
|
|
642
|
+
const tierThreshold = 200_000;
|
|
589
643
|
|
|
590
644
|
let tier = '';
|
|
591
645
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
tier = inputTokens <= tierThresholds[modelWithTier] ? 'tier1' : 'tier2';
|
|
646
|
+
if (isProModel) {
|
|
647
|
+
tier = inputTokens <= tierThreshold ? 'tier1' : 'tier2';
|
|
595
648
|
}
|
|
596
649
|
// #endregion
|
|
597
650
|
|
|
@@ -888,12 +941,16 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
888
941
|
let functionCallCount = 0; // Track function call parts for thoughtSignature handling
|
|
889
942
|
|
|
890
943
|
// Map roles to valid Google AI roles
|
|
944
|
+
// Note: System role is preserved so it can be extracted as systemInstruction later
|
|
891
945
|
switch (_message.role) {
|
|
892
946
|
case TLLMMessageRole.Assistant:
|
|
893
|
-
case TLLMMessageRole.System:
|
|
894
947
|
case TLLMMessageRole.Model:
|
|
895
948
|
_message.role = TLLMMessageRole.Model;
|
|
896
949
|
break;
|
|
950
|
+
case TLLMMessageRole.System:
|
|
951
|
+
// Keep system role as-is for later extraction to systemInstruction
|
|
952
|
+
_message.role = TLLMMessageRole.System;
|
|
953
|
+
break;
|
|
897
954
|
case TLLMMessageRole.Function:
|
|
898
955
|
case TLLMMessageRole.Tool:
|
|
899
956
|
_message.role = TLLMMessageRole.Function;
|
|
@@ -1030,6 +1087,31 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
1030
1087
|
});
|
|
1031
1088
|
}
|
|
1032
1089
|
|
|
1090
|
+
/**
|
|
1091
|
+
* Extracts text content from a message block, handling multiple formats (.parts, .content as string/array)
|
|
1092
|
+
* This ensures compatibility with messages that have been normalized by getConsistentMessages or come in various formats
|
|
1093
|
+
*/
|
|
1094
|
+
private extractMessageContent(message: TLLMMessageBlock | any): string {
|
|
1095
|
+
if (!message) return '';
|
|
1096
|
+
|
|
1097
|
+
// Handle .parts array format (Google AI native format)
|
|
1098
|
+
if (message.parts && Array.isArray(message.parts)) {
|
|
1099
|
+
return message.parts.map((part) => part?.text || '').join(' ');
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// Handle .content as string
|
|
1103
|
+
if (typeof message.content === 'string') {
|
|
1104
|
+
return message.content;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// Handle .content as array
|
|
1108
|
+
if (Array.isArray(message.content)) {
|
|
1109
|
+
return message.content.map((part) => (typeof part === 'string' ? part : part?.text || '')).join(' ');
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
return '';
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1033
1115
|
private async prepareMessages(params: TLLMPreparedParams): Promise<string | TLLMMessageBlock[] | TGoogleAIToolPrompt> {
|
|
1034
1116
|
let messages: string | TLLMMessageBlock[] | TGoogleAIToolPrompt = (params?.messages as any) || '';
|
|
1035
1117
|
|
|
@@ -1050,7 +1132,6 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
1050
1132
|
const model = params.model;
|
|
1051
1133
|
|
|
1052
1134
|
let messages: string | TLLMMessageBlock[] = params?.messages || '';
|
|
1053
|
-
let systemInstruction = '';
|
|
1054
1135
|
const files: BinaryInput[] = params?.files || [];
|
|
1055
1136
|
|
|
1056
1137
|
// #region Upload files
|
|
@@ -1101,12 +1182,7 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
1101
1182
|
const fileData = this.getFileData(uploadedFiles);
|
|
1102
1183
|
|
|
1103
1184
|
const userMessage: TLLMMessageBlock = Array.isArray(messages) ? messages.pop() : { role: TLLMMessageRole.User, content: '' };
|
|
1104
|
-
let prompt = userMessage
|
|
1105
|
-
|
|
1106
|
-
// if the the model does not support system instruction, we will add it to the prompt
|
|
1107
|
-
if (!MODELS_SUPPORT_SYSTEM_INSTRUCTION.includes(model as string)) {
|
|
1108
|
-
prompt = `${prompt}\n${systemInstruction}`;
|
|
1109
|
-
}
|
|
1185
|
+
let prompt = this.extractMessageContent(userMessage);
|
|
1110
1186
|
//#endregion Separate system message and add JSON response instruction if needed
|
|
1111
1187
|
|
|
1112
1188
|
// Adjust input structure handling for multiple image files to accommodate variations.
|
|
@@ -1116,30 +1192,12 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
1116
1192
|
}
|
|
1117
1193
|
|
|
1118
1194
|
private async prepareMessagesWithTools(params: TLLMPreparedParams): Promise<TGoogleAIToolPrompt> {
|
|
1119
|
-
|
|
1120
|
-
let systemInstruction = '';
|
|
1121
|
-
|
|
1122
|
-
let messages = params?.messages || [];
|
|
1123
|
-
|
|
1124
|
-
const hasSystemMessage = LLMHelper.hasSystemMessage(messages);
|
|
1125
|
-
|
|
1126
|
-
if (hasSystemMessage) {
|
|
1127
|
-
const separateMessages = LLMHelper.separateSystemMessages(messages);
|
|
1128
|
-
const systemMessageContent = (separateMessages.systemMessage as TLLMMessageBlock)?.content;
|
|
1129
|
-
systemInstruction = typeof systemMessageContent === 'string' ? systemMessageContent : '';
|
|
1130
|
-
formattedMessages = separateMessages.otherMessages;
|
|
1131
|
-
} else {
|
|
1132
|
-
formattedMessages = messages;
|
|
1133
|
-
}
|
|
1195
|
+
const messages = params?.messages || [];
|
|
1134
1196
|
|
|
1135
1197
|
const toolsPrompt: TGoogleAIToolPrompt = {
|
|
1136
|
-
contents:
|
|
1198
|
+
contents: messages as any,
|
|
1137
1199
|
};
|
|
1138
1200
|
|
|
1139
|
-
if (systemInstruction) {
|
|
1140
|
-
toolsPrompt.systemInstruction = systemInstruction;
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
1201
|
if (params?.toolsConfig?.tools) toolsPrompt.tools = params?.toolsConfig?.tools as any;
|
|
1144
1202
|
if (params?.toolsConfig?.tool_choice) {
|
|
1145
1203
|
// Map tool choice to valid Google AI function calling modes
|
|
@@ -1172,37 +1230,13 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
1172
1230
|
}
|
|
1173
1231
|
|
|
1174
1232
|
private async prepareMessagesWithTextQuery(params: TLLMPreparedParams): Promise<string> {
|
|
1175
|
-
const
|
|
1176
|
-
let systemInstruction = '';
|
|
1233
|
+
const messages = (params?.messages as TLLMMessageBlock[]) || [];
|
|
1177
1234
|
let prompt = '';
|
|
1178
1235
|
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
systemInstruction = systemMessage.content as string;
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
const responseFormat = params?.responseFormat || '';
|
|
1186
|
-
let responseMimeType = '';
|
|
1187
|
-
|
|
1188
|
-
if (responseFormat === 'json') {
|
|
1189
|
-
systemInstruction += JSON_RESPONSE_INSTRUCTION;
|
|
1190
|
-
|
|
1191
|
-
if (MODELS_SUPPORT_JSON_RESPONSE.includes(model as string)) {
|
|
1192
|
-
responseMimeType = 'application/json';
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
if (otherMessages?.length > 0) {
|
|
1197
|
-
// Concatenate messages with prompt and remove messages from params as it's not supported
|
|
1198
|
-
prompt += otherMessages.map((message) => message?.parts?.[0]?.text || '').join('\n');
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
// if the the model does not support system instruction, we will add it to the prompt
|
|
1202
|
-
if (!MODELS_SUPPORT_SYSTEM_INSTRUCTION.includes(model as string)) {
|
|
1203
|
-
prompt = `${prompt}\n${systemInstruction}`;
|
|
1236
|
+
if (messages?.length > 0) {
|
|
1237
|
+
// Concatenate messages using the helper method
|
|
1238
|
+
prompt = messages.map((message) => this.extractMessageContent(message)).join('\n');
|
|
1204
1239
|
}
|
|
1205
|
-
//#endregion Separate system message and add JSON response instruction if needed
|
|
1206
1240
|
|
|
1207
1241
|
return prompt;
|
|
1208
1242
|
}
|
|
@@ -1370,7 +1404,7 @@ export class GoogleAIConnector extends LLMConnector {
|
|
|
1370
1404
|
files: {
|
|
1371
1405
|
url: string;
|
|
1372
1406
|
mimetype: string;
|
|
1373
|
-
}[]
|
|
1407
|
+
}[],
|
|
1374
1408
|
): {
|
|
1375
1409
|
fileData: {
|
|
1376
1410
|
mimeType: string;
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
ILLMRequestContext,
|
|
15
15
|
TLLMPreparedParams,
|
|
16
16
|
TLLMToolResultMessageBlock,
|
|
17
|
+
TLLMFinishReason,
|
|
17
18
|
} from '@sre/types/LLM.types';
|
|
18
19
|
import { LLMHelper } from '@sre/LLMManager/LLM.helper';
|
|
19
20
|
|
|
@@ -52,13 +53,13 @@ export class GroqConnector extends LLMConnector {
|
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
@hookAsync('LLMConnector.request')
|
|
55
|
-
protected async request({ acRequest, body, context }: ILLMRequestFuncParams): Promise<TLLMChatResponse> {
|
|
56
|
+
protected async request({ acRequest, body, context, abortSignal }: ILLMRequestFuncParams): Promise<TLLMChatResponse> {
|
|
56
57
|
try {
|
|
57
58
|
logger.debug(`request ${this.name}`, acRequest.candidate);
|
|
58
59
|
const groq = await this.getClient(context);
|
|
59
|
-
const result = await groq.chat.completions.create(body);
|
|
60
|
+
const result = await groq.chat.completions.create(body, { signal: abortSignal });
|
|
60
61
|
const message = result?.choices?.[0]?.message;
|
|
61
|
-
const finishReason = result?.choices?.[0]?.finish_reason;
|
|
62
|
+
const finishReason = LLMHelper.normalizeFinishReason(result?.choices?.[0]?.finish_reason);
|
|
62
63
|
const toolCalls = message?.tool_calls;
|
|
63
64
|
const usage = result.usage;
|
|
64
65
|
this.reportUsage(usage, {
|
|
@@ -97,85 +98,147 @@ export class GroqConnector extends LLMConnector {
|
|
|
97
98
|
}
|
|
98
99
|
}
|
|
99
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Stream request implementation.
|
|
103
|
+
*
|
|
104
|
+
* **Error Handling Pattern:**
|
|
105
|
+
* - Always returns emitters, never throws errors - ensures consistent error handling
|
|
106
|
+
* - Uses setImmediate for event emission - prevents race conditions where events fire before listeners attach
|
|
107
|
+
* - Emits End after terminal events (Error, Abort) - ensures cleanup code always runs
|
|
108
|
+
*
|
|
109
|
+
* **Why setImmediate?**
|
|
110
|
+
* Since streamRequest is async, callers must await to get the emitter, creating a timing gap.
|
|
111
|
+
* setImmediate defers event emission to the next event loop tick, ensuring events fire AFTER
|
|
112
|
+
* listeners are attached. This prevents race conditions where synchronous event emission
|
|
113
|
+
* would occur before listeners can be registered.
|
|
114
|
+
*
|
|
115
|
+
* @param acRequest - Access request for authorization
|
|
116
|
+
* @param body - Request body parameters
|
|
117
|
+
* @param context - LLM request context
|
|
118
|
+
* @param abortSignal - AbortSignal for cancellation
|
|
119
|
+
* @returns EventEmitter that emits TLLMEvent events (Data, Content, Error, Abort, End, etc.)
|
|
120
|
+
*/
|
|
100
121
|
@hookAsync('LLMConnector.streamRequest')
|
|
101
|
-
protected async streamRequest({ acRequest, body, context }: ILLMRequestFuncParams): Promise<EventEmitter> {
|
|
122
|
+
protected async streamRequest({ acRequest, body, context, abortSignal }: ILLMRequestFuncParams): Promise<EventEmitter> {
|
|
123
|
+
const emitter = new EventEmitter();
|
|
124
|
+
|
|
102
125
|
try {
|
|
103
126
|
logger.debug(`streamRequest ${this.name}`, acRequest.candidate);
|
|
104
|
-
const emitter = new EventEmitter();
|
|
105
127
|
const usage_data = [];
|
|
106
128
|
|
|
107
129
|
const groq = await this.getClient(context);
|
|
108
|
-
const stream = await groq.chat.completions.create(
|
|
130
|
+
const stream = await groq.chat.completions.create(
|
|
131
|
+
{ ...body, stream: true, stream_options: { include_usage: true } },
|
|
132
|
+
{ signal: abortSignal }
|
|
133
|
+
);
|
|
109
134
|
|
|
110
135
|
let toolsData: ToolData[] = [];
|
|
111
|
-
let finishReason =
|
|
136
|
+
let finishReason: TLLMFinishReason = TLLMFinishReason.Stop;
|
|
112
137
|
|
|
113
|
-
(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
138
|
+
setImmediate(() => {
|
|
139
|
+
(async () => {
|
|
140
|
+
try {
|
|
141
|
+
for await (const chunk of stream as any) {
|
|
142
|
+
const delta = chunk.choices[0]?.delta;
|
|
143
|
+
const usage = chunk['x_groq']?.usage || chunk['usage'];
|
|
117
144
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
145
|
+
if (usage) {
|
|
146
|
+
usage_data.push(usage);
|
|
147
|
+
}
|
|
148
|
+
emitter.emit(TLLMEvent.Data, delta);
|
|
122
149
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
150
|
+
if (delta?.content) {
|
|
151
|
+
emitter.emit(TLLMEvent.Content, delta.content);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (delta?.tool_calls) {
|
|
155
|
+
delta.tool_calls.forEach((toolCall, index) => {
|
|
156
|
+
if (!toolsData[index]) {
|
|
157
|
+
toolsData[index] = {
|
|
158
|
+
index,
|
|
159
|
+
id: toolCall.id,
|
|
160
|
+
type: toolCall.type,
|
|
161
|
+
name: toolCall.function?.name,
|
|
162
|
+
arguments: toolCall.function?.arguments,
|
|
163
|
+
role: 'assistant',
|
|
164
|
+
};
|
|
165
|
+
} else {
|
|
166
|
+
toolsData[index].arguments += toolCall.function?.arguments || '';
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
126
170
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
toolsData[index] = {
|
|
131
|
-
index,
|
|
132
|
-
id: toolCall.id,
|
|
133
|
-
type: toolCall.type,
|
|
134
|
-
name: toolCall.function?.name,
|
|
135
|
-
arguments: toolCall.function?.arguments,
|
|
136
|
-
role: 'assistant',
|
|
137
|
-
};
|
|
138
|
-
} else {
|
|
139
|
-
toolsData[index].arguments += toolCall.function?.arguments || '';
|
|
171
|
+
// Capture finish reason
|
|
172
|
+
if (chunk.choices[0]?.finish_reason) {
|
|
173
|
+
finishReason = LLMHelper.normalizeFinishReason(chunk.choices[0].finish_reason);
|
|
140
174
|
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (toolsData.length > 0) {
|
|
178
|
+
emitter.emit(TLLMEvent.ToolInfo, toolsData);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const reportedUsage: any[] = [];
|
|
182
|
+
usage_data.forEach((usage) => {
|
|
183
|
+
const reported = this.reportUsage(usage, {
|
|
184
|
+
modelEntryName: context.modelEntryName,
|
|
185
|
+
keySource: context.isUserKey ? APIKeySource.User : APIKeySource.Smyth,
|
|
186
|
+
agentId: context.agentId,
|
|
187
|
+
teamId: context.teamId,
|
|
188
|
+
});
|
|
189
|
+
reportedUsage.push(reported);
|
|
141
190
|
});
|
|
142
|
-
}
|
|
143
191
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
192
|
+
// Emit interrupted event if finishReason is not 'stop'
|
|
193
|
+
if (finishReason !== TLLMFinishReason.Stop) {
|
|
194
|
+
emitter.emit(TLLMEvent.Interrupted, finishReason);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
setTimeout(() => {
|
|
198
|
+
emitter.emit(TLLMEvent.End, toolsData, reportedUsage, finishReason);
|
|
199
|
+
}, 100);
|
|
200
|
+
} catch (error: any) {
|
|
201
|
+
const isAbort = error?.name === 'AbortError' || abortSignal?.aborted;
|
|
202
|
+
if (isAbort) {
|
|
203
|
+
logger.debug(`streamRequest ${this.name} aborted`, error, acRequest.candidate);
|
|
204
|
+
// Always use DOMException with name 'AbortError' per Web API standards for consistency
|
|
205
|
+
const abortError = new DOMException('Request aborted', 'AbortError');
|
|
206
|
+
emitter.emit(TLLMEvent.Abort, abortError);
|
|
207
|
+
setImmediate(() => {
|
|
208
|
+
emitter.emit(TLLMEvent.End, [], [], TLLMFinishReason.Abort);
|
|
209
|
+
});
|
|
210
|
+
} else {
|
|
211
|
+
logger.error(`streamRequest ${this.name}`, error, acRequest.candidate);
|
|
212
|
+
emitter.emit(TLLMEvent.Error, error);
|
|
213
|
+
setImmediate(() => {
|
|
214
|
+
emitter.emit(TLLMEvent.End, [], [], TLLMFinishReason.Error);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
147
217
|
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (toolsData.length > 0) {
|
|
151
|
-
emitter.emit(TLLMEvent.ToolInfo, toolsData);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const reportedUsage: any[] = [];
|
|
155
|
-
usage_data.forEach((usage) => {
|
|
156
|
-
const reported = this.reportUsage(usage, {
|
|
157
|
-
modelEntryName: context.modelEntryName,
|
|
158
|
-
keySource: context.isUserKey ? APIKeySource.User : APIKeySource.Smyth,
|
|
159
|
-
agentId: context.agentId,
|
|
160
|
-
teamId: context.teamId,
|
|
161
|
-
});
|
|
162
|
-
reportedUsage.push(reported);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
// Emit interrupted event if finishReason is not 'stop'
|
|
166
|
-
if (finishReason !== 'stop') {
|
|
167
|
-
emitter.emit(TLLMEvent.Interrupted, finishReason);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
setTimeout(() => {
|
|
171
|
-
emitter.emit(TLLMEvent.End, toolsData, reportedUsage, finishReason);
|
|
172
|
-
}, 100);
|
|
173
|
-
})();
|
|
218
|
+
})();
|
|
219
|
+
});
|
|
174
220
|
|
|
175
221
|
return emitter;
|
|
176
222
|
} catch (error: any) {
|
|
223
|
+
const isAbort = error?.name === 'AbortError' || abortSignal?.aborted;
|
|
224
|
+
|
|
225
|
+
if (isAbort) {
|
|
226
|
+
// Always use DOMException with name 'AbortError' per Web API standards for consistency
|
|
227
|
+
const abortError = new DOMException('Request aborted', 'AbortError');
|
|
228
|
+
logger.debug(`streamRequest ${this.name} aborted`, abortError, acRequest.candidate);
|
|
229
|
+
setImmediate(() => {
|
|
230
|
+
emitter.emit(TLLMEvent.Abort, abortError);
|
|
231
|
+
emitter.emit(TLLMEvent.End, [], [], TLLMFinishReason.Abort);
|
|
232
|
+
});
|
|
233
|
+
return emitter;
|
|
234
|
+
}
|
|
235
|
+
|
|
177
236
|
logger.error(`streamRequest ${this.name}`, error, acRequest.candidate);
|
|
178
|
-
|
|
237
|
+
setImmediate(() => {
|
|
238
|
+
emitter.emit(TLLMEvent.Error, error);
|
|
239
|
+
emitter.emit(TLLMEvent.End, [], [], TLLMFinishReason.Error);
|
|
240
|
+
});
|
|
241
|
+
return emitter;
|
|
179
242
|
}
|
|
180
243
|
}
|
|
181
244
|
|