@robota-sdk/agent-provider 3.0.0-beta.64

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 (220) hide show
  1. package/LICENSE +21 -0
  2. package/dist/browser/index.d.ts +1104 -0
  3. package/dist/browser/index.d.ts.map +1 -0
  4. package/dist/browser/index.js +7 -0
  5. package/dist/browser/index.js.map +1 -0
  6. package/dist/loggers/index.cjs +1 -0
  7. package/dist/loggers/index.d.ts +151 -0
  8. package/dist/loggers/index.d.ts.map +1 -0
  9. package/dist/loggers/index.js +2 -0
  10. package/dist/loggers/index.js.map +1 -0
  11. package/dist/node/anthropic/index.cjs +1 -0
  12. package/dist/node/anthropic/index.d.ts +158 -0
  13. package/dist/node/anthropic/index.d.ts.map +1 -0
  14. package/dist/node/anthropic/index.js +1 -0
  15. package/dist/node/anthropic--1vgLC-e.js +5 -0
  16. package/dist/node/anthropic--1vgLC-e.js.map +1 -0
  17. package/dist/node/anthropic-BFQ6DSCP.cjs +4 -0
  18. package/dist/node/bytedance/index.cjs +1 -0
  19. package/dist/node/bytedance/index.d.ts +74 -0
  20. package/dist/node/bytedance/index.d.ts.map +1 -0
  21. package/dist/node/bytedance/index.js +1 -0
  22. package/dist/node/bytedance-C_0sF_pJ.js +2 -0
  23. package/dist/node/bytedance-C_0sF_pJ.js.map +1 -0
  24. package/dist/node/bytedance-DVPxqEiC.cjs +1 -0
  25. package/dist/node/chunk-Bmb41Sf3.cjs +1 -0
  26. package/dist/node/deepseek/index.cjs +1 -0
  27. package/dist/node/deepseek/index.d.ts +2 -0
  28. package/dist/node/deepseek/index.js +1 -0
  29. package/dist/node/deepseek-_8Ixx7rA.js +2 -0
  30. package/dist/node/deepseek-_8Ixx7rA.js.map +1 -0
  31. package/dist/node/deepseek-oA2Y6bD0.cjs +1 -0
  32. package/dist/node/gemini/index.cjs +1 -0
  33. package/dist/node/gemini/index.d.ts +173 -0
  34. package/dist/node/gemini/index.d.ts.map +1 -0
  35. package/dist/node/gemini/index.js +1 -0
  36. package/dist/node/gemini-Bh2U87MY.js +4 -0
  37. package/dist/node/gemini-Bh2U87MY.js.map +1 -0
  38. package/dist/node/gemini-DSaNCxZj.cjs +3 -0
  39. package/dist/node/gemma/index.cjs +1 -0
  40. package/dist/node/gemma/index.d.ts +2 -0
  41. package/dist/node/gemma/index.js +1 -0
  42. package/dist/node/gemma-Dp_AfCUR.js +2 -0
  43. package/dist/node/gemma-Dp_AfCUR.js.map +1 -0
  44. package/dist/node/gemma-G-Pf_PnX.cjs +1 -0
  45. package/dist/node/google/index.cjs +1 -0
  46. package/dist/node/google/index.d.ts +14 -0
  47. package/dist/node/google/index.d.ts.map +1 -0
  48. package/dist/node/google/index.js +2 -0
  49. package/dist/node/google/index.js.map +1 -0
  50. package/dist/node/index-B6PnlDMd.d.ts +82 -0
  51. package/dist/node/index-B6PnlDMd.d.ts.map +1 -0
  52. package/dist/node/index-B7UvPJcI.d.ts +315 -0
  53. package/dist/node/index-B7UvPJcI.d.ts.map +1 -0
  54. package/dist/node/index-BLPOTNb5.d.ts +98 -0
  55. package/dist/node/index-BLPOTNb5.d.ts.map +1 -0
  56. package/dist/node/index-BqixM_XD.d.ts +231 -0
  57. package/dist/node/index-BqixM_XD.d.ts.map +1 -0
  58. package/dist/node/index-C3beaqKO.d.ts +231 -0
  59. package/dist/node/index-C3beaqKO.d.ts.map +1 -0
  60. package/dist/node/index-Cp2XRh9G.d.ts +82 -0
  61. package/dist/node/index-Cp2XRh9G.d.ts.map +1 -0
  62. package/dist/node/index-DSv5xruI.d.ts +98 -0
  63. package/dist/node/index-DSv5xruI.d.ts.map +1 -0
  64. package/dist/node/index-w0bV1uaP.d.ts +315 -0
  65. package/dist/node/index-w0bV1uaP.d.ts.map +1 -0
  66. package/dist/node/index.cjs +1 -0
  67. package/dist/node/index.d.ts +8 -0
  68. package/dist/node/index.js +1 -0
  69. package/dist/node/openai/index.cjs +1 -0
  70. package/dist/node/openai/index.d.ts +2 -0
  71. package/dist/node/openai/index.js +1 -0
  72. package/dist/node/openai-CRQjg4xF.js +2 -0
  73. package/dist/node/openai-CRQjg4xF.js.map +1 -0
  74. package/dist/node/openai-compatible-BYfyY5lb.cjs +1 -0
  75. package/dist/node/openai-compatible-Dm4Sof9e.js +2 -0
  76. package/dist/node/openai-compatible-Dm4Sof9e.js.map +1 -0
  77. package/dist/node/openai-xWC6pY7r.cjs +1 -0
  78. package/dist/node/qwen/index.cjs +1 -0
  79. package/dist/node/qwen/index.d.ts +2 -0
  80. package/dist/node/qwen/index.js +1 -0
  81. package/dist/node/qwen-ChUZobTL.js +2 -0
  82. package/dist/node/qwen-ChUZobTL.js.map +1 -0
  83. package/dist/node/qwen-CjT71vSM.cjs +1 -0
  84. package/package.json +157 -0
  85. package/src/anthropic/__tests__/abort-streaming.test.ts +199 -0
  86. package/src/anthropic/__tests__/model-catalog-refresh.test.ts +92 -0
  87. package/src/anthropic/__tests__/provider-definition.test.ts +55 -0
  88. package/src/anthropic/__tests__/provider.test.ts +1357 -0
  89. package/src/anthropic/__tests__/response-parser.test.ts +326 -0
  90. package/src/anthropic/index.ts +22 -0
  91. package/src/anthropic/message-converter.ts +181 -0
  92. package/src/anthropic/model-catalog-refresh.ts +128 -0
  93. package/src/anthropic/parsers/response-parser.ts +184 -0
  94. package/src/anthropic/provider-definition.ts +93 -0
  95. package/src/anthropic/provider.ts +290 -0
  96. package/src/anthropic/streaming-handler.ts +204 -0
  97. package/src/anthropic/types/api-types.ts +158 -0
  98. package/src/anthropic/types.ts +79 -0
  99. package/src/bytedance/http-client.test.ts +288 -0
  100. package/src/bytedance/http-client.ts +163 -0
  101. package/src/bytedance/index.ts +2 -0
  102. package/src/bytedance/provider.spec.ts +320 -0
  103. package/src/bytedance/provider.ts +171 -0
  104. package/src/bytedance/status-mapper.test.ts +299 -0
  105. package/src/bytedance/status-mapper.ts +141 -0
  106. package/src/bytedance/types.ts +68 -0
  107. package/src/deepseek/defaults.ts +4 -0
  108. package/src/deepseek/index.ts +22 -0
  109. package/src/deepseek/model-catalog-refresh.test.ts +57 -0
  110. package/src/deepseek/model-catalog-refresh.ts +105 -0
  111. package/src/deepseek/model-catalog.ts +55 -0
  112. package/src/deepseek/provider-definition.test.ts +109 -0
  113. package/src/deepseek/provider-definition.ts +132 -0
  114. package/src/deepseek/provider.test.ts +324 -0
  115. package/src/deepseek/provider.ts +298 -0
  116. package/src/deepseek/types.ts +37 -0
  117. package/src/gemini/execution-helpers.ts +233 -0
  118. package/src/gemini/genai-transport.test.ts +208 -0
  119. package/src/gemini/image-operations.test.ts +448 -0
  120. package/src/gemini/image-operations.ts +261 -0
  121. package/src/gemini/index.ts +11 -0
  122. package/src/gemini/message-converter.test.ts +616 -0
  123. package/src/gemini/message-converter.ts +140 -0
  124. package/src/gemini/model-catalog-refresh.test.ts +107 -0
  125. package/src/gemini/model-catalog-refresh.ts +92 -0
  126. package/src/gemini/provider-definition.test.ts +70 -0
  127. package/src/gemini/provider-definition.ts +78 -0
  128. package/src/gemini/provider-extended.test.ts +898 -0
  129. package/src/gemini/provider.spec.ts +216 -0
  130. package/src/gemini/provider.ts +279 -0
  131. package/src/gemini/request-converter.ts +226 -0
  132. package/src/gemini/tool-schema-converter.ts +78 -0
  133. package/src/gemini/types/api-types.ts +235 -0
  134. package/src/gemini/types.ts +121 -0
  135. package/src/gemma/index.ts +5 -0
  136. package/src/gemma/message-factory.ts +38 -0
  137. package/src/gemma/provider-definition.test.ts +43 -0
  138. package/src/gemma/provider-definition.ts +84 -0
  139. package/src/gemma/provider-projection.ts +49 -0
  140. package/src/gemma/provider.test.ts +628 -0
  141. package/src/gemma/provider.ts +308 -0
  142. package/src/gemma/pseudo-command-envelope.ts +58 -0
  143. package/src/gemma/pseudo-tool-call-projector.ts +243 -0
  144. package/src/gemma/pseudo-tool-call-tag-parser.ts +153 -0
  145. package/src/gemma/pseudo-tool-call-types.ts +31 -0
  146. package/src/gemma/reasoning-projector.test.ts +52 -0
  147. package/src/gemma/reasoning-projector.ts +144 -0
  148. package/src/gemma/streaming-projection.ts +79 -0
  149. package/src/gemma/tool-call-argument-parser.ts +126 -0
  150. package/src/gemma/tool-call-projector.test.ts +227 -0
  151. package/src/gemma/tool-call-projector.ts +264 -0
  152. package/src/gemma/types.ts +27 -0
  153. package/src/google/index.ts +11 -0
  154. package/src/google/provider-compat.test.ts +19 -0
  155. package/src/google/provider-definition.ts +6 -0
  156. package/src/google/provider.ts +10 -0
  157. package/src/google/types.ts +5 -0
  158. package/src/index.ts +9 -0
  159. package/src/openai/adapter.test.ts +494 -0
  160. package/src/openai/adapter.ts +145 -0
  161. package/src/openai/chat-completions-chat.ts +189 -0
  162. package/src/openai/executor-integration.test.ts +206 -0
  163. package/src/openai/index.ts +21 -0
  164. package/src/openai/interfaces/payload-logger.ts +48 -0
  165. package/src/openai/loggers/console-payload-logger.test.ts +173 -0
  166. package/src/openai/loggers/console-payload-logger.ts +94 -0
  167. package/src/openai/loggers/console.ts +9 -0
  168. package/src/openai/loggers/file-payload-logger.test.ts +238 -0
  169. package/src/openai/loggers/file-payload-logger.ts +112 -0
  170. package/src/openai/loggers/file.ts +9 -0
  171. package/src/openai/loggers/index.ts +12 -0
  172. package/src/openai/loggers/sanitize-openai-log-data.test.ts +89 -0
  173. package/src/openai/loggers/sanitize-openai-log-data.ts +14 -0
  174. package/src/openai/message-converter.ts +22 -0
  175. package/src/openai/model-catalog-refresh.test.ts +92 -0
  176. package/src/openai/model-catalog-refresh.ts +115 -0
  177. package/src/openai/openai-request-format.ts +92 -0
  178. package/src/openai/parsers/response-parser.test.ts +407 -0
  179. package/src/openai/parsers/response-parser.ts +47 -0
  180. package/src/openai/provider-definition.test.ts +75 -0
  181. package/src/openai/provider-definition.ts +132 -0
  182. package/src/openai/provider.test.ts +1402 -0
  183. package/src/openai/provider.ts +237 -0
  184. package/src/openai/responses-chat.ts +258 -0
  185. package/src/openai/responses-converter.ts +112 -0
  186. package/src/openai/responses-parser.ts +285 -0
  187. package/src/openai/responses-stream-utils.ts +45 -0
  188. package/src/openai/responses-types.ts +195 -0
  189. package/src/openai/streaming/stream-assembler.ts +3 -0
  190. package/src/openai/streaming/stream-handler.test.ts +367 -0
  191. package/src/openai/streaming/stream-handler.ts +119 -0
  192. package/src/openai/types/api-types.ts +112 -0
  193. package/src/openai/types.ts +194 -0
  194. package/src/qwen/defaults.ts +26 -0
  195. package/src/qwen/index.ts +5 -0
  196. package/src/qwen/model-catalog-refresh.test.ts +91 -0
  197. package/src/qwen/model-catalog-refresh.ts +97 -0
  198. package/src/qwen/provider-capabilities.ts +34 -0
  199. package/src/qwen/provider-definition.test.ts +139 -0
  200. package/src/qwen/provider-definition.ts +173 -0
  201. package/src/qwen/provider-streaming-assembly.ts +40 -0
  202. package/src/qwen/provider.test.ts +640 -0
  203. package/src/qwen/provider.ts +293 -0
  204. package/src/qwen/responses-chat.ts +194 -0
  205. package/src/qwen/responses-converter.ts +104 -0
  206. package/src/qwen/responses-parser.ts +299 -0
  207. package/src/qwen/responses-stream-utils.ts +38 -0
  208. package/src/qwen/types.ts +228 -0
  209. package/src/shared/openai-compatible/endpoint-probe.test.ts +52 -0
  210. package/src/shared/openai-compatible/endpoint-probe.ts +43 -0
  211. package/src/shared/openai-compatible/index.ts +6 -0
  212. package/src/shared/openai-compatible/message-converter.test.ts +111 -0
  213. package/src/shared/openai-compatible/message-converter.ts +84 -0
  214. package/src/shared/openai-compatible/native-payload-observer.test.ts +43 -0
  215. package/src/shared/openai-compatible/native-payload-observer.ts +26 -0
  216. package/src/shared/openai-compatible/response-parser.test.ts +172 -0
  217. package/src/shared/openai-compatible/response-parser.ts +180 -0
  218. package/src/shared/openai-compatible/stream-assembler.test.ts +266 -0
  219. package/src/shared/openai-compatible/stream-assembler.ts +248 -0
  220. package/src/shared/openai-compatible/types.ts +59 -0
