@smythos/sre 1.5.43 → 1.5.45

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 (233) hide show
  1. package/CHANGELOG +90 -90
  2. package/LICENSE +18 -18
  3. package/README.md +135 -135
  4. package/dist/index.js +13 -13
  5. package/dist/index.js.map +1 -1
  6. package/dist/types/Components/GenAILLM.class.d.ts +6 -0
  7. package/dist/types/helpers/AWSLambdaCode.helper.d.ts +8 -5
  8. package/dist/types/index.d.ts +1 -0
  9. package/dist/types/subsystems/LLMManager/LLM.service/connectors/Groq.class.d.ts +5 -0
  10. package/dist/types/subsystems/LLMManager/LLM.service/connectors/openai/OpenAIConnector.class.d.ts +13 -1
  11. package/dist/types/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/ChatCompletionsApiInterface.d.ts +0 -4
  12. package/dist/types/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/ResponsesApiInterface.d.ts +44 -29
  13. package/dist/types/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/constants.d.ts +4 -2
  14. package/dist/types/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/utils.d.ts +6 -0
  15. package/dist/types/subsystems/LLMManager/LLM.service/connectors/openai/types.d.ts +0 -4
  16. package/dist/types/subsystems/LLMManager/ModelsProvider.service/connectors/SmythModelsProvider.class.d.ts +39 -0
  17. package/dist/types/types/LLM.types.d.ts +4 -1
  18. package/package.json +5 -2
  19. package/src/Components/APICall/APICall.class.ts +156 -156
  20. package/src/Components/APICall/AccessTokenManager.ts +130 -130
  21. package/src/Components/APICall/ArrayBufferResponse.helper.ts +58 -58
  22. package/src/Components/APICall/OAuth.helper.ts +294 -294
  23. package/src/Components/APICall/mimeTypeCategories.ts +46 -46
  24. package/src/Components/APICall/parseData.ts +167 -167
  25. package/src/Components/APICall/parseHeaders.ts +41 -41
  26. package/src/Components/APICall/parseProxy.ts +68 -68
  27. package/src/Components/APICall/parseUrl.ts +91 -91
  28. package/src/Components/APIEndpoint.class.ts +234 -234
  29. package/src/Components/APIOutput.class.ts +58 -58
  30. package/src/Components/AgentPlugin.class.ts +102 -102
  31. package/src/Components/Async.class.ts +155 -155
  32. package/src/Components/Await.class.ts +90 -90
  33. package/src/Components/Classifier.class.ts +158 -158
  34. package/src/Components/Component.class.ts +132 -132
  35. package/src/Components/ComponentHost.class.ts +38 -38
  36. package/src/Components/DataSourceCleaner.class.ts +92 -92
  37. package/src/Components/DataSourceIndexer.class.ts +181 -181
  38. package/src/Components/DataSourceLookup.class.ts +161 -161
  39. package/src/Components/ECMASandbox.class.ts +71 -71
  40. package/src/Components/FEncDec.class.ts +29 -29
  41. package/src/Components/FHash.class.ts +33 -33
  42. package/src/Components/FSign.class.ts +80 -80
  43. package/src/Components/FSleep.class.ts +25 -25
  44. package/src/Components/FTimestamp.class.ts +25 -25
  45. package/src/Components/FileStore.class.ts +78 -78
  46. package/src/Components/ForEach.class.ts +97 -97
  47. package/src/Components/GPTPlugin.class.ts +70 -70
  48. package/src/Components/GenAILLM.class.ts +586 -579
  49. package/src/Components/HuggingFace.class.ts +314 -314
  50. package/src/Components/Image/imageSettings.config.ts +70 -70
  51. package/src/Components/ImageGenerator.class.ts +502 -502
  52. package/src/Components/JSONFilter.class.ts +54 -54
  53. package/src/Components/LLMAssistant.class.ts +213 -213
  54. package/src/Components/LogicAND.class.ts +28 -28
  55. package/src/Components/LogicAtLeast.class.ts +85 -85
  56. package/src/Components/LogicAtMost.class.ts +86 -86
  57. package/src/Components/LogicOR.class.ts +29 -29
  58. package/src/Components/LogicXOR.class.ts +34 -34
  59. package/src/Components/MCPClient.class.ts +112 -112
  60. package/src/Components/MemoryDeleteKeyVal.class.ts +70 -70
  61. package/src/Components/MemoryReadKeyVal.class.ts +66 -66
  62. package/src/Components/MemoryWriteKeyVal.class.ts +62 -62
  63. package/src/Components/MemoryWriteObject.class.ts +97 -97
  64. package/src/Components/MultimodalLLM.class.ts +128 -128
  65. package/src/Components/OpenAPI.class.ts +72 -72
  66. package/src/Components/PromptGenerator.class.ts +122 -122
  67. package/src/Components/ScrapflyWebScrape.class.ts +159 -159
  68. package/src/Components/ServerlessCode.class.ts +123 -123
  69. package/src/Components/TavilyWebSearch.class.ts +98 -98
  70. package/src/Components/VisionLLM.class.ts +104 -104
  71. package/src/Components/ZapierAction.class.ts +127 -127
  72. package/src/Components/index.ts +97 -97
  73. package/src/Core/AgentProcess.helper.ts +240 -240
  74. package/src/Core/Connector.class.ts +123 -123
  75. package/src/Core/ConnectorsService.ts +197 -197
  76. package/src/Core/DummyConnector.ts +49 -49
  77. package/src/Core/HookService.ts +105 -105
  78. package/src/Core/SmythRuntime.class.ts +235 -235
  79. package/src/Core/SystemEvents.ts +16 -16
  80. package/src/Core/boot.ts +56 -56
  81. package/src/config.ts +15 -15
  82. package/src/constants.ts +126 -126
  83. package/src/data/hugging-face.params.json +579 -579
  84. package/src/helpers/AWSLambdaCode.helper.ts +588 -528
  85. package/src/helpers/BinaryInput.helper.ts +331 -331
  86. package/src/helpers/Conversation.helper.ts +1119 -1119
  87. package/src/helpers/ECMASandbox.helper.ts +54 -54
  88. package/src/helpers/JsonContent.helper.ts +97 -97
  89. package/src/helpers/LocalCache.helper.ts +97 -97
  90. package/src/helpers/Log.helper.ts +274 -274
  91. package/src/helpers/OpenApiParser.helper.ts +150 -150
  92. package/src/helpers/S3Cache.helper.ts +147 -147
  93. package/src/helpers/SmythURI.helper.ts +5 -5
  94. package/src/helpers/Sysconfig.helper.ts +77 -77
  95. package/src/helpers/TemplateString.helper.ts +243 -243
  96. package/src/helpers/TypeChecker.helper.ts +329 -329
  97. package/src/index.ts +4 -3
  98. package/src/index.ts.bak +4 -3
  99. package/src/subsystems/AgentManager/Agent.class.ts +1114 -1114
  100. package/src/subsystems/AgentManager/Agent.helper.ts +3 -3
  101. package/src/subsystems/AgentManager/AgentData.service/AgentDataConnector.ts +230 -230
  102. package/src/subsystems/AgentManager/AgentData.service/connectors/CLIAgentDataConnector.class.ts +66 -66
  103. package/src/subsystems/AgentManager/AgentData.service/connectors/LocalAgentDataConnector.class.ts +142 -142
  104. package/src/subsystems/AgentManager/AgentData.service/connectors/NullAgentData.class.ts +39 -39
  105. package/src/subsystems/AgentManager/AgentData.service/index.ts +18 -18
  106. package/src/subsystems/AgentManager/AgentLogger.class.ts +297 -297
  107. package/src/subsystems/AgentManager/AgentRequest.class.ts +51 -51
  108. package/src/subsystems/AgentManager/AgentRuntime.class.ts +559 -559
  109. package/src/subsystems/AgentManager/AgentSSE.class.ts +101 -101
  110. package/src/subsystems/AgentManager/AgentSettings.class.ts +52 -52
  111. package/src/subsystems/AgentManager/Component.service/ComponentConnector.ts +32 -32
  112. package/src/subsystems/AgentManager/Component.service/connectors/LocalComponentConnector.class.ts +60 -60
  113. package/src/subsystems/AgentManager/Component.service/index.ts +11 -11
  114. package/src/subsystems/AgentManager/EmbodimentSettings.class.ts +47 -47
  115. package/src/subsystems/AgentManager/ForkedAgent.class.ts +154 -154
  116. package/src/subsystems/AgentManager/OSResourceMonitor.ts +77 -77
  117. package/src/subsystems/ComputeManager/Code.service/CodeConnector.ts +98 -98
  118. package/src/subsystems/ComputeManager/Code.service/connectors/AWSLambdaCode.class.ts +172 -170
  119. package/src/subsystems/ComputeManager/Code.service/connectors/ECMASandbox.class.ts +131 -131
  120. package/src/subsystems/ComputeManager/Code.service/index.ts +13 -13
  121. package/src/subsystems/IO/CLI.service/CLIConnector.ts +47 -47
  122. package/src/subsystems/IO/CLI.service/index.ts +9 -9
  123. package/src/subsystems/IO/Log.service/LogConnector.ts +32 -32
  124. package/src/subsystems/IO/Log.service/connectors/ConsoleLog.class.ts +28 -28
  125. package/src/subsystems/IO/Log.service/index.ts +13 -13
  126. package/src/subsystems/IO/NKV.service/NKVConnector.ts +43 -43
  127. package/src/subsystems/IO/NKV.service/connectors/NKVLocalStorage.class.ts +234 -234
  128. package/src/subsystems/IO/NKV.service/connectors/NKVRAM.class.ts +204 -204
  129. package/src/subsystems/IO/NKV.service/connectors/NKVRedis.class.ts +182 -182
  130. package/src/subsystems/IO/NKV.service/index.ts +14 -14
  131. package/src/subsystems/IO/Router.service/RouterConnector.ts +21 -21
  132. package/src/subsystems/IO/Router.service/connectors/ExpressRouter.class.ts +48 -48
  133. package/src/subsystems/IO/Router.service/connectors/NullRouter.class.ts +40 -40
  134. package/src/subsystems/IO/Router.service/index.ts +11 -11
  135. package/src/subsystems/IO/Storage.service/SmythFS.class.ts +489 -489
  136. package/src/subsystems/IO/Storage.service/StorageConnector.ts +66 -66
  137. package/src/subsystems/IO/Storage.service/connectors/LocalStorage.class.ts +327 -327
  138. package/src/subsystems/IO/Storage.service/connectors/S3Storage.class.ts +482 -482
  139. package/src/subsystems/IO/Storage.service/index.ts +13 -13
  140. package/src/subsystems/IO/VectorDB.service/VectorDBConnector.ts +108 -108
  141. package/src/subsystems/IO/VectorDB.service/connectors/MilvusVectorDB.class.ts +454 -454
  142. package/src/subsystems/IO/VectorDB.service/connectors/PineconeVectorDB.class.ts +384 -384
  143. package/src/subsystems/IO/VectorDB.service/connectors/RAMVecrtorDB.class.ts +421 -421
  144. package/src/subsystems/IO/VectorDB.service/embed/BaseEmbedding.ts +107 -107
  145. package/src/subsystems/IO/VectorDB.service/embed/OpenAIEmbedding.ts +109 -109
  146. package/src/subsystems/IO/VectorDB.service/embed/index.ts +21 -21
  147. package/src/subsystems/IO/VectorDB.service/index.ts +14 -14
  148. package/src/subsystems/LLMManager/LLM.helper.ts +251 -251
  149. package/src/subsystems/LLMManager/LLM.inference.ts +339 -339
  150. package/src/subsystems/LLMManager/LLM.service/LLMConnector.ts +489 -489
  151. package/src/subsystems/LLMManager/LLM.service/LLMCredentials.helper.ts +171 -171
  152. package/src/subsystems/LLMManager/LLM.service/connectors/Anthropic.class.ts +659 -659
  153. package/src/subsystems/LLMManager/LLM.service/connectors/Bedrock.class.ts +400 -400
  154. package/src/subsystems/LLMManager/LLM.service/connectors/Echo.class.ts +77 -77
  155. package/src/subsystems/LLMManager/LLM.service/connectors/GoogleAI.class.ts +757 -757
  156. package/src/subsystems/LLMManager/LLM.service/connectors/Groq.class.ts +304 -291
  157. package/src/subsystems/LLMManager/LLM.service/connectors/Perplexity.class.ts +250 -250
  158. package/src/subsystems/LLMManager/LLM.service/connectors/VertexAI.class.ts +423 -423
  159. package/src/subsystems/LLMManager/LLM.service/connectors/openai/OpenAIConnector.class.ts +488 -455
  160. package/src/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/ChatCompletionsApiInterface.ts +528 -528
  161. package/src/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/OpenAIApiInterface.ts +100 -100
  162. package/src/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/OpenAIApiInterfaceFactory.ts +81 -81
  163. package/src/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/ResponsesApiInterface.ts +1168 -853
  164. package/src/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/constants.ts +13 -37
  165. package/src/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/index.ts +4 -4
  166. package/src/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/utils.ts +11 -0
  167. package/src/subsystems/LLMManager/LLM.service/connectors/openai/types.ts +32 -37
  168. package/src/subsystems/LLMManager/LLM.service/connectors/xAI.class.ts +471 -471
  169. package/src/subsystems/LLMManager/LLM.service/index.ts +44 -44
  170. package/src/subsystems/LLMManager/ModelsProvider.service/ModelsProviderConnector.ts +300 -300
  171. package/src/subsystems/LLMManager/ModelsProvider.service/connectors/JSONModelsProvider.class.ts +252 -252
  172. package/src/subsystems/LLMManager/ModelsProvider.service/index.ts +11 -11
  173. package/src/subsystems/LLMManager/custom-models.ts +854 -854
  174. package/src/subsystems/LLMManager/models.ts +2540 -2540
  175. package/src/subsystems/LLMManager/paramMappings.ts +69 -69
  176. package/src/subsystems/MemoryManager/Cache.service/CacheConnector.ts +86 -86
  177. package/src/subsystems/MemoryManager/Cache.service/connectors/LocalStorageCache.class.ts +297 -297
  178. package/src/subsystems/MemoryManager/Cache.service/connectors/RAMCache.class.ts +201 -201
  179. package/src/subsystems/MemoryManager/Cache.service/connectors/RedisCache.class.ts +252 -252
  180. package/src/subsystems/MemoryManager/Cache.service/connectors/S3Cache.class.ts +373 -373
  181. package/src/subsystems/MemoryManager/Cache.service/index.ts +15 -15
  182. package/src/subsystems/MemoryManager/LLMCache.ts +72 -72
  183. package/src/subsystems/MemoryManager/LLMContext.ts +124 -124
  184. package/src/subsystems/MemoryManager/LLMMemory.service/LLMMemoryConnector.ts +26 -26
  185. package/src/subsystems/MemoryManager/RuntimeContext.ts +266 -266
  186. package/src/subsystems/Security/AccessControl/ACL.class.ts +208 -208
  187. package/src/subsystems/Security/AccessControl/AccessCandidate.class.ts +82 -82
  188. package/src/subsystems/Security/AccessControl/AccessRequest.class.ts +52 -52
  189. package/src/subsystems/Security/Account.service/AccountConnector.ts +44 -44
  190. package/src/subsystems/Security/Account.service/connectors/AWSAccount.class.ts +76 -76
  191. package/src/subsystems/Security/Account.service/connectors/DummyAccount.class.ts +130 -130
  192. package/src/subsystems/Security/Account.service/connectors/JSONFileAccount.class.ts +159 -159
  193. package/src/subsystems/Security/Account.service/index.ts +14 -14
  194. package/src/subsystems/Security/Credentials.helper.ts +62 -62
  195. package/src/subsystems/Security/ManagedVault.service/ManagedVaultConnector.ts +38 -38
  196. package/src/subsystems/Security/ManagedVault.service/connectors/NullManagedVault.class.ts +53 -53
  197. package/src/subsystems/Security/ManagedVault.service/connectors/SecretManagerManagedVault.ts +154 -154
  198. package/src/subsystems/Security/ManagedVault.service/index.ts +12 -12
  199. package/src/subsystems/Security/SecureConnector.class.ts +110 -110
  200. package/src/subsystems/Security/Vault.service/Vault.helper.ts +30 -30
  201. package/src/subsystems/Security/Vault.service/VaultConnector.ts +29 -29
  202. package/src/subsystems/Security/Vault.service/connectors/HashicorpVault.class.ts +46 -46
  203. package/src/subsystems/Security/Vault.service/connectors/JSONFileVault.class.ts +221 -221
  204. package/src/subsystems/Security/Vault.service/connectors/NullVault.class.ts +54 -54
  205. package/src/subsystems/Security/Vault.service/connectors/SecretsManager.class.ts +140 -140
  206. package/src/subsystems/Security/Vault.service/index.ts +12 -12
  207. package/src/types/ACL.types.ts +104 -104
  208. package/src/types/AWS.types.ts +10 -10
  209. package/src/types/Agent.types.ts +61 -61
  210. package/src/types/AgentLogger.types.ts +17 -17
  211. package/src/types/Cache.types.ts +1 -1
  212. package/src/types/Common.types.ts +2 -2
  213. package/src/types/LLM.types.ts +496 -491
  214. package/src/types/Redis.types.ts +8 -8
  215. package/src/types/SRE.types.ts +64 -64
  216. package/src/types/Security.types.ts +14 -14
  217. package/src/types/Storage.types.ts +5 -5
  218. package/src/types/VectorDB.types.ts +86 -86
  219. package/src/utils/base64.utils.ts +275 -275
  220. package/src/utils/cli.utils.ts +68 -68
  221. package/src/utils/data.utils.ts +322 -322
  222. package/src/utils/date-time.utils.ts +22 -22
  223. package/src/utils/general.utils.ts +238 -238
  224. package/src/utils/index.ts +12 -12
  225. package/src/utils/lazy-client.ts +261 -261
  226. package/src/utils/numbers.utils.ts +13 -13
  227. package/src/utils/oauth.utils.ts +35 -35
  228. package/src/utils/string.utils.ts +414 -414
  229. package/src/utils/url.utils.ts +19 -19
  230. package/src/utils/validation.utils.ts +74 -74
  231. package/dist/bundle-analysis-lazy.html +0 -4949
  232. package/dist/bundle-analysis.html +0 -4949
  233. package/dist/types/utils/package-manager.utils.d.ts +0 -26
