@assistant-ui/ai-sdk 0.0.1

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 (167) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +32 -0
  3. package/dist/adapters/aiSDKFormatAdapter.d.ts +8 -0
  4. package/dist/adapters/aiSDKFormatAdapter.d.ts.map +1 -0
  5. package/dist/adapters/aiSDKFormatAdapter.js +26 -0
  6. package/dist/adapters/aiSDKFormatAdapter.js.map +1 -0
  7. package/dist/adapters/vercelAttachmentAdapter.d.ts +6 -0
  8. package/dist/adapters/vercelAttachmentAdapter.d.ts.map +1 -0
  9. package/dist/adapters/vercelAttachmentAdapter.js +37 -0
  10. package/dist/adapters/vercelAttachmentAdapter.js.map +1 -0
  11. package/dist/aiSDKExtras.d.ts +11 -0
  12. package/dist/aiSDKExtras.d.ts.map +1 -0
  13. package/dist/aiSDKExtras.js +7 -0
  14. package/dist/aiSDKExtras.js.map +1 -0
  15. package/dist/converters/convertMessage.d.ts +28 -0
  16. package/dist/converters/convertMessage.d.ts.map +1 -0
  17. package/dist/converters/convertMessage.js +249 -0
  18. package/dist/converters/convertMessage.js.map +1 -0
  19. package/dist/converters/modelContentEnvelope.d.ts +16 -0
  20. package/dist/converters/modelContentEnvelope.d.ts.map +1 -0
  21. package/dist/converters/modelContentEnvelope.js +22 -0
  22. package/dist/converters/modelContentEnvelope.js.map +1 -0
  23. package/dist/converters/toCreateMessage.d.ts +7 -0
  24. package/dist/converters/toCreateMessage.d.ts.map +1 -0
  25. package/dist/converters/toCreateMessage.js +58 -0
  26. package/dist/converters/toCreateMessage.js.map +1 -0
  27. package/dist/converters/toolOutputConversion.d.ts +29 -0
  28. package/dist/converters/toolOutputConversion.d.ts.map +1 -0
  29. package/dist/converters/toolOutputConversion.js +31 -0
  30. package/dist/converters/toolOutputConversion.js.map +1 -0
  31. package/dist/hooks.d.ts +19 -0
  32. package/dist/hooks.d.ts.map +1 -0
  33. package/dist/hooks.js +20 -0
  34. package/dist/hooks.js.map +1 -0
  35. package/dist/index.d.ts +14 -0
  36. package/dist/index.js +13 -0
  37. package/dist/index.native.d.ts +12 -0
  38. package/dist/index.native.js +11 -0
  39. package/dist/model-context/injectInteractableContext.d.ts +37 -0
  40. package/dist/model-context/injectInteractableContext.d.ts.map +1 -0
  41. package/dist/model-context/injectInteractableContext.js +52 -0
  42. package/dist/model-context/injectInteractableContext.js.map +1 -0
  43. package/dist/model-context/injectQuoteContext.d.ts +27 -0
  44. package/dist/model-context/injectQuoteContext.d.ts.map +1 -0
  45. package/dist/model-context/injectQuoteContext.js +51 -0
  46. package/dist/model-context/injectQuoteContext.js.map +1 -0
  47. package/dist/runtime/AISDKChat.d.ts +16 -0
  48. package/dist/runtime/AISDKChat.d.ts.map +1 -0
  49. package/dist/runtime/AISDKChat.js +69 -0
  50. package/dist/runtime/AISDKChat.js.map +1 -0
  51. package/dist/runtime/AISDKThreads.d.ts +45 -0
  52. package/dist/runtime/AISDKThreads.d.ts.map +1 -0
  53. package/dist/runtime/AISDKThreads.js +203 -0
  54. package/dist/runtime/AISDKThreads.js.map +1 -0
  55. package/dist/runtime/useAISDKRuntime.d.ts +49 -0
  56. package/dist/runtime/useAISDKRuntime.d.ts.map +1 -0
  57. package/dist/runtime/useAISDKRuntime.js +266 -0
  58. package/dist/runtime/useAISDKRuntime.js.map +1 -0
  59. package/dist/runtime/useChatRuntime.d.ts +13 -0
  60. package/dist/runtime/useChatRuntime.d.ts.map +1 -0
  61. package/dist/runtime/useChatRuntime.js +52 -0
  62. package/dist/runtime/useChatRuntime.js.map +1 -0
  63. package/dist/runtime/useChatThread.d.ts +77 -0
  64. package/dist/runtime/useChatThread.d.ts.map +1 -0
  65. package/dist/runtime/useChatThread.js +134 -0
  66. package/dist/runtime/useChatThread.js.map +1 -0
  67. package/dist/runtime/useExternalHistory.d.ts +11 -0
  68. package/dist/runtime/useExternalHistory.d.ts.map +1 -0
  69. package/dist/runtime/useExternalHistory.js +278 -0
  70. package/dist/runtime/useExternalHistory.js.map +1 -0
  71. package/dist/runtime/useResourceCleanup.d.ts +5 -0
  72. package/dist/runtime/useResourceCleanup.d.ts.map +1 -0
  73. package/dist/runtime/useResourceCleanup.js +47 -0
  74. package/dist/runtime/useResourceCleanup.js.map +1 -0
  75. package/dist/runtime/useStreamingTiming.d.ts +15 -0
  76. package/dist/runtime/useStreamingTiming.d.ts.map +1 -0
  77. package/dist/runtime/useStreamingTiming.js +38 -0
  78. package/dist/runtime/useStreamingTiming.js.map +1 -0
  79. package/dist/tools/frontendTools.d.ts +33 -0
  80. package/dist/tools/frontendTools.d.ts.map +1 -0
  81. package/dist/tools/frontendTools.js +33 -0
  82. package/dist/tools/frontendTools.js.map +1 -0
  83. package/dist/tools/generativeTools.d.ts +73 -0
  84. package/dist/tools/generativeTools.d.ts.map +1 -0
  85. package/dist/tools/generativeTools.js +245 -0
  86. package/dist/tools/generativeTools.js.map +1 -0
  87. package/dist/tools/mcp-stdio.node.d.ts +2 -0
  88. package/dist/tools/mcp-stdio.node.js +2 -0
  89. package/dist/tools/mcp-stdio.unsupported.d.ts +6 -0
  90. package/dist/tools/mcp-stdio.unsupported.d.ts.map +1 -0
  91. package/dist/tools/mcp-stdio.unsupported.js +11 -0
  92. package/dist/tools/mcp-stdio.unsupported.js.map +1 -0
  93. package/dist/transport/AssistantChatTransport.d.ts +23 -0
  94. package/dist/transport/AssistantChatTransport.d.ts.map +1 -0
  95. package/dist/transport/AssistantChatTransport.js +134 -0
  96. package/dist/transport/AssistantChatTransport.js.map +1 -0
  97. package/dist/transport/resumable.d.ts +38 -0
  98. package/dist/transport/resumable.d.ts.map +1 -0
  99. package/dist/transport/resumable.js +113 -0
  100. package/dist/transport/resumable.js.map +1 -0
  101. package/dist/usage.d.ts +19 -0
  102. package/dist/usage.d.ts.map +1 -0
  103. package/dist/usage.js +127 -0
  104. package/dist/usage.js.map +1 -0
  105. package/dist/utils/getVercelAIMessages.d.ts +7 -0
  106. package/dist/utils/getVercelAIMessages.d.ts.map +1 -0
  107. package/dist/utils/getVercelAIMessages.js +9 -0
  108. package/dist/utils/getVercelAIMessages.js.map +1 -0
  109. package/dist/utils/sliceMessagesUntil.d.ts +6 -0
  110. package/dist/utils/sliceMessagesUntil.d.ts.map +1 -0
  111. package/dist/utils/sliceMessagesUntil.js +12 -0
  112. package/dist/utils/sliceMessagesUntil.js.map +1 -0
  113. package/package.json +100 -0
  114. package/src/__tests__/standalone.e2e.ts +59 -0
  115. package/src/adapters/aiSDKFormatAdapter.ts +41 -0
  116. package/src/adapters/vercelAttachmentAdapter.test.ts +29 -0
  117. package/src/adapters/vercelAttachmentAdapter.ts +36 -0
  118. package/src/aiSDKExtras.ts +11 -0
  119. package/src/converters/convertMessage.test.ts +1130 -0
  120. package/src/converters/convertMessage.ts +457 -0
  121. package/src/converters/modelContentEnvelope.ts +41 -0
  122. package/src/converters/toCreateMessage.test.ts +596 -0
  123. package/src/converters/toCreateMessage.ts +99 -0
  124. package/src/converters/toolOutputConversion.ts +26 -0
  125. package/src/hooks.ts +23 -0
  126. package/src/index.native.ts +21 -0
  127. package/src/index.ts +29 -0
  128. package/src/model-context/injectInteractableContext.test.ts +167 -0
  129. package/src/model-context/injectInteractableContext.ts +61 -0
  130. package/src/model-context/injectQuoteContext.test.ts +33 -0
  131. package/src/model-context/injectQuoteContext.ts +62 -0
  132. package/src/runtime/AISDKChat.integration.test.tsx +47 -0
  133. package/src/runtime/AISDKChat.react-compiler.test.ts +49 -0
  134. package/src/runtime/AISDKChat.test.ts +193 -0
  135. package/src/runtime/AISDKChat.ts +47 -0
  136. package/src/runtime/AISDKThreads.cloud.test.ts +136 -0
  137. package/src/runtime/AISDKThreads.test.ts +548 -0
  138. package/src/runtime/AISDKThreads.ts +223 -0
  139. package/src/runtime/__tests__/controlled-transport.ts +43 -0
  140. package/src/runtime/useAISDKRuntime.approval.test.tsx +69 -0
  141. package/src/runtime/useAISDKRuntime.test.ts +1021 -0
  142. package/src/runtime/useAISDKRuntime.tool-output.test.tsx +113 -0
  143. package/src/runtime/useAISDKRuntime.ts +524 -0
  144. package/src/runtime/useChatOnTap.test.ts +58 -0
  145. package/src/runtime/useChatRuntime.integration.test.tsx +67 -0
  146. package/src/runtime/useChatRuntime.test.ts +522 -0
  147. package/src/runtime/useChatRuntime.ts +49 -0
  148. package/src/runtime/useChatThread.ts +286 -0
  149. package/src/runtime/useExternalHistory.test.ts +1289 -0
  150. package/src/runtime/useExternalHistory.ts +435 -0
  151. package/src/runtime/useResourceCleanup.ts +32 -0
  152. package/src/runtime/useStreamingTiming.ts +68 -0
  153. package/src/tools/frontendTools.test.ts +205 -0
  154. package/src/tools/frontendTools.ts +83 -0
  155. package/src/tools/generativeTools.test.ts +785 -0
  156. package/src/tools/generativeTools.ts +470 -0
  157. package/src/tools/mcp-stdio.node.ts +1 -0
  158. package/src/tools/mcp-stdio.unsupported.ts +12 -0
  159. package/src/transport/AssistantChatTransport.test.ts +300 -0
  160. package/src/transport/AssistantChatTransport.ts +204 -0
  161. package/src/transport/resumable.test.ts +262 -0
  162. package/src/transport/resumable.ts +161 -0
  163. package/src/usage.test.ts +186 -0
  164. package/src/usage.ts +174 -0
  165. package/src/utils/getVercelAIMessages.ts +11 -0
  166. package/src/utils/sliceMessagesUntil.test.ts +55 -0
  167. package/src/utils/sliceMessagesUntil.ts +20 -0
@@ -0,0 +1,596 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { AppendMessage } from "@assistant-ui/core";
3
+ import { toCreateMessage } from "./toCreateMessage";
4
+
5
+ const baseMessage = {
6
+ role: "user",
7
+ parentId: null,
8
+ sourceId: null,
9
+ runConfig: undefined,
10
+ metadata: undefined,
11
+ } as const;
12
+
13
+ describe("toCreateMessage", () => {
14
+ it("converts a direct file part in message content", () => {
15
+ const message = {
16
+ ...baseMessage,
17
+ content: [
18
+ {
19
+ type: "file",
20
+ data: "data:application/pdf;base64,JVBERi0xLjQ=",
21
+ mimeType: "application/pdf",
22
+ filename: "invoice.pdf",
23
+ },
24
+ ],
25
+ } as unknown as AppendMessage;
26
+
27
+ const result = toCreateMessage(message);
28
+
29
+ expect(result.parts).toEqual([
30
+ {
31
+ type: "file",
32
+ url: "data:application/pdf;base64,JVBERi0xLjQ=",
33
+ mediaType: "application/pdf",
34
+ filename: "invoice.pdf",
35
+ },
36
+ ]);
37
+ });
38
+
39
+ it("preserves text and direct file parts together", () => {
40
+ const message = {
41
+ ...baseMessage,
42
+ content: [
43
+ {
44
+ type: "text",
45
+ text: "Summarize this invoice and list the due date.",
46
+ },
47
+ {
48
+ type: "file",
49
+ data: "data:application/pdf;base64,JVBERi0xLjQ=",
50
+ mimeType: "application/pdf",
51
+ filename: "invoice.pdf",
52
+ },
53
+ ],
54
+ } as unknown as AppendMessage;
55
+
56
+ const result = toCreateMessage(message);
57
+
58
+ expect(result.parts).toEqual([
59
+ {
60
+ type: "text",
61
+ text: "Summarize this invoice and list the due date.",
62
+ },
63
+ {
64
+ type: "file",
65
+ url: "data:application/pdf;base64,JVBERi0xLjQ=",
66
+ mediaType: "application/pdf",
67
+ filename: "invoice.pdf",
68
+ },
69
+ ]);
70
+ });
71
+
72
+ it("preserves the media type from image data URLs", () => {
73
+ const message = {
74
+ ...baseMessage,
75
+ content: [
76
+ {
77
+ type: "image",
78
+ image: "data:image/jpeg;base64,/9j/4AAQSkZJRg==",
79
+ filename: "photo.jpg",
80
+ },
81
+ ],
82
+ } as unknown as AppendMessage;
83
+
84
+ const result = toCreateMessage(message);
85
+
86
+ expect(result.parts).toEqual([
87
+ {
88
+ type: "file",
89
+ url: "data:image/jpeg;base64,/9j/4AAQSkZJRg==",
90
+ filename: "photo.jpg",
91
+ mediaType: "image/jpeg",
92
+ },
93
+ ]);
94
+ });
95
+
96
+ it("infers a lowercase media type from uppercase-scheme image data URLs", () => {
97
+ const message = {
98
+ ...baseMessage,
99
+ content: [
100
+ {
101
+ type: "image",
102
+ image: "DATA:IMAGE/JPEG;base64,/9j/4AAQSkZJRg==",
103
+ filename: "photo.jpg",
104
+ },
105
+ ],
106
+ } as unknown as AppendMessage;
107
+
108
+ const result = toCreateMessage(message);
109
+
110
+ expect(result.parts).toEqual([
111
+ {
112
+ type: "file",
113
+ url: "DATA:IMAGE/JPEG;base64,/9j/4AAQSkZJRg==",
114
+ filename: "photo.jpg",
115
+ mediaType: "image/jpeg",
116
+ },
117
+ ]);
118
+ });
119
+
120
+ it("uses attachment contentType for image attachments", () => {
121
+ const message = {
122
+ ...baseMessage,
123
+ content: [],
124
+ attachments: [
125
+ {
126
+ id: "some-id",
127
+ type: "image",
128
+ name: "photo.webp",
129
+ contentType: "image/webp",
130
+ status: { type: "complete" },
131
+ content: [
132
+ {
133
+ type: "image",
134
+ image: "https://cdn.example.com/photo",
135
+ },
136
+ ],
137
+ },
138
+ ],
139
+ } as unknown as AppendMessage;
140
+
141
+ const result = toCreateMessage(message);
142
+
143
+ expect(result.parts).toEqual([
144
+ {
145
+ type: "file",
146
+ url: "https://cdn.example.com/photo",
147
+ filename: "photo.webp",
148
+ mediaType: "image/webp",
149
+ },
150
+ ]);
151
+ });
152
+
153
+ it("falls back to image/png for images without a known media type", () => {
154
+ const message = {
155
+ ...baseMessage,
156
+ content: [
157
+ {
158
+ type: "image",
159
+ image: "https://cdn.example.com/photo",
160
+ },
161
+ ],
162
+ } as unknown as AppendMessage;
163
+
164
+ const result = toCreateMessage(message);
165
+
166
+ expect(result.parts).toEqual([
167
+ {
168
+ type: "file",
169
+ url: "https://cdn.example.com/photo",
170
+ mediaType: "image/png",
171
+ },
172
+ ]);
173
+ });
174
+
175
+ it("converts a data part in message content into a data-<name> part", () => {
176
+ const message = {
177
+ ...baseMessage,
178
+ content: [{ type: "data", name: "workflow", data: { field: 1 } }],
179
+ } as unknown as AppendMessage;
180
+
181
+ const result = toCreateMessage(message);
182
+
183
+ expect(result.parts).toEqual([
184
+ { type: "data-workflow", data: { field: 1 } },
185
+ ]);
186
+ });
187
+
188
+ it("converts a data part inside an attachment without throwing", () => {
189
+ const message = {
190
+ ...baseMessage,
191
+ content: [],
192
+ attachments: [
193
+ {
194
+ id: "some-id",
195
+ type: "document",
196
+ name: "some-name",
197
+ status: { type: "complete" },
198
+ content: [
199
+ {
200
+ type: "data",
201
+ name: "some-content-name",
202
+ data: { field: 1 },
203
+ },
204
+ ],
205
+ },
206
+ ],
207
+ } as unknown as AppendMessage;
208
+
209
+ const result = toCreateMessage(message);
210
+
211
+ expect(result.parts).toEqual([
212
+ { type: "data-some-content-name", data: { field: 1 } },
213
+ ]);
214
+ });
215
+
216
+ it("keeps converting attachment file content", () => {
217
+ const message = {
218
+ ...baseMessage,
219
+ content: [],
220
+ attachments: [
221
+ {
222
+ id: "some-id",
223
+ type: "document",
224
+ name: "invoice.pdf",
225
+ contentType: "application/pdf",
226
+ status: { type: "complete" },
227
+ content: [
228
+ {
229
+ type: "file",
230
+ data: "data:application/pdf;base64,JVBERi0xLjQ=",
231
+ mimeType: "application/pdf",
232
+ },
233
+ ],
234
+ },
235
+ ],
236
+ } as unknown as AppendMessage;
237
+
238
+ const result = toCreateMessage(message);
239
+
240
+ expect(result.parts).toEqual([
241
+ {
242
+ type: "file",
243
+ url: "data:application/pdf;base64,JVBERi0xLjQ=",
244
+ mediaType: "application/pdf",
245
+ filename: "invoice.pdf",
246
+ },
247
+ ]);
248
+ });
249
+
250
+ it("wraps bare base64 file data so the url survives convertToModelMessages", async () => {
251
+ const message = {
252
+ ...baseMessage,
253
+ content: [
254
+ { type: "file", data: "JVBERi0xLjQ=", mimeType: "application/pdf" },
255
+ ],
256
+ } as unknown as AppendMessage;
257
+
258
+ const result = toCreateMessage(message);
259
+
260
+ expect(result.parts).toEqual([
261
+ {
262
+ type: "file",
263
+ url: "data:application/pdf;base64,JVBERi0xLjQ=",
264
+ mediaType: "application/pdf",
265
+ },
266
+ ]);
267
+
268
+ // convertToModelMessages is async and passes `url` to an unguarded
269
+ // `new URL()`, so the bare payload rejects while the wrapped one resolves.
270
+ const { convertToModelMessages } = await import("ai");
271
+ await expect(
272
+ convertToModelMessages([{ ...result, id: "m1" } as never]),
273
+ ).resolves.toBeDefined();
274
+ await expect(
275
+ convertToModelMessages([
276
+ {
277
+ id: "m1",
278
+ role: "user",
279
+ parts: [
280
+ {
281
+ type: "file",
282
+ url: "JVBERi0xLjQ=",
283
+ mediaType: "application/pdf",
284
+ },
285
+ ],
286
+ } as never,
287
+ ]),
288
+ ).rejects.toThrow(/Invalid URL/);
289
+ });
290
+
291
+ it("sniffs the media type of a bare base64 image", () => {
292
+ const cases = [
293
+ { image: "/9j/4AAQSkZJRg==", mediaType: "image/jpeg" },
294
+ { image: "iVBORw0KGgoAAAANSUhEUg==", mediaType: "image/png" },
295
+ { image: "R0lGODlhAQABAA==", mediaType: "image/gif" },
296
+ { image: "UklGRiIAAABXRUJQVlA4", mediaType: "image/webp" },
297
+ ];
298
+
299
+ for (const { image, mediaType } of cases) {
300
+ const message = {
301
+ ...baseMessage,
302
+ content: [{ type: "image", image }],
303
+ } as unknown as AppendMessage;
304
+
305
+ expect(toCreateMessage(message).parts).toEqual([
306
+ { type: "file", url: `data:${mediaType};base64,${image}`, mediaType },
307
+ ]);
308
+ }
309
+ });
310
+
311
+ it("does not sniff a payload that is not base64", () => {
312
+ for (const image of [
313
+ "https://cdn.example.com/photo",
314
+ "blob:https://app.example/1-2-3",
315
+ "not base64 at all!!",
316
+ ]) {
317
+ const message = {
318
+ ...baseMessage,
319
+ content: [{ type: "image", image }],
320
+ } as unknown as AppendMessage;
321
+
322
+ expect(() => toCreateMessage(message)).not.toThrow();
323
+ expect(toCreateMessage(message).parts[0]).toMatchObject({
324
+ mediaType: "image/png",
325
+ });
326
+ }
327
+ });
328
+
329
+ it("reads the declared type of a non-base64 data url image", () => {
330
+ const message = {
331
+ ...baseMessage,
332
+ content: [
333
+ { type: "image", image: "data:image/svg+xml,%3Csvg%3E%3C/svg%3E" },
334
+ ],
335
+ } as unknown as AppendMessage;
336
+
337
+ expect(toCreateMessage(message).parts[0]).toMatchObject({
338
+ mediaType: "image/svg+xml",
339
+ url: "data:image/svg+xml,%3Csvg%3E%3C/svg%3E",
340
+ });
341
+ });
342
+
343
+ it("re-envelopes an image whose envelope disagrees with the resolved type", () => {
344
+ const message = {
345
+ ...baseMessage,
346
+ content: [
347
+ {
348
+ type: "image",
349
+ image: "data:application/octet-stream;base64,/9j/4AAQSkZJRg==",
350
+ },
351
+ ],
352
+ } as unknown as AppendMessage;
353
+
354
+ expect(toCreateMessage(message).parts[0]).toMatchObject({
355
+ mediaType: "image/jpeg",
356
+ url: "data:image/jpeg;base64,/9j/4AAQSkZJRg==",
357
+ });
358
+ });
359
+
360
+ it("keeps a good envelope when the declared file mime is empty", () => {
361
+ const message = {
362
+ ...baseMessage,
363
+ content: [
364
+ {
365
+ type: "file",
366
+ data: "data:application/octet-stream;base64,JVBERi0xLjQ=",
367
+ mimeType: "",
368
+ },
369
+ ],
370
+ } as unknown as AppendMessage;
371
+
372
+ expect(toCreateMessage(message).parts[0]).toMatchObject({
373
+ mediaType: "application/octet-stream",
374
+ url: "data:application/octet-stream;base64,JVBERi0xLjQ=",
375
+ });
376
+ });
377
+
378
+ it("reads the declared type of a non-base64 data url file", () => {
379
+ const message = {
380
+ ...baseMessage,
381
+ content: [{ type: "file", data: "data:text/plain,hello", mimeType: "" }],
382
+ } as unknown as AppendMessage;
383
+
384
+ expect(toCreateMessage(message).parts[0]).toMatchObject({
385
+ mediaType: "text/plain",
386
+ url: "data:text/plain,hello",
387
+ });
388
+ });
389
+
390
+ it("floors a bare base64 file payload with no declared mime", () => {
391
+ const message = {
392
+ ...baseMessage,
393
+ content: [{ type: "file", data: "JVBERi0xLjQ=", mimeType: "" }],
394
+ } as unknown as AppendMessage;
395
+
396
+ expect(toCreateMessage(message).parts[0]).toMatchObject({
397
+ mediaType: "application/octet-stream",
398
+ url: "data:application/octet-stream;base64,JVBERi0xLjQ=",
399
+ });
400
+ });
401
+
402
+ it("re-envelopes a file whose envelope disagrees with its declared type", () => {
403
+ const message = {
404
+ ...baseMessage,
405
+ content: [
406
+ {
407
+ type: "file",
408
+ data: "data:application/octet-stream;base64,JVBERi0xLjQ=",
409
+ mimeType: "application/pdf",
410
+ },
411
+ ],
412
+ } as unknown as AppendMessage;
413
+
414
+ expect(toCreateMessage(message).parts[0]).toMatchObject({
415
+ mediaType: "application/pdf",
416
+ url: "data:application/pdf;base64,JVBERi0xLjQ=",
417
+ });
418
+ });
419
+
420
+ it("falls back to image/png when the bytes match no known signature", () => {
421
+ const message = {
422
+ ...baseMessage,
423
+ content: [{ type: "image", image: "QUJD" }],
424
+ } as unknown as AppendMessage;
425
+
426
+ expect(toCreateMessage(message).parts).toEqual([
427
+ {
428
+ type: "file",
429
+ url: "data:image/png;base64,QUJD",
430
+ mediaType: "image/png",
431
+ },
432
+ ]);
433
+ });
434
+
435
+ it("wraps bare base64 image data using the resolved media type", () => {
436
+ const message = {
437
+ ...baseMessage,
438
+ content: [{ type: "image", image: "QUJD", contentType: "image/webp" }],
439
+ } as unknown as AppendMessage;
440
+
441
+ expect(toCreateMessage(message).parts).toEqual([
442
+ {
443
+ type: "file",
444
+ url: "data:image/webp;base64,QUJD",
445
+ mediaType: "image/webp",
446
+ },
447
+ ]);
448
+ });
449
+
450
+ it("leaves an id reference unwrapped rather than shipping it as base64", () => {
451
+ const message = {
452
+ ...baseMessage,
453
+ content: [
454
+ {
455
+ type: "file",
456
+ data: "file-abc123",
457
+ mimeType: "application/pdf",
458
+ sourceType: "id",
459
+ },
460
+ ],
461
+ } as unknown as AppendMessage;
462
+
463
+ expect(toCreateMessage(message).parts).toEqual([
464
+ { type: "file", url: "file-abc123", mediaType: "application/pdf" },
465
+ ]);
466
+ });
467
+
468
+ it("forwards blob and other parsable url schemes untouched", () => {
469
+ const message = {
470
+ ...baseMessage,
471
+ content: [
472
+ { type: "image", image: "blob:https://app.example/1-2-3" },
473
+ {
474
+ type: "file",
475
+ data: "blob:https://app.example/4-5-6",
476
+ mimeType: "application/pdf",
477
+ },
478
+ ],
479
+ } as unknown as AppendMessage;
480
+
481
+ const urls = toCreateMessage(message).parts.map((p) =>
482
+ "url" in p ? p.url : undefined,
483
+ );
484
+
485
+ expect(urls).toEqual([
486
+ "blob:https://app.example/1-2-3",
487
+ "blob:https://app.example/4-5-6",
488
+ ]);
489
+ });
490
+
491
+ it("converts an audio part into a file part with the format-derived media type", () => {
492
+ const message = {
493
+ ...baseMessage,
494
+ content: [{ type: "audio", audio: { data: "QUJD", format: "mp3" } }],
495
+ } as unknown as AppendMessage;
496
+
497
+ const result = toCreateMessage(message);
498
+
499
+ expect(result.parts).toEqual([
500
+ {
501
+ type: "file",
502
+ url: "data:audio/mp3;base64,QUJD",
503
+ mediaType: "audio/mp3",
504
+ },
505
+ ]);
506
+ });
507
+
508
+ it("converts a wav audio part", () => {
509
+ const message = {
510
+ ...baseMessage,
511
+ content: [{ type: "audio", audio: { data: "QUJD", format: "wav" } }],
512
+ } as unknown as AppendMessage;
513
+
514
+ const result = toCreateMessage(message);
515
+
516
+ expect(result.parts).toEqual([
517
+ {
518
+ type: "file",
519
+ url: "data:audio/wav;base64,QUJD",
520
+ mediaType: "audio/wav",
521
+ },
522
+ ]);
523
+ });
524
+
525
+ it("rebuilds the audio data URL envelope from the typed format", () => {
526
+ const message = {
527
+ ...baseMessage,
528
+ content: [
529
+ {
530
+ type: "audio",
531
+ audio: { data: "data:audio/mpeg;base64,QUJD", format: "mp3" },
532
+ },
533
+ ],
534
+ } as unknown as AppendMessage;
535
+
536
+ const result = toCreateMessage(message);
537
+
538
+ expect(result.parts).toEqual([
539
+ {
540
+ type: "file",
541
+ url: "data:audio/mp3;base64,QUJD",
542
+ mediaType: "audio/mp3",
543
+ },
544
+ ]);
545
+ });
546
+
547
+ it("forwards an http audio source instead of wrapping it in a data URL", () => {
548
+ const message = {
549
+ ...baseMessage,
550
+ content: [
551
+ {
552
+ type: "audio",
553
+ audio: { data: "https://cdn.example.com/memo.mp3", format: "mp3" },
554
+ },
555
+ ],
556
+ } as unknown as AppendMessage;
557
+
558
+ const result = toCreateMessage(message);
559
+
560
+ expect(result.parts).toEqual([
561
+ {
562
+ type: "file",
563
+ url: "https://cdn.example.com/memo.mp3",
564
+ mediaType: "audio/mp3",
565
+ },
566
+ ]);
567
+ });
568
+
569
+ it("keeps the attachment name on audio attachment content", () => {
570
+ const message = {
571
+ ...baseMessage,
572
+ content: [],
573
+ attachments: [
574
+ {
575
+ id: "some-id",
576
+ type: "audio",
577
+ name: "audio.mp3",
578
+ contentType: "audio/mp3",
579
+ status: { type: "complete" },
580
+ content: [{ type: "audio", audio: { data: "QUJD", format: "mp3" } }],
581
+ },
582
+ ],
583
+ } as unknown as AppendMessage;
584
+
585
+ const result = toCreateMessage(message);
586
+
587
+ expect(result.parts).toEqual([
588
+ {
589
+ type: "file",
590
+ url: "data:audio/mp3;base64,QUJD",
591
+ mediaType: "audio/mp3",
592
+ filename: "audio.mp3",
593
+ },
594
+ ]);
595
+ });
596
+ });
@@ -0,0 +1,99 @@
1
+ import type { AppendMessage } from "@assistant-ui/core";
2
+ import {
3
+ httpUrlPattern,
4
+ parseDataUrl,
5
+ resolveFileMediaType,
6
+ resolveImageMediaType,
7
+ toMediaWireUrl,
8
+ } from "@assistant-ui/core/internal";
9
+ import type {
10
+ CreateUIMessage,
11
+ UIDataTypes,
12
+ UIMessage,
13
+ UIMessagePart,
14
+ UITools,
15
+ } from "ai";
16
+
17
+ type InputPart = AppendMessage["content"][number] & {
18
+ readonly contentType?: string | undefined;
19
+ readonly filename?: string | undefined;
20
+ };
21
+
22
+ export const toCreateMessage = <UI_MESSAGE extends UIMessage = UIMessage>(
23
+ message: AppendMessage,
24
+ ): CreateUIMessage<UI_MESSAGE> => {
25
+ const inputParts: InputPart[] = [
26
+ ...message.content,
27
+ ...(message.attachments?.flatMap((a) =>
28
+ a.content.map((c) => ({
29
+ ...c,
30
+ filename: a.name,
31
+ contentType: a.contentType,
32
+ })),
33
+ ) ?? []),
34
+ ];
35
+
36
+ const parts = inputParts.map((part): UIMessagePart<UIDataTypes, UITools> => {
37
+ switch (part.type) {
38
+ case "text":
39
+ return {
40
+ type: "text",
41
+ text: part.text,
42
+ };
43
+ case "image": {
44
+ const mediaType = resolveImageMediaType(part.image, part.contentType);
45
+ return {
46
+ type: "file",
47
+ url: toMediaWireUrl(part.image, mediaType),
48
+ ...(part.filename && { filename: part.filename }),
49
+ mediaType,
50
+ };
51
+ }
52
+ case "file": {
53
+ // `mimeType` is a plain string, and an adapter reading `file.type` on a
54
+ // file the OS cannot type yields "". Same ladder as images: declared,
55
+ // then the envelope, then the floor.
56
+ const mediaType = resolveFileMediaType(part.data, part.mimeType);
57
+ return {
58
+ type: "file",
59
+ // An `id` reference is an opaque provider handle, not base64, and
60
+ // this adapter has no way to send one. Left unwrapped so it fails
61
+ // loudly upstream rather than shipping a corrupt payload.
62
+ url:
63
+ part.sourceType === "id"
64
+ ? part.data
65
+ : toMediaWireUrl(part.data, mediaType),
66
+ mediaType,
67
+ ...(part.filename && { filename: part.filename }),
68
+ };
69
+ }
70
+ case "audio": {
71
+ // A data URL's own media type wins over `mediaType` downstream, so the
72
+ // envelope is rebuilt from the typed format rather than forwarded.
73
+ const mediaType = `audio/${part.audio.format}`;
74
+ const data = part.audio.data;
75
+ return {
76
+ type: "file",
77
+ url: httpUrlPattern.test(data)
78
+ ? data
79
+ : `data:${mediaType};base64,${parseDataUrl(data)?.data ?? data}`,
80
+ mediaType,
81
+ ...(part.filename && { filename: part.filename }),
82
+ };
83
+ }
84
+ case "data":
85
+ return {
86
+ type: `data-${part.name}`,
87
+ data: part.data,
88
+ };
89
+ default:
90
+ throw new Error(`Unsupported part type: ${part.type}`);
91
+ }
92
+ });
93
+
94
+ return {
95
+ role: message.role,
96
+ parts,
97
+ metadata: message.metadata,
98
+ } satisfies CreateUIMessage<UIMessage> as CreateUIMessage<UI_MESSAGE>;
99
+ };