assistant-stream 0.2.46 → 0.3.0

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 (40) hide show
  1. package/dist/core/converters/index.d.ts +2 -0
  2. package/dist/core/converters/index.d.ts.map +1 -0
  3. package/dist/core/converters/index.js +2 -0
  4. package/dist/core/converters/index.js.map +1 -0
  5. package/dist/core/converters/toGenericMessages.d.ts +71 -0
  6. package/dist/core/converters/toGenericMessages.d.ts.map +1 -0
  7. package/dist/core/converters/toGenericMessages.js +155 -0
  8. package/dist/core/converters/toGenericMessages.js.map +1 -0
  9. package/dist/core/index.d.ts +2 -0
  10. package/dist/core/index.d.ts.map +1 -1
  11. package/dist/core/index.js +2 -0
  12. package/dist/core/index.js.map +1 -1
  13. package/dist/core/serialization/ui-message-stream/UIMessageStream.d.ts +19 -0
  14. package/dist/core/serialization/ui-message-stream/UIMessageStream.d.ts.map +1 -0
  15. package/dist/core/serialization/ui-message-stream/UIMessageStream.js +231 -0
  16. package/dist/core/serialization/ui-message-stream/UIMessageStream.js.map +1 -0
  17. package/dist/core/serialization/ui-message-stream/chunk-types.d.ts +78 -0
  18. package/dist/core/serialization/ui-message-stream/chunk-types.d.ts.map +1 -0
  19. package/dist/core/serialization/ui-message-stream/chunk-types.js +2 -0
  20. package/dist/core/serialization/ui-message-stream/chunk-types.js.map +1 -0
  21. package/dist/core/tool/index.d.ts +1 -0
  22. package/dist/core/tool/index.d.ts.map +1 -1
  23. package/dist/core/tool/index.js +1 -0
  24. package/dist/core/tool/index.js.map +1 -1
  25. package/dist/core/tool/schema-utils.d.ts +32 -0
  26. package/dist/core/tool/schema-utils.d.ts.map +1 -0
  27. package/dist/core/tool/schema-utils.js +67 -0
  28. package/dist/core/tool/schema-utils.js.map +1 -0
  29. package/package.json +3 -3
  30. package/src/core/converters/index.ts +12 -0
  31. package/src/core/converters/toGenericMessages.test.ts +721 -0
  32. package/src/core/converters/toGenericMessages.ts +264 -0
  33. package/src/core/index.ts +8 -0
  34. package/src/core/serialization/ui-message-stream/UIMessageStream.test.ts +370 -0
  35. package/src/core/serialization/ui-message-stream/UIMessageStream.ts +300 -0
  36. package/src/core/serialization/ui-message-stream/chunk-types.ts +60 -0
  37. package/src/core/tool/index.ts +6 -0
  38. package/src/core/tool/schema-utils.test.ts +382 -0
  39. package/src/core/tool/schema-utils.ts +112 -0
  40. package/src/core/tool/toolResultStream.test.ts +1 -1
