@agentica/core 0.43.2 → 0.44.0-dev.20260313

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 (205) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +218 -218
  3. package/lib/constants/AgenticaSystemPrompt.js +1 -1
  4. package/lib/constants/AgenticaSystemPrompt.js.map +1 -1
  5. package/lib/context/AgenticaOperation.d.ts +3 -2
  6. package/lib/context/internal/AgenticaOperationComposer.js.map +1 -1
  7. package/lib/context/internal/AgenticaTokenUsageAggregator.js.map +1 -1
  8. package/lib/context/internal/__IChatInitialApplication.d.ts +1 -1
  9. package/lib/errors/AgenticaJsonParseError.d.ts +5 -8
  10. package/lib/errors/AgenticaJsonParseError.js +11 -6
  11. package/lib/errors/AgenticaJsonParseError.js.map +1 -1
  12. package/lib/errors/AgenticaValidationError.d.ts +1 -1
  13. package/lib/events/AgenticaExecuteEvent.d.ts +1 -1
  14. package/lib/events/AgenticaJsonParseErrorEvent.d.ts +2 -2
  15. package/lib/factory/events.d.ts +2 -3
  16. package/lib/factory/events.js +1 -2
  17. package/lib/factory/events.js.map +1 -1
  18. package/lib/functional/assertHttpController.d.ts +2 -3
  19. package/lib/functional/assertHttpController.js +3939 -2656
  20. package/lib/functional/assertHttpController.js.map +1 -1
  21. package/lib/functional/assertHttpLlmApplication.d.ts +3 -3
  22. package/lib/functional/assertHttpLlmApplication.js +3939 -2656
  23. package/lib/functional/assertHttpLlmApplication.js.map +1 -1
  24. package/lib/functional/assertMcpController.d.ts +2 -2
  25. package/lib/functional/assertMcpController.js +202 -629
  26. package/lib/functional/assertMcpController.js.map +1 -1
  27. package/lib/functional/createMcpLlmApplication.d.ts +6 -0
  28. package/lib/functional/createMcpLlmApplication.js +56 -0
  29. package/lib/functional/createMcpLlmApplication.js.map +1 -0
  30. package/lib/functional/validateHttpController.d.ts +3 -3
  31. package/lib/functional/validateHttpController.js +3367 -2268
  32. package/lib/functional/validateHttpController.js.map +1 -1
  33. package/lib/functional/validateHttpLlmApplication.d.ts +3 -4
  34. package/lib/functional/validateHttpLlmApplication.js +3367 -2268
  35. package/lib/functional/validateHttpLlmApplication.js.map +1 -1
  36. package/lib/functional/validateMcpController.d.ts +2 -2
  37. package/lib/functional/validateMcpController.js +388 -1161
  38. package/lib/functional/validateMcpController.js.map +1 -1
  39. package/lib/histories/AgenticaExecuteHistory.d.ts +1 -1
  40. package/lib/index.mjs +21256 -18948
  41. package/lib/index.mjs.map +1 -1
  42. package/lib/orchestrate/call.js +43 -56
  43. package/lib/orchestrate/call.js.map +1 -1
  44. package/lib/orchestrate/cancel.js +9 -66
  45. package/lib/orchestrate/cancel.js.map +1 -1
  46. package/lib/orchestrate/initialize.js +4 -947
  47. package/lib/orchestrate/initialize.js.map +1 -1
  48. package/lib/orchestrate/select.js +11 -68
  49. package/lib/orchestrate/select.js.map +1 -1
  50. package/lib/structures/IAgenticaController.d.ts +143 -151
  51. package/lib/structures/IMcpTool.d.ts +52 -0
  52. package/lib/structures/IMcpTool.js +3 -0
  53. package/lib/structures/IMcpTool.js.map +1 -0
  54. package/lib/utils/ChatGptCompletionMessageUtil.js +16 -5
  55. package/lib/utils/ChatGptCompletionMessageUtil.js.map +1 -1
  56. package/lib/utils/ChatGptCompletionMessageUtil.spec.js +0 -5
  57. package/lib/utils/ChatGptCompletionMessageUtil.spec.js.map +1 -1
  58. package/package.json +7 -9
  59. package/prompts/cancel.md +5 -5
  60. package/prompts/common.md +3 -3
  61. package/prompts/describe.md +7 -7
  62. package/prompts/execute.md +122 -122
  63. package/prompts/initialize.md +3 -3
  64. package/prompts/json_parse_error.md +35 -33
  65. package/prompts/select.md +7 -7
  66. package/prompts/validate.md +123 -123
  67. package/prompts/validate_repeated.md +31 -31
  68. package/src/Agentica.ts +367 -367
  69. package/src/MicroAgentica.ts +357 -357
  70. package/src/constants/AgenticaConstant.ts +4 -4
  71. package/src/constants/AgenticaDefaultPrompt.ts +44 -44
  72. package/src/constants/AgenticaSystemPrompt.ts +1 -1
  73. package/src/constants/index.ts +2 -2
  74. package/src/context/AgenticaContext.ts +136 -136
  75. package/src/context/AgenticaContextRequestResult.ts +14 -14
  76. package/src/context/AgenticaOperation.ts +73 -72
  77. package/src/context/AgenticaOperationCollection.ts +49 -49
  78. package/src/context/AgenticaOperationSelection.ts +9 -9
  79. package/src/context/AgenticaTokenUsage.ts +186 -186
  80. package/src/context/MicroAgenticaContext.ts +99 -99
  81. package/src/context/index.ts +5 -5
  82. package/src/context/internal/AgenticaOperationComposer.ts +177 -177
  83. package/src/context/internal/AgenticaTokenUsageAggregator.ts +66 -66
  84. package/src/context/internal/__IChatCancelFunctionsApplication.ts +23 -23
  85. package/src/context/internal/__IChatFunctionReference.ts +21 -21
  86. package/src/context/internal/__IChatInitialApplication.ts +15 -15
  87. package/src/context/internal/__IChatSelectFunctionsApplication.ts +24 -24
  88. package/src/context/internal/isAgenticaContext.ts +11 -11
  89. package/src/errors/AgenticaJsonParseError.ts +52 -47
  90. package/src/errors/AgenticaValidationError.ts +49 -49
  91. package/src/errors/index.ts +2 -2
  92. package/src/events/AgenticaAssistantMessageEvent.ts +12 -12
  93. package/src/events/AgenticaCallEvent.ts +27 -27
  94. package/src/events/AgenticaCancelEvent.ts +9 -9
  95. package/src/events/AgenticaDescribeEvent.ts +14 -14
  96. package/src/events/AgenticaEvent.ts +59 -59
  97. package/src/events/AgenticaEvent.type.ts +19 -19
  98. package/src/events/AgenticaEventBase.ts +18 -18
  99. package/src/events/AgenticaEventSource.ts +6 -6
  100. package/src/events/AgenticaExecuteEvent.ts +45 -45
  101. package/src/events/AgenticaInitializeEvent.ts +7 -7
  102. package/src/events/AgenticaJsonParseErrorEvent.ts +16 -15
  103. package/src/events/AgenticaRequestEvent.ts +27 -27
  104. package/src/events/AgenticaResponseEvent.ts +32 -32
  105. package/src/events/AgenticaSelectEvent.ts +11 -11
  106. package/src/events/AgenticaUserMessageEvent.ts +12 -12
  107. package/src/events/AgenticaValidateEvent.ts +32 -32
  108. package/src/events/MicroAgenticaEvent.ts +45 -45
  109. package/src/events/index.ts +15 -15
  110. package/src/factory/events.ts +357 -359
  111. package/src/factory/histories.ts +348 -348
  112. package/src/factory/index.ts +3 -3
  113. package/src/factory/operations.ts +16 -16
  114. package/src/functional/assertHttpController.ts +106 -104
  115. package/src/functional/assertHttpLlmApplication.ts +52 -57
  116. package/src/functional/assertMcpController.ts +47 -44
  117. package/src/functional/createMcpLlmApplication.ts +72 -0
  118. package/src/functional/index.ts +7 -7
  119. package/src/functional/validateHttpController.ts +113 -110
  120. package/src/functional/validateHttpLlmApplication.ts +65 -70
  121. package/src/functional/validateMcpController.ts +53 -50
  122. package/src/histories/AgenticaAssistantMessageHistory.ts +10 -10
  123. package/src/histories/AgenticaCancelHistory.ts +8 -8
  124. package/src/histories/AgenticaDescribeHistory.ts +18 -18
  125. package/src/histories/AgenticaExecuteHistory.ts +64 -64
  126. package/src/histories/AgenticaHistory.ts +28 -28
  127. package/src/histories/AgenticaHistoryBase.ts +35 -35
  128. package/src/histories/AgenticaSelectHistory.ts +8 -8
  129. package/src/histories/AgenticaSystemMessageHistory.ts +10 -10
  130. package/src/histories/AgenticaUserMessageHistory.ts +11 -11
  131. package/src/histories/MicroAgenticaHistory.ts +19 -19
  132. package/src/histories/contents/AgenticaUserMessageAudioContent.ts +21 -21
  133. package/src/histories/contents/AgenticaUserMessageContent.ts +19 -19
  134. package/src/histories/contents/AgenticaUserMessageContentBase.ts +6 -6
  135. package/src/histories/contents/AgenticaUserMessageFileContent.ts +25 -25
  136. package/src/histories/contents/AgenticaUserMessageImageContent.ts +33 -33
  137. package/src/histories/contents/AgenticaUserMessageTextContent.ts +15 -15
  138. package/src/histories/contents/index.ts +5 -5
  139. package/src/histories/index.ts +10 -10
  140. package/src/index.ts +15 -15
  141. package/src/json/IAgenticaEventJson.ts +265 -265
  142. package/src/json/IAgenticaEventJson.type.ts +19 -19
  143. package/src/json/IAgenticaHistoryJson.ts +165 -165
  144. package/src/json/IAgenticaHistoryJson.type.ts +19 -19
  145. package/src/json/IAgenticaOperationJson.ts +36 -36
  146. package/src/json/IAgenticaOperationSelectionJson.ts +26 -26
  147. package/src/json/IAgenticaTokenUsageJson.ts +107 -107
  148. package/src/json/IMicroAgenticaEventJson.ts +22 -22
  149. package/src/json/IMicroAgenticaHistoryJson.ts +25 -25
  150. package/src/json/index.ts +7 -7
  151. package/src/orchestrate/call.ts +542 -558
  152. package/src/orchestrate/cancel.ts +265 -269
  153. package/src/orchestrate/describe.ts +66 -66
  154. package/src/orchestrate/execute.ts +61 -61
  155. package/src/orchestrate/index.ts +6 -6
  156. package/src/orchestrate/initialize.ts +102 -102
  157. package/src/orchestrate/internal/cancelFunctionFromContext.ts +33 -33
  158. package/src/orchestrate/internal/selectFunctionFromContext.ts +34 -34
  159. package/src/orchestrate/select.ts +320 -322
  160. package/src/structures/IAgenticaConfig.ts +83 -83
  161. package/src/structures/IAgenticaConfigBase.ts +87 -87
  162. package/src/structures/IAgenticaController.ts +143 -151
  163. package/src/structures/IAgenticaExecutor.ts +167 -167
  164. package/src/structures/IAgenticaProps.ts +78 -78
  165. package/src/structures/IAgenticaSystemPrompt.ts +236 -236
  166. package/src/structures/IAgenticaVendor.ts +54 -54
  167. package/src/structures/IMcpTool.ts +60 -0
  168. package/src/structures/IMicroAgenticaConfig.ts +56 -56
  169. package/src/structures/IMicroAgenticaExecutor.ts +67 -67
  170. package/src/structures/IMicroAgenticaProps.ts +77 -77
  171. package/src/structures/IMicroAgenticaSystemPrompt.ts +169 -169
  172. package/src/structures/index.ts +10 -10
  173. package/src/transformers/transformHistory.ts +172 -172
  174. package/src/utils/AssistantMessageEmptyError.ts +20 -20
  175. package/src/utils/AsyncQueue.spec.ts +355 -355
  176. package/src/utils/AsyncQueue.ts +95 -95
  177. package/src/utils/ByteArrayUtil.ts +5 -5
  178. package/src/utils/ChatGptCompletionMessageUtil.spec.ts +314 -320
  179. package/src/utils/ChatGptCompletionMessageUtil.ts +210 -195
  180. package/src/utils/ChatGptCompletionStreamingUtil.spec.ts +909 -909
  181. package/src/utils/ChatGptCompletionStreamingUtil.ts +91 -91
  182. package/src/utils/ChatGptTokenUsageAggregator.spec.ts +226 -226
  183. package/src/utils/ChatGptTokenUsageAggregator.ts +57 -57
  184. package/src/utils/MPSC.spec.ts +276 -276
  185. package/src/utils/MPSC.ts +42 -42
  186. package/src/utils/Singleton.spec.ts +138 -138
  187. package/src/utils/Singleton.ts +42 -42
  188. package/src/utils/StreamUtil.spec.ts +512 -512
  189. package/src/utils/StreamUtil.ts +87 -87
  190. package/src/utils/__map_take.spec.ts +140 -140
  191. package/src/utils/__map_take.ts +13 -13
  192. package/src/utils/__retry.spec.ts +198 -198
  193. package/src/utils/__retry.ts +18 -18
  194. package/src/utils/assertExecuteFailure.ts +16 -16
  195. package/src/utils/index.ts +4 -4
  196. package/src/utils/request.ts +140 -140
  197. package/src/utils/types.ts +50 -50
  198. package/lib/context/internal/AgenticaOperationComposer.spec.d.ts +0 -1
  199. package/lib/context/internal/AgenticaOperationComposer.spec.js +0 -353
  200. package/lib/context/internal/AgenticaOperationComposer.spec.js.map +0 -1
  201. package/lib/utils/JsonUtil.d.ts +0 -8
  202. package/lib/utils/JsonUtil.js +0 -350
  203. package/lib/utils/JsonUtil.js.map +0 -1
  204. package/src/context/internal/AgenticaOperationComposer.spec.ts +0 -401
  205. package/src/utils/JsonUtil.ts +0 -462