@@ -1,853 +1,1168 @@
1
- import EventEmitter from 'events';
2
- import OpenAI from 'openai';
3
- import type { Stream } from 'openai/streaming';
4
-
5
- import { BinaryInput } from '@sre/helpers/BinaryInput.helper';
6
- import { AccessCandidate } from '@sre/Security/AccessControl/AccessCandidate.class';
7
- import {
8
- TLLMParams,
9
- TLLMPreparedParams,
10
- ILLMRequestContext,
11
- TLLMMessageBlock,
12
- ToolData,
13
- TLLMToolResultMessageBlock,
14
- TLLMMessageRole,
15
- APIKeySource,
16
- TLLMEvent,
17
- OpenAIToolDefinition,
18
- LegacyToolDefinition,
19
- LLMModelInfo,
20
- } from '@sre/types/LLM.types';
21
- import { OpenAIApiInterface, ToolConfig } from './OpenAIApiInterface';
22
- import { HandlerDependencies, TToolType } from '../types';
23
- import { SUPPORTED_MIME_TYPES_MAP } from '@sre/constants';
24
- import { MODELS_WITHOUT_TEMPERATURE_SUPPORT, SEARCH_TOOL_COSTS } from './constants';
25
-
26
- // File size limits in bytes
27
- const MAX_IMAGE_SIZE = 20 * 1024 * 1024; // 20MB
28
- const MAX_DOCUMENT_SIZE = 25 * 1024 * 1024; // 25MB
29
-
30
- type TSearchContextSize = 'low' | 'medium' | 'high';
31
- type TSearchLocation = {
32
- type: 'approximate';
33
- city?: string;
34
- country?: string;
35
- region?: string;
36
- timezone?: string;
37
- };
38
-
39
- /**
40
- * OpenAI Responses API interface implementation
41
- * Handles all Responses API-specific logic including:
42
- * - Stream creation and handling
43
- * - Request body preparation
44
- * - Tool and message transformations
45
- * - File attachment handling
46
- */
47
- export class ResponsesApiInterface extends OpenAIApiInterface {
48
- private deps: HandlerDependencies;
49
- private validImageMimeTypes = SUPPORTED_MIME_TYPES_MAP.OpenAI.image;
50
- private validDocumentMimeTypes = SUPPORTED_MIME_TYPES_MAP.OpenAI.document;
51
-
52
- constructor(context: ILLMRequestContext, deps: HandlerDependencies) {
53
- super(context);
54
- this.deps = deps;
55
- }
56
-
57
- async createRequest(body: OpenAI.Responses.ResponseCreateParams, context: ILLMRequestContext): Promise<OpenAI.Responses.Response> {
58
- const openai = await this.deps.getClient(context);
59
- return await openai.responses.create({
60
- ...body,
61
- stream: false,
62
- });
63
- }
64
-
65
- async createStream(
66
- body: OpenAI.Responses.ResponseCreateParams,
67
- context: ILLMRequestContext
68
- ): Promise<Stream<OpenAI.Responses.ResponseStreamEvent>> {
69
- const openai = await this.deps.getClient(context);
70
- return (await openai.responses.create({
71
- ...body,
72
- stream: true,
73
- })) as Stream<OpenAI.Responses.ResponseStreamEvent>;
74
- }
75
-
76
- public handleStream(stream: Stream<OpenAI.Responses.ResponseStreamEvent>, context: ILLMRequestContext): EventEmitter {
77
- const emitter = new EventEmitter();
78
- const usage_data: any[] = [];
79
- const reportedUsage: any[] = [];
80
- let finishReason = 'stop';
81
-
82
- // Process stream asynchronously while returning emitter immediately
83
- (async () => {
84
- let finalToolsData: ToolData[] = [];
85
-
86
- try {
87
- // Step 1: Process the stream
88
- const streamResult = await this.processStream(stream, emitter, usage_data);
89
- finalToolsData = streamResult.toolsData;
90
- finishReason = streamResult.finishReason;
91
-
92
- // Step 2: Report usage statistics
93
- this.reportUsageStatistics(usage_data, context, reportedUsage);
94
-
95
- // Step 3: Emit final events
96
- this.emitFinalEvents(emitter, finalToolsData, reportedUsage, finishReason);
97
- } catch (error) {
98
- emitter.emit('error', error);
99
- }
100
- })();
101
-
102
- return emitter;
103
- }
104
-
105
- /**
106
- * Process the responses API stream format
107
- */
108
- private async processStream(
109
- stream: Stream<OpenAI.Responses.ResponseStreamEvent>,
110
- emitter: EventEmitter,
111
- usage_data: any[]
112
- ): Promise<{ toolsData: ToolData[]; finishReason: string }> {
113
- let toolsData: ToolData[] = [];
114
- let finishReason = 'stop';
115
-
116
- for await (const part of stream) {
117
- // Handle different event types from the Responses API stream
118
- if ('type' in part) {
119
- const event = part.type;
120
-
121
- switch (event) {
122
- case 'response.output_text.delta': {
123
- if ('delta' in part && part.delta) {
124
- // Emit content in delta format for compatibility
125
- const deltaMsg = {
126
- role: 'assistant',
127
- content: part.delta,
128
- };
129
- emitter.emit('data', deltaMsg);
130
- emitter.emit('content', part.delta, 'assistant');
131
- }
132
- break;
133
- }
134
- case 'response.function_call_arguments.delta': {
135
- // Handle function call arguments streaming - use any to work around type issues
136
- const partAny = part as any;
137
- if (partAny?.delta && partAny?.call_id) {
138
- // Find or create tool data entry
139
- let toolIndex = toolsData.findIndex((t) => t.id === partAny.call_id);
140
- if (toolIndex === -1) {
141
- toolIndex = toolsData.length;
142
- toolsData.push({
143
- index: toolIndex,
144
- id: partAny.call_id,
145
- type: 'function',
146
- name: partAny?.name || '',
147
- arguments: '',
148
- role: 'tool',
149
- });
150
- }
151
- toolsData[toolIndex].arguments += partAny.delta;
152
- }
153
- break;
154
- }
155
- case 'response.web_search_call.started' as any:
156
- case 'response.web_search_call.completed' as any: {
157
- // Handle web search events - these are newer event types not yet in the official types
158
- const partAny = part as any;
159
- if (partAny?.id) {
160
- // Find or create web search tool data entry
161
- let toolIndex = toolsData.findIndex((t) => t.id === partAny.id);
162
- if (toolIndex === -1) {
163
- toolIndex = toolsData.length;
164
- toolsData.push({
165
- index: toolIndex,
166
- id: partAny.id,
167
- type: TToolType.WebSearch,
168
- name: 'web_search',
169
- arguments: partAny?.query || '',
170
- role: 'tool',
171
- });
172
- } else {
173
- // Update existing entry
174
- if (partAny?.query) {
175
- toolsData[toolIndex].arguments = partAny.query;
176
- }
177
- }
178
- }
179
- break;
180
- }
181
- default: {
182
- // Handle other event types including response completion
183
- if (event.includes('done')) {
184
- finishReason = 'stop';
185
- }
186
- break;
187
- }
188
- }
189
- }
190
-
191
- // Handle usage statistics from response object
192
- if ('response' in part && (part as any).response?.usage) {
193
- usage_data.push((part as any).response.usage);
194
- }
195
- }
196
-
197
- return { toolsData: this.extractToolCalls(toolsData), finishReason };
198
- }
199
-
200
- /**
201
- * Extract and format tool calls from the accumulated data
202
- */
203
- private extractToolCalls(output: ToolData[]): ToolData[] {
204
- return output.map((tool) => ({
205
- index: tool.index,
206
- name: tool.name,
207
- arguments: tool.arguments,
208
- id: tool.id,
209
- type: tool.type,
210
- role: tool.role,
211
- }));
212
- }
213
-
214
- /**
215
- * Report usage statistics
216
- */
217
- private reportUsageStatistics(usage_data: any[], context: ILLMRequestContext, reportedUsage: any[]): void {
218
- // Report normal usage
219
- usage_data.forEach((usage) => {
220
- // Convert ResponseUsage to CompletionUsage format for compatibility
221
- const convertedUsage = {
222
- completion_tokens: usage.completion_tokens || 0,
223
- prompt_tokens: usage.prompt_tokens || 0,
224
- total_tokens: usage.total_tokens || 0,
225
- ...usage,
226
- };
227
- const reported = this.deps.reportUsage(convertedUsage, this.buildUsageContext(context));
228
- reportedUsage.push(reported);
229
- });
230
-
231
- // Report search tool usage if enabled
232
- if (context.toolsInfo?.openai?.webSearch?.enabled) {
233
- const searchUsage = this.calculateSearchToolUsage(context);
234
- const reported = this.deps.reportUsage(searchUsage, this.buildUsageContext(context));
235
- reportedUsage.push(reported);
236
- }
237
- }
238
-
239
- /**
240
- * Emit final events
241
- */
242
- private emitFinalEvents(emitter: EventEmitter, toolsData: ToolData[], reportedUsage: any[], finishReason: string): void {
243
- // Emit tool info event if tools were called
244
- if (toolsData.length > 0) {
245
- emitter.emit(TLLMEvent.ToolInfo, toolsData);
246
- }
247
-
248
- // Emit interrupted event if finishReason is not 'stop'
249
- if (finishReason !== 'stop') {
250
- emitter.emit('interrupted', finishReason);
251
- }
252
-
253
- // Emit end event with setImmediate to ensure proper event ordering
254
- setImmediate(() => {
255
- emitter.emit('end', toolsData, reportedUsage, finishReason);
256
- });
257
- }
258
-
259
- /**
260
- * Build usage context parameters from request context
261
- */
262
- private buildUsageContext(context: ILLMRequestContext) {
263
- return {
264
- modelEntryName: context.modelEntryName,
265
- keySource: context.isUserKey ? APIKeySource.User : APIKeySource.Smyth,
266
- agentId: context.agentId,
267
- teamId: context.teamId,
268
- };
269
- }
270
-
271
- /**
272
- * Calculate search tool usage with cost
273
- */
274
- private calculateSearchToolUsage(context: ILLMRequestContext) {
275
- const modelName = context.modelEntryName?.replace('smythos/', '');
276
- const cost = this.getSearchToolCost(modelName, context.toolsInfo?.openai?.webSearch?.contextSize);
277
-
278
- return {
279
- cost,
280
- completion_tokens: 0,
281
- prompt_tokens: 0,
282
- total_tokens: 0,
283
- };
284
- }
285
-
286
- public async prepareRequestBody(params: TLLMPreparedParams): Promise<OpenAI.Responses.ResponseCreateParams> {
287
- let input = await this.prepareInputMessages(params);
288
-
289
- // Apply tool message transformation to input messages
290
- // There's a difference in the tools message data structures between `Chat Completions` and the `Response` interface.
291
- // Since we don't have enough context for the interface in `transformToolMessageBlocks`, we need to perform the transformation here so it's compatible with the `Responses` interface.
292
- input = this.applyToolMessageTransformation(input);
293
-
294
- const body: OpenAI.Responses.ResponseCreateParams = {
295
- model: params.model as string,
296
- input,
297
- };
298
-
299
- // Handle max tokens
300
- if (params?.maxTokens !== undefined) {
301
- body.max_output_tokens = params.maxTokens;
302
- }
303
-
304
- // o3-pro does not support temperature
305
- if (params?.temperature !== undefined && !MODELS_WITHOUT_TEMPERATURE_SUPPORT.includes(params.modelEntryName)) {
306
- body.temperature = params.temperature;
307
- }
308
-
309
- if (params?.topP !== undefined) {
310
- body.top_p = params.topP;
311
- }
312
-
313
- let tools: OpenAI.Responses.Tool[] = [];
314
-
315
- if (params?.toolsConfig?.tools && params?.toolsConfig?.tools?.length > 0) {
316
- tools = await this.prepareFunctionTools(params);
317
- }
318
-
319
- // Add null safety check before accessing toolsInfo
320
- if (params.toolsInfo?.openai?.webSearch?.enabled) {
321
- const searchTool = this.prepareWebSearchTool(params);
322
- tools.push(searchTool);
323
- }
324
-
325
- if (tools.length > 0) {
326
- body.tools = tools;
327
-
328
- if (params?.toolsConfig?.tool_choice) {
329
- body.tool_choice = params?.toolsConfig?.tool_choice as any;
330
- }
331
- }
332
-
333
- return body;
334
- }
335
-
336
- /**
337
- * Type guard to check if a tool is an OpenAI tool definition
338
- */
339
- private isOpenAIToolDefinition(tool: OpenAIToolDefinition | LegacyToolDefinition): tool is OpenAIToolDefinition {
340
- return 'parameters' in tool;
341
- }
342
-
343
- /**
344
- * Transform OpenAI tool definitions to Responses.Tool format
345
- */
346
- public transformToolsConfig(config: ToolConfig): OpenAI.Responses.Tool[] {
347
- return config.toolDefinitions.map((tool) => {
348
- // Handle OpenAI tool definition format
349
- if (this.isOpenAIToolDefinition(tool)) {
350
- return {
351
- type: 'function' as const,
352
- name: tool.name,
353
- description: tool.description,
354
- parameters: tool.parameters,
355
- strict: false, // Add required property for OpenAI Responses API
356
- } as OpenAI.Responses.Tool;
357
- }
358
-
359
- // Handle legacy format for backward compatibility
360
- return {
361
- type: 'function' as const,
362
- name: tool.name,
363
- description: tool.description,
364
- parameters: {
365
- type: 'object',
366
- properties: tool.properties || {},
367
- required: tool.requiredFields || [],
368
- },
369
- strict: false, // Add required property for OpenAI Responses API
370
- } as OpenAI.Responses.Tool;
371
- });
372
- }
373
-
374
- /**
375
- * Transform assistant message block with tool calls for Responses API
376
- */
377
- private transformAssistantMessageBlock(messageBlock: TLLMMessageBlock): TLLMToolResultMessageBlock {
378
- const transformedMessageBlock: TLLMToolResultMessageBlock = {
379
- ...messageBlock,
380
- content: this.normalizeContent(messageBlock.content),
381
- };
382
-
383
- // Transform tool calls if present
384
- if (transformedMessageBlock.tool_calls) {
385
- transformedMessageBlock.tool_calls = this.transformToolCalls(transformedMessageBlock.tool_calls);
386
- }
387
-
388
- return transformedMessageBlock;
389
- }
390
-
391
- /**
392
- * Transform individual tool calls to ensure proper formatting
393
- */
394
- private transformToolCalls(toolCalls: ToolData[]): ToolData[] {
395
- return toolCalls.map((toolCall) => ({
396
- ...toolCall,
397
- // Ensure function arguments are properly stringified for Responses API
398
- function: {
399
- ...toolCall.function,
400
- arguments: this.normalizeToolArguments(toolCall.function?.arguments || toolCall.arguments),
401
- },
402
- // Ensure arguments at root level are also normalized (for backward compatibility)
403
- arguments: this.normalizeToolArguments(toolCall.arguments),
404
- }));
405
- }
406
-
407
- /**
408
- * Transform tool results with comprehensive error handling and type support
409
- */
410
- private transformToolResults(toolsData: ToolData[]): TLLMToolResultMessageBlock[] {
411
- return toolsData.filter((toolData) => this.isValidToolData(toolData)).map((toolData) => this.createToolResultMessage(toolData));
412
- }
413
-
414
- /**
415
- * Create a tool result message for the Responses API format
416
- */
417
- private createToolResultMessage(toolData: ToolData): TLLMToolResultMessageBlock {
418
- const baseMessage: TLLMToolResultMessageBlock = {
419
- tool_call_id: toolData.id,
420
- role: TLLMMessageRole.Tool,
421
- name: toolData.name,
422
- content: this.formatToolResult(toolData),
423
- };
424
-
425
- // Handle tool errors specifically
426
- if (toolData.error) {
427
- baseMessage.content = this.formatToolError(toolData);
428
- }
429
-
430
- return baseMessage;
431
- }
432
-
433
- /**
434
- * Format tool result content based on type and handle special cases
435
- */
436
- private formatToolResult(toolData: ToolData): string {
437
- const result = toolData.result;
438
-
439
- // Handle different result types
440
- if (typeof result === 'string') {
441
- return result;
442
- }
443
-
444
- if (typeof result === 'object' && result !== null) {
445
- try {
446
- return JSON.stringify(result, null, 2);
447
- } catch (error) {
448
- return `[Error serializing result: ${error instanceof Error ? error.message : 'Unknown error'}]`;
449
- }
450
- }
451
-
452
- // Handle special tool types
453
- if (this.isWebSearchTool(toolData)) {
454
- return this.formatWebSearchResult(result);
455
- }
456
-
457
- // Handle undefined/null results
458
- if (result === undefined || result === null) {
459
- return `[Tool ${toolData.name} completed with no result]`;
460
- }
461
-
462
- // Fallback to string conversion
463
- return String(result);
464
- }
465
-
466
- /**
467
- * Format tool error messages with context
468
- */
469
- private formatToolError(toolData: ToolData): string {
470
- const errorMessage = toolData.error || 'Unknown error occurred';
471
- return `[Tool Error in ${toolData.name}]: ${errorMessage}`;
472
- }
473
-
474
- /**
475
- * Normalize content to string format for Responses API
476
- */
477
- private normalizeContent(content: any): string {
478
- if (typeof content === 'string') {
479
- return content;
480
- }
481
-
482
- if (Array.isArray(content)) {
483
- // Handle array content by extracting text parts
484
- return content
485
- .map((item) => {
486
- if (typeof item === 'string') return item;
487
- if (item?.text) return item.text;
488
- if (item?.type === 'text' && item?.text) return item.text;
489
- return JSON.stringify(item);
490
- })
491
- .join(' ');
492
- }
493
-
494
- if (typeof content === 'object' && content !== null) {
495
- try {
496
- return JSON.stringify(content);
497
- } catch (error) {
498
- return '[Error serializing content]';
499
- }
500
- }
501
-
502
- return String(content || '');
503
- }
504
-
505
- /**
506
- * Normalize tool arguments to string format for Responses API
507
- */
508
- private normalizeToolArguments(args: any): string {
509
- if (typeof args === 'string') {
510
- // If it's already a string, validate it's proper JSON
511
- try {
512
- JSON.parse(args);
513
- return args;
514
- } catch {
515
- // If not valid JSON, wrap it in quotes to make it valid
516
- return JSON.stringify(args);
517
- }
518
- }
519
-
520
- if (typeof args === 'object' && args !== null) {
521
- try {
522
- return JSON.stringify(args);
523
- } catch (error) {
524
- return '{}'; // Fallback to empty object
525
- }
526
- }
527
-
528
- if (args === undefined || args === null) {
529
- return '{}';
530
- }
531
-
532
- // For primitive types, convert to JSON
533
- return JSON.stringify(args);
534
- }
535
-
536
- /**
537
- * Validate if tool data is complete and valid for transformation
538
- */
539
- private isValidToolData(toolData: ToolData): boolean {
540
- return !!(toolData && toolData.id && toolData.name && (toolData.result !== undefined || toolData.error !== undefined));
541
- }
542
-
543
- /**
544
- * Check if the tool is a web search tool based on type or name
545
- */
546
- private isWebSearchTool(toolData: ToolData): boolean {
547
- return (
548
- toolData.type === TToolType.WebSearch || toolData.name?.toLowerCase().includes('search') || toolData.name?.toLowerCase().includes('web')
549
- );
550
- }
551
-
552
- /**
553
- * Format web search results with better structure
554
- */
555
- private formatWebSearchResult(result: any): string {
556
- if (!result) return '[Web search completed with no results]';
557
-
558
- // If result is already a well-formatted string, use it
559
- if (typeof result === 'string') {
560
- return result;
561
- }
562
-
563
- // If result is an object with search-specific structure, format it nicely
564
- if (typeof result === 'object') {
565
- try {
566
- // Check for common web search result structures
567
- if (result.results || result.items || result.data) {
568
- return JSON.stringify(result, null, 2);
569
- }
570
- return JSON.stringify(result, null, 2);
571
- } catch (error) {
572
- return '[Error formatting web search results]';
573
- }
574
- }
575
-
576
- return String(result);
577
- }
578
-
579
- async handleFileAttachments(files: BinaryInput[], agentId: string, messages: any[]): Promise<any[]> {
580
- if (files.length === 0) return messages;
581
-
582
- const uploadedFiles = await this.uploadFiles(files, agentId);
583
- const validImageFiles = this.getValidImageFiles(uploadedFiles);
584
- const validDocumentFiles = this.getValidDocumentFiles(uploadedFiles);
585
-
586
- // Process images and documents with Responses API specific formatting
587
- const imageData = await this.processImageData(validImageFiles, agentId);
588
- const documentData = await this.processDocumentData(validDocumentFiles, agentId);
589
-
590
- // Find the last user message and add files to it
591
- for (let i = messages.length - 1; i >= 0; i--) {
592
- if (messages[i].role === 'user') {
593
- // Ensure content is an array before pushing files
594
- if (typeof messages[i].content === 'string') {
595
- messages[i].content = [{ type: 'input_text', text: messages[i].content }];
596
- } else if (!Array.isArray(messages[i].content)) {
597
- messages[i].content = [];
598
- }
599
- messages[i].content.push(...imageData, ...documentData);
600
- break;
601
- }
602
- }
603
-
604
- // If no user message found, create one with files
605
- if (!messages.some((item) => item.role === 'user')) {
606
- messages.push({
607
- role: 'user',
608
- content: [...imageData, ...documentData],
609
- });
610
- }
611
-
612
- return messages;
613
- }
614
-
615
- /**
616
- * Get valid image files based on supported MIME types
617
- */
618
- private getValidImageFiles(files: BinaryInput[]): BinaryInput[] {
619
- return files.filter((file) => this.validImageMimeTypes.includes(file?.mimetype));
620
- }
621
-
622
- /**
623
- * Get valid document files based on supported MIME types
624
- */
625
- private getValidDocumentFiles(files: BinaryInput[]): BinaryInput[] {
626
- return files.filter((file) => this.validDocumentMimeTypes.includes(file?.mimetype));
627
- }
628
-
629
- /**
630
- * Upload files to storage
631
- */
632
- private async uploadFiles(files: BinaryInput[], agentId: string): Promise<BinaryInput[]> {
633
- const promises = files.map((file) => {
634
- const binaryInput = BinaryInput.from(file);
635
- return binaryInput.upload(AccessCandidate.agent(agentId)).then(() => binaryInput);
636
- });
637
-
638
- return Promise.all(promises);
639
- }
640
-
641
- /**
642
- * Process image files with Responses API specific formatting
643
- */
644
- private async processImageData(files: BinaryInput[], agentId: string): Promise<any[]> {
645
- if (files.length === 0) return [];
646
-
647
- const imageData = [];
648
- for (const file of files) {
649
- await this.validateFileSize(file, MAX_IMAGE_SIZE, 'Image');
650
-
651
- const bufferData = await file.readData(AccessCandidate.agent(agentId));
652
- const base64Data = bufferData.toString('base64');
653
- const url = `data:${file.mimetype};base64,${base64Data}`;
654
-
655
- imageData.push({
656
- type: 'input_image',
657
- image_url: url,
658
- });
659
- }
660
-
661
- return imageData;
662
- }
663
-
664
- /**
665
- * Process document files with Responses API specific formatting
666
- */
667
- private async processDocumentData(files: BinaryInput[], agentId: string): Promise<any[]> {
668
- if (files.length === 0) return [];
669
-
670
- const documentData = [];
671
- for (const file of files) {
672
- await this.validateFileSize(file, MAX_DOCUMENT_SIZE, 'Document');
673
-
674
- const bufferData = await file.readData(AccessCandidate.agent(agentId));
675
- const base64Data = bufferData.toString('base64');
676
- const fileData = `data:${file.mimetype};base64,${base64Data}`;
677
- const filename = await file.getName();
678
-
679
- documentData.push({
680
- type: 'input_file',
681
- file: {
682
- file_data: fileData,
683
- filename,
684
- },
685
- });
686
- }
687
-
688
- return documentData;
689
- }
690
-
691
- /**
692
- * Validate file size before processing
693
- */
694
- private async validateFileSize(file: BinaryInput, maxSize: number, fileType: string): Promise<void> {
695
- await file.ready();
696
- const fileInfo = await file.getJsonData(AccessCandidate.agent('temp'));
697
- if (fileInfo.size > maxSize) {
698
- throw new Error(`${fileType} file size (${fileInfo.size} bytes) exceeds maximum allowed size of ${maxSize} bytes`);
699
- }
700
- }
701
-
702
- getInterfaceName(): string {
703
- return 'responses';
704
- }
705
-
706
- validateParameters(params: TLLMParams): boolean {
707
- // Basic validation for Responses API parameters
708
- return !!params.model;
709
- }
710
-
711
- /**
712
- * Prepare input messages for Responses API
713
- */
714
- private async prepareInputMessages(params: TLLMParams): Promise<any[]> {
715
- const messages = params?.messages || [];
716
- const files: BinaryInput[] = params?.files || [];
717
-
718
- // Start with raw messages - transformation now happens in applyToolMessageTransformation
719
- let input = [...messages];
720
-
721
- // Handle files if present
722
- if (files.length > 0) {
723
- input = await this.handleFileAttachments(files, params.agentId, input);
724
- }
725
-
726
- return input;
727
- }
728
-
729
- /**
730
- * Prepare tools for request
731
- */
732
- private async prepareFunctionTools(params: TLLMParams): Promise<OpenAI.Responses.Tool[]> {
733
- const tools: OpenAI.Responses.Tool[] = [];
734
-
735
- // Add regular function tools
736
- if (params?.toolsConfig?.tools && params?.toolsConfig?.tools?.length > 0) {
737
- // Now we can pass the tools directly to transformToolsConfig
738
- // which handles type detection and conversion properly
739
- const toolsConfig = this.transformToolsConfig({
740
- type: 'function',
741
- toolDefinitions: params.toolsConfig.tools as (OpenAIToolDefinition | LegacyToolDefinition)[],
742
- toolChoice: 'auto',
743
- modelInfo: (params.modelInfo as LLMModelInfo) || null,
744
- });
745
- tools.push(...toolsConfig);
746
- }
747
-
748
- return tools;
749
- }
750
-
751
- /**
752
- * Get web search tool configuration for OpenAI Responses API
753
- * According to OpenAI documentation: https://platform.openai.com/docs/api-reference/responses/create
754
- */
755
- private prepareWebSearchTool(params: TLLMPreparedParams): OpenAI.Responses.WebSearchTool {
756
- const webSearch = params?.toolsInfo?.openai?.webSearch;
757
- const contextSize = webSearch?.contextSize;
758
- const searchCity = webSearch?.city;
759
- const searchCountry = webSearch?.country;
760
- const searchRegion = webSearch?.region;
761
- const searchTimezone = webSearch?.timezone;
762
-
763
- // Prepare location object - build incrementally if any location parameters exist
764
- const userLocation: TSearchLocation = {
765
- type: 'approximate', // Required, always be 'approximate' when we implement location
766
- };
767
-
768
- // Add location fields if they exist
769
- if (searchCity) userLocation.city = searchCity;
770
- if (searchCountry) userLocation.country = searchCountry;
771
- if (searchRegion) userLocation.region = searchRegion;
772
- if (searchTimezone) userLocation.timezone = searchTimezone;
773
-
774
- // Only include location in config if we have actual location data
775
- const hasLocationData = searchCity || searchCountry || searchRegion || searchTimezone;
776
-
777
- // Configure web search tool according to OpenAI Responses API specification
778
- const searchTool: OpenAI.Responses.WebSearchTool = {
779
- type: 'web_search_preview' as any, // Use correct type as per OpenAI docs
780
- };
781
-
782
- // Add optional configuration properties
783
- const webSearchConfig: any = {};
784
-
785
- if (contextSize) {
786
- webSearchConfig.search_context_size = contextSize;
787
- }
788
-
789
- if (hasLocationData) {
790
- webSearchConfig.user_location = userLocation;
791
- }
792
-
793
- return { ...searchTool, ...webSearchConfig };
794
- }
795
-
796
- private applyToolMessageTransformation(input: any[]): any[] {
797
- const transformedMessages: any[] = [];
798
-
799
- input.forEach((message) => {
800
- if (message.role === 'assistant' && message.tool_calls) {
801
- // Split assistant message with tool_calls into separate items (Responses API format)
802
- if (message.content) {
803
- transformedMessages.push({
804
- role: 'assistant',
805
- content: typeof message.content === 'object' ? JSON.stringify(message.content) : message.content,
806
- });
807
- }
808
-
809
- message.tool_calls.forEach((toolCall) => {
810
- transformedMessages.push({
811
- type: 'function_call',
812
- name: toolCall.function.name,
813
- arguments:
814
- typeof toolCall.function.arguments === 'object'
815
- ? JSON.stringify(toolCall.function.arguments)
816
- : toolCall.function.arguments,
817
- call_id: toolCall.id,
818
- });
819
- });
820
- } else if (message.role === 'tool') {
821
- // Transform tool message to function_call_output (Responses API format)
822
- transformedMessages.push({
823
- type: 'function_call_output',
824
- call_id: message.tool_call_id,
825
- output: typeof message.content === 'string' ? message.content : JSON.stringify(message.content),
826
- });
827
- } else {
828
- transformedMessages.push(message);
829
- }
830
- });
831
-
832
- return transformedMessages;
833
- }
834
-
835
- /**
836
- * Get search tool cost for a specific model and context size
837
- */
838
- private getSearchToolCost(modelName: string, contextSize: string): number {
839
- const normalizedModelName = modelName?.replace('smythos/', '');
840
-
841
- // Check normal models first
842
- if (SEARCH_TOOL_COSTS.normalModels[normalizedModelName]) {
843
- return SEARCH_TOOL_COSTS.normalModels[normalizedModelName][contextSize] || 0;
844
- }
845
-
846
- // Check mini models
847
- if (SEARCH_TOOL_COSTS.miniModels[normalizedModelName]) {
848
- return SEARCH_TOOL_COSTS.miniModels[normalizedModelName][contextSize] || 0;
849
- }
850
-
851
- return 0;
852
- }
853
- }
1
+ import EventEmitter from 'events';
2
+ import OpenAI from 'openai';
3
+ import type { Stream } from 'openai/streaming';
4
+
5
+ import { BinaryInput } from '@sre/helpers/BinaryInput.helper';
6
+ import { AccessCandidate } from '@sre/Security/AccessControl/AccessCandidate.class';
7
+ import {
8
+ TLLMParams,
9
+ TLLMPreparedParams,
10
+ ILLMRequestContext,
11
+ TLLMMessageBlock,
12
+ ToolData,
13
+ TLLMToolResultMessageBlock,
14
+ TLLMMessageRole,
15
+ APIKeySource,
16
+ TLLMEvent,
17
+ OpenAIToolDefinition,
18
+ LegacyToolDefinition,
19
+ LLMModelInfo,
20
+ } from '@sre/types/LLM.types';
21
+ import { OpenAIApiInterface, ToolConfig } from './OpenAIApiInterface';
22
+ import { HandlerDependencies, TToolType } from '../types';
23
+ import { SUPPORTED_MIME_TYPES_MAP } from '@sre/constants';
24
+ import { MODELS_WITHOUT_TEMPERATURE_SUPPORT, SEARCH_TOOL_COSTS } from './constants';
25
+ import { isValidOpenAIReasoningEffort } from './utils';
26
+
27
+ // File size limits in bytes
28
+ const MAX_IMAGE_SIZE = 20 * 1024 * 1024; // 20MB
29
+ const MAX_DOCUMENT_SIZE = 25 * 1024 * 1024; // 25MB
30
+
31
+ // Event type constants for type safety and maintainability
32
+ const EVENT_TYPES = {
33
+ // Officially supported web search events (OpenAI SDK >= 5.12.x)
34
+ WEB_SEARCH_IN_PROGRESS: 'response.web_search_call.in_progress',
35
+ WEB_SEARCH_SEARCHING: 'response.web_search_call.searching',
36
+ WEB_SEARCH_COMPLETED: 'response.web_search_call.completed',
37
+ // Legacy alias observed historically (kept for backward compat if emitted)
38
+ WEB_SEARCH_STARTED: 'response.web_search_call.started',
39
+
40
+ RESPONSE_COMPLETED: 'response.completed',
41
+ OUTPUT_TEXT_DELTA: 'response.output_text.delta',
42
+ OUTPUT_ITEM_ADDED: 'response.output_item.added',
43
+ FUNCTION_CALL_ARGUMENTS_DELTA: 'response.function_call_arguments.delta',
44
+ FUNCTION_CALL_ARGUMENTS_DONE: 'response.function_call_arguments.done',
45
+ OUTPUT_ITEM_DONE: 'response.output_item.done',
46
+ } as const;
47
+
48
+ // Type definitions for web search events (augmenting SDK types locally)
49
+ interface WebSearchInProgressEvent {
50
+ type: typeof EVENT_TYPES.WEB_SEARCH_IN_PROGRESS;
51
+ item_id: string;
52
+ }
53
+
54
+ interface WebSearchSearchingEvent {
55
+ type: typeof EVENT_TYPES.WEB_SEARCH_SEARCHING;
56
+ item_id: string;
57
+ }
58
+
59
+ interface WebSearchCompletedEvent {
60
+ type: typeof EVENT_TYPES.WEB_SEARCH_COMPLETED;
61
+ item_id: string;
62
+ }
63
+
64
+ type TSearchLocation = {
65
+ type: 'approximate';
66
+ city?: string;
67
+ country?: string;
68
+ region?: string;
69
+ timezone?: string;
70
+ };
71
+
72
+ /**
73
+ * OpenAI Responses API interface implementation
74
+ * Handles all Responses API-specific logic including:
75
+ * - Stream creation and handling
76
+ * - Request body preparation
77
+ * - Tool and message transformations
78
+ * - File attachment handling
79
+ */
80
+ export class ResponsesApiInterface extends OpenAIApiInterface {
81
+ private deps: HandlerDependencies;
82
+ private validImageMimeTypes = SUPPORTED_MIME_TYPES_MAP.OpenAI.image;
83
+ private validDocumentMimeTypes = SUPPORTED_MIME_TYPES_MAP.OpenAI.document;
84
+
85
+ constructor(context: ILLMRequestContext, deps: HandlerDependencies) {
86
+ super(context);
87
+ this.deps = deps;
88
+ }
89
+
90
+ async createRequest(body: OpenAI.Responses.ResponseCreateParams, context: ILLMRequestContext): Promise<OpenAI.Responses.Response> {
91
+ const openai = await this.deps.getClient(context);
92
+ return await openai.responses.create({
93
+ ...body,
94
+ stream: false,
95
+ });
96
+ }
97
+
98
+ async createStream(
99
+ body: OpenAI.Responses.ResponseCreateParams,
100
+ context: ILLMRequestContext
101
+ ): Promise<Stream<OpenAI.Responses.ResponseStreamEvent>> {
102
+ const openai = await this.deps.getClient(context);
103
+ return (await openai.responses.create({
104
+ ...body,
105
+ stream: true,
106
+ })) as Stream<OpenAI.Responses.ResponseStreamEvent>;
107
+ }
108
+
109
+ public handleStream(stream: Stream<OpenAI.Responses.ResponseStreamEvent>, context: ILLMRequestContext): EventEmitter {
110
+ const emitter = new EventEmitter();
111
+
112
+ // Process stream asynchronously while returning emitter immediately
113
+ (async () => {
114
+ let finalToolsData: ToolData[] = [];
115
+
116
+ try {
117
+ // Step 1: Process the stream
118
+ const streamResult = await this.processStream(stream, emitter);
119
+ finalToolsData = streamResult.toolsData;
120
+
121
+ const finishReason = streamResult.finishReason || 'stop';
122
+ const usageData = streamResult.usageData;
123
+
124
+ // Step 2: Report usage statistics
125
+ const reportedUsage = this.reportUsageStatistics(usageData, context);
126
+
127
+ // Step 3: Emit final events
128
+ this.emitFinalEvents(emitter, finalToolsData, reportedUsage, finishReason);
129
+ } catch (error) {
130
+ emitter.emit('error', error);
131
+ }
132
+ })();
133
+
134
+ return emitter;
135
+ }
136
+
137
+ /**
138
+ * Process the responses API stream format
139
+ */
140
+ private async processStream(
141
+ stream: Stream<OpenAI.Responses.ResponseStreamEvent>,
142
+ emitter: EventEmitter
143
+ ): Promise<{ toolsData: ToolData[]; finishReason: string; usageData: any[] }> {
144
+ let toolsData: ToolData[] = [];
145
+ let finishReason = 'stop';
146
+ const usageData = [];
147
+
148
+ for await (const part of stream) {
149
+ try {
150
+ // Handle different event types from the Responses API stream
151
+ if ('type' in part) {
152
+ // Handle officially typed events using constants
153
+ switch (part.type) {
154
+ case EVENT_TYPES.WEB_SEARCH_IN_PROGRESS:
155
+ toolsData = this.handleWebSearchInProgress(part as any, toolsData);
156
+ break;
157
+ case EVENT_TYPES.WEB_SEARCH_SEARCHING:
158
+ toolsData = this.handleWebSearchSearching(part as any, toolsData);
159
+ break;
160
+ case EVENT_TYPES.WEB_SEARCH_COMPLETED:
161
+ toolsData = this.handleWebSearchCompleted(part as any, toolsData);
162
+ break;
163
+ case EVENT_TYPES.OUTPUT_TEXT_DELTA:
164
+ this.handleOutputTextDelta(part, emitter);
165
+ break;
166
+
167
+ case EVENT_TYPES.OUTPUT_ITEM_ADDED:
168
+ toolsData = this.handleOutputItemAdded(part, toolsData, emitter);
169
+ break;
170
+
171
+ case EVENT_TYPES.FUNCTION_CALL_ARGUMENTS_DELTA:
172
+ toolsData = this.handleFunctionCallArgumentsDelta(part, toolsData, emitter);
173
+ break;
174
+
175
+ case EVENT_TYPES.FUNCTION_CALL_ARGUMENTS_DONE:
176
+ toolsData = this.handleFunctionCallArgumentsDone(part, toolsData, emitter);
177
+ break;
178
+
179
+ case EVENT_TYPES.OUTPUT_ITEM_DONE:
180
+ toolsData = this.handleOutputItemDone(part, toolsData);
181
+ break;
182
+
183
+ case EVENT_TYPES.RESPONSE_COMPLETED: {
184
+ finishReason = 'stop';
185
+ const responseData = (part as any)?.response;
186
+ if (responseData?.usage) {
187
+ usageData.push(responseData.usage);
188
+ }
189
+ break;
190
+ }
191
+
192
+ default: {
193
+ const eventType = String(part.type);
194
+ // Handle legacy started event if ever emitted
195
+ if (eventType === EVENT_TYPES.WEB_SEARCH_STARTED) {
196
+ const legacyId = (part as any)?.id;
197
+ if (typeof legacyId === 'string') {
198
+ const result = this.upsertWebSearchToolImmutable(toolsData, legacyId);
199
+ toolsData = result.toolsData;
200
+ }
201
+ break;
202
+ }
203
+ // Handle any other unknown 'done' style events as completion
204
+ finishReason = this.handleCompletionEvent(eventType);
205
+ break;
206
+ }
207
+ }
208
+ }
209
+ } catch (error) {
210
+ // Log error but continue processing to prevent stream interruption
211
+ console.warn('Error processing stream event:', error, 'Event:', part);
212
+ }
213
+ }
214
+
215
+ return { toolsData: this.extractToolCalls(toolsData), finishReason, usageData };
216
+ }
217
+
218
+ /**
219
+ * Extract and format tool calls from the accumulated data
220
+ */
221
+ private extractToolCalls(output: ToolData[]): ToolData[] {
222
+ return output.map((tool) => ({
223
+ index: tool.index,
224
+ name: tool.name,
225
+ arguments: tool.arguments,
226
+ id: tool.callId || tool.id, // Use callId for final output if available
227
+ type: tool.type,
228
+ role: tool.role,
229
+ callId: tool.callId, // Preserve callId for reference
230
+ }));
231
+ }
232
+
233
+ /**
234
+ * Report usage statistics
235
+ */
236
+ private reportUsageStatistics(usage_data: any[], context: ILLMRequestContext): any[] {
237
+ const reportedUsage: any[] = [];
238
+
239
+ // Report normal usage
240
+ usage_data.forEach((usage) => {
241
+ // Convert ResponseUsage to CompletionUsage format for compatibility
242
+ const convertedUsage = {
243
+ completion_tokens: usage.completion_tokens || 0,
244
+ prompt_tokens: usage.prompt_tokens || 0,
245
+ total_tokens: usage.total_tokens || 0,
246
+ ...usage,
247
+ };
248
+ const reported = this.deps.reportUsage(convertedUsage, this.buildUsageContext(context));
249
+ reportedUsage.push(reported);
250
+ });
251
+
252
+ // Report search tool usage if enabled
253
+ if (context.toolsInfo?.openai?.webSearch?.enabled) {
254
+ const searchUsage = this.calculateSearchToolUsage(context);
255
+ const reported = this.deps.reportUsage(searchUsage, this.buildUsageContext(context));
256
+ reportedUsage.push(reported);
257
+ }
258
+
259
+ return reportedUsage;
260
+ }
261
+
262
+ /**
263
+ * Emit final events
264
+ */
265
+ private emitFinalEvents(emitter: EventEmitter, toolsData: ToolData[], reportedUsage: any[], finishReason: string): void {
266
+ // Emit tool info event if tools were called
267
+ if (toolsData.length > 0) {
268
+ emitter.emit(TLLMEvent.ToolInfo, toolsData);
269
+ }
270
+
271
+ // Emit interrupted event if finishReason is not 'stop'
272
+ if (finishReason !== 'stop') {
273
+ emitter.emit('interrupted', finishReason);
274
+ }
275
+
276
+ // Emit end event with setImmediate to ensure proper event ordering
277
+ setImmediate(() => {
278
+ emitter.emit('end', toolsData, reportedUsage, finishReason);
279
+ });
280
+ }
281
+
282
+ /**
283
+ * Build usage context parameters from request context
284
+ */
285
+ private buildUsageContext(context: ILLMRequestContext) {
286
+ return {
287
+ modelEntryName: context.modelEntryName,
288
+ keySource: context.isUserKey ? APIKeySource.User : APIKeySource.Smyth,
289
+ agentId: context.agentId,
290
+ teamId: context.teamId,
291
+ };
292
+ }
293
+
294
+ /**
295
+ * Calculate search tool usage with cost
296
+ */
297
+ private calculateSearchToolUsage(context: ILLMRequestContext) {
298
+ const modelName = context.modelEntryName?.replace('smythos/', '');
299
+ const cost = this.getSearchToolCost(modelName);
300
+
301
+ return {
302
+ cost,
303
+ completion_tokens: 0,
304
+ prompt_tokens: 0,
305
+ total_tokens: 0,
306
+ };
307
+ }
308
+
309
+ // =====================
310
+ // Event handlers (private)
311
+ // =====================
312
+
313
+ /**
314
+ * Handle web search completed event with proper type safety
315
+ */
316
+ private handleWebSearchCompleted(event: WebSearchCompletedEvent, toolsData: ToolData[]): ToolData[] {
317
+ try {
318
+ const { item_id: itemId } = event;
319
+ const result = this.upsertWebSearchToolImmutable(toolsData, itemId);
320
+ return result.toolsData;
321
+ } catch (error) {
322
+ console.warn('Error handling web search completed event:', error);
323
+ return toolsData;
324
+ }
325
+ }
326
+
327
+ /**
328
+ * Handle web search in-progress event (official typed)
329
+ */
330
+ private handleWebSearchInProgress(event: WebSearchInProgressEvent, toolsData: ToolData[]): ToolData[] {
331
+ try {
332
+ const { item_id: itemId } = event;
333
+ const result = this.upsertWebSearchToolImmutable(toolsData, itemId);
334
+ return result.toolsData;
335
+ } catch (error) {
336
+ console.warn('Error handling web search in_progress event:', error);
337
+ return toolsData;
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Handle web search searching event (official typed)
343
+ */
344
+ private handleWebSearchSearching(event: WebSearchSearchingEvent, toolsData: ToolData[]): ToolData[] {
345
+ try {
346
+ const { item_id: itemId } = event;
347
+ const result = this.upsertWebSearchToolImmutable(toolsData, itemId);
348
+ return result.toolsData;
349
+ } catch (error) {
350
+ console.warn('Error handling web search searching event:', error);
351
+ return toolsData;
352
+ }
353
+ }
354
+
355
+ /**
356
+ * Handle output text delta events
357
+ */
358
+ private handleOutputTextDelta(part: any, emitter: EventEmitter): void {
359
+ try {
360
+ if ('delta' in part && part.delta) {
361
+ const deltaMsg = {
362
+ role: 'assistant',
363
+ content: part.delta,
364
+ };
365
+ emitter.emit('data', deltaMsg);
366
+ emitter.emit('content', part.delta, 'assistant');
367
+ }
368
+ } catch (error) {
369
+ console.warn('Error handling output text delta:', error);
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Handle output item added events (function calls)
375
+ */
376
+ private handleOutputItemAdded(part: any, toolsData: ToolData[], emitter: EventEmitter): ToolData[] {
377
+ try {
378
+ const partAny = part as any;
379
+ if (partAny.item && partAny.item.type === 'function_call') {
380
+ const item = partAny.item;
381
+ const callId = item.call_id;
382
+ const functionName = item.name;
383
+ const itemId = item.id;
384
+
385
+ if (callId && itemId) {
386
+ const existingIndex = toolsData.findIndex((t) => t.id === itemId || t.id === callId);
387
+ const addingNew = existingIndex === -1;
388
+ const nextIndex = addingNew ? toolsData.length : existingIndex;
389
+
390
+ let updated: ToolData[];
391
+ if (addingNew) {
392
+ const newItem: ToolData = {
393
+ index: nextIndex,
394
+ id: itemId,
395
+ callId: callId,
396
+ type: 'function',
397
+ name: functionName || '',
398
+ arguments: item.arguments || '',
399
+ role: 'tool',
400
+ } as ToolData;
401
+ updated = [...toolsData, newItem];
402
+ } else {
403
+ updated = toolsData.map((t, idx) => {
404
+ if (idx !== existingIndex) return t;
405
+ return {
406
+ ...t,
407
+ name: functionName || t.name,
408
+ arguments: item.arguments !== undefined ? item.arguments : t.arguments,
409
+ callId: t.callId || callId,
410
+ };
411
+ });
412
+ }
413
+
414
+ if (addingNew) {
415
+ emitter.emit('tool_call_started', {
416
+ id: callId,
417
+ name: functionName || '',
418
+ type: 'function',
419
+ });
420
+ }
421
+
422
+ return updated;
423
+ }
424
+ }
425
+ return toolsData;
426
+ } catch (error) {
427
+ console.warn('Error handling output item added:', error);
428
+ return toolsData;
429
+ }
430
+ }
431
+
432
+ /**
433
+ * Handle function call arguments delta events
434
+ */
435
+ private handleFunctionCallArgumentsDelta(part: any, toolsData: ToolData[], emitter: EventEmitter): ToolData[] {
436
+ try {
437
+ if ('delta' in part && 'item_id' in part && typeof part.delta === 'string' && typeof part.item_id === 'string') {
438
+ const delta = part.delta;
439
+ const itemId = part.item_id;
440
+
441
+ const existingIndex = toolsData.findIndex((t) => t.id === itemId);
442
+ let updated: ToolData[];
443
+ let finalIndex: number;
444
+ if (existingIndex === -1) {
445
+ finalIndex = toolsData.length;
446
+ const newItem: ToolData = {
447
+ index: finalIndex,
448
+ id: itemId,
449
+ type: 'function',
450
+ name: '',
451
+ arguments: delta,
452
+ role: 'tool',
453
+ } as ToolData;
454
+ updated = [...toolsData, newItem];
455
+ } else {
456
+ finalIndex = existingIndex;
457
+ updated = toolsData.map((t, idx) => (idx === existingIndex ? { ...t, arguments: String(t.arguments || '') + delta } : t));
458
+ }
459
+
460
+ const entry = existingIndex === -1 ? updated[finalIndex] : updated[finalIndex];
461
+ emitter.emit('tool_call_progress', {
462
+ id: entry.callId || itemId,
463
+ name: entry.name,
464
+ arguments: entry.arguments,
465
+ delta: delta,
466
+ });
467
+
468
+ return updated;
469
+ }
470
+ return toolsData;
471
+ } catch (error) {
472
+ console.warn('Error handling function call arguments delta:', error);
473
+ return toolsData;
474
+ }
475
+ }
476
+
477
+ /**
478
+ * Handle function call arguments done events
479
+ */
480
+ private handleFunctionCallArgumentsDone(part: any, toolsData: ToolData[], emitter: EventEmitter): ToolData[] {
481
+ try {
482
+ const partAny = part;
483
+ if (partAny.item_id && partAny.arguments) {
484
+ const itemId = partAny.item_id;
485
+ const finalArguments = partAny.arguments;
486
+
487
+ const toolIndex = toolsData.findIndex((t) => t.id === itemId);
488
+ if (toolIndex !== -1) {
489
+ const updated = toolsData.map((t, idx) => (idx === toolIndex ? { ...t, arguments: finalArguments } : t));
490
+
491
+ const updatedEntry = updated[toolIndex];
492
+ emitter.emit('tool_call_completed', {
493
+ id: updatedEntry.callId || itemId,
494
+ name: updatedEntry.name,
495
+ arguments: finalArguments,
496
+ });
497
+
498
+ return updated;
499
+ }
500
+ }
501
+ return toolsData;
502
+ } catch (error) {
503
+ console.warn('Error handling function call arguments done:', error);
504
+ return toolsData;
505
+ }
506
+ }
507
+
508
+ /**
509
+ * Handle output item done events
510
+ */
511
+ private handleOutputItemDone(part: any, toolsData: ToolData[]): ToolData[] {
512
+ try {
513
+ const partAny = part as any;
514
+ if (partAny.item && partAny.item.type === 'function_call' && partAny.item.status === 'completed') {
515
+ const item = partAny.item;
516
+ const callId = item.call_id;
517
+ const itemId = item.id;
518
+
519
+ const toolIndex = toolsData.findIndex((t) => t.id === itemId || t.id === callId);
520
+ if (toolIndex !== -1 && item.arguments) {
521
+ const updated = toolsData.map((t, idx) =>
522
+ idx === toolIndex
523
+ ? {
524
+ ...t,
525
+ arguments: item.arguments,
526
+ callId: t.callId || callId,
527
+ }
528
+ : t
529
+ );
530
+ return updated;
531
+ }
532
+ }
533
+ return toolsData;
534
+ } catch (error) {
535
+ console.warn('Error handling output item done:', error);
536
+ return toolsData;
537
+ }
538
+ }
539
+
540
+ /**
541
+ * Handle completion events and unknown event types
542
+ */
543
+ private handleCompletionEvent(eventType: string): string {
544
+ if (eventType === EVENT_TYPES.RESPONSE_COMPLETED || eventType.includes('done')) {
545
+ return 'stop';
546
+ }
547
+ return 'stop'; // Default finish reason
548
+ }
549
+
550
+ public async prepareRequestBody(params: TLLMPreparedParams): Promise<OpenAI.Responses.ResponseCreateParams> {
551
+ let input = await this.prepareInputMessages(params);
552
+
553
+ // Apply tool message transformation to input messages
554
+ // There's a difference in the tools message data structures between `Chat Completions` and the `Response` interface.
555
+ // Since we don't have enough context for the interface in `transformToolMessageBlocks`, we need to perform the transformation here so it's compatible with the `Responses` interface.
556
+ input = this.applyToolMessageTransformation(input);
557
+
558
+ const body: OpenAI.Responses.ResponseCreateParams = {
559
+ model: params.model as string,
560
+ input,
561
+ };
562
+
563
+ // Handle max tokens
564
+ if (params?.maxTokens !== undefined) {
565
+ body.max_output_tokens = params.maxTokens;
566
+ }
567
+
568
+ // o3-pro does not support temperature
569
+ if (params?.temperature !== undefined && !MODELS_WITHOUT_TEMPERATURE_SUPPORT.includes(params.modelEntryName)) {
570
+ body.temperature = params.temperature;
571
+ }
572
+
573
+ if (params?.topP !== undefined) {
574
+ body.top_p = params.topP;
575
+ }
576
+
577
+ // #region GPT 5 specific fields
578
+
579
+ const isGPT5ReasoningModels = params.modelEntryName?.includes('gpt-5') && params?.capabilities?.reasoning;
580
+ if (isGPT5ReasoningModels && params?.verbosity) {
581
+ body.text = { verbosity: params.verbosity };
582
+ }
583
+
584
+ // We need to validate the `reasoningEffort` parameter for OpenAI models, since models like `qwen/qwen3-32b` and `deepseek-r1-distill-llama-70b` (available via Groq) also support this parameter but use different values, such as `none` and `default`. These values are valid in our system but not specifically for OpenAI.
585
+ if (isGPT5ReasoningModels && isValidOpenAIReasoningEffort(params.reasoningEffort)) {
586
+ body.reasoning = { effort: params.reasoningEffort };
587
+ }
588
+ // #endregion GPT 5 specific fields
589
+
590
+ let tools: OpenAI.Responses.Tool[] = [];
591
+
592
+ if (params?.toolsConfig?.tools && params?.toolsConfig?.tools?.length > 0) {
593
+ tools = await this.prepareFunctionTools(params);
594
+ }
595
+
596
+ // Add null safety check before accessing toolsInfo
597
+ if (params.toolsInfo?.openai?.webSearch?.enabled) {
598
+ const searchTool = this.prepareWebSearchTool(params);
599
+ tools.push(searchTool);
600
+ }
601
+
602
+ if (tools.length > 0) {
603
+ body.tools = tools;
604
+
605
+ if (params?.toolsConfig?.tool_choice) {
606
+ const toolChoice = params.toolsConfig.tool_choice;
607
+
608
+ // Validate tool choice before applying
609
+ if (this.validateToolChoice(toolChoice, tools)) {
610
+ if (typeof toolChoice === 'string') {
611
+ // Handle string-based tool choices
612
+ body.tool_choice = toolChoice;
613
+ } else if (typeof toolChoice === 'object' && toolChoice !== null) {
614
+ // Handle object-based tool choices (specific function selection)
615
+ if ('type' in toolChoice && toolChoice.type === 'function' && 'function' in toolChoice && 'name' in toolChoice.function) {
616
+ // Transform Chat Completions specific function choice to Responses API format
617
+ body.tool_choice = {
618
+ type: 'function',
619
+ name: toolChoice.function.name,
620
+ };
621
+ } else {
622
+ // For other object formats, pass through with type assertion
623
+ body.tool_choice = toolChoice as any;
624
+ }
625
+ }
626
+ } else {
627
+ body.tool_choice = 'auto';
628
+ }
629
+ } else {
630
+ // Default to auto if tools are present but no choice is specified
631
+ body.tool_choice = 'auto';
632
+ }
633
+ }
634
+
635
+ return body;
636
+ }
637
+
638
+ /**
639
+ * Transform OpenAI tool definitions to Responses.Tool format
640
+ * Handles multiple tool definition formats and ensures compatibility
641
+ */
642
+ public transformToolsConfig(config: ToolConfig): OpenAI.Responses.Tool[] {
643
+ if (!config?.toolDefinitions || !Array.isArray(config.toolDefinitions)) {
644
+ return [];
645
+ }
646
+
647
+ return config.toolDefinitions
648
+ .map((tool, index) => {
649
+ // Validate basic tool structure
650
+ if (!tool || typeof tool !== 'object') {
651
+ // Return a minimal tool structure for compatibility
652
+ return {
653
+ type: 'function' as const,
654
+ name: undefined,
655
+ description: undefined,
656
+ parameters: {
657
+ type: 'object',
658
+ properties: undefined,
659
+ required: undefined,
660
+ },
661
+ strict: false,
662
+ } as OpenAI.Responses.Tool;
663
+ }
664
+
665
+ // Handle tools that are already in ChatCompletionTool format (with nested function object)
666
+ if ('function' in tool && tool.function && typeof tool.function === 'object' && tool.function !== null) {
667
+ const funcTool = tool.function as { name: string; description?: string; parameters?: any };
668
+
669
+ if (!funcTool.name || typeof funcTool.name !== 'string') {
670
+ return {
671
+ type: 'function' as const,
672
+ name: undefined,
673
+ description: tool.description || '',
674
+ parameters: { type: 'object', properties: undefined, required: undefined },
675
+ strict: false,
676
+ } as OpenAI.Responses.Tool;
677
+ }
678
+
679
+ return {
680
+ type: 'function' as const,
681
+ name: funcTool.name,
682
+ description: funcTool.description || tool.description || '',
683
+ parameters: funcTool.parameters || { type: 'object', properties: {}, required: [] },
684
+ strict: false,
685
+ } as OpenAI.Responses.Tool;
686
+ }
687
+
688
+ // Handle OpenAI tool definition format (direct parameters)
689
+ if ('parameters' in tool) {
690
+ return {
691
+ type: 'function' as const,
692
+ name: tool.name,
693
+ description: tool.description || '',
694
+ parameters: tool.parameters || { type: 'object', properties: {}, required: [] },
695
+ strict: false,
696
+ } as OpenAI.Responses.Tool;
697
+ }
698
+
699
+ // Handle legacy format for backward compatibility
700
+ const legacyTool = tool as any;
701
+ return {
702
+ type: 'function' as const,
703
+ name: tool.name,
704
+ description: tool.description || legacyTool.desc,
705
+ parameters: {
706
+ type: 'object',
707
+ properties: legacyTool.properties,
708
+ required: legacyTool.requiredFields || legacyTool.required,
709
+ },
710
+ strict: false,
711
+ } as OpenAI.Responses.Tool;
712
+ })
713
+ .filter(Boolean) as OpenAI.Responses.Tool[];
714
+ }
715
+
716
+ /**
717
+ * Normalize tool arguments to string format for Responses API
718
+ */
719
+ private normalizeToolArguments(args: any): string {
720
+ if (typeof args === 'string') {
721
+ // If it's already a string, validate it's proper JSON
722
+ try {
723
+ JSON.parse(args);
724
+ return args;
725
+ } catch {
726
+ // If not valid JSON, wrap it in quotes to make it valid
727
+ return JSON.stringify(args);
728
+ }
729
+ }
730
+
731
+ if (typeof args === 'object' && args !== null) {
732
+ try {
733
+ return JSON.stringify(args);
734
+ } catch (error) {
735
+ return '{}'; // Fallback to empty object
736
+ }
737
+ }
738
+
739
+ if (args === undefined || args === null) {
740
+ return '{}';
741
+ }
742
+
743
+ // For primitive types, convert to JSON
744
+ return JSON.stringify(args);
745
+ }
746
+
747
+ /**
748
+ * Validate if tool data is complete and valid for transformation
749
+ */
750
+ private isValidToolData(toolData: ToolData): boolean {
751
+ return !!(toolData && toolData.id && toolData.name && (toolData.result !== undefined || toolData.error !== undefined));
752
+ }
753
+
754
+ async handleFileAttachments(files: BinaryInput[], agentId: string, messages: any[]): Promise<any[]> {
755
+ if (files.length === 0) return messages;
756
+
757
+ const uploadedFiles = await this.uploadFiles(files, agentId);
758
+ const validImageFiles = this.getValidImageFiles(uploadedFiles);
759
+ const validDocumentFiles = this.getValidDocumentFiles(uploadedFiles);
760
+
761
+ // Process images and documents with Responses API specific formatting
762
+ const imageData = await this.processImageData(validImageFiles, agentId);
763
+ const documentData = await this.processDocumentData(validDocumentFiles, agentId);
764
+
765
+ // Find the last user message and add files to it
766
+ for (let i = messages.length - 1; i >= 0; i--) {
767
+ if (messages[i].role === 'user') {
768
+ // Ensure content is an array before pushing files
769
+ if (typeof messages[i].content === 'string') {
770
+ messages[i].content = [{ type: 'input_text', text: messages[i].content }];
771
+ } else if (!Array.isArray(messages[i].content)) {
772
+ messages[i].content = [];
773
+ }
774
+ messages[i].content.push(...imageData, ...documentData);
775
+ break;
776
+ }
777
+ }
778
+
779
+ // If no user message found, create one with files
780
+ if (!messages.some((item) => item.role === 'user')) {
781
+ messages.push({
782
+ role: 'user',
783
+ content: [...imageData, ...documentData],
784
+ });
785
+ }
786
+
787
+ return messages;
788
+ }
789
+
790
+ /**
791
+ * Get valid image files based on supported MIME types
792
+ */
793
+ private getValidImageFiles(files: BinaryInput[]): BinaryInput[] {
794
+ return files.filter((file) => this.validImageMimeTypes.includes(file?.mimetype));
795
+ }
796
+
797
+ /**
798
+ * Get valid document files based on supported MIME types
799
+ */
800
+ private getValidDocumentFiles(files: BinaryInput[]): BinaryInput[] {
801
+ return files.filter((file) => this.validDocumentMimeTypes.includes(file?.mimetype));
802
+ }
803
+
804
+ /**
805
+ * Upload files to storage
806
+ */
807
+ private async uploadFiles(files: BinaryInput[], agentId: string): Promise<BinaryInput[]> {
808
+ const promises = files.map((file) => {
809
+ const binaryInput = BinaryInput.from(file);
810
+ return binaryInput.upload(AccessCandidate.agent(agentId)).then(() => binaryInput);
811
+ });
812
+
813
+ return Promise.all(promises);
814
+ }
815
+
816
+ /**
817
+ * Process image files with Responses API specific formatting
818
+ */
819
+ private async processImageData(files: BinaryInput[], agentId: string): Promise<any[]> {
820
+ if (files.length === 0) return [];
821
+
822
+ const imageData = [];
823
+ for (const file of files) {
824
+ await this.validateFileSize(file, MAX_IMAGE_SIZE, 'Image');
825
+
826
+ const bufferData = await file.readData(AccessCandidate.agent(agentId));
827
+ const base64Data = bufferData.toString('base64');
828
+ const url = `data:${file.mimetype};base64,${base64Data}`;
829
+
830
+ imageData.push({
831
+ type: 'input_image',
832
+ image_url: url,
833
+ });
834
+ }
835
+
836
+ return imageData;
837
+ }
838
+
839
+ /**
840
+ * Process document files with Responses API specific formatting
841
+ */
842
+ private async processDocumentData(files: BinaryInput[], agentId: string): Promise<any[]> {
843
+ if (files.length === 0) return [];
844
+
845
+ const documentData = [];
846
+ for (const file of files) {
847
+ await this.validateFileSize(file, MAX_DOCUMENT_SIZE, 'Document');
848
+
849
+ const bufferData = await file.readData(AccessCandidate.agent(agentId));
850
+ const base64Data = bufferData.toString('base64');
851
+ const fileData = `data:${file.mimetype};base64,${base64Data}`;
852
+ const filename = await file.getName();
853
+
854
+ documentData.push({
855
+ type: 'input_file',
856
+ file: {
857
+ file_data: fileData,
858
+ filename,
859
+ },
860
+ });
861
+ }
862
+
863
+ return documentData;
864
+ }
865
+
866
+ /**
867
+ * Validate file size before processing
868
+ */
869
+ private async validateFileSize(file: BinaryInput, maxSize: number, fileType: string): Promise<void> {
870
+ await file.ready();
871
+ const fileInfo = await file.getJsonData(AccessCandidate.agent('temp'));
872
+ if (fileInfo.size > maxSize) {
873
+ throw new Error(`${fileType} file size (${fileInfo.size} bytes) exceeds maximum allowed size of ${maxSize} bytes`);
874
+ }
875
+ }
876
+
877
+ getInterfaceName(): string {
878
+ return 'responses';
879
+ }
880
+
881
+ validateParameters(params: TLLMParams): boolean {
882
+ // Basic validation for Responses API parameters
883
+ return !!params.model;
884
+ }
885
+
886
+ /**
887
+ * Prepare input messages for Responses API
888
+ */
889
+ private async prepareInputMessages(params: TLLMParams): Promise<any[]> {
890
+ const messages = params?.messages || [];
891
+ const files: BinaryInput[] = params?.files || [];
892
+
893
+ // Start with raw messages - transformation now happens in applyToolMessageTransformation
894
+ let input = [...messages];
895
+
896
+ // Handle files if present
897
+ if (files.length > 0) {
898
+ input = await this.handleFileAttachments(files, params.agentId, input);
899
+ }
900
+
901
+ return input;
902
+ }
903
+
904
+ /**
905
+ * Prepare function tools for Responses API request
906
+ * Transforms tools from various formats to Responses API format
907
+ */
908
+ private async prepareFunctionTools(params: TLLMParams): Promise<OpenAI.Responses.Tool[]> {
909
+ const tools: OpenAI.Responses.Tool[] = [];
910
+
911
+ // Validate and process function tools
912
+ if (params?.toolsConfig?.tools && Array.isArray(params.toolsConfig.tools) && params.toolsConfig.tools.length > 0) {
913
+ try {
914
+ // Transform tools using the enhanced transformToolsConfig method
915
+ const toolsConfig = this.transformToolsConfig({
916
+ type: 'function',
917
+ toolDefinitions: params.toolsConfig.tools as any[],
918
+ toolChoice: params.toolsConfig.tool_choice || 'auto',
919
+ modelInfo: (params.modelInfo as LLMModelInfo) || null,
920
+ });
921
+
922
+ // Validate transformed tools before adding them
923
+ const validTools = toolsConfig.filter((tool, index) => {
924
+ if (tool.type !== 'function' || !(tool as any).name) {
925
+ return false;
926
+ }
927
+ return true;
928
+ });
929
+
930
+ tools.push(...validTools);
931
+ } catch (error) {
932
+ // Don't throw here to allow the request to continue without tools
933
+ // This provides better resilience in production
934
+ }
935
+ }
936
+
937
+ return tools;
938
+ }
939
+
940
+ /**
941
+ * Get web search tool configuration for OpenAI Responses API
942
+ * According to OpenAI documentation: https://platform.openai.com/docs/api-reference/responses/create
943
+ */
944
+ private prepareWebSearchTool(params: TLLMPreparedParams): OpenAI.Responses.WebSearchTool {
945
+ const webSearch = params?.toolsInfo?.openai?.webSearch;
946
+ const contextSize = webSearch?.contextSize;
947
+ const searchCity = webSearch?.city;
948
+ const searchCountry = webSearch?.country;
949
+ const searchRegion = webSearch?.region;
950
+ const searchTimezone = webSearch?.timezone;
951
+
952
+ // Prepare location object - build incrementally if any location parameters exist
953
+ const userLocation: TSearchLocation = {
954
+ type: 'approximate', // Required, always be 'approximate' when we implement location
955
+ };
956
+
957
+ // Add location fields if they exist
958
+ if (searchCity) userLocation.city = searchCity;
959
+ if (searchCountry) userLocation.country = searchCountry;
960
+ if (searchRegion) userLocation.region = searchRegion;
961
+ if (searchTimezone) userLocation.timezone = searchTimezone;
962
+
963
+ // Only include location in config if we have actual location data
964
+ const hasLocationData = searchCity || searchCountry || searchRegion || searchTimezone;
965
+
966
+ // Configure web search tool according to OpenAI Responses API specification
967
+ const searchTool = {
968
+ type: 'web_search_preview' as const, // Use literal type to ensure consistency
969
+ };
970
+
971
+ // Add optional configuration properties
972
+ const webSearchConfig: any = {};
973
+
974
+ if (contextSize) {
975
+ webSearchConfig.search_context_size = contextSize;
976
+ }
977
+
978
+ if (hasLocationData) {
979
+ webSearchConfig.user_location = userLocation;
980
+ }
981
+
982
+ return { ...searchTool, ...webSearchConfig };
983
+ }
984
+
985
+ /**
986
+ * Transform messages for Responses API compatibility
987
+ * Handles the differences between Chat Completions and Responses API message formats
988
+ */
989
+ private applyToolMessageTransformation(input: any[]): any[] {
990
+ const transformedMessages: any[] = [];
991
+
992
+ for (let i = 0; i < input.length; i++) {
993
+ const message = input[i];
994
+
995
+ try {
996
+ if (message.role === 'assistant' && message.tool_calls && Array.isArray(message.tool_calls)) {
997
+ // Split assistant message with tool_calls into separate items (Responses API format)
998
+
999
+ // Add assistant content first if present
1000
+ if (message.content !== undefined && message.content !== null) {
1001
+ const contentStr = typeof message.content === 'string' ? message.content : JSON.stringify(message.content);
1002
+ if (contentStr.trim().length > 0) {
1003
+ transformedMessages.push({
1004
+ role: 'assistant',
1005
+ content: contentStr,
1006
+ });
1007
+ }
1008
+ }
1009
+
1010
+ // Transform each tool call to function_call format
1011
+ message.tool_calls.forEach((toolCall: any, index: number) => {
1012
+ if (!toolCall || !toolCall.function) {
1013
+ return;
1014
+ }
1015
+
1016
+ const functionArgs = toolCall.function.arguments;
1017
+ const normalizedArgs =
1018
+ functionArgs === undefined || functionArgs === null
1019
+ ? undefined
1020
+ : typeof functionArgs === 'object'
1021
+ ? JSON.stringify(functionArgs)
1022
+ : String(functionArgs);
1023
+
1024
+ transformedMessages.push({
1025
+ type: 'function_call',
1026
+ name: toolCall.function.name || '',
1027
+ arguments: normalizedArgs,
1028
+ call_id: toolCall.id || toolCall.call_id || `call_${Date.now()}_${index}`, // Ensure unique ID
1029
+ });
1030
+ });
1031
+ } else if (message.role === 'tool') {
1032
+ // Transform tool message to function_call_output (Responses API format)
1033
+ if (!message.tool_call_id) {
1034
+ return;
1035
+ }
1036
+
1037
+ const outputContent = message.content;
1038
+ const normalizedOutput = typeof outputContent === 'string' ? outputContent : JSON.stringify(outputContent || 'null');
1039
+
1040
+ transformedMessages.push({
1041
+ type: 'function_call_output',
1042
+ call_id: message.tool_call_id,
1043
+ output: normalizedOutput,
1044
+ });
1045
+ } else {
1046
+ // Pass through other message types without content modification
1047
+ // The Responses API can handle various content formats
1048
+ transformedMessages.push(message);
1049
+ }
1050
+ } catch (error) {
1051
+ // Add the original message to prevent data loss
1052
+ transformedMessages.push(message);
1053
+ }
1054
+ }
1055
+
1056
+ // Validate the final message structure
1057
+ const validMessages = transformedMessages.filter((msg, index) => {
1058
+ if (!msg || typeof msg !== 'object') {
1059
+ return false;
1060
+ }
1061
+ return true;
1062
+ });
1063
+
1064
+ return validMessages;
1065
+ }
1066
+
1067
+ /**
1068
+ * Get search tool cost for a specific model and context size
1069
+ */
1070
+ private getSearchToolCost(modelName: string): number {
1071
+ if (!modelName) return 0;
1072
+ // Normalize: remove built-in prefix and compare case-insensitively
1073
+ const normalized = String(modelName)
1074
+ .toLowerCase()
1075
+ .replace(/^smythos\//, '');
1076
+
1077
+ // Match by prefix with any configured family in SEARCH_TOOL_COSTS
1078
+ const match = Object.entries(SEARCH_TOOL_COSTS).find(([family]) => normalized.startsWith(family));
1079
+ return match ? (match[1] as number) : 0;
1080
+ }
1081
+
1082
+ /**
1083
+ * Process function call responses and integrate them back into the conversation
1084
+ * This method helps maintain compatibility with the chat completion flow
1085
+ */
1086
+ public async processFunctionCallResults(toolsData: ToolData[]): Promise<ToolData[]> {
1087
+ const processedTools: ToolData[] = [];
1088
+
1089
+ for (const tool of toolsData) {
1090
+ if (!this.isValidToolData(tool)) {
1091
+ continue;
1092
+ }
1093
+
1094
+ try {
1095
+ const processedTool: ToolData = {
1096
+ ...tool,
1097
+ // Ensure arguments are properly formatted as JSON string
1098
+ arguments: this.normalizeToolArguments(tool.arguments),
1099
+ // Ensure function property is properly structured for compatibility
1100
+ function: tool.function || {
1101
+ name: tool.name,
1102
+ arguments: this.normalizeToolArguments(tool.arguments),
1103
+ },
1104
+ };
1105
+
1106
+ processedTools.push(processedTool);
1107
+ } catch (error) {
1108
+ // Add error information to the tool result
1109
+ processedTools.push({
1110
+ ...tool,
1111
+ error: error instanceof Error ? error.message : 'Unknown processing error',
1112
+ result: undefined,
1113
+ });
1114
+ }
1115
+ }
1116
+
1117
+ return processedTools;
1118
+ }
1119
+
1120
+ /**
1121
+ * Validate tool choice parameter for Responses API
1122
+ */
1123
+ private validateToolChoice(toolChoice: any, availableTools: OpenAI.Responses.Tool[]): boolean {
1124
+ if (!toolChoice) return true;
1125
+
1126
+ if (typeof toolChoice === 'string') {
1127
+ const validStringChoices = ['auto', 'required', 'none'];
1128
+ return validStringChoices.includes(toolChoice);
1129
+ }
1130
+
1131
+ if (typeof toolChoice === 'object' && toolChoice !== null) {
1132
+ // For specific function selection
1133
+ if (toolChoice.type === 'function' && toolChoice.function?.name) {
1134
+ // Check if the specified function exists in available tools
1135
+ return availableTools.some((tool) => tool.type === 'function' && tool.name === toolChoice.function.name);
1136
+ }
1137
+ }
1138
+
1139
+ return false;
1140
+ }
1141
+
1142
+ /**
1143
+ * Upsert a web search tool entry in toolsData and return its index
1144
+ */
1145
+ private upsertWebSearchToolImmutable(toolsData: ToolData[], id: string, args: string = ''): { toolsData: ToolData[]; index: number } {
1146
+ const existingIndex = toolsData.findIndex((t) => t.id === id);
1147
+ if (existingIndex === -1) {
1148
+ const index = toolsData.length;
1149
+ const newItem: ToolData = {
1150
+ index,
1151
+ id,
1152
+ type: TToolType.WebSearch,
1153
+ name: 'web_search',
1154
+ arguments: args,
1155
+ role: 'tool',
1156
+ } as ToolData;
1157
+ const updated: ToolData[] = [...toolsData, newItem];
1158
+ return { toolsData: updated, index };
1159
+ }
1160
+
1161
+ if (args) {
1162
+ const updated: ToolData[] = toolsData.map((t, idx) => (idx === existingIndex ? { ...t, arguments: args } : t));
1163
+ return { toolsData: updated, index: existingIndex };
1164
+ }
1165
+
1166
+ return { toolsData, index: existingIndex };
1167
+ }
1168
+ }