@@ -0,0 +1,721 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { toGenericMessages } from "./toGenericMessages";
3
+
4
+ describe("toGenericMessages", () => {
5
+ describe("system messages", () => {
6
+ it("converts text content", () => {
7
+ const result = toGenericMessages([
8
+ {
9
+ role: "system",
10
+ content: [{ type: "text", text: "You are a helpful assistant." }],
11
+ },
12
+ ]);
13
+
14
+ expect(result).toEqual([
15
+ { role: "system", content: "You are a helpful assistant." },
16
+ ]);
17
+ });
18
+
19
+ it("handles empty text", () => {
20
+ const result = toGenericMessages([
21
+ {
22
+ role: "system",
23
+ content: [{ type: "text", text: "" }],
24
+ },
25
+ ]);
26
+
27
+ expect(result).toEqual([]);
28
+ });
29
+
30
+ it("handles missing text property", () => {
31
+ const result = toGenericMessages([
32
+ {
33
+ role: "system",
34
+ content: [{ type: "text" }],
35
+ },
36
+ ]);
37
+
38
+ expect(result).toEqual([]);
39
+ });
40
+
41
+ it("uses first text part when multiple exist", () => {
42
+ const result = toGenericMessages([
43
+ {
44
+ role: "system",
45
+ content: [
46
+ { type: "text", text: "First" },
47
+ { type: "text", text: "Second" },
48
+ ],
49
+ },
50
+ ]);
51
+
52
+ expect(result).toEqual([{ role: "system", content: "First" }]);
53
+ });
54
+ });
55
+
56
+ describe("user messages", () => {
57
+ it("converts text parts", () => {
58
+ const result = toGenericMessages([
59
+ {
60
+ role: "user",
61
+ content: [{ type: "text", text: "Hello!" }],
62
+ },
63
+ ]);
64
+
65
+ expect(result).toEqual([
66
+ { role: "user", content: [{ type: "text", text: "Hello!" }] },
67
+ ]);
68
+ });
69
+
70
+ it("converts image parts with media type inference", () => {
71
+ const result = toGenericMessages([
72
+ {
73
+ role: "user",
74
+ content: [{ type: "image", image: "https://example.com/photo.jpg" }],
75
+ },
76
+ ]);
77
+
78
+ expect(result).toEqual([
79
+ {
80
+ role: "user",
81
+ content: [
82
+ {
83
+ type: "file",
84
+ data: new URL("https://example.com/photo.jpg"),
85
+ mediaType: "image/jpeg",
86
+ },
87
+ ],
88
+ },
89
+ ]);
90
+ });
91
+
92
+ it("converts file parts", () => {
93
+ const result = toGenericMessages([
94
+ {
95
+ role: "user",
96
+ content: [
97
+ {
98
+ type: "file",
99
+ data: "https://example.com/doc.pdf",
100
+ mimeType: "application/pdf",
101
+ },
102
+ ],
103
+ },
104
+ ]);
105
+
106
+ expect(result).toEqual([
107
+ {
108
+ role: "user",
109
+ content: [
110
+ {
111
+ type: "file",
112
+ data: new URL("https://example.com/doc.pdf"),
113
+ mediaType: "application/pdf",
114
+ },
115
+ ],
116
+ },
117
+ ]);
118
+ });
119
+
120
+ it("handles attachments", () => {
121
+ const result = toGenericMessages([
122
+ {
123
+ role: "user",
124
+ content: [{ type: "text", text: "See attached" }],
125
+ attachments: [
126
+ {
127
+ content: [
128
+ {
129
+ type: "file",
130
+ data: "https://example.com/file.txt",
131
+ mimeType: "text/plain",
132
+ },
133
+ ],
134
+ },
135
+ ],
136
+ },
137
+ ]);
138
+
139
+ expect(result).toEqual([
140
+ {
141
+ role: "user",
142
+ content: [
143
+ { type: "text", text: "See attached" },
144
+ {
145
+ type: "file",
146
+ data: new URL("https://example.com/file.txt"),
147
+ mediaType: "text/plain",
148
+ },
149
+ ],
150
+ },
151
+ ]);
152
+ });
153
+
154
+ it("filters invalid parts", () => {
155
+ const result = toGenericMessages([
156
+ {
157
+ role: "user",
158
+ content: [
159
+ { type: "text", text: "Valid" },
160
+ { type: "image" }, // missing image property
161
+ { type: "file", data: "some-data" }, // missing mimeType
162
+ { type: "unknown" },
163
+ ],
164
+ },
165
+ ]);
166
+
167
+ expect(result).toEqual([
168
+ { role: "user", content: [{ type: "text", text: "Valid" }] },
169
+ ]);
170
+ });
171
+
172
+ it("handles data URL as URL object", () => {
173
+ const dataUrl =
174
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
175
+ const result = toGenericMessages([
176
+ {
177
+ role: "user",
178
+ content: [{ type: "image", image: dataUrl }],
179
+ },
180
+ ]);
181
+
182
+ expect(result).toHaveLength(1);
183
+ expect(result[0]!.role).toBe("user");
184
+ const content = (result[0] as { content: unknown[] }).content;
185
+ expect(content[0]).toMatchObject({
186
+ type: "file",
187
+ mediaType: "image/png",
188
+ });
189
+ expect((content[0] as { data: unknown }).data).toBeInstanceOf(URL);
190
+ });
191
+
192
+ it("handles relative/invalid URL as string", () => {
193
+ const result = toGenericMessages([
194
+ {
195
+ role: "user",
196
+ content: [
197
+ {
198
+ type: "file",
199
+ data: "/relative/path/file.txt",
200
+ mimeType: "text/plain",
201
+ },
202
+ ],
203
+ },
204
+ ]);
205
+
206
+ expect(result).toEqual([
207
+ {
208
+ role: "user",
209
+ content: [
210
+ {
211
+ type: "file",
212
+ data: "/relative/path/file.txt",
213
+ mediaType: "text/plain",
214
+ },
215
+ ],
216
+ },
217
+ ]);
218
+ });
219
+
220
+ it("produces empty result when all parts are invalid", () => {
221
+ const result = toGenericMessages([
222
+ {
223
+ role: "user",
224
+ content: [{ type: "unknown" }],
225
+ },
226
+ ]);
227
+
228
+ expect(result).toEqual([]);
229
+ });
230
+ });
231
+
232
+ describe("assistant messages", () => {
233
+ it("converts text parts", () => {
234
+ const result = toGenericMessages([
235
+ {
236
+ role: "assistant",
237
+ content: [{ type: "text", text: "Hello!" }],
238
+ },
239
+ ]);
240
+
241
+ expect(result).toEqual([
242
+ { role: "assistant", content: [{ type: "text", text: "Hello!" }] },
243
+ ]);
244
+ });
245
+
246
+ it("converts tool calls without results", () => {
247
+ const result = toGenericMessages([
248
+ {
249
+ role: "assistant",
250
+ content: [
251
+ {
252
+ type: "tool-call",
253
+ toolCallId: "call_123",
254
+ toolName: "get_weather",
255
+ args: { city: "London" },
256
+ },
257
+ ],
258
+ },
259
+ ]);
260
+
261
+ expect(result).toEqual([
262
+ {
263
+ role: "assistant",
264
+ content: [
265
+ {
266
+ type: "tool-call",
267
+ toolCallId: "call_123",
268
+ toolName: "get_weather",
269
+ args: { city: "London" },
270
+ },
271
+ ],
272
+ },
273
+ ]);
274
+ });
275
+
276
+ it("converts tool calls with results (creates tool message)", () => {
277
+ const result = toGenericMessages([
278
+ {
279
+ role: "assistant",
280
+ content: [
281
+ {
282
+ type: "tool-call",
283
+ toolCallId: "call_123",
284
+ toolName: "get_weather",
285
+ args: { city: "London" },
286
+ result: { temperature: 20, unit: "celsius" },
287
+ },
288
+ ],
289
+ },
290
+ ]);
291
+
292
+ expect(result).toEqual([
293
+ {
294
+ role: "assistant",
295
+ content: [
296
+ {
297
+ type: "tool-call",
298
+ toolCallId: "call_123",
299
+ toolName: "get_weather",
300
+ args: { city: "London" },
301
+ },
302
+ ],
303
+ },
304
+ {
305
+ role: "tool",
306
+ content: [
307
+ {
308
+ type: "tool-result",
309
+ toolCallId: "call_123",
310
+ toolName: "get_weather",
311
+ result: { temperature: 20, unit: "celsius" },
312
+ },
313
+ ],
314
+ },
315
+ ]);
316
+ });
317
+
318
+ it("includes isError in tool result when present", () => {
319
+ const result = toGenericMessages([
320
+ {
321
+ role: "assistant",
322
+ content: [
323
+ {
324
+ type: "tool-call",
325
+ toolCallId: "call_123",
326
+ toolName: "get_weather",
327
+ args: { city: "London" },
328
+ result: "Error: City not found",
329
+ isError: true,
330
+ },
331
+ ],
332
+ },
333
+ ]);
334
+
335
+ expect(result).toEqual([
336
+ {
337
+ role: "assistant",
338
+ content: [
339
+ {
340
+ type: "tool-call",
341
+ toolCallId: "call_123",
342
+ toolName: "get_weather",
343
+ args: { city: "London" },
344
+ },
345
+ ],
346
+ },
347
+ {
348
+ role: "tool",
349
+ content: [
350
+ {
351
+ type: "tool-result",
352
+ toolCallId: "call_123",
353
+ toolName: "get_weather",
354
+ result: "Error: City not found",
355
+ isError: true,
356
+ },
357
+ ],
358
+ },
359
+ ]);
360
+ });
361
+
362
+ it("handles text-tool interleaving (flush on text boundary)", () => {
363
+ const result = toGenericMessages([
364
+ {
365
+ role: "assistant",
366
+ content: [
367
+ { type: "text", text: "Let me check the weather." },
368
+ {
369
+ type: "tool-call",
370
+ toolCallId: "call_1",
371
+ toolName: "get_weather",
372
+ args: { city: "London" },
373
+ result: { temp: 20 },
374
+ },
375
+ { type: "text", text: "The weather is nice." },
376
+ ],
377
+ },
378
+ ]);
379
+
380
+ // The flush happens BEFORE adding text when there are pending tool results
381
+ // So first assistant message contains both initial text and tool call
382
+ expect(result).toEqual([
383
+ {
384
+ role: "assistant",
385
+ content: [
386
+ { type: "text", text: "Let me check the weather." },
387
+ {
388
+ type: "tool-call",
389
+ toolCallId: "call_1",
390
+ toolName: "get_weather",
391
+ args: { city: "London" },
392
+ },
393
+ ],
394
+ },
395
+ {
396
+ role: "tool",
397
+ content: [
398
+ {
399
+ type: "tool-result",
400
+ toolCallId: "call_1",
401
+ toolName: "get_weather",
402
+ result: { temp: 20 },
403
+ },
404
+ ],
405
+ },
406
+ {
407
+ role: "assistant",
408
+ content: [{ type: "text", text: "The weather is nice." }],
409
+ },
410
+ ]);
411
+ });
412
+
413
+ it("handles missing toolCallId", () => {
414
+ const result = toGenericMessages([
415
+ {
416
+ role: "assistant",
417
+ content: [
418
+ {
419
+ type: "tool-call",
420
+ toolName: "get_weather",
421
+ args: { city: "London" },
422
+ },
423
+ ],
424
+ },
425
+ ]);
426
+
427
+ // Tool call should be skipped due to missing toolCallId
428
+ expect(result).toEqual([]);
429
+ });
430
+
431
+ it("handles missing toolName", () => {
432
+ const result = toGenericMessages([
433
+ {
434
+ role: "assistant",
435
+ content: [
436
+ {
437
+ type: "tool-call",
438
+ toolCallId: "call_123",
439
+ args: { city: "London" },
440
+ },
441
+ ],
442
+ },
443
+ ]);
444
+
445
+ // Tool call should be skipped due to missing toolName
446
+ expect(result).toEqual([]);
447
+ });
448
+
449
+ it("defaults args to empty object when missing", () => {
450
+ const result = toGenericMessages([
451
+ {
452
+ role: "assistant",
453
+ content: [
454
+ {
455
+ type: "tool-call",
456
+ toolCallId: "call_123",
457
+ toolName: "no_args_tool",
458
+ },
459
+ ],
460
+ },
461
+ ]);
462
+
463
+ expect(result).toEqual([
464
+ {
465
+ role: "assistant",
466
+ content: [
467
+ {
468
+ type: "tool-call",
469
+ toolCallId: "call_123",
470
+ toolName: "no_args_tool",
471
+ args: {},
472
+ },
473
+ ],
474
+ },
475
+ ]);
476
+ });
477
+
478
+ it("handles multiple tool calls with mixed results", () => {
479
+ const result = toGenericMessages([
480
+ {
481
+ role: "assistant",
482
+ content: [
483
+ {
484
+ type: "tool-call",
485
+ toolCallId: "call_1",
486
+ toolName: "tool_a",
487
+ args: {},
488
+ result: "result_a",
489
+ },
490
+ {
491
+ type: "tool-call",
492
+ toolCallId: "call_2",
493
+ toolName: "tool_b",
494
+ args: {},
495
+ // no result
496
+ },
497
+ {
498
+ type: "tool-call",
499
+ toolCallId: "call_3",
500
+ toolName: "tool_c",
501
+ args: {},
502
+ result: "result_c",
503
+ },
504
+ ],
505
+ },
506
+ ]);
507
+
508
+ expect(result).toEqual([
509
+ {
510
+ role: "assistant",
511
+ content: [
512
+ {
513
+ type: "tool-call",
514
+ toolCallId: "call_1",
515
+ toolName: "tool_a",
516
+ args: {},
517
+ },
518
+ {
519
+ type: "tool-call",
520
+ toolCallId: "call_2",
521
+ toolName: "tool_b",
522
+ args: {},
523
+ },
524
+ {
525
+ type: "tool-call",
526
+ toolCallId: "call_3",
527
+ toolName: "tool_c",
528
+ args: {},
529
+ },
530
+ ],
531
+ },
532
+ {
533
+ role: "tool",
534
+ content: [
535
+ {
536
+ type: "tool-result",
537
+ toolCallId: "call_1",
538
+ toolName: "tool_a",
539
+ result: "result_a",
540
+ },
541
+ {
542
+ type: "tool-result",
543
+ toolCallId: "call_3",
544
+ toolName: "tool_c",
545
+ result: "result_c",
546
+ },
547
+ ],
548
+ },
549
+ ]);
550
+ });
551
+ });
552
+
553
+ describe("inferImageMediaType", () => {
554
+ const testMediaType = (url: string, expectedType: string) => {
555
+ const result = toGenericMessages([
556
+ {
557
+ role: "user",
558
+ content: [{ type: "image", image: url }],
559
+ },
560
+ ]);
561
+ const content = (result[0] as { content: { mediaType: string }[] })
562
+ .content;
563
+ expect(content[0]!.mediaType).toBe(expectedType);
564
+ };
565
+
566
+ it("handles jpg extension", () => {
567
+ testMediaType("https://example.com/photo.jpg", "image/jpeg");
568
+ });
569
+
570
+ it("handles jpeg extension", () => {
571
+ testMediaType("https://example.com/photo.jpeg", "image/jpeg");
572
+ });
573
+
574
+ it("handles png extension", () => {
575
+ testMediaType("https://example.com/photo.png", "image/png");
576
+ });
577
+
578
+ it("handles gif extension", () => {
579
+ testMediaType("https://example.com/photo.gif", "image/gif");
580
+ });
581
+
582
+ it("handles webp extension", () => {
583
+ testMediaType("https://example.com/photo.webp", "image/webp");
584
+ });
585
+
586
+ it("handles svg extension", () => {
587
+ testMediaType("https://example.com/icon.svg", "image/svg+xml");
588
+ });
589
+
590
+ it("handles avif extension", () => {
591
+ testMediaType("https://example.com/photo.avif", "image/avif");
592
+ });
593
+
594
+ it("handles bmp extension", () => {
595
+ testMediaType("https://example.com/photo.bmp", "image/bmp");
596
+ });
597
+
598
+ it("handles ico extension", () => {
599
+ testMediaType("https://example.com/favicon.ico", "image/x-icon");
600
+ });
601
+
602
+ it("handles tiff extension", () => {
603
+ testMediaType("https://example.com/photo.tiff", "image/tiff");
604
+ });
605
+
606
+ it("handles tif extension", () => {
607
+ testMediaType("https://example.com/photo.tif", "image/tiff");
608
+ });
609
+
610
+ it("defaults to image/png for unknown extension", () => {
611
+ testMediaType("https://example.com/photo.unknown", "image/png");
612
+ });
613
+
614
+ it("defaults to image/png for no extension", () => {
615
+ testMediaType("https://example.com/photo", "image/png");
616
+ });
617
+
618
+ it("handles query params in URL", () => {
619
+ testMediaType(
620
+ "https://example.com/photo.jpg?size=large&quality=high",
621
+ "image/jpeg",
622
+ );
623
+ });
624
+
625
+ it("handles uppercase extensions", () => {
626
+ testMediaType("https://example.com/photo.JPG", "image/jpeg");
627
+ });
628
+ });
629
+
630
+ describe("toUrlOrString", () => {
631
+ it("converts valid URL to URL object", () => {
632
+ const result = toGenericMessages([
633
+ {
634
+ role: "user",
635
+ content: [{ type: "image", image: "https://example.com/photo.png" }],
636
+ },
637
+ ]);
638
+ const content = (result[0] as { content: { data: unknown }[] }).content;
639
+ expect(content[0]!.data).toBeInstanceOf(URL);
640
+ expect((content[0]!.data as URL).href).toBe(
641
+ "https://example.com/photo.png",
642
+ );
643
+ });
644
+
645
+ it("converts data URL to URL object", () => {
646
+ const dataUrl = "data:image/png;base64,abc123";
647
+ const result = toGenericMessages([
648
+ {
649
+ role: "user",
650
+ content: [{ type: "image", image: dataUrl }],
651
+ },
652
+ ]);
653
+ const content = (result[0] as { content: { data: unknown }[] }).content;
654
+ expect(content[0]!.data).toBeInstanceOf(URL);
655
+ });
656
+
657
+ it("keeps relative path as string", () => {
658
+ const result = toGenericMessages([
659
+ {
660
+ role: "user",
661
+ content: [
662
+ { type: "file", data: "/path/to/file.txt", mimeType: "text/plain" },
663
+ ],
664
+ },
665
+ ]);
666
+ const content = (result[0] as { content: { data: unknown }[] }).content;
667
+ expect(content[0]!.data).toBe("/path/to/file.txt");
668
+ });
669
+
670
+ it("keeps invalid URL as string", () => {
671
+ const result = toGenericMessages([
672
+ {
673
+ role: "user",
674
+ content: [
675
+ { type: "file", data: "not a valid url", mimeType: "text/plain" },
676
+ ],
677
+ },
678
+ ]);
679
+ const content = (result[0] as { content: { data: unknown }[] }).content;
680
+ expect(content[0]!.data).toBe("not a valid url");
681
+ });
682
+ });
683
+
684
+ describe("multiple messages", () => {
685
+ it("converts a full conversation", () => {
686
+ const result = toGenericMessages([
687
+ {
688
+ role: "system",
689
+ content: [{ type: "text", text: "You are helpful." }],
690
+ },
691
+ {
692
+ role: "user",
693
+ content: [{ type: "text", text: "What is 2+2?" }],
694
+ },
695
+ {
696
+ role: "assistant",
697
+ content: [{ type: "text", text: "2+2 equals 4." }],
698
+ },
699
+ {
700
+ role: "user",
701
+ content: [{ type: "text", text: "Thanks!" }],
702
+ },
703
+ ]);
704
+
705
+ expect(result).toEqual([
706
+ { role: "system", content: "You are helpful." },
707
+ { role: "user", content: [{ type: "text", text: "What is 2+2?" }] },
708
+ {
709
+ role: "assistant",
710
+ content: [{ type: "text", text: "2+2 equals 4." }],
711
+ },
712
+ { role: "user", content: [{ type: "text", text: "Thanks!" }] },
713
+ ]);
714
+ });
715
+
716
+ it("handles empty messages array", () => {
717
+ const result = toGenericMessages([]);
718
+ expect(result).toEqual([]);
719
+ });
720
+ });
721
+ });