@@ -1,909 +1,909 @@
1
- import type { ChatCompletionChunk } from "openai/resources";
2
-
3
- import { beforeEach, describe, expect, it, vi } from "vitest";
4
-
5
- import { reduceStreamingWithDispatch } from "./ChatGptCompletionStreamingUtil";
6
- import { StreamUtil } from "./StreamUtil";
7
-
8
- describe("reduceStreamingWithDispatch", () => {
9
- beforeEach(() => {
10
- vi.clearAllMocks();
11
- });
12
-
13
- describe("basic functionality", () => {
14
- it("should process single chunk successfully", async () => {
15
- const mockChunk: ChatCompletionChunk = {
16
- id: "test-id",
17
- object: "chat.completion.chunk",
18
- created: 1234567890,
19
- model: "gpt-3.5-turbo",
20
- choices: [
21
- {
22
- index: 0,
23
- delta: { content: "Hello" },
24
- finish_reason: null,
25
- },
26
- ],
27
- };
28
-
29
- const stream = new ReadableStream<ChatCompletionChunk>({
30
- start(controller) {
31
- controller.enqueue(mockChunk);
32
- controller.close();
33
- },
34
- });
35
-
36
- const eventProcessor = vi.fn();
37
- const result = await reduceStreamingWithDispatch(stream, eventProcessor);
38
-
39
- expect(result).toBeDefined();
40
- expect(result.object).toBe("chat.completion");
41
- expect(eventProcessor).toHaveBeenCalledTimes(1);
42
- });
43
-
44
- it("should handle multiple chunks with content accumulation", async () => {
45
- const chunks: ChatCompletionChunk[] = [
46
- {
47
- id: "test-id",
48
- object: "chat.completion.chunk",
49
- created: 1234567890,
50
- model: "gpt-3.5-turbo",
51
- choices: [
52
- {
53
- index: 0,
54
- delta: { content: "Hello" },
55
- finish_reason: null,
56
- },
57
- ],
58
- },
59
- {
60
- id: "test-id",
61
- object: "chat.completion.chunk",
62
- created: 1234567890,
63
- model: "gpt-3.5-turbo",
64
- choices: [
65
- {
66
- index: 0,
67
- delta: { content: " World" },
68
- finish_reason: null,
69
- },
70
- ],
71
- },
72
- {
73
- id: "test-id",
74
- object: "chat.completion.chunk",
75
- created: 1234567890,
76
- model: "gpt-3.5-turbo",
77
- choices: [
78
- {
79
- index: 0,
80
- delta: { content: "!" },
81
- finish_reason: "stop",
82
- },
83
- ],
84
- },
85
- ];
86
-
87
- const stream = new ReadableStream<ChatCompletionChunk>({
88
- start(controller) {
89
- chunks.forEach(chunk => controller.enqueue(chunk));
90
- controller.close();
91
- },
92
- });
93
-
94
- const eventProcessor = vi.fn();
95
- const result = await reduceStreamingWithDispatch(stream, eventProcessor);
96
-
97
- expect(result).toBeDefined();
98
- expect(result.object).toBe("chat.completion");
99
- expect(eventProcessor).toHaveBeenCalledTimes(1);
100
-
101
- const eventCall = eventProcessor.mock.calls[0]?.[0];
102
- expect(eventCall.get()).toBe("Hello World!");
103
- });
104
-
105
- it("should handle empty content chunks", async () => {
106
- const chunks: ChatCompletionChunk[] = [
107
- {
108
- id: "test-id",
109
- object: "chat.completion.chunk",
110
- created: 1234567890,
111
- model: "gpt-3.5-turbo",
112
- choices: [
113
- {
114
- index: 0,
115
- delta: { content: "" },
116
- finish_reason: null,
117
- },
118
- ],
119
- },
120
- {
121
- id: "test-id",
122
- object: "chat.completion.chunk",
123
- created: 1234567890,
124
- model: "gpt-3.5-turbo",
125
- choices: [
126
- {
127
- index: 0,
128
- delta: { content: "Hello" },
129
- finish_reason: null,
130
- },
131
- ],
132
- },
133
- ];
134
-
135
- const stream = new ReadableStream<ChatCompletionChunk>({
136
- start(controller) {
137
- chunks.forEach(chunk => controller.enqueue(chunk));
138
- controller.close();
139
- },
140
- });
141
-
142
- const eventProcessor = vi.fn();
143
- const result = await reduceStreamingWithDispatch(stream, eventProcessor);
144
-
145
- expect(result).toBeDefined();
146
- expect(eventProcessor).toHaveBeenCalledTimes(1);
147
-
148
- const eventCall = eventProcessor.mock.calls[0]?.[0];
149
- expect(eventCall.get()).toBe("Hello");
150
- });
151
- });
152
-
153
- describe("multiple choices handling", () => {
154
- it("should handle multiple choices with different indices", async () => {
155
- const chunks: ChatCompletionChunk[] = [
156
- {
157
- id: "test-id",
158
- object: "chat.completion.chunk",
159
- created: 1234567890,
160
- model: "gpt-3.5-turbo",
161
- choices: [
162
- {
163
- index: 0,
164
- delta: { content: "Choice 1" },
165
- finish_reason: null,
166
- },
167
- {
168
- index: 1,
169
- delta: { content: "Choice 2" },
170
- finish_reason: null,
171
- },
172
- ],
173
- },
174
- {
175
- id: "test-id",
176
- object: "chat.completion.chunk",
177
- created: 1234567890,
178
- model: "gpt-3.5-turbo",
179
- choices: [
180
- {
181
- index: 0,
182
- delta: { content: " continued" },
183
- finish_reason: "stop",
184
- },
185
- {
186
- index: 1,
187
- delta: { content: " continued" },
188
- finish_reason: "stop",
189
- },
190
- ],
191
- },
192
- ];
193
-
194
- const stream = new ReadableStream<ChatCompletionChunk>({
195
- start(controller) {
196
- chunks.forEach(chunk => controller.enqueue(chunk));
197
- controller.close();
198
- },
199
- });
200
-
201
- const eventProcessor = vi.fn();
202
- const result = await reduceStreamingWithDispatch(stream, eventProcessor);
203
-
204
- expect(result).toBeDefined();
205
- expect(eventProcessor).toHaveBeenCalledTimes(2);
206
-
207
- const firstCall = eventProcessor.mock.calls[0]?.[0];
208
- const secondCall = eventProcessor.mock.calls[1]?.[0];
209
- expect(firstCall.get()).toBe("Choice 1 continued");
210
- expect(secondCall.get()).toBe("Choice 2 continued");
211
- });
212
- });
213
-
214
- describe("finish reason handling", () => {
215
- it("should close context when finish_reason is provided", async () => {
216
- const chunks: ChatCompletionChunk[] = [
217
- {
218
- id: "test-id",
219
- object: "chat.completion.chunk",
220
- created: 1234567890,
221
- model: "gpt-3.5-turbo",
222
- choices: [
223
- {
224
- index: 0,
225
- delta: { content: "Hello" },
226
- finish_reason: null,
227
- },
228
- ],
229
- },
230
- {
231
- id: "test-id",
232
- object: "chat.completion.chunk",
233
- created: 1234567890,
234
- model: "gpt-3.5-turbo",
235
- choices: [
236
- {
237
- index: 0,
238
- delta: { content: " World" },
239
- finish_reason: "stop",
240
- },
241
- ],
242
- },
243
- ];
244
-
245
- const stream = new ReadableStream<ChatCompletionChunk>({
246
- start(controller) {
247
- chunks.forEach(chunk => controller.enqueue(chunk));
248
- controller.close();
249
- },
250
- });
251
-
252
- const eventProcessor = vi.fn();
253
- const result = await reduceStreamingWithDispatch(stream, eventProcessor);
254
-
255
- expect(result).toBeDefined();
256
- expect(eventProcessor).toHaveBeenCalledTimes(1);
257
-
258
- const eventCall = eventProcessor.mock.calls[0]?.[0];
259
- expect(eventCall.get()).toBe("Hello World");
260
- expect(eventCall.done()).toBe(true);
261
- });
262
- });
263
-
264
- describe("stream processing", () => {
265
- it("should provide working stream in event processor", async () => {
266
- const chunks: ChatCompletionChunk[] = [
267
- {
268
- id: "test-id",
269
- object: "chat.completion.chunk",
270
- created: 1234567890,
271
- model: "gpt-3.5-turbo",
272
- choices: [
273
- {
274
- index: 0,
275
- delta: { content: "Hello" },
276
- finish_reason: null,
277
- },
278
- ],
279
- },
280
- {
281
- id: "test-id",
282
- object: "chat.completion.chunk",
283
- created: 1234567890,
284
- model: "gpt-3.5-turbo",
285
- choices: [
286
- {
287
- index: 0,
288
- delta: { content: " World" },
289
- finish_reason: "stop",
290
- },
291
- ],
292
- },
293
- ];
294
-
295
- const stream = new ReadableStream<ChatCompletionChunk>({
296
- start(controller) {
297
- chunks.forEach(chunk => controller.enqueue(chunk));
298
- controller.close();
299
- },
300
- });
301
- const streamedContent: string[] = [];
302
- await new Promise(async (resolve) => {
303
- const eventProcessor = vi.fn(({ stream: contentStream }) => {
304
- void (async () => {
305
- for await (const content of contentStream) {
306
- streamedContent.push(content as string);
307
- }
308
- resolve(true);
309
- })().catch(() => {});
310
- });
311
- await reduceStreamingWithDispatch(stream, eventProcessor);
312
- });
313
- expect(streamedContent).toEqual(["Hello", " World"]);
314
- });
315
-
316
- it("should provide working join function", async () => {
317
- const chunks: ChatCompletionChunk[] = [
318
- {
319
- id: "test-id",
320
- object: "chat.completion.chunk",
321
- created: 1234567890,
322
- model: "gpt-3.5-turbo",
323
- choices: [
324
- {
325
- index: 0,
326
- delta: { content: "Hello" },
327
- finish_reason: null,
328
- },
329
- ],
330
- },
331
- {
332
- id: "test-id",
333
- object: "chat.completion.chunk",
334
- created: 1234567890,
335
- model: "gpt-3.5-turbo",
336
- choices: [
337
- {
338
- index: 0,
339
- delta: { content: " World" },
340
- finish_reason: "stop",
341
- },
342
- ],
343
- },
344
- ];
345
-
346
- const stream = new ReadableStream<ChatCompletionChunk>({
347
- start(controller) {
348
- chunks.forEach(chunk => controller.enqueue(chunk));
349
- controller.close();
350
- },
351
- });
352
-
353
- let joinedContent = "";
354
- const eventProcessor = vi.fn(async ({ join }) => {
355
- joinedContent = await join();
356
- });
357
-
358
- const result = await reduceStreamingWithDispatch(stream, eventProcessor);
359
-
360
- expect(result).toBeDefined();
361
- expect(joinedContent).toBe("Hello World");
362
- });
363
- });
364
-
365
- describe("error handling", () => {
366
- it("should throw error for empty stream", async () => {
367
- const stream = new ReadableStream<ChatCompletionChunk>({
368
- start(controller) {
369
- controller.close();
370
- },
371
- });
372
-
373
- const eventProcessor = vi.fn();
374
-
375
- await expect(reduceStreamingWithDispatch(stream, eventProcessor)).rejects.toThrow(
376
- "StreamUtil.reduce did not produce a ChatCompletion",
377
- );
378
- });
379
-
380
- it("should handle stream with only finish_reason chunks", async () => {
381
- const chunks: ChatCompletionChunk[] = [
382
- {
383
- id: "test-id",
384
- object: "chat.completion.chunk",
385
- created: 1234567890,
386
- model: "gpt-3.5-turbo",
387
- choices: [
388
- {
389
- index: 0,
390
- delta: { content: null },
391
- finish_reason: "stop",
392
- },
393
- ],
394
- },
395
- ];
396
-
397
- const stream = new ReadableStream<ChatCompletionChunk>({
398
- start(controller) {
399
- chunks.forEach(chunk => controller.enqueue(chunk));
400
- controller.close();
401
- },
402
- });
403
-
404
- const eventProcessor = vi.fn();
405
- const result = await reduceStreamingWithDispatch(stream, eventProcessor);
406
-
407
- expect(result).toBeDefined();
408
- expect(eventProcessor).not.toHaveBeenCalled();
409
- });
410
- });
411
-
412
- describe("complex scenarios", () => {
413
- it("should handle mixed content and finish_reason chunks", async () => {
414
- const chunks: ChatCompletionChunk[] = [
415
- {
416
- id: "test-id",
417
- object: "chat.completion.chunk",
418
- created: 1234567890,
419
- model: "gpt-3.5-turbo",
420
- choices: [
421
- {
422
- index: 0,
423
- delta: { content: "Hello" },
424
- finish_reason: null,
425
- },
426
- ],
427
- },
428
- {
429
- id: "test-id",
430
- object: "chat.completion.chunk",
431
- created: 1234567890,
432
- model: "gpt-3.5-turbo",
433
- choices: [
434
- {
435
- index: 0,
436
- delta: { content: null },
437
- finish_reason: null,
438
- },
439
- ],
440
- },
441
- {
442
- id: "test-id",
443
- object: "chat.completion.chunk",
444
- created: 1234567890,
445
- model: "gpt-3.5-turbo",
446
- choices: [
447
- {
448
- index: 0,
449
- delta: { content: " World" },
450
- finish_reason: null,
451
- },
452
- ],
453
- },
454
- {
455
- id: "test-id",
456
- object: "chat.completion.chunk",
457
- created: 1234567890,
458
- model: "gpt-3.5-turbo",
459
- choices: [
460
- {
461
- index: 0,
462
- delta: { content: null },
463
- finish_reason: "stop",
464
- },
465
- ],
466
- },
467
- ];
468
-
469
- const stream = new ReadableStream<ChatCompletionChunk>({
470
- start(controller) {
471
- chunks.forEach(chunk => controller.enqueue(chunk));
472
- controller.close();
473
- },
474
- });
475
-
476
- const eventProcessor = vi.fn();
477
- const result = await reduceStreamingWithDispatch(stream, eventProcessor);
478
-
479
- expect(result).toBeDefined();
480
- expect(eventProcessor).toHaveBeenCalledTimes(1);
481
-
482
- const eventCall = eventProcessor.mock.calls[0]?.[0];
483
- expect(eventCall.get()).toBe("Hello World");
484
- });
485
- });
486
-
487
- describe("edge cases and exceptions", () => {
488
- it("should handle null delta content", async () => {
489
- const chunks: ChatCompletionChunk[] = [
490
- {
491
- id: "test-id",
492
- object: "chat.completion.chunk",
493
- created: 1234567890,
494
- model: "gpt-3.5-turbo",
495
- choices: [
496
- {
497
- index: 0,
498
- delta: { content: null },
499
- finish_reason: null,
500
- },
501
- ],
502
- },
503
- {
504
- id: "test-id",
505
- object: "chat.completion.chunk",
506
- created: 1234567890,
507
- model: "gpt-3.5-turbo",
508
- choices: [
509
- {
510
- index: 0,
511
- delta: { content: "Hello" },
512
- finish_reason: "stop",
513
- },
514
- ],
515
- },
516
- ];
517
-
518
- const stream = new ReadableStream<ChatCompletionChunk>({
519
- start(controller) {
520
- chunks.forEach(chunk => controller.enqueue(chunk));
521
- controller.close();
522
- },
523
- });
524
-
525
- const eventProcessor = vi.fn();
526
- const result = await reduceStreamingWithDispatch(stream, eventProcessor);
527
-
528
- expect(result).toBeDefined();
529
- expect(eventProcessor).toHaveBeenCalledTimes(1);
530
-
531
- const eventCall = eventProcessor.mock.calls[0]?.[0];
532
- expect(eventCall.get()).toBe("Hello");
533
- });
534
-
535
- it("should handle missing delta object", async () => {
536
- const chunks: ChatCompletionChunk[] = [
537
- {
538
- id: "test-id",
539
- object: "chat.completion.chunk",
540
- created: 1234567890,
541
- model: "gpt-3.5-turbo",
542
- choices: [
543
- {
544
- index: 0,
545
- delta: {},
546
- finish_reason: null,
547
- },
548
- ],
549
- },
550
- {
551
- id: "test-id",
552
- object: "chat.completion.chunk",
553
- created: 1234567890,
554
- model: "gpt-3.5-turbo",
555
- choices: [
556
- {
557
- index: 0,
558
- delta: { content: "Hello" },
559
- finish_reason: "stop",
560
- },
561
- ],
562
- },
563
- ];
564
-
565
- const stream = new ReadableStream<ChatCompletionChunk>({
566
- start(controller) {
567
- chunks.forEach(chunk => controller.enqueue(chunk));
568
- controller.close();
569
- },
570
- });
571
-
572
- const eventProcessor = vi.fn();
573
- const result = await reduceStreamingWithDispatch(stream, eventProcessor);
574
-
575
- expect(result).toBeDefined();
576
- expect(eventProcessor).toHaveBeenCalledTimes(1);
577
-
578
- const eventCall = eventProcessor.mock.calls[0]?.[0];
579
- expect(eventCall.get()).toBe("Hello");
580
- });
581
-
582
- it("should handle chunks with no choices", async () => {
583
- const chunks: ChatCompletionChunk[] = [
584
- {
585
- id: "test-id",
586
- object: "chat.completion.chunk",
587
- created: 1234567890,
588
- model: "gpt-3.5-turbo",
589
- choices: [],
590
- },
591
- {
592
- id: "test-id",
593
- object: "chat.completion.chunk",
594
- created: 1234567890,
595
- model: "gpt-3.5-turbo",
596
- choices: [
597
- {
598
- index: 0,
599
- delta: { content: "Hello" },
600
- finish_reason: "stop",
601
- },
602
- ],
603
- },
604
- ];
605
-
606
- const stream = new ReadableStream<ChatCompletionChunk>({
607
- start(controller) {
608
- chunks.forEach(chunk => controller.enqueue(chunk));
609
- controller.close();
610
- },
611
- });
612
-
613
- const eventProcessor = vi.fn();
614
- const result = await reduceStreamingWithDispatch(stream, eventProcessor);
615
-
616
- expect(result).toBeDefined();
617
- expect(eventProcessor).toHaveBeenCalledTimes(1);
618
-
619
- const eventCall = eventProcessor.mock.calls[0]?.[0];
620
- expect(eventCall.get()).toBe("Hello");
621
- });
622
-
623
- it("should handle very large content chunks", async () => {
624
- const largeContent = "x".repeat(10000);
625
- const chunks: ChatCompletionChunk[] = [
626
- {
627
- id: "test-id",
628
- object: "chat.completion.chunk",
629
- created: 1234567890,
630
- model: "gpt-3.5-turbo",
631
- choices: [
632
- {
633
- index: 0,
634
- delta: { content: largeContent },
635
- finish_reason: "stop",
636
- },
637
- ],
638
- },
639
- ];
640
-
641
- const stream = new ReadableStream<ChatCompletionChunk>({
642
- start(controller) {
643
- chunks.forEach(chunk => controller.enqueue(chunk));
644
- controller.close();
645
- },
646
- });
647
-
648
- const eventProcessor = vi.fn();
649
- const result = await reduceStreamingWithDispatch(stream, eventProcessor);
650
-
651
- expect(result).toBeDefined();
652
- // Now single chunk with content should trigger eventProcessor
653
- expect(eventProcessor).toHaveBeenCalledOnce();
654
-
655
- const eventCall = eventProcessor.mock.calls[0]?.[0];
656
- expect(eventCall.get()).toBe(largeContent);
657
- });
658
-
659
- it("should handle rapid consecutive chunks", async () => {
660
- const chunks: ChatCompletionChunk[] = Array.from({ length: 100 }, (_, i) => ({
661
- id: "test-id",
662
- object: "chat.completion.chunk" as const,
663
- created: 1234567890,
664
- model: "gpt-3.5-turbo",
665
- choices: [
666
- {
667
- index: 0,
668
- delta: { content: i.toString() },
669
- finish_reason: i === 99 ? "stop" as const : null,
670
- },
671
- ],
672
- }));
673
-
674
- const stream = new ReadableStream<ChatCompletionChunk>({
675
- start(controller) {
676
- chunks.forEach(chunk => controller.enqueue(chunk));
677
- controller.close();
678
- },
679
- });
680
-
681
- const eventProcessor = vi.fn();
682
- const result = await reduceStreamingWithDispatch(stream, eventProcessor);
683
-
684
- expect(result).toBeDefined();
685
- expect(eventProcessor).toHaveBeenCalledTimes(1);
686
-
687
- const eventCall = eventProcessor.mock.calls[0]?.[0];
688
- const expectedContent = Array.from({ length: 100 }, (_, i) => i.toString()).join("");
689
- expect(eventCall.get()).toBe(expectedContent);
690
- });
691
-
692
- it("should handle out-of-order choice indices", async () => {
693
- const chunks: ChatCompletionChunk[] = [
694
- {
695
- id: "test-id",
696
- object: "chat.completion.chunk",
697
- created: 1234567890,
698
- model: "gpt-3.5-turbo",
699
- choices: [
700
- {
701
- index: 2,
702
- delta: { content: "Third" },
703
- finish_reason: null,
704
- },
705
- {
706
- index: 0,
707
- delta: { content: "First" },
708
- finish_reason: null,
709
- },
710
- {
711
- index: 1,
712
- delta: { content: "Second" },
713
- finish_reason: null,
714
- },
715
- ],
716
- },
717
- {
718
- id: "test-id",
719
- object: "chat.completion.chunk",
720
- created: 1234567890,
721
- model: "gpt-3.5-turbo",
722
- choices: [
723
- {
724
- index: 0,
725
- delta: { content: " content" },
726
- finish_reason: "stop",
727
- },
728
- {
729
- index: 1,
730
- delta: { content: " content" },
731
- finish_reason: "stop",
732
- },
733
- {
734
- index: 2,
735
- delta: { content: " content" },
736
- finish_reason: "stop",
737
- },
738
- ],
739
- },
740
- ];
741
-
742
- const stream = new ReadableStream<ChatCompletionChunk>({
743
- start(controller) {
744
- chunks.forEach(chunk => controller.enqueue(chunk));
745
- controller.close();
746
- },
747
- });
748
-
749
- const eventProcessor = vi.fn();
750
- const result = await reduceStreamingWithDispatch(stream, eventProcessor);
751
-
752
- expect(result).toBeDefined();
753
- expect(eventProcessor).toHaveBeenCalledTimes(3);
754
-
755
- const calls = eventProcessor.mock.calls.map(call => call[0]);
756
- expect(calls[0].get()).toBe("Third content");
757
- expect(calls[1].get()).toBe("First content");
758
- expect(calls[2].get()).toBe("Second content");
759
- });
760
-
761
- it("should handle mixed finish reasons", async () => {
762
- const chunks: ChatCompletionChunk[] = [
763
- {
764
- id: "test-id",
765
- object: "chat.completion.chunk",
766
- created: 1234567890,
767
- model: "gpt-3.5-turbo",
768
- choices: [
769
- {
770
- index: 0,
771
- delta: { content: "Hello" },
772
- finish_reason: null,
773
- },
774
- {
775
- index: 1,
776
- delta: { content: "World" },
777
- finish_reason: null,
778
- },
779
- ],
780
- },
781
- {
782
- id: "test-id",
783
- object: "chat.completion.chunk",
784
- created: 1234567890,
785
- model: "gpt-3.5-turbo",
786
- choices: [
787
- {
788
- index: 0,
789
- delta: { content: " there" },
790
- finish_reason: "stop",
791
- },
792
- {
793
- index: 1,
794
- delta: { content: "!" },
795
- finish_reason: "length",
796
- },
797
- ],
798
- },
799
- ];
800
-
801
- const stream = StreamUtil.from(...chunks);
802
-
803
- const eventProcessor = vi.fn();
804
- const result = await reduceStreamingWithDispatch(stream, eventProcessor);
805
-
806
- expect(result).toBeDefined();
807
- expect(eventProcessor).toHaveBeenCalledTimes(2);
808
-
809
- const firstCall = eventProcessor.mock.calls[0]?.[0];
810
- const secondCall = eventProcessor.mock.calls[1]?.[0];
811
- expect(firstCall.get()).toBe("Hello there");
812
- expect(secondCall.get()).toBe("World!");
813
- await firstCall.join();
814
- await secondCall.join();
815
- expect(firstCall.done()).toBe(true);
816
- expect(secondCall.done()).toBe(true);
817
- });
818
-
819
- it("should handle Unicode and special characters", async () => {
820
- const specialContent = "Hello 🌍! 안녕하세요 مرحبا 🚀 ñáéíóú";
821
- const chunks: ChatCompletionChunk[] = [
822
- {
823
- id: "test-id",
824
- object: "chat.completion.chunk",
825
- created: 1234567890,
826
- model: "gpt-3.5-turbo",
827
- choices: [
828
- {
829
- index: 0,
830
- delta: { content: specialContent },
831
- finish_reason: "stop",
832
- },
833
- ],
834
- },
835
- ];
836
-
837
- const stream = StreamUtil.from(...chunks);
838
-
839
- const eventProcessor = vi.fn();
840
- const result = await reduceStreamingWithDispatch(stream, eventProcessor);
841
-
842
- expect(result).toBeDefined();
843
- // Now single chunk with content should trigger eventProcessor
844
- expect(eventProcessor).toHaveBeenCalledOnce();
845
-
846
- const eventCall = eventProcessor.mock.calls[0]?.[0];
847
- expect(eventCall.get()).toBe(specialContent);
848
- });
849
-
850
- it("should handle stream reader errors gracefully", async () => {
851
- const chunks: ChatCompletionChunk[] = [
852
- {
853
- id: "test-id",
854
- object: "chat.completion.chunk",
855
- created: 1234567890,
856
- model: "gpt-3.5-turbo",
857
- choices: [
858
- {
859
- index: 0,
860
- delta: { content: "Hello" },
861
- finish_reason: null,
862
- },
863
- ],
864
- },
865
- ];
866
-
867
- const stream = new ReadableStream<ChatCompletionChunk>({
868
- start(controller) {
869
- controller.enqueue(chunks[0]);
870
- // Simulate an error in the stream
871
- controller.error(new Error("Stream error"));
872
- },
873
- });
874
-
875
- const eventProcessor = vi.fn();
876
-
877
- await expect(reduceStreamingWithDispatch(stream, eventProcessor))
878
- .rejects
879
- .toThrow("Stream error");
880
- });
881
-
882
- it("should handle completely malformed chunks gracefully", async () => {
883
- const malformedChunk = {
884
- // Missing required fields
885
- object: "chat.completion.chunk",
886
- choices: [
887
- {
888
- // Missing index
889
- delta: { content: "Hello" },
890
- finish_reason: null,
891
- },
892
- ],
893
- } as any;
894
-
895
- const stream = new ReadableStream<ChatCompletionChunk>({
896
- start(controller) {
897
- controller.enqueue(malformedChunk as ChatCompletionChunk);
898
- controller.close();
899
- },
900
- });
901
-
902
- const eventProcessor = vi.fn();
903
-
904
- // Should not throw, but should handle gracefully
905
- const result = await reduceStreamingWithDispatch(stream, eventProcessor);
906
- expect(result).toBeDefined();
907
- });
908
- });
909
- });
1
+ import type { ChatCompletionChunk } from "openai/resources";
2
+
3
+ import { beforeEach, describe, expect, it, vi } from "vitest";
4
+
5
+ import { reduceStreamingWithDispatch } from "./ChatGptCompletionStreamingUtil";
6
+ import { StreamUtil } from "./StreamUtil";
7
+
8
+ describe("reduceStreamingWithDispatch", () => {
9
+ beforeEach(() => {
10
+ vi.clearAllMocks();
11
+ });
12
+
13
+ describe("basic functionality", () => {
14
+ it("should process single chunk successfully", async () => {
15
+ const mockChunk: ChatCompletionChunk = {
16
+ id: "test-id",
17
+ object: "chat.completion.chunk",
18
+ created: 1234567890,
19
+ model: "gpt-3.5-turbo",
20
+ choices: [
21
+ {
22
+ index: 0,
23
+ delta: { content: "Hello" },
24
+ finish_reason: null,
25
+ },
26
+ ],
27
+ };
28
+
29
+ const stream = new ReadableStream<ChatCompletionChunk>({
30
+ start(controller) {
31
+ controller.enqueue(mockChunk);
32
+ controller.close();
33
+ },
34
+ });
35
+
36
+ const eventProcessor = vi.fn();
37
+ const result = await reduceStreamingWithDispatch(stream, eventProcessor);
38
+
39
+ expect(result).toBeDefined();
40
+ expect(result.object).toBe("chat.completion");
41
+ expect(eventProcessor).toHaveBeenCalledTimes(1);
42
+ });
43
+
44
+ it("should handle multiple chunks with content accumulation", async () => {
45
+ const chunks: ChatCompletionChunk[] = [
46
+ {
47
+ id: "test-id",
48
+ object: "chat.completion.chunk",
49
+ created: 1234567890,
50
+ model: "gpt-3.5-turbo",
51
+ choices: [
52
+ {
53
+ index: 0,
54
+ delta: { content: "Hello" },
55
+ finish_reason: null,
56
+ },
57
+ ],
58
+ },
59
+ {
60
+ id: "test-id",
61
+ object: "chat.completion.chunk",
62
+ created: 1234567890,
63
+ model: "gpt-3.5-turbo",
64
+ choices: [
65
+ {
66
+ index: 0,
67
+ delta: { content: " World" },
68
+ finish_reason: null,
69
+ },
70
+ ],
71
+ },
72
+ {
73
+ id: "test-id",
74
+ object: "chat.completion.chunk",
75
+ created: 1234567890,
76
+ model: "gpt-3.5-turbo",
77
+ choices: [
78
+ {
79
+ index: 0,
80
+ delta: { content: "!" },
81
+ finish_reason: "stop",
82
+ },
83
+ ],
84
+ },
85
+ ];
86
+
87
+ const stream = new ReadableStream<ChatCompletionChunk>({
88
+ start(controller) {
89
+ chunks.forEach(chunk => controller.enqueue(chunk));
90
+ controller.close();
91
+ },
92
+ });
93
+
94
+ const eventProcessor = vi.fn();
95
+ const result = await reduceStreamingWithDispatch(stream, eventProcessor);
96
+
97
+ expect(result).toBeDefined();
98
+ expect(result.object).toBe("chat.completion");
99
+ expect(eventProcessor).toHaveBeenCalledTimes(1);
100
+
101
+ const eventCall = eventProcessor.mock.calls[0]?.[0];
102
+ expect(eventCall.get()).toBe("Hello World!");
103
+ });
104
+
105
+ it("should handle empty content chunks", async () => {
106
+ const chunks: ChatCompletionChunk[] = [
107
+ {
108
+ id: "test-id",
109
+ object: "chat.completion.chunk",
110
+ created: 1234567890,
111
+ model: "gpt-3.5-turbo",
112
+ choices: [
113
+ {
114
+ index: 0,
115
+ delta: { content: "" },
116
+ finish_reason: null,
117
+ },
118
+ ],
119
+ },
120
+ {
121
+ id: "test-id",
122
+ object: "chat.completion.chunk",
123
+ created: 1234567890,
124
+ model: "gpt-3.5-turbo",
125
+ choices: [
126
+ {
127
+ index: 0,
128
+ delta: { content: "Hello" },
129
+ finish_reason: null,
130
+ },
131
+ ],
132
+ },
133
+ ];
134
+
135
+ const stream = new ReadableStream<ChatCompletionChunk>({
136
+ start(controller) {
137
+ chunks.forEach(chunk => controller.enqueue(chunk));
138
+ controller.close();
139
+ },
140
+ });
141
+
142
+ const eventProcessor = vi.fn();
143
+ const result = await reduceStreamingWithDispatch(stream, eventProcessor);
144
+
145
+ expect(result).toBeDefined();
146
+ expect(eventProcessor).toHaveBeenCalledTimes(1);
147
+
148
+ const eventCall = eventProcessor.mock.calls[0]?.[0];
149
+ expect(eventCall.get()).toBe("Hello");
150
+ });
151
+ });
152
+
153
+ describe("multiple choices handling", () => {
154
+ it("should handle multiple choices with different indices", async () => {
155
+ const chunks: ChatCompletionChunk[] = [
156
+ {
157
+ id: "test-id",
158
+ object: "chat.completion.chunk",
159
+ created: 1234567890,
160
+ model: "gpt-3.5-turbo",
161
+ choices: [
162
+ {
163
+ index: 0,
164
+ delta: { content: "Choice 1" },
165
+ finish_reason: null,
166
+ },
167
+ {
168
+ index: 1,
169
+ delta: { content: "Choice 2" },
170
+ finish_reason: null,
171
+ },
172
+ ],
173
+ },
174
+ {
175
+ id: "test-id",
176
+ object: "chat.completion.chunk",
177
+ created: 1234567890,
178
+ model: "gpt-3.5-turbo",
179
+ choices: [
180
+ {
181
+ index: 0,
182
+ delta: { content: " continued" },
183
+ finish_reason: "stop",
184
+ },
185
+ {
186
+ index: 1,
187
+ delta: { content: " continued" },
188
+ finish_reason: "stop",
189
+ },
190
+ ],
191
+ },
192
+ ];
193
+
194
+ const stream = new ReadableStream<ChatCompletionChunk>({
195
+ start(controller) {
196
+ chunks.forEach(chunk => controller.enqueue(chunk));
197
+ controller.close();
198
+ },
199
+ });
200
+
201
+ const eventProcessor = vi.fn();
202
+ const result = await reduceStreamingWithDispatch(stream, eventProcessor);
203
+
204
+ expect(result).toBeDefined();
205
+ expect(eventProcessor).toHaveBeenCalledTimes(2);
206
+
207
+ const firstCall = eventProcessor.mock.calls[0]?.[0];
208
+ const secondCall = eventProcessor.mock.calls[1]?.[0];
209
+ expect(firstCall.get()).toBe("Choice 1 continued");
210
+ expect(secondCall.get()).toBe("Choice 2 continued");
211
+ });
212
+ });
213
+
214
+ describe("finish reason handling", () => {
215
+ it("should close context when finish_reason is provided", async () => {
216
+ const chunks: ChatCompletionChunk[] = [
217
+ {
218
+ id: "test-id",
219
+ object: "chat.completion.chunk",
220
+ created: 1234567890,
221
+ model: "gpt-3.5-turbo",
222
+ choices: [
223
+ {
224
+ index: 0,
225
+ delta: { content: "Hello" },
226
+ finish_reason: null,
227
+ },
228
+ ],
229
+ },
230
+ {
231
+ id: "test-id",
232
+ object: "chat.completion.chunk",
233
+ created: 1234567890,
234
+ model: "gpt-3.5-turbo",
235
+ choices: [
236
+ {
237
+ index: 0,
238
+ delta: { content: " World" },
239
+ finish_reason: "stop",
240
+ },
241
+ ],
242
+ },
243
+ ];
244
+
245
+ const stream = new ReadableStream<ChatCompletionChunk>({
246
+ start(controller) {
247
+ chunks.forEach(chunk => controller.enqueue(chunk));
248
+ controller.close();
249
+ },
250
+ });
251
+
252
+ const eventProcessor = vi.fn();
253
+ const result = await reduceStreamingWithDispatch(stream, eventProcessor);
254
+
255
+ expect(result).toBeDefined();
256
+ expect(eventProcessor).toHaveBeenCalledTimes(1);
257
+
258
+ const eventCall = eventProcessor.mock.calls[0]?.[0];
259
+ expect(eventCall.get()).toBe("Hello World");
260
+ expect(eventCall.done()).toBe(true);
261
+ });
262
+ });
263
+
264
+ describe("stream processing", () => {
265
+ it("should provide working stream in event processor", async () => {
266
+ const chunks: ChatCompletionChunk[] = [
267
+ {
268
+ id: "test-id",
269
+ object: "chat.completion.chunk",
270
+ created: 1234567890,
271
+ model: "gpt-3.5-turbo",
272
+ choices: [
273
+ {
274
+ index: 0,
275
+ delta: { content: "Hello" },
276
+ finish_reason: null,
277
+ },
278
+ ],
279
+ },
280
+ {
281
+ id: "test-id",
282
+ object: "chat.completion.chunk",
283
+ created: 1234567890,
284
+ model: "gpt-3.5-turbo",
285
+ choices: [
286
+ {
287
+ index: 0,
288
+ delta: { content: " World" },
289
+ finish_reason: "stop",
290
+ },
291
+ ],
292
+ },
293
+ ];
294
+
295
+ const stream = new ReadableStream<ChatCompletionChunk>({
296
+ start(controller) {
297
+ chunks.forEach(chunk => controller.enqueue(chunk));
298
+ controller.close();
299
+ },
300
+ });
301
+ const streamedContent: string[] = [];
302
+ await new Promise(async (resolve) => {
303
+ const eventProcessor = vi.fn(({ stream: contentStream }) => {
304
+ void (async () => {
305
+ for await (const content of contentStream) {
306
+ streamedContent.push(content as string);
307
+ }
308
+ resolve(true);
309
+ })().catch(() => {});
310
+ });
311
+ await reduceStreamingWithDispatch(stream, eventProcessor);
312
+ });
313
+ expect(streamedContent).toEqual(["Hello", " World"]);
314
+ });
315
+
316
+ it("should provide working join function", async () => {
317
+ const chunks: ChatCompletionChunk[] = [
318
+ {
319
+ id: "test-id",
320
+ object: "chat.completion.chunk",
321
+ created: 1234567890,
322
+ model: "gpt-3.5-turbo",
323
+ choices: [
324
+ {
325
+ index: 0,
326
+ delta: { content: "Hello" },
327
+ finish_reason: null,
328
+ },
329
+ ],
330
+ },
331
+ {
332
+ id: "test-id",
333
+ object: "chat.completion.chunk",
334
+ created: 1234567890,
335
+ model: "gpt-3.5-turbo",
336
+ choices: [
337
+ {
338
+ index: 0,
339
+ delta: { content: " World" },
340
+ finish_reason: "stop",
341
+ },
342
+ ],
343
+ },
344
+ ];
345
+
346
+ const stream = new ReadableStream<ChatCompletionChunk>({
347
+ start(controller) {
348
+ chunks.forEach(chunk => controller.enqueue(chunk));
349
+ controller.close();
350
+ },
351
+ });
352
+
353
+ let joinedContent = "";
354
+ const eventProcessor = vi.fn(async ({ join }) => {
355
+ joinedContent = await join();
356
+ });
357
+
358
+ const result = await reduceStreamingWithDispatch(stream, eventProcessor);
359
+
360
+ expect(result).toBeDefined();
361
+ expect(joinedContent).toBe("Hello World");
362
+ });
363
+ });
364
+
365
+ describe("error handling", () => {
366
+ it("should throw error for empty stream", async () => {
367
+ const stream = new ReadableStream<ChatCompletionChunk>({
368
+ start(controller) {
369
+ controller.close();
370
+ },
371
+ });
372
+
373
+ const eventProcessor = vi.fn();
374
+
375
+ await expect(reduceStreamingWithDispatch(stream, eventProcessor)).rejects.toThrow(
376
+ "StreamUtil.reduce did not produce a ChatCompletion",
377
+ );
378
+ });
379
+
380
+ it("should handle stream with only finish_reason chunks", async () => {
381
+ const chunks: ChatCompletionChunk[] = [
382
+ {
383
+ id: "test-id",
384
+ object: "chat.completion.chunk",
385
+ created: 1234567890,
386
+ model: "gpt-3.5-turbo",
387
+ choices: [
388
+ {
389
+ index: 0,
390
+ delta: { content: null },
391
+ finish_reason: "stop",
392
+ },
393
+ ],
394
+ },
395
+ ];
396
+
397
+ const stream = new ReadableStream<ChatCompletionChunk>({
398
+ start(controller) {
399
+ chunks.forEach(chunk => controller.enqueue(chunk));
400
+ controller.close();
401
+ },
402
+ });
403
+
404
+ const eventProcessor = vi.fn();
405
+ const result = await reduceStreamingWithDispatch(stream, eventProcessor);
406
+
407
+ expect(result).toBeDefined();
408
+ expect(eventProcessor).not.toHaveBeenCalled();
409
+ });
410
+ });
411
+
412
+ describe("complex scenarios", () => {
413
+ it("should handle mixed content and finish_reason chunks", async () => {
414
+ const chunks: ChatCompletionChunk[] = [
415
+ {
416
+ id: "test-id",
417
+ object: "chat.completion.chunk",
418
+ created: 1234567890,
419
+ model: "gpt-3.5-turbo",
420
+ choices: [
421
+ {
422
+ index: 0,
423
+ delta: { content: "Hello" },
424
+ finish_reason: null,
425
+ },
426
+ ],
427
+ },
428
+ {
429
+ id: "test-id",
430
+ object: "chat.completion.chunk",
431
+ created: 1234567890,
432
+ model: "gpt-3.5-turbo",
433
+ choices: [
434
+ {
435
+ index: 0,
436
+ delta: { content: null },
437
+ finish_reason: null,
438
+ },
439
+ ],
440
+ },
441
+ {
442
+ id: "test-id",
443
+ object: "chat.completion.chunk",
444
+ created: 1234567890,
445
+ model: "gpt-3.5-turbo",
446
+ choices: [
447
+ {
448
+ index: 0,
449
+ delta: { content: " World" },
450
+ finish_reason: null,
451
+ },
452
+ ],
453
+ },
454
+ {
455
+ id: "test-id",
456
+ object: "chat.completion.chunk",
457
+ created: 1234567890,
458
+ model: "gpt-3.5-turbo",
459
+ choices: [
460
+ {
461
+ index: 0,
462
+ delta: { content: null },
463
+ finish_reason: "stop",
464
+ },
465
+ ],
466
+ },
467
+ ];
468
+
469
+ const stream = new ReadableStream<ChatCompletionChunk>({
470
+ start(controller) {
471
+ chunks.forEach(chunk => controller.enqueue(chunk));
472
+ controller.close();
473
+ },
474
+ });
475
+
476
+ const eventProcessor = vi.fn();
477
+ const result = await reduceStreamingWithDispatch(stream, eventProcessor);
478
+
479
+ expect(result).toBeDefined();
480
+ expect(eventProcessor).toHaveBeenCalledTimes(1);
481
+
482
+ const eventCall = eventProcessor.mock.calls[0]?.[0];
483
+ expect(eventCall.get()).toBe("Hello World");
484
+ });
485
+ });
486
+
487
+ describe("edge cases and exceptions", () => {
488
+ it("should handle null delta content", async () => {
489
+ const chunks: ChatCompletionChunk[] = [
490
+ {
491
+ id: "test-id",
492
+ object: "chat.completion.chunk",
493
+ created: 1234567890,
494
+ model: "gpt-3.5-turbo",
495
+ choices: [
496
+ {
497
+ index: 0,
498
+ delta: { content: null },
499
+ finish_reason: null,
500
+ },
501
+ ],
502
+ },
503
+ {
504
+ id: "test-id",
505
+ object: "chat.completion.chunk",
506
+ created: 1234567890,
507
+ model: "gpt-3.5-turbo",
508
+ choices: [
509
+ {
510
+ index: 0,
511
+ delta: { content: "Hello" },
512
+ finish_reason: "stop",
513
+ },
514
+ ],
515
+ },
516
+ ];
517
+
518
+ const stream = new ReadableStream<ChatCompletionChunk>({
519
+ start(controller) {
520
+ chunks.forEach(chunk => controller.enqueue(chunk));
521
+ controller.close();
522
+ },
523
+ });
524
+
525
+ const eventProcessor = vi.fn();
526
+ const result = await reduceStreamingWithDispatch(stream, eventProcessor);
527
+
528
+ expect(result).toBeDefined();
529
+ expect(eventProcessor).toHaveBeenCalledTimes(1);
530
+
531
+ const eventCall = eventProcessor.mock.calls[0]?.[0];
532
+ expect(eventCall.get()).toBe("Hello");
533
+ });
534
+
535
+ it("should handle missing delta object", async () => {
536
+ const chunks: ChatCompletionChunk[] = [
537
+ {
538
+ id: "test-id",
539
+ object: "chat.completion.chunk",
540
+ created: 1234567890,
541
+ model: "gpt-3.5-turbo",
542
+ choices: [
543
+ {
544
+ index: 0,
545
+ delta: {},
546
+ finish_reason: null,
547
+ },
548
+ ],
549
+ },
550
+ {
551
+ id: "test-id",
552
+ object: "chat.completion.chunk",
553
+ created: 1234567890,
554
+ model: "gpt-3.5-turbo",
555
+ choices: [
556
+ {
557
+ index: 0,
558
+ delta: { content: "Hello" },
559
+ finish_reason: "stop",
560
+ },
561
+ ],
562
+ },
563
+ ];
564
+
565
+ const stream = new ReadableStream<ChatCompletionChunk>({
566
+ start(controller) {
567
+ chunks.forEach(chunk => controller.enqueue(chunk));
568
+ controller.close();
569
+ },
570
+ });
571
+
572
+ const eventProcessor = vi.fn();
573
+ const result = await reduceStreamingWithDispatch(stream, eventProcessor);
574
+
575
+ expect(result).toBeDefined();
576
+ expect(eventProcessor).toHaveBeenCalledTimes(1);
577
+
578
+ const eventCall = eventProcessor.mock.calls[0]?.[0];
579
+ expect(eventCall.get()).toBe("Hello");
580
+ });
581
+
582
+ it("should handle chunks with no choices", async () => {
583
+ const chunks: ChatCompletionChunk[] = [
584
+ {
585
+ id: "test-id",
586
+ object: "chat.completion.chunk",
587
+ created: 1234567890,
588
+ model: "gpt-3.5-turbo",
589
+ choices: [],
590
+ },
591
+ {
592
+ id: "test-id",
593
+ object: "chat.completion.chunk",
594
+ created: 1234567890,
595
+ model: "gpt-3.5-turbo",
596
+ choices: [
597
+ {
598
+ index: 0,
599
+ delta: { content: "Hello" },
600
+ finish_reason: "stop",
601
+ },
602
+ ],
603
+ },
604
+ ];
605
+
606
+ const stream = new ReadableStream<ChatCompletionChunk>({
607
+ start(controller) {
608
+ chunks.forEach(chunk => controller.enqueue(chunk));
609
+ controller.close();
610
+ },
611
+ });
612
+
613
+ const eventProcessor = vi.fn();
614
+ const result = await reduceStreamingWithDispatch(stream, eventProcessor);
615
+
616
+ expect(result).toBeDefined();
617
+ expect(eventProcessor).toHaveBeenCalledTimes(1);
618
+
619
+ const eventCall = eventProcessor.mock.calls[0]?.[0];
620
+ expect(eventCall.get()).toBe("Hello");
621
+ });
622
+
623
+ it("should handle very large content chunks", async () => {
624
+ const largeContent = "x".repeat(10000);
625
+ const chunks: ChatCompletionChunk[] = [
626
+ {
627
+ id: "test-id",
628
+ object: "chat.completion.chunk",
629
+ created: 1234567890,
630
+ model: "gpt-3.5-turbo",
631
+ choices: [
632
+ {
633
+ index: 0,
634
+ delta: { content: largeContent },
635
+ finish_reason: "stop",
636
+ },
637
+ ],
638
+ },
639
+ ];
640
+
641
+ const stream = new ReadableStream<ChatCompletionChunk>({
642
+ start(controller) {
643
+ chunks.forEach(chunk => controller.enqueue(chunk));
644
+ controller.close();
645
+ },
646
+ });
647
+
648
+ const eventProcessor = vi.fn();
649
+ const result = await reduceStreamingWithDispatch(stream, eventProcessor);
650
+
651
+ expect(result).toBeDefined();
652
+ // Now single chunk with content should trigger eventProcessor
653
+ expect(eventProcessor).toHaveBeenCalledOnce();
654
+
655
+ const eventCall = eventProcessor.mock.calls[0]?.[0];
656
+ expect(eventCall.get()).toBe(largeContent);
657
+ });
658
+
659
+ it("should handle rapid consecutive chunks", async () => {
660
+ const chunks: ChatCompletionChunk[] = Array.from({ length: 100 }, (_, i) => ({
661
+ id: "test-id",
662
+ object: "chat.completion.chunk" as const,
663
+ created: 1234567890,
664
+ model: "gpt-3.5-turbo",
665
+ choices: [
666
+ {
667
+ index: 0,
668
+ delta: { content: i.toString() },
669
+ finish_reason: i === 99 ? "stop" as const : null,
670
+ },
671
+ ],
672
+ }));
673
+
674
+ const stream = new ReadableStream<ChatCompletionChunk>({
675
+ start(controller) {
676
+ chunks.forEach(chunk => controller.enqueue(chunk));
677
+ controller.close();
678
+ },
679
+ });
680
+
681
+ const eventProcessor = vi.fn();
682
+ const result = await reduceStreamingWithDispatch(stream, eventProcessor);
683
+
684
+ expect(result).toBeDefined();
685
+ expect(eventProcessor).toHaveBeenCalledTimes(1);
686
+
687
+ const eventCall = eventProcessor.mock.calls[0]?.[0];
688
+ const expectedContent = Array.from({ length: 100 }, (_, i) => i.toString()).join("");
689
+ expect(eventCall.get()).toBe(expectedContent);
690
+ });
691
+
692
+ it("should handle out-of-order choice indices", async () => {
693
+ const chunks: ChatCompletionChunk[] = [
694
+ {
695
+ id: "test-id",
696
+ object: "chat.completion.chunk",
697
+ created: 1234567890,
698
+ model: "gpt-3.5-turbo",
699
+ choices: [
700
+ {
701
+ index: 2,
702
+ delta: { content: "Third" },
703
+ finish_reason: null,
704
+ },
705
+ {
706
+ index: 0,
707
+ delta: { content: "First" },
708
+ finish_reason: null,
709
+ },
710
+ {
711
+ index: 1,
712
+ delta: { content: "Second" },
713
+ finish_reason: null,
714
+ },
715
+ ],
716
+ },
717
+ {
718
+ id: "test-id",
719
+ object: "chat.completion.chunk",
720
+ created: 1234567890,
721
+ model: "gpt-3.5-turbo",
722
+ choices: [
723
+ {
724
+ index: 0,
725
+ delta: { content: " content" },
726
+ finish_reason: "stop",
727
+ },
728
+ {
729
+ index: 1,
730
+ delta: { content: " content" },
731
+ finish_reason: "stop",
732
+ },
733
+ {
734
+ index: 2,
735
+ delta: { content: " content" },
736
+ finish_reason: "stop",
737
+ },
738
+ ],
739
+ },
740
+ ];
741
+
742
+ const stream = new ReadableStream<ChatCompletionChunk>({
743
+ start(controller) {
744
+ chunks.forEach(chunk => controller.enqueue(chunk));
745
+ controller.close();
746
+ },
747
+ });
748
+
749
+ const eventProcessor = vi.fn();
750
+ const result = await reduceStreamingWithDispatch(stream, eventProcessor);
751
+
752
+ expect(result).toBeDefined();
753
+ expect(eventProcessor).toHaveBeenCalledTimes(3);
754
+
755
+ const calls = eventProcessor.mock.calls.map(call => call[0]);
756
+ expect(calls[0].get()).toBe("Third content");
757
+ expect(calls[1].get()).toBe("First content");
758
+ expect(calls[2].get()).toBe("Second content");
759
+ });
760
+
761
+ it("should handle mixed finish reasons", async () => {
762
+ const chunks: ChatCompletionChunk[] = [
763
+ {
764
+ id: "test-id",
765
+ object: "chat.completion.chunk",
766
+ created: 1234567890,
767
+ model: "gpt-3.5-turbo",
768
+ choices: [
769
+ {
770
+ index: 0,
771
+ delta: { content: "Hello" },
772
+ finish_reason: null,
773
+ },
774
+ {
775
+ index: 1,
776
+ delta: { content: "World" },
777
+ finish_reason: null,
778
+ },
779
+ ],
780
+ },
781
+ {
782
+ id: "test-id",
783
+ object: "chat.completion.chunk",
784
+ created: 1234567890,
785
+ model: "gpt-3.5-turbo",
786
+ choices: [
787
+ {
788
+ index: 0,
789
+ delta: { content: " there" },
790
+ finish_reason: "stop",
791
+ },
792
+ {
793
+ index: 1,
794
+ delta: { content: "!" },
795
+ finish_reason: "length",
796
+ },
797
+ ],
798
+ },
799
+ ];
800
+
801
+ const stream = StreamUtil.from(...chunks);
802
+
803
+ const eventProcessor = vi.fn();
804
+ const result = await reduceStreamingWithDispatch(stream, eventProcessor);
805
+
806
+ expect(result).toBeDefined();
807
+ expect(eventProcessor).toHaveBeenCalledTimes(2);
808
+
809
+ const firstCall = eventProcessor.mock.calls[0]?.[0];
810
+ const secondCall = eventProcessor.mock.calls[1]?.[0];
811
+ expect(firstCall.get()).toBe("Hello there");
812
+ expect(secondCall.get()).toBe("World!");
813
+ await firstCall.join();
814
+ await secondCall.join();
815
+ expect(firstCall.done()).toBe(true);
816
+ expect(secondCall.done()).toBe(true);
817
+ });
818
+
819
+ it("should handle Unicode and special characters", async () => {
820
+ const specialContent = "Hello 🌍! 안녕하세요 مرحبا 🚀 ñáéíóú";
821
+ const chunks: ChatCompletionChunk[] = [
822
+ {
823
+ id: "test-id",
824
+ object: "chat.completion.chunk",
825
+ created: 1234567890,
826
+ model: "gpt-3.5-turbo",
827
+ choices: [
828
+ {
829
+ index: 0,
830
+ delta: { content: specialContent },
831
+ finish_reason: "stop",
832
+ },
833
+ ],
834
+ },
835
+ ];
836
+
837
+ const stream = StreamUtil.from(...chunks);
838
+
839
+ const eventProcessor = vi.fn();
840
+ const result = await reduceStreamingWithDispatch(stream, eventProcessor);
841
+
842
+ expect(result).toBeDefined();
843
+ // Now single chunk with content should trigger eventProcessor
844
+ expect(eventProcessor).toHaveBeenCalledOnce();
845
+
846
+ const eventCall = eventProcessor.mock.calls[0]?.[0];
847
+ expect(eventCall.get()).toBe(specialContent);
848
+ });
849
+
850
+ it("should handle stream reader errors gracefully", async () => {
851
+ const chunks: ChatCompletionChunk[] = [
852
+ {
853
+ id: "test-id",
854
+ object: "chat.completion.chunk",
855
+ created: 1234567890,
856
+ model: "gpt-3.5-turbo",
857
+ choices: [
858
+ {
859
+ index: 0,
860
+ delta: { content: "Hello" },
861
+ finish_reason: null,
862
+ },
863
+ ],
864
+ },
865
+ ];
866
+
867
+ const stream = new ReadableStream<ChatCompletionChunk>({
868
+ start(controller) {
869
+ controller.enqueue(chunks[0]);
870
+ // Simulate an error in the stream
871
+ controller.error(new Error("Stream error"));
872
+ },
873
+ });
874
+
875
+ const eventProcessor = vi.fn();
876
+
877
+ await expect(reduceStreamingWithDispatch(stream, eventProcessor))
878
+ .rejects
879
+ .toThrow("Stream error");
880
+ });
881
+
882
+ it("should handle completely malformed chunks gracefully", async () => {
883
+ const malformedChunk = {
884
+ // Missing required fields
885
+ object: "chat.completion.chunk",
886
+ choices: [
887
+ {
888
+ // Missing index
889
+ delta: { content: "Hello" },
890
+ finish_reason: null,
891
+ },
892
+ ],
893
+ } as any;
894
+
895
+ const stream = new ReadableStream<ChatCompletionChunk>({
896
+ start(controller) {
897
+ controller.enqueue(malformedChunk as ChatCompletionChunk);
898
+ controller.close();
899
+ },
900
+ });
901
+
902
+ const eventProcessor = vi.fn();
903
+
904
+ // Should not throw, but should handle gracefully
905
+ const result = await reduceStreamingWithDispatch(stream, eventProcessor);
906
+ expect(result).toBeDefined();
907
+ });
908
+ });
909
+ });