@@ -0,0 +1,367 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { OpenAIStreamHandler } from './stream-handler';
3
+ import type { IPayloadLogger } from '../interfaces/payload-logger';
4
+ import type { ILogger } from '@robota-sdk/agent-core';
5
+ import type { IOpenAIStreamRequestParams, IOpenAIChatRequestParams } from '../types/api-types';
6
+ import type OpenAI from 'openai';
7
+
8
+ function createMockLogger(): ILogger {
9
+ return {
10
+ info: vi.fn(),
11
+ warn: vi.fn(),
12
+ error: vi.fn(),
13
+ debug: vi.fn(),
14
+ log: vi.fn(),
15
+ };
16
+ }
17
+
18
+ function createMockPayloadLogger(enabled = true): IPayloadLogger {
19
+ return {
20
+ isEnabled: vi.fn().mockReturnValue(enabled),
21
+ logPayload: vi.fn().mockResolvedValue(undefined),
22
+ };
23
+ }
24
+
25
+ // Create an async iterable from an array of chunks
26
+ async function* asyncIterableFrom<T>(items: T[]): AsyncIterable<T> {
27
+ for (const item of items) {
28
+ yield item;
29
+ }
30
+ }
31
+
32
+ function createMockClient(chunks: OpenAI.Chat.ChatCompletionChunk[]): OpenAI {
33
+ return {
34
+ chat: {
35
+ completions: {
36
+ create: vi.fn().mockResolvedValue(asyncIterableFrom(chunks)),
37
+ },
38
+ },
39
+ } as unknown as OpenAI;
40
+ }
41
+
42
+ function createStreamChunk(
43
+ content: string,
44
+ finishReason: string | null = null,
45
+ ): OpenAI.Chat.ChatCompletionChunk {
46
+ return {
47
+ id: 'chatcmpl-chunk',
48
+ object: 'chat.completion.chunk',
49
+ created: Date.now(),
50
+ model: 'gpt-4',
51
+ choices: [
52
+ {
53
+ index: 0,
54
+ delta: { content },
55
+ finish_reason: finishReason,
56
+ logprobs: null,
57
+ },
58
+ ],
59
+ } as OpenAI.Chat.ChatCompletionChunk;
60
+ }
61
+
62
+ describe('OpenAIStreamHandler', () => {
63
+ let mockClient: OpenAI;
64
+
65
+ beforeEach(() => {
66
+ vi.clearAllMocks();
67
+ });
68
+
69
+ describe('handleStream', () => {
70
+ it('should yield parsed messages from stream', async () => {
71
+ const chunks = [
72
+ createStreamChunk('Hello'),
73
+ createStreamChunk(' world'),
74
+ createStreamChunk('', 'stop'),
75
+ ];
76
+ mockClient = createMockClient(chunks);
77
+ const handler = new OpenAIStreamHandler(mockClient);
78
+
79
+ const requestParams: IOpenAIStreamRequestParams = {
80
+ model: 'gpt-4',
81
+ messages: [{ role: 'user', content: 'Hi' }],
82
+ stream: true,
83
+ };
84
+
85
+ const results: Array<{ content: string | null }> = [];
86
+ for await (const msg of handler.handleStream(requestParams)) {
87
+ results.push(msg);
88
+ }
89
+
90
+ expect(results).toHaveLength(3);
91
+ expect(results[0].content).toBe('Hello');
92
+ expect(results[1].content).toBe(' world');
93
+ });
94
+
95
+ it('should log payload when payload logger is enabled', async () => {
96
+ const chunks = [createStreamChunk('Hi', 'stop')];
97
+ mockClient = createMockClient(chunks);
98
+ const payloadLogger = createMockPayloadLogger(true);
99
+ const handler = new OpenAIStreamHandler(mockClient, payloadLogger);
100
+
101
+ const requestParams: IOpenAIStreamRequestParams = {
102
+ model: 'gpt-4',
103
+ messages: [{ role: 'user', content: 'Hi' }],
104
+ stream: true,
105
+ temperature: 0.5,
106
+ max_tokens: 100,
107
+ };
108
+
109
+ const results = [];
110
+ for await (const msg of handler.handleStream(requestParams)) {
111
+ results.push(msg);
112
+ }
113
+
114
+ expect(payloadLogger.logPayload).toHaveBeenCalledWith(
115
+ expect.objectContaining({
116
+ model: 'gpt-4',
117
+ messagesCount: 1,
118
+ hasTools: false,
119
+ temperature: 0.5,
120
+ maxTokens: 100,
121
+ }),
122
+ 'stream',
123
+ );
124
+ });
125
+
126
+ it('should not log payload when payload logger is disabled', async () => {
127
+ const chunks = [createStreamChunk('Hi', 'stop')];
128
+ mockClient = createMockClient(chunks);
129
+ const payloadLogger = createMockPayloadLogger(false);
130
+ const handler = new OpenAIStreamHandler(mockClient, payloadLogger);
131
+
132
+ const requestParams: IOpenAIStreamRequestParams = {
133
+ model: 'gpt-4',
134
+ messages: [{ role: 'user', content: 'Hi' }],
135
+ stream: true,
136
+ };
137
+
138
+ for await (const _msg of handler.handleStream(requestParams)) {
139
+ // consume stream
140
+ }
141
+
142
+ expect(payloadLogger.logPayload).not.toHaveBeenCalled();
143
+ });
144
+
145
+ it('should pass tools and tool_choice to API when provided', async () => {
146
+ const chunks = [createStreamChunk('Hi', 'stop')];
147
+ mockClient = createMockClient(chunks);
148
+ const handler = new OpenAIStreamHandler(mockClient);
149
+
150
+ const tools: OpenAI.Chat.ChatCompletionTool[] = [
151
+ {
152
+ type: 'function',
153
+ function: {
154
+ name: 'search',
155
+ description: 'Search the web',
156
+ parameters: { type: 'object', properties: {} },
157
+ },
158
+ },
159
+ ];
160
+
161
+ const requestParams: IOpenAIStreamRequestParams = {
162
+ model: 'gpt-4',
163
+ messages: [{ role: 'user', content: 'Hi' }],
164
+ stream: true,
165
+ tools,
166
+ tool_choice: 'auto',
167
+ };
168
+
169
+ for await (const _msg of handler.handleStream(requestParams)) {
170
+ // consume
171
+ }
172
+
173
+ const createCall = vi.mocked(mockClient.chat.completions.create);
174
+ expect(createCall).toHaveBeenCalledWith(
175
+ expect.objectContaining({
176
+ tools,
177
+ tool_choice: 'auto',
178
+ stream: true,
179
+ }),
180
+ );
181
+ });
182
+
183
+ it('should throw with descriptive error when API call fails', async () => {
184
+ mockClient = {
185
+ chat: {
186
+ completions: {
187
+ create: vi.fn().mockRejectedValue(new Error('API rate limit')),
188
+ },
189
+ },
190
+ } as unknown as OpenAI;
191
+
192
+ const logger = createMockLogger();
193
+ const handler = new OpenAIStreamHandler(mockClient, undefined, logger);
194
+
195
+ const requestParams: IOpenAIStreamRequestParams = {
196
+ model: 'gpt-4',
197
+ messages: [{ role: 'user', content: 'Hi' }],
198
+ stream: true,
199
+ };
200
+
201
+ const results = [];
202
+ await expect(async () => {
203
+ for await (const msg of handler.handleStream(requestParams)) {
204
+ results.push(msg);
205
+ }
206
+ }).rejects.toThrow('OpenAI streaming failed: API rate limit');
207
+ expect(logger.error).toHaveBeenCalled();
208
+ });
209
+
210
+ it('should handle empty stream', async () => {
211
+ mockClient = createMockClient([]);
212
+ const handler = new OpenAIStreamHandler(mockClient);
213
+
214
+ const requestParams: IOpenAIStreamRequestParams = {
215
+ model: 'gpt-4',
216
+ messages: [{ role: 'user', content: 'Hi' }],
217
+ stream: true,
218
+ };
219
+
220
+ const results = [];
221
+ for await (const msg of handler.handleStream(requestParams)) {
222
+ results.push(msg);
223
+ }
224
+
225
+ expect(results).toHaveLength(0);
226
+ });
227
+
228
+ it('should work without payload logger', async () => {
229
+ const chunks = [createStreamChunk('Hello', 'stop')];
230
+ mockClient = createMockClient(chunks);
231
+ const handler = new OpenAIStreamHandler(mockClient);
232
+
233
+ const requestParams: IOpenAIStreamRequestParams = {
234
+ model: 'gpt-4',
235
+ messages: [{ role: 'user', content: 'Hi' }],
236
+ stream: true,
237
+ };
238
+
239
+ const results = [];
240
+ for await (const msg of handler.handleStream(requestParams)) {
241
+ results.push(msg);
242
+ }
243
+
244
+ expect(results).toHaveLength(1);
245
+ expect(results[0].content).toBe('Hello');
246
+ });
247
+ });
248
+
249
+ describe('generateStreamingResponse', () => {
250
+ it('should convert chat request params and delegate to handleStream', async () => {
251
+ const chunks = [createStreamChunk('Response'), createStreamChunk('', 'stop')];
252
+ mockClient = createMockClient(chunks);
253
+ const handler = new OpenAIStreamHandler(mockClient);
254
+
255
+ const request: IOpenAIChatRequestParams = {
256
+ model: 'gpt-4',
257
+ messages: [{ role: 'user', content: 'Hi' }],
258
+ temperature: 0.8,
259
+ max_tokens: 200,
260
+ };
261
+
262
+ const results = [];
263
+ for await (const msg of handler.generateStreamingResponse(request)) {
264
+ results.push(msg);
265
+ }
266
+
267
+ expect(results).toHaveLength(2);
268
+ expect(results[0].content).toBe('Response');
269
+ });
270
+
271
+ it('should default model to gpt-4o-mini when not specified', async () => {
272
+ const chunks = [createStreamChunk('Hi', 'stop')];
273
+ mockClient = createMockClient(chunks);
274
+ const handler = new OpenAIStreamHandler(mockClient);
275
+
276
+ const request: IOpenAIChatRequestParams = {
277
+ model: '',
278
+ messages: [{ role: 'user', content: 'Hi' }],
279
+ };
280
+
281
+ for await (const _msg of handler.generateStreamingResponse(request)) {
282
+ // consume
283
+ }
284
+
285
+ const createCall = vi.mocked(mockClient.chat.completions.create);
286
+ expect(createCall).toHaveBeenCalledWith(expect.objectContaining({ model: 'gpt-4o-mini' }));
287
+ });
288
+
289
+ it('should include tools when provided in request', async () => {
290
+ const chunks = [createStreamChunk('Hi', 'stop')];
291
+ mockClient = createMockClient(chunks);
292
+ const handler = new OpenAIStreamHandler(mockClient);
293
+
294
+ const tools: OpenAI.Chat.ChatCompletionTool[] = [
295
+ {
296
+ type: 'function',
297
+ function: {
298
+ name: 'calc',
299
+ description: 'Calculate',
300
+ parameters: { type: 'object', properties: {} },
301
+ },
302
+ },
303
+ ];
304
+
305
+ const request: IOpenAIChatRequestParams = {
306
+ model: 'gpt-4',
307
+ messages: [{ role: 'user', content: 'Hi' }],
308
+ tools,
309
+ };
310
+
311
+ for await (const _msg of handler.generateStreamingResponse(request)) {
312
+ // consume
313
+ }
314
+
315
+ const createCall = vi.mocked(mockClient.chat.completions.create);
316
+ expect(createCall).toHaveBeenCalledWith(
317
+ expect.objectContaining({
318
+ tools,
319
+ tool_choice: 'auto',
320
+ }),
321
+ );
322
+ });
323
+
324
+ it('should throw and log error when streaming fails', async () => {
325
+ mockClient = {
326
+ chat: {
327
+ completions: {
328
+ create: vi.fn().mockRejectedValue(new Error('Network error')),
329
+ },
330
+ },
331
+ } as unknown as OpenAI;
332
+
333
+ const logger = createMockLogger();
334
+ const handler = new OpenAIStreamHandler(mockClient, undefined, logger);
335
+
336
+ const request: IOpenAIChatRequestParams = {
337
+ model: 'gpt-4',
338
+ messages: [{ role: 'user', content: 'Hi' }],
339
+ };
340
+
341
+ await expect(async () => {
342
+ for await (const _msg of handler.generateStreamingResponse(request)) {
343
+ // consume
344
+ }
345
+ }).rejects.toThrow();
346
+ expect(logger.error).toHaveBeenCalled();
347
+ });
348
+
349
+ it('should handle empty messages array', async () => {
350
+ const chunks = [createStreamChunk('Hi', 'stop')];
351
+ mockClient = createMockClient(chunks);
352
+ const handler = new OpenAIStreamHandler(mockClient);
353
+
354
+ const request: IOpenAIChatRequestParams = {
355
+ model: 'gpt-4',
356
+ messages: [],
357
+ };
358
+
359
+ const results = [];
360
+ for await (const msg of handler.generateStreamingResponse(request)) {
361
+ results.push(msg);
362
+ }
363
+
364
+ expect(results).toHaveLength(1);
365
+ });
366
+ });
367
+ });
@@ -0,0 +1,119 @@
1
+ import OpenAI from 'openai';
2
+ import type { TUniversalMessage } from '@robota-sdk/agent-core';
3
+ import type { IPayloadLogger } from '../interfaces/payload-logger';
4
+ import type { IOpenAIChatRequestParams, IOpenAIStreamRequestParams } from '../types/api-types';
5
+ import { SilentLogger, type ILogger } from '@robota-sdk/agent-core';
6
+ import { OpenAIResponseParser } from '../parsers/response-parser';
7
+
8
+ /**
9
+ * OpenAI streaming response handler
10
+ *
11
+ * Handles streaming chat completions from OpenAI API.
12
+ * Extracts streaming logic from the main provider for better modularity.
13
+ */
14
+ export class OpenAIStreamHandler {
15
+ private readonly logger: ILogger;
16
+ private readonly parser: OpenAIResponseParser;
17
+
18
+ constructor(
19
+ private readonly client: OpenAI,
20
+ private readonly payloadLogger?: IPayloadLogger,
21
+ logger?: ILogger,
22
+ ) {
23
+ this.logger = logger || SilentLogger;
24
+ this.parser = new OpenAIResponseParser(logger);
25
+ }
26
+
27
+ /**
28
+ * Handle streaming response for OpenAI chat completions
29
+ *
30
+ * @param requestParams - OpenAI API request parameters
31
+ * @returns AsyncGenerator yielding universal messages
32
+ */
33
+ async *handleStream(
34
+ requestParams: IOpenAIStreamRequestParams,
35
+ ): AsyncGenerator<TUniversalMessage, void, undefined> {
36
+ try {
37
+ // Log payload for debugging if logger is available
38
+ if (this.payloadLogger?.isEnabled()) {
39
+ const logData = {
40
+ model: requestParams.model,
41
+ messagesCount: requestParams.messages.length,
42
+ hasTools: !!requestParams.tools,
43
+ temperature: requestParams.temperature,
44
+ maxTokens: requestParams.max_tokens,
45
+ timestamp: new Date().toISOString(),
46
+ };
47
+ await this.payloadLogger.logPayload(logData, 'stream');
48
+ }
49
+
50
+ // Create streaming chat completion with proper type-safe parameters
51
+ const streamParams: OpenAI.Chat.ChatCompletionCreateParamsStreaming = {
52
+ model: requestParams.model,
53
+ messages: requestParams.messages,
54
+ stream: true,
55
+ ...(requestParams.temperature !== undefined && { temperature: requestParams.temperature }),
56
+ ...(requestParams.max_tokens !== undefined && { max_tokens: requestParams.max_tokens }),
57
+ ...(requestParams.tools && {
58
+ tools: requestParams.tools,
59
+ tool_choice: requestParams.tool_choice || 'auto',
60
+ }),
61
+ };
62
+ const response = await this.client.chat.completions.create(streamParams);
63
+
64
+ // Process each chunk in the stream
65
+ for await (const chunk of response) {
66
+ const parsed = this.parser.parseStreamingChunk(chunk);
67
+ if (parsed) {
68
+ yield parsed;
69
+ }
70
+ }
71
+ } catch (error) {
72
+ const errorMessage =
73
+ error instanceof Error ? error.message : 'OpenAI streaming request failed';
74
+ this.logger.error('Stream creation failed', { error: errorMessage });
75
+ throw new Error(`OpenAI streaming failed: ${errorMessage}`);
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Generate streaming response using raw request payload (for agents package compatibility)
81
+ *
82
+ * @param request - Raw request payload from ConversationService
83
+ * @returns AsyncGenerator yielding universal messages
84
+ */
85
+ async *generateStreamingResponse(
86
+ request: IOpenAIChatRequestParams,
87
+ ): AsyncGenerator<TUniversalMessage, void, undefined> {
88
+ try {
89
+ // Extract parameters from request payload
90
+ const model = request.model;
91
+ const messages = request.messages || [];
92
+ const temperature = request.temperature;
93
+ const maxTokens = request.max_tokens;
94
+ const tools = request.tools;
95
+
96
+ // Build OpenAI request parameters
97
+ const requestParams: IOpenAIStreamRequestParams = {
98
+ model: model || 'gpt-4o-mini',
99
+ messages,
100
+ temperature,
101
+ max_tokens: maxTokens,
102
+ stream: true,
103
+ };
104
+
105
+ // Add tools if provided
106
+ if (tools && Array.isArray(tools) && tools.length > 0) {
107
+ requestParams.tools = tools;
108
+ requestParams.tool_choice = 'auto';
109
+ }
110
+
111
+ // Use existing stream handler
112
+ yield* this.handleStream(requestParams);
113
+ } catch (error) {
114
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
115
+ this.logger.error('OpenAI generateStreamingResponse error:', { message: errorMessage });
116
+ throw error;
117
+ }
118
+ }
119
+ }
@@ -0,0 +1,112 @@
1
+ import type OpenAI from 'openai';
2
+
3
+ /**
4
+ * OpenAI API Request Parameters
5
+ * Replaces any types with specific OpenAI API structure
6
+ */
7
+ export interface IOpenAIChatRequestParams {
8
+ model: string;
9
+ messages: OpenAI.Chat.ChatCompletionMessageParam[];
10
+ temperature?: number | undefined;
11
+ max_tokens?: number | undefined;
12
+ tools?: OpenAI.Chat.ChatCompletionTool[] | undefined;
13
+ tool_choice?: 'auto' | 'none' | OpenAI.Chat.ChatCompletionNamedToolChoice | undefined;
14
+ stream?: boolean | undefined;
15
+ }
16
+
17
+ /**
18
+ * OpenAI API streaming request parameters
19
+ */
20
+ export interface IOpenAIStreamRequestParams extends Omit<IOpenAIChatRequestParams, 'stream'> {
21
+ stream: true;
22
+ }
23
+
24
+ /**
25
+ * OpenAI API tool call structure
26
+ */
27
+ export interface IOpenAIToolCall {
28
+ id: string;
29
+ type: 'function';
30
+ function: {
31
+ name: string;
32
+ arguments: string;
33
+ };
34
+ }
35
+
36
+ /**
37
+ * OpenAI API message structure with tool calls
38
+ */
39
+ export interface IOpenAIAssistantMessage {
40
+ role: 'assistant';
41
+ content: string | null;
42
+ tool_calls?: IOpenAIToolCall[];
43
+ }
44
+
45
+ /**
46
+ * OpenAI API tool message structure
47
+ */
48
+ export interface IOpenAIToolMessage {
49
+ role: 'tool';
50
+ content: string;
51
+ tool_call_id: string;
52
+ }
53
+
54
+ /**
55
+ * OpenAI streaming chunk delta structure
56
+ */
57
+ export interface IOpenAIStreamDelta {
58
+ role?: 'assistant';
59
+ content?: string | null;
60
+ tool_calls?: Array<{
61
+ index: number;
62
+ id?: string;
63
+ type?: 'function';
64
+ function?: {
65
+ name?: string;
66
+ arguments?: string;
67
+ };
68
+ }>;
69
+ }
70
+
71
+ /**
72
+ * OpenAI streaming chunk structure
73
+ */
74
+ export interface IOpenAIStreamChunk {
75
+ id: string;
76
+ object: 'chat.completion.chunk';
77
+ created: number;
78
+ model: string;
79
+ choices: Array<{
80
+ index: number;
81
+ delta: IOpenAIStreamDelta;
82
+ finish_reason?: string | null;
83
+ }>;
84
+ usage?: {
85
+ prompt_tokens: number;
86
+ completion_tokens: number;
87
+ total_tokens: number;
88
+ };
89
+ }
90
+
91
+ /**
92
+ * OpenAI error structure for type-safe error handling
93
+ */
94
+ export interface IOpenAIError {
95
+ message: string;
96
+ type?: string;
97
+ param?: string | null;
98
+ code?: string | null;
99
+ }
100
+
101
+ /**
102
+ * Payload logging data structure
103
+ */
104
+ export interface IOpenAILogData {
105
+ model: string;
106
+ messagesCount: number;
107
+ hasTools: boolean;
108
+ temperature?: number | undefined;
109
+ maxTokens?: number | undefined;
110
+ timestamp: string;
111
+ requestId?: string | undefined;
112
+ }