@smythos/sre 1.5.26 → 1.5.31

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.
@@ -0,0 +1,476 @@
1
+ import EventEmitter from 'events';
2
+ import axios, { AxiosInstance } from 'axios';
3
+
4
+ import { JSON_RESPONSE_INSTRUCTION, BUILT_IN_MODEL_PREFIX } from '@sre/constants';
5
+
6
+ import {
7
+ TLLMParams,
8
+ ToolData,
9
+ TLLMMessageBlock,
10
+ TLLMToolResultMessageBlock,
11
+ TLLMMessageRole,
12
+ APIKeySource,
13
+ BasicCredentials,
14
+ ILLMRequestFuncParams,
15
+ TLLMChatResponse,
16
+ ILLMRequestContext,
17
+ } from '@sre/types/LLM.types';
18
+ import { LLMHelper } from '@sre/LLMManager/LLM.helper';
19
+
20
+ import { LLMConnector } from '../LLMConnector';
21
+ import { SystemEvents } from '@sre/Core/SystemEvents';
22
+
23
+ type ChatCompletionParams = {
24
+ model: string;
25
+ messages: any[];
26
+ max_tokens?: number;
27
+ temperature?: number;
28
+ top_p?: number;
29
+ frequency_penalty?: number;
30
+ presence_penalty?: number;
31
+ stop?: string | string[];
32
+ response_format?: { type: string };
33
+ stream?: boolean;
34
+ stream_options?: { include_usage: boolean };
35
+ tools?: any[];
36
+ tool_choice?: any;
37
+ // xAI search parameters - nested structure
38
+ search_parameters?: {
39
+ mode?: 'auto' | 'on' | 'off';
40
+ return_citations?: boolean;
41
+ max_search_results?: number;
42
+ sources?: Array<{
43
+ type: 'web' | 'x' | 'news' | 'rss';
44
+ country?: string;
45
+ excluded_websites?: string[];
46
+ allowed_websites?: string[];
47
+ safe_search?: boolean;
48
+ included_x_handles?: string[];
49
+ excluded_x_handles?: string[];
50
+ post_favorite_count?: number;
51
+ post_view_count?: number;
52
+ links?: string[];
53
+ }>;
54
+ from_date?: string;
55
+ to_date?: string;
56
+ };
57
+ };
58
+
59
+ type TUsage = {
60
+ prompt_tokens: number;
61
+ completion_tokens: number;
62
+ total_tokens: number;
63
+ prompt_tokens_details?: {
64
+ text_tokens?: number;
65
+ audio_tokens?: number;
66
+ image_tokens?: number;
67
+ cached_tokens?: number;
68
+ };
69
+ completion_tokens_details?: {
70
+ reasoning_tokens?: number;
71
+ audio_tokens?: number;
72
+ accepted_prediction_tokens?: number;
73
+ rejected_prediction_tokens?: number;
74
+ };
75
+ reasoning_tokens?: number; // for backward compatibility
76
+ num_sources_used?: number;
77
+ };
78
+
79
+ export class xAIConnector extends LLMConnector {
80
+ public name = 'LLM:Grok';
81
+
82
+ private async getClient(params: ILLMRequestContext): Promise<AxiosInstance> {
83
+ const apiKey = (params.credentials as BasicCredentials)?.apiKey;
84
+ const baseURL = params?.modelInfo?.baseURL || 'https://api.x.ai/v1';
85
+
86
+ if (!apiKey) throw new Error('Please provide an API key for Grok');
87
+
88
+ return axios.create({
89
+ baseURL,
90
+ headers: {
91
+ Authorization: `Bearer ${apiKey}`,
92
+ 'Content-Type': 'application/json',
93
+ },
94
+ });
95
+ }
96
+
97
+ protected async request({ acRequest, body, context }: ILLMRequestFuncParams): Promise<TLLMChatResponse> {
98
+ try {
99
+ const grok = await this.getClient(context);
100
+ const response = await grok.post('/chat/completions', body);
101
+
102
+ const message = response?.data?.choices?.[0]?.message;
103
+ const finishReason = response?.data?.choices?.[0]?.finish_reason;
104
+ const usage = response?.data?.usage as TUsage;
105
+ const citations = response?.data?.citations;
106
+
107
+ let toolsData: ToolData[] = [];
108
+ let useTool = false;
109
+
110
+ if (finishReason === 'tool_calls') {
111
+ toolsData =
112
+ message?.tool_calls?.map((tool, index) => ({
113
+ index,
114
+ id: tool?.id,
115
+ type: tool?.type,
116
+ name: tool?.function?.name,
117
+ arguments: tool?.function?.arguments,
118
+ role: 'tool',
119
+ })) || [];
120
+
121
+ useTool = true;
122
+ }
123
+
124
+ // Handle citations from live search
125
+ let content = message?.content ?? '';
126
+ if (citations && citations.length > 0) {
127
+ const citationsText = '\n\n**Sources:**\n' + citations.map((url, index) => `${index + 1}. ${url}`).join('\n');
128
+ content += citationsText;
129
+ }
130
+
131
+ this.reportUsage(usage, {
132
+ modelEntryName: context.modelEntryName,
133
+ keySource: context.isUserKey ? APIKeySource.User : APIKeySource.Smyth,
134
+ agentId: context.agentId,
135
+ teamId: context.teamId,
136
+ });
137
+
138
+ return {
139
+ content,
140
+ finishReason,
141
+ useTool,
142
+ toolsData,
143
+ message,
144
+ usage,
145
+ };
146
+ } catch (error) {
147
+ throw error;
148
+ }
149
+ }
150
+
151
+ protected async streamRequest({ acRequest, body, context }: ILLMRequestFuncParams): Promise<EventEmitter> {
152
+ const emitter = new EventEmitter();
153
+
154
+ try {
155
+ const grok = await this.getClient(context);
156
+ const response = await grok.post(
157
+ '/chat/completions',
158
+ { ...body, stream: true, stream_options: { include_usage: true } },
159
+ {
160
+ responseType: 'stream',
161
+ }
162
+ );
163
+
164
+ const reportedUsage: any[] = [];
165
+ let finishReason = 'stop';
166
+ let toolsData: any[] = [];
167
+ let usage: any = {};
168
+ let citations: any[] = [];
169
+
170
+ response.data.on('data', (chunk) => {
171
+ const lines = chunk.toString().split('\n');
172
+
173
+ for (const line of lines) {
174
+ if (line.startsWith('data: ')) {
175
+ const data = line.slice(6);
176
+ if (data === '[DONE]') {
177
+ // Handle stream completion
178
+ continue;
179
+ }
180
+
181
+ try {
182
+ const parsed = JSON.parse(data);
183
+ const delta = parsed.choices?.[0]?.delta;
184
+
185
+ // Usage data comes in final chunk when stream_options.include_usage is true
186
+ if (parsed?.usage) {
187
+ usage = parsed.usage;
188
+ }
189
+
190
+ // Handle citations from xAI - they come at the top level as an array of URLs
191
+ if (parsed.citations) {
192
+ citations = parsed.citations;
193
+ }
194
+
195
+ if (delta) {
196
+ emitter.emit('data', delta);
197
+
198
+ if (delta.content) {
199
+ emitter.emit('content', delta.content, delta.role);
200
+ }
201
+
202
+ if (delta.tool_calls) {
203
+ const toolCall = delta.tool_calls[0];
204
+ const index = toolCall?.index;
205
+
206
+ toolsData[index] = {
207
+ index,
208
+ role: 'tool',
209
+ id: (toolsData?.[index]?.id || '') + (toolCall?.id || ''),
210
+ type: (toolsData?.[index]?.type || '') + (toolCall?.type || ''),
211
+ name: (toolsData?.[index]?.name || '') + (toolCall?.function?.name || ''),
212
+ arguments: (toolsData?.[index]?.arguments || '') + (toolCall?.function?.arguments || ''),
213
+ };
214
+ }
215
+ }
216
+
217
+ if (parsed.choices?.[0]?.finish_reason) {
218
+ finishReason = parsed.choices[0].finish_reason;
219
+ }
220
+ } catch (e) {
221
+ // Ignore parsing errors for incomplete chunks
222
+ }
223
+ }
224
+ }
225
+ });
226
+
227
+ response.data.on('end', () => {
228
+ // Include citations in content if available
229
+ if (citations && citations.length > 0) {
230
+ const citationsText = '\n\n**Sources:**\n' + citations.map((url, index) => `${index + 1}. ${url}`).join('\n');
231
+
232
+ emitter.emit('content', citationsText, 'assistant');
233
+ }
234
+
235
+ if (toolsData.length > 0) {
236
+ emitter.emit('toolInfo', toolsData);
237
+ }
238
+
239
+ // Report usage if available
240
+ if (Object.keys(usage).length > 0) {
241
+ const _reported = this.reportUsage(usage, {
242
+ modelEntryName: context.modelEntryName,
243
+ keySource: context.isUserKey ? APIKeySource.User : APIKeySource.Smyth,
244
+ agentId: context.agentId,
245
+ teamId: context.teamId,
246
+ });
247
+ reportedUsage.push(_reported);
248
+ }
249
+
250
+ if (finishReason !== 'stop') {
251
+ emitter.emit('interrupted', finishReason);
252
+ }
253
+
254
+ setTimeout(() => {
255
+ emitter.emit('end', toolsData, reportedUsage, finishReason);
256
+ }, 100);
257
+ });
258
+
259
+ response.data.on('error', (error) => {
260
+ emitter.emit('error', error);
261
+ });
262
+ } catch (error) {
263
+ emitter.emit('error', error);
264
+ }
265
+
266
+ return emitter;
267
+ }
268
+
269
+ // TODO: will be removed when we merge with interface support of OpenAI
270
+ protected async webSearchRequest({ acRequest, body, context }: ILLMRequestFuncParams): Promise<EventEmitter> {
271
+ throw new Error('Not implemented');
272
+ }
273
+
274
+ protected async reqBodyAdapter(params: TLLMParams): Promise<ChatCompletionParams> {
275
+ const messages = params?.messages || [];
276
+ const modelName = params.model as string;
277
+
278
+ // Handle JSON response format
279
+ const responseFormat = params?.responseFormat || '';
280
+ if (responseFormat === 'json') {
281
+ if (messages?.[0]?.role === TLLMMessageRole.System) {
282
+ messages[0].content += JSON_RESPONSE_INSTRUCTION;
283
+ } else {
284
+ messages.unshift({ role: TLLMMessageRole.System, content: JSON_RESPONSE_INSTRUCTION });
285
+ }
286
+
287
+ params.responseFormat = { type: 'json_object' };
288
+ }
289
+
290
+ const body: ChatCompletionParams = {
291
+ model: modelName,
292
+ messages,
293
+ };
294
+
295
+ // Add parameters if they're not undefined and not 0 (for reasoning models)
296
+ if (params?.maxTokens !== undefined) body.max_tokens = params.maxTokens;
297
+ if (params?.temperature !== undefined) body.temperature = params.temperature;
298
+ if (params?.topP !== undefined) body.top_p = params.topP;
299
+
300
+ if (params?.responseFormat) {
301
+ body.response_format = params.responseFormat;
302
+ }
303
+
304
+ // Add tools configuration if available
305
+ if (params?.toolsConfig?.tools && params?.toolsConfig?.tools?.length > 0) {
306
+ body.tools = params.toolsConfig.tools;
307
+ }
308
+
309
+ if (params?.toolsConfig?.tool_choice) {
310
+ body.tool_choice = params.toolsConfig.tool_choice;
311
+ }
312
+
313
+ // Add xAI search configuration if useSearch is enabled
314
+ if (params?.useSearch) {
315
+ body.search_parameters = {};
316
+
317
+ // Basic search parameters
318
+ if (params.searchMode) body.search_parameters.mode = params.searchMode;
319
+ if (params.returnCitations !== undefined) body.search_parameters.return_citations = params.returnCitations;
320
+ if (params.maxSearchResults !== undefined) body.search_parameters.max_search_results = params.maxSearchResults;
321
+
322
+ // Date filtering
323
+ if (params.fromDate) body.search_parameters.from_date = params.fromDate;
324
+ if (params.toDate) body.search_parameters.to_date = params.toDate;
325
+
326
+ // Create sources array
327
+ const sources: any[] = [];
328
+
329
+ // If searchDataSources is provided, use it as source types
330
+ if (params.searchDataSources && params.searchDataSources.length > 0) {
331
+ params.searchDataSources.forEach((sourceType) => {
332
+ const source: any = { type: sourceType };
333
+
334
+ // Add parameters based on source type
335
+ if (sourceType === 'web' || sourceType === 'news') {
336
+ if (params.searchCountry) source.country = params.searchCountry;
337
+
338
+ // Website filtering (mutually exclusive)
339
+ if (params.excludedWebsites && params.excludedWebsites.length > 0) {
340
+ source.excluded_websites = params.excludedWebsites;
341
+ } else if (params.allowedWebsites && params.allowedWebsites.length > 0) {
342
+ source.allowed_websites = params.allowedWebsites;
343
+ }
344
+
345
+ if (params.safeSearch !== undefined) source.safe_search = params.safeSearch;
346
+ }
347
+
348
+ if (sourceType === 'x') {
349
+ if (params.includedXHandles && params.includedXHandles.length > 0) {
350
+ source.included_x_handles = params.includedXHandles;
351
+ } else if (params.excludedXHandles && params.excludedXHandles.length > 0) {
352
+ source.excluded_x_handles = params.excludedXHandles;
353
+ }
354
+ if (params.postFavoriteCount !== undefined && params.postFavoriteCount > 0) {
355
+ source.post_favorite_count = params.postFavoriteCount;
356
+ }
357
+ if (params.postViewCount !== undefined && params.postViewCount > 0) {
358
+ source.post_view_count = params.postViewCount;
359
+ }
360
+ }
361
+
362
+ if (sourceType === 'rss') {
363
+ if (params.rssLinks) source.links = params.rssLinks;
364
+ }
365
+
366
+ sources.push(source);
367
+ });
368
+ }
369
+
370
+ if (sources.length > 0) {
371
+ body.search_parameters.sources = sources;
372
+ }
373
+ }
374
+
375
+ return body;
376
+ }
377
+
378
+ protected reportUsage(usage: TUsage, metadata: { modelEntryName: string; keySource: APIKeySource; agentId: string; teamId: string }) {
379
+ // SmythOS (built-in) models have a prefix, so we need to remove it to get the model name
380
+ const modelName = metadata.modelEntryName.replace(BUILT_IN_MODEL_PREFIX, '');
381
+
382
+ const usageData = {
383
+ sourceId: `llm:${modelName}`,
384
+ input_tokens: usage?.prompt_tokens - (usage?.prompt_tokens_details?.cached_tokens || 0),
385
+ output_tokens: usage?.completion_tokens,
386
+ input_tokens_cache_write: 0,
387
+ input_tokens_cache_read: usage?.prompt_tokens_details?.cached_tokens || 0,
388
+ reasoning_tokens: usage?.completion_tokens_details?.reasoning_tokens || usage?.reasoning_tokens || 0,
389
+ keySource: metadata.keySource,
390
+ agentId: metadata.agentId,
391
+ teamId: metadata.teamId,
392
+ };
393
+ SystemEvents.emit('USAGE:LLM', usageData);
394
+
395
+ return usageData;
396
+ }
397
+
398
+ public formatToolsConfig({ type = 'function', toolDefinitions, toolChoice = 'auto' }) {
399
+ let tools: any[] = [];
400
+
401
+ if (type === 'function') {
402
+ tools = toolDefinitions.map((tool) => {
403
+ const { name, description, properties, requiredFields } = tool;
404
+
405
+ return {
406
+ type: 'function',
407
+ function: {
408
+ name,
409
+ description,
410
+ parameters: {
411
+ type: 'object',
412
+ properties,
413
+ required: requiredFields,
414
+ },
415
+ },
416
+ };
417
+ });
418
+ }
419
+
420
+ return tools?.length > 0 ? { tools, tool_choice: toolChoice || 'auto' } : {};
421
+ }
422
+
423
+ public transformToolMessageBlocks({
424
+ messageBlock,
425
+ toolsData,
426
+ }: {
427
+ messageBlock: TLLMMessageBlock;
428
+ toolsData: ToolData[];
429
+ }): TLLMToolResultMessageBlock[] {
430
+ const messageBlocks: TLLMToolResultMessageBlock[] = [];
431
+
432
+ if (messageBlock) {
433
+ const transformedMessageBlock = {
434
+ ...messageBlock,
435
+ content: typeof messageBlock.content === 'object' ? JSON.stringify(messageBlock.content) : messageBlock.content,
436
+ };
437
+ if (transformedMessageBlock.tool_calls) {
438
+ for (let toolCall of transformedMessageBlock.tool_calls) {
439
+ toolCall.function.arguments =
440
+ typeof toolCall.function.arguments === 'object' ? JSON.stringify(toolCall.function.arguments) : toolCall.function.arguments;
441
+ }
442
+ }
443
+ messageBlocks.push(transformedMessageBlock);
444
+ }
445
+
446
+ const transformedToolsData = toolsData.map((toolData) => ({
447
+ tool_call_id: toolData.id,
448
+ role: TLLMMessageRole.Tool,
449
+ name: toolData.name,
450
+ content: typeof toolData.result === 'string' ? toolData.result : JSON.stringify(toolData.result),
451
+ }));
452
+
453
+ return [...messageBlocks, ...transformedToolsData];
454
+ }
455
+
456
+ public getConsistentMessages(messages) {
457
+ const _messages = LLMHelper.removeDuplicateUserMessages(messages);
458
+
459
+ return _messages.map((message) => {
460
+ const _message = { ...message };
461
+ let textContent = '';
462
+
463
+ if (message?.parts) {
464
+ textContent = message.parts.map((textBlock) => textBlock?.text || '').join(' ');
465
+ } else if (Array.isArray(message?.content)) {
466
+ textContent = message.content.map((textBlock) => textBlock?.text || '').join(' ');
467
+ } else if (message?.content) {
468
+ textContent = message.content;
469
+ }
470
+
471
+ _message.content = textContent;
472
+
473
+ return _message;
474
+ });
475
+ }
476
+ }
@@ -10,6 +10,7 @@ import { GroqConnector } from './connectors/Groq.class';
10
10
  import { BedrockConnector } from './connectors/Bedrock.class';
11
11
  import { VertexAIConnector } from './connectors/VertexAI.class';
12
12
  import { PerplexityConnector } from './connectors/Perplexity.class';
13
+ import { xAIConnector } from './connectors/xAI';
13
14
 
14
15
  export class LLMService extends ConnectorServiceProvider {
15
16
  public register() {
@@ -22,7 +23,7 @@ export class LLMService extends ConnectorServiceProvider {
22
23
  ConnectorService.register(TConnectorService.LLM, 'TogetherAI', OpenAIConnector);
23
24
  ConnectorService.register(TConnectorService.LLM, 'Bedrock', BedrockConnector);
24
25
  ConnectorService.register(TConnectorService.LLM, 'VertexAI', VertexAIConnector);
25
- ConnectorService.register(TConnectorService.LLM, 'xAI', OpenAIConnector);
26
+ ConnectorService.register(TConnectorService.LLM, 'xAI', xAIConnector);
26
27
  ConnectorService.register(TConnectorService.LLM, 'Perplexity', PerplexityConnector);
27
28
  }
28
29
 
@@ -95,7 +95,7 @@ export abstract class ModelsProviderConnector extends SecureConnector {
95
95
  adjustMaxCompletionTokens: async (
96
96
  model: string | TLLMModel | TCustomLLMModel,
97
97
  maxCompletionTokens: number,
98
- hasAPIKey: boolean = false,
98
+ hasAPIKey: boolean = false
99
99
  ) => {
100
100
  const teamModels = typeof model === 'string' ? await loadTeamModels() : {};
101
101
  const modelInfo = await this.getModelInfo(candidate.readRequest, teamModels, model, hasAPIKey);
@@ -143,20 +143,26 @@ export abstract class ModelsProviderConnector extends SecureConnector {
143
143
  acRequest: AccessRequest,
144
144
  models: TLLMModelsList,
145
145
  model: string | TLLMModel | TCustomLLMModel,
146
- hasAPIKey: boolean = false,
146
+ hasAPIKey: boolean = false
147
147
  ): Promise<TLLMModel> {
148
148
  //model can be passed directly, in which case we do not need to look it up in the models list
149
- if (typeof model === 'object' && model.modelId) {
150
- return model;
151
- }
152
-
153
- //model can be passed as a string, in which case we need to look it up in the models list
154
149
 
155
- const modelId = await this.getModelId(acRequest, models, model);
156
- const alias = models?.[model as string]?.alias;
157
- const aliasModelInfo = models?.[alias];
150
+ let modelId, alias, aliasModelInfo, modelInfo;
158
151
 
159
- const modelInfo = models?.[model as string];
152
+ if (typeof model === 'object' && model.modelId) {
153
+ //return model;
154
+ modelId = model.modelId;
155
+ alias = model.alias;
156
+ aliasModelInfo = models?.[alias];
157
+ modelInfo = model;
158
+ } else {
159
+ //model can be passed as a string, in which case we need to look it up in the models list
160
+
161
+ modelId = await this.getModelId(acRequest, models, model);
162
+ alias = models?.[model as string]?.alias;
163
+ aliasModelInfo = models?.[alias];
164
+ modelInfo = models?.[model as string];
165
+ }
160
166
 
161
167
  const aliasKeyOptions = aliasModelInfo && hasAPIKey ? aliasModelInfo?.keyOptions : null;
162
168
 
@@ -2504,6 +2504,7 @@ export const models = {
2504
2504
  tokens: 0,
2505
2505
  completionTokens: 0,
2506
2506
  enabled: false,
2507
+ hidden: true,
2507
2508
  keyOptions: { tokens: 131_072, completionTokens: 8192, enabled: true },
2508
2509
 
2509
2510
  baseURL: 'https://api.x.ai/v1',
@@ -90,6 +90,24 @@ export type TLLMParams = {
90
90
  webSearchCountry?: string;
91
91
  webSearchRegion?: string;
92
92
  webSearchTimezone?: string;
93
+
94
+ // xAI specific search parameters
95
+ useSearch?: boolean;
96
+ searchMode?: 'auto' | 'on' | 'off';
97
+ returnCitations?: boolean;
98
+ maxSearchResults?: number;
99
+ searchDataSources?: string[];
100
+ searchCountry?: string;
101
+ excludedWebsites?: string[];
102
+ allowedWebsites?: string[];
103
+ includedXHandles?: string[];
104
+ excludedXHandles?: string[];
105
+ postFavoriteCount?: number;
106
+ postViewCount?: number;
107
+ rssLinks?: string;
108
+ safeSearch?: boolean;
109
+ fromDate?: string;
110
+ toDate?: string;
93
111
  // #endregion
94
112
 
95
113
  useReasoning?: boolean;