@ulearning/ai-sdk-adapter 0.0.0-stage → 1.0.0-next.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 (210) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +29 -2
  3. package/dist/adapters/aiSDKFormatAdapter.d.ts +6 -0
  4. package/dist/adapters/aiSDKFormatAdapter.d.ts.map +1 -0
  5. package/dist/adapters/aiSDKFormatAdapter.js +17 -0
  6. package/dist/adapters/aiSDKFormatAdapter.js.map +1 -0
  7. package/dist/adapters/vercelAttachmentAdapter.d.ts +3 -0
  8. package/dist/adapters/vercelAttachmentAdapter.d.ts.map +1 -0
  9. package/dist/adapters/vercelAttachmentAdapter.js +36 -0
  10. package/dist/adapters/vercelAttachmentAdapter.js.map +1 -0
  11. package/dist/aiSDKExtras.d.ts +13 -0
  12. package/dist/aiSDKExtras.d.ts.map +1 -0
  13. package/dist/aiSDKExtras.js +6 -0
  14. package/dist/aiSDKExtras.js.map +1 -0
  15. package/dist/converters/convertMessage.d.ts +44 -0
  16. package/dist/converters/convertMessage.d.ts.map +1 -0
  17. package/dist/converters/convertMessage.js +465 -0
  18. package/dist/converters/convertMessage.js.map +1 -0
  19. package/dist/converters/toCreateMessage.d.ts +4 -0
  20. package/dist/converters/toCreateMessage.d.ts.map +1 -0
  21. package/dist/converters/toCreateMessage.js +65 -0
  22. package/dist/converters/toCreateMessage.js.map +1 -0
  23. package/dist/converters/toolApprovalAnswers.d.ts +3 -0
  24. package/dist/converters/toolApprovalAnswers.d.ts.map +1 -0
  25. package/dist/converters/toolApprovalAnswers.js +16 -0
  26. package/dist/converters/toolApprovalAnswers.js.map +1 -0
  27. package/dist/hooks.d.ts +17 -0
  28. package/dist/hooks.d.ts.map +1 -0
  29. package/dist/hooks.js +19 -0
  30. package/dist/hooks.js.map +1 -0
  31. package/dist/index.d.ts +17 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +13 -0
  34. package/dist/model-context/injectInteractableContext.d.ts +37 -0
  35. package/dist/model-context/injectInteractableContext.d.ts.map +1 -0
  36. package/dist/model-context/injectInteractableContext.js +54 -0
  37. package/dist/model-context/injectInteractableContext.js.map +1 -0
  38. package/dist/model-context/injectQuoteContext.d.ts +25 -0
  39. package/dist/model-context/injectQuoteContext.d.ts.map +1 -0
  40. package/dist/model-context/injectQuoteContext.js +51 -0
  41. package/dist/model-context/injectQuoteContext.js.map +1 -0
  42. package/dist/runtime/AISDKChat.d.ts +13 -0
  43. package/dist/runtime/AISDKChat.d.ts.map +1 -0
  44. package/dist/runtime/AISDKChat.js +34 -0
  45. package/dist/runtime/AISDKChat.js.map +1 -0
  46. package/dist/runtime/AISDKThreads.d.ts +24 -0
  47. package/dist/runtime/AISDKThreads.d.ts.map +1 -0
  48. package/dist/runtime/AISDKThreads.js +92 -0
  49. package/dist/runtime/AISDKThreads.js.map +1 -0
  50. package/dist/runtime/DynamicChatTransport.d.ts +50 -0
  51. package/dist/runtime/DynamicChatTransport.d.ts.map +1 -0
  52. package/dist/runtime/DynamicChatTransport.js +153 -0
  53. package/dist/runtime/DynamicChatTransport.js.map +1 -0
  54. package/dist/runtime/getResumableAdapter.d.ts +5 -0
  55. package/dist/runtime/getResumableAdapter.d.ts.map +1 -0
  56. package/dist/runtime/getResumableAdapter.js +11 -0
  57. package/dist/runtime/getResumableAdapter.js.map +1 -0
  58. package/dist/runtime/toolHistoryCodec.d.ts +20 -0
  59. package/dist/runtime/toolHistoryCodec.d.ts.map +1 -0
  60. package/dist/runtime/toolHistoryCodec.js +122 -0
  61. package/dist/runtime/toolHistoryCodec.js.map +1 -0
  62. package/dist/runtime/useAISDKRuntime.d.ts +95 -0
  63. package/dist/runtime/useAISDKRuntime.d.ts.map +1 -0
  64. package/dist/runtime/useAISDKRuntime.js +754 -0
  65. package/dist/runtime/useAISDKRuntime.js.map +1 -0
  66. package/dist/runtime/useChatRuntime.d.ts +17 -0
  67. package/dist/runtime/useChatRuntime.d.ts.map +1 -0
  68. package/dist/runtime/useChatRuntime.js +50 -0
  69. package/dist/runtime/useChatRuntime.js.map +1 -0
  70. package/dist/runtime/useChatSnapshot.d.ts +6 -0
  71. package/dist/runtime/useChatSnapshot.d.ts.map +1 -0
  72. package/dist/runtime/useChatSnapshot.js +62 -0
  73. package/dist/runtime/useChatSnapshot.js.map +1 -0
  74. package/dist/runtime/useChatThread.d.ts +121 -0
  75. package/dist/runtime/useChatThread.d.ts.map +1 -0
  76. package/dist/runtime/useChatThread.js +255 -0
  77. package/dist/runtime/useChatThread.js.map +1 -0
  78. package/dist/runtime/useDynamicChatTransport.d.ts +4 -0
  79. package/dist/runtime/useDynamicChatTransport.d.ts.map +1 -0
  80. package/dist/runtime/useDynamicChatTransport.js +26 -0
  81. package/dist/runtime/useDynamicChatTransport.js.map +1 -0
  82. package/dist/runtime/useExternalHistory.d.ts +12 -0
  83. package/dist/runtime/useExternalHistory.d.ts.map +1 -0
  84. package/dist/runtime/useExternalHistory.js +556 -0
  85. package/dist/runtime/useExternalHistory.js.map +1 -0
  86. package/dist/runtime/useMessageQueue.d.ts +14 -0
  87. package/dist/runtime/useMessageQueue.d.ts.map +1 -0
  88. package/dist/runtime/useMessageQueue.js +142 -0
  89. package/dist/runtime/useMessageQueue.js.map +1 -0
  90. package/dist/runtime/useResourceCleanup.d.ts +2 -0
  91. package/dist/runtime/useResourceCleanup.d.ts.map +1 -0
  92. package/dist/runtime/useResourceCleanup.js +47 -0
  93. package/dist/runtime/useResourceCleanup.js.map +1 -0
  94. package/dist/runtime/useStreamingTiming.d.ts +12 -0
  95. package/dist/runtime/useStreamingTiming.d.ts.map +1 -0
  96. package/dist/runtime/useStreamingTiming.js +36 -0
  97. package/dist/runtime/useStreamingTiming.js.map +1 -0
  98. package/dist/tools/generativeTools.d.ts +71 -0
  99. package/dist/tools/generativeTools.d.ts.map +1 -0
  100. package/dist/tools/generativeTools.js +258 -0
  101. package/dist/tools/generativeTools.js.map +1 -0
  102. package/dist/tools/mcp-stdio.node.d.ts +2 -0
  103. package/dist/tools/mcp-stdio.node.d.ts.map +1 -0
  104. package/dist/tools/mcp-stdio.node.js +1 -0
  105. package/dist/tools/mcp-stdio.unsupported.d.ts +3 -0
  106. package/dist/tools/mcp-stdio.unsupported.d.ts.map +1 -0
  107. package/dist/tools/mcp-stdio.unsupported.js +10 -0
  108. package/dist/tools/mcp-stdio.unsupported.js.map +1 -0
  109. package/dist/transport/AssistantChatTransport.d.ts +20 -0
  110. package/dist/transport/AssistantChatTransport.d.ts.map +1 -0
  111. package/dist/transport/AssistantChatTransport.js +151 -0
  112. package/dist/transport/AssistantChatTransport.js.map +1 -0
  113. package/dist/transport/resumable.d.ts +36 -0
  114. package/dist/transport/resumable.d.ts.map +1 -0
  115. package/dist/transport/resumable.js +112 -0
  116. package/dist/transport/resumable.js.map +1 -0
  117. package/dist/usage.d.ts +24 -0
  118. package/dist/usage.d.ts.map +1 -0
  119. package/dist/usage.js +121 -0
  120. package/dist/usage.js.map +1 -0
  121. package/dist/utils/getVercelAIMessages.d.ts +4 -0
  122. package/dist/utils/getVercelAIMessages.d.ts.map +1 -0
  123. package/dist/utils/getVercelAIMessages.js +8 -0
  124. package/dist/utils/getVercelAIMessages.js.map +1 -0
  125. package/dist/utils/sliceMessagesUntil.d.ts +3 -0
  126. package/dist/utils/sliceMessagesUntil.d.ts.map +1 -0
  127. package/dist/utils/sliceMessagesUntil.js +10 -0
  128. package/dist/utils/sliceMessagesUntil.js.map +1 -0
  129. package/package.json +96 -3
  130. package/src/__tests__/standalone.e2e.ts +118 -0
  131. package/src/adapters/aiSDKFormatAdapter.test.ts +36 -0
  132. package/src/adapters/aiSDKFormatAdapter.ts +18 -0
  133. package/src/adapters/vercelAttachmentAdapter.test.ts +52 -0
  134. package/src/adapters/vercelAttachmentAdapter.ts +36 -0
  135. package/src/aiSDKExtras.ts +31 -0
  136. package/src/converters/convertMessage.metadata-key.test.tsx +182 -0
  137. package/src/converters/convertMessage.test.ts +2428 -0
  138. package/src/converters/convertMessage.tool-args-status.test.tsx +185 -0
  139. package/src/converters/convertMessage.ts +919 -0
  140. package/src/converters/toCreateMessage.test.ts +716 -0
  141. package/src/converters/toCreateMessage.ts +110 -0
  142. package/src/converters/toolApprovalAnswers.ts +27 -0
  143. package/src/hooks.test.ts +168 -0
  144. package/src/hooks.ts +26 -0
  145. package/src/index.ts +29 -0
  146. package/src/model-context/injectInteractableContext.test.ts +167 -0
  147. package/src/model-context/injectInteractableContext.ts +64 -0
  148. package/src/model-context/injectQuoteContext.test.ts +33 -0
  149. package/src/model-context/injectQuoteContext.ts +63 -0
  150. package/src/runtime/AISDKChat.artifacts.test.ts +48 -0
  151. package/src/runtime/AISDKChat.integration.test.tsx +148 -0
  152. package/src/runtime/AISDKChat.test.ts +241 -0
  153. package/src/runtime/AISDKChat.ts +43 -0
  154. package/src/runtime/AISDKThreads.test.ts +811 -0
  155. package/src/runtime/AISDKThreads.ts +194 -0
  156. package/src/runtime/DynamicChatTransport.test.ts +228 -0
  157. package/src/runtime/DynamicChatTransport.ts +281 -0
  158. package/src/runtime/__tests__/controlled-transport.ts +70 -0
  159. package/src/runtime/__tests__/tap.ts +80 -0
  160. package/src/runtime/__tests__/vue.ts +46 -0
  161. package/src/runtime/getResumableAdapter.ts +16 -0
  162. package/src/runtime/toolHistoryCodec.test.ts +187 -0
  163. package/src/runtime/toolHistoryCodec.ts +245 -0
  164. package/src/runtime/useAISDKRuntime.approval.integration.test.tsx +1956 -0
  165. package/src/runtime/useAISDKRuntime.approval.test.tsx +580 -0
  166. package/src/runtime/useAISDKRuntime.denied-tool.test.tsx +122 -0
  167. package/src/runtime/useAISDKRuntime.fast-refresh.test.tsx +302 -0
  168. package/src/runtime/useAISDKRuntime.metadata-cache.test.tsx +157 -0
  169. package/src/runtime/useAISDKRuntime.queue.test.tsx +707 -0
  170. package/src/runtime/useAISDKRuntime.reload.test.tsx +217 -0
  171. package/src/runtime/useAISDKRuntime.test.ts +2345 -0
  172. package/src/runtime/useAISDKRuntime.tool-output.test.tsx +113 -0
  173. package/src/runtime/useAISDKRuntime.ts +1503 -0
  174. package/src/runtime/useAISDKRuntime.voice.test.tsx +319 -0
  175. package/src/runtime/useChatOnTap.test.ts +60 -0
  176. package/src/runtime/useChatRuntime.fast-refresh.test.tsx +83 -0
  177. package/src/runtime/useChatRuntime.integration.test.tsx +661 -0
  178. package/src/runtime/useChatRuntime.local-storage.test.tsx +114 -0
  179. package/src/runtime/useChatRuntime.native.test.ts +147 -0
  180. package/src/runtime/useChatRuntime.run-telemetry.test.tsx +226 -0
  181. package/src/runtime/useChatRuntime.test.ts +619 -0
  182. package/src/runtime/useChatRuntime.ts +80 -0
  183. package/src/runtime/useChatSnapshot.test.ts +60 -0
  184. package/src/runtime/useChatSnapshot.ts +86 -0
  185. package/src/runtime/useChatThread.binding.test.tsx +140 -0
  186. package/src/runtime/useChatThread.test.ts +993 -0
  187. package/src/runtime/useChatThread.transport.test.tsx +149 -0
  188. package/src/runtime/useChatThread.ts +533 -0
  189. package/src/runtime/useDynamicChatTransport.ts +26 -0
  190. package/src/runtime/useExternalHistory.test.ts +4187 -0
  191. package/src/runtime/useExternalHistory.ts +902 -0
  192. package/src/runtime/useMessageQueue.effects.test.tsx +88 -0
  193. package/src/runtime/useMessageQueue.test.tsx +256 -0
  194. package/src/runtime/useMessageQueue.ts +189 -0
  195. package/src/runtime/useResourceCleanup.ts +58 -0
  196. package/src/runtime/useStreamingTiming.test.tsx +96 -0
  197. package/src/runtime/useStreamingTiming.ts +69 -0
  198. package/src/tools/generativeTools.test.ts +1058 -0
  199. package/src/tools/generativeTools.ts +491 -0
  200. package/src/tools/mcp-stdio.node.ts +1 -0
  201. package/src/tools/mcp-stdio.unsupported.ts +12 -0
  202. package/src/transport/AssistantChatTransport.test.ts +631 -0
  203. package/src/transport/AssistantChatTransport.ts +236 -0
  204. package/src/transport/resumable.test.ts +262 -0
  205. package/src/transport/resumable.ts +162 -0
  206. package/src/usage.test.ts +204 -0
  207. package/src/usage.ts +179 -0
  208. package/src/utils/getVercelAIMessages.ts +11 -0
  209. package/src/utils/sliceMessagesUntil.test.ts +51 -0
  210. package/src/utils/sliceMessagesUntil.ts +16 -0
@@ -0,0 +1,716 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { AppendMessage } from "@ulearning/ai-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.each([
15
+ [
16
+ "data:;base64,SGVsbG8=",
17
+ "data:application/octet-stream;base64,SGVsbG8=",
18
+ "application/octet-stream",
19
+ ],
20
+ [
21
+ "data:;charset=utf-8,hello",
22
+ "data:text/plain;charset=utf-8,hello",
23
+ "text/plain",
24
+ ],
25
+ [
26
+ "data:;base64,",
27
+ "data:application/octet-stream;base64,",
28
+ "application/octet-stream",
29
+ ],
30
+ ])("uses the resolved media type for %s", (data, url, mediaType) => {
31
+ const message = {
32
+ ...baseMessage,
33
+ content: [{ type: "file", data, mimeType: "" }],
34
+ } as unknown as AppendMessage;
35
+
36
+ expect(toCreateMessage(message).parts).toEqual([
37
+ { type: "file", url, mediaType },
38
+ ]);
39
+ });
40
+
41
+ it("converts a direct file part in message content", () => {
42
+ const message = {
43
+ ...baseMessage,
44
+ content: [
45
+ {
46
+ type: "file",
47
+ data: "data:application/pdf;base64,JVBERi0xLjQ=",
48
+ mimeType: "application/pdf",
49
+ filename: "invoice.pdf",
50
+ },
51
+ ],
52
+ } as unknown as AppendMessage;
53
+
54
+ const result = toCreateMessage(message);
55
+
56
+ expect(result.parts).toEqual([
57
+ {
58
+ type: "file",
59
+ url: "data:application/pdf;base64,JVBERi0xLjQ=",
60
+ mediaType: "application/pdf",
61
+ filename: "invoice.pdf",
62
+ },
63
+ ]);
64
+ });
65
+
66
+ it("preserves text and direct file parts together", () => {
67
+ const message = {
68
+ ...baseMessage,
69
+ content: [
70
+ {
71
+ type: "text",
72
+ text: "Summarize this invoice and list the due date.",
73
+ },
74
+ {
75
+ type: "file",
76
+ data: "data:application/pdf;base64,JVBERi0xLjQ=",
77
+ mimeType: "application/pdf",
78
+ filename: "invoice.pdf",
79
+ },
80
+ ],
81
+ } as unknown as AppendMessage;
82
+
83
+ const result = toCreateMessage(message);
84
+
85
+ expect(result.parts).toEqual([
86
+ {
87
+ type: "text",
88
+ text: "Summarize this invoice and list the due date.",
89
+ },
90
+ {
91
+ type: "file",
92
+ url: "data:application/pdf;base64,JVBERi0xLjQ=",
93
+ mediaType: "application/pdf",
94
+ filename: "invoice.pdf",
95
+ },
96
+ ]);
97
+ });
98
+
99
+ it("preserves the media type from image data URLs", () => {
100
+ const message = {
101
+ ...baseMessage,
102
+ content: [
103
+ {
104
+ type: "image",
105
+ image: "data:image/jpeg;base64,/9j/4AAQSkZJRg==",
106
+ filename: "photo.jpg",
107
+ },
108
+ ],
109
+ } as unknown as AppendMessage;
110
+
111
+ const result = toCreateMessage(message);
112
+
113
+ expect(result.parts).toEqual([
114
+ {
115
+ type: "file",
116
+ url: "data:image/jpeg;base64,/9j/4AAQSkZJRg==",
117
+ filename: "photo.jpg",
118
+ mediaType: "image/jpeg",
119
+ },
120
+ ]);
121
+ });
122
+
123
+ it("infers a lowercase media type from uppercase-scheme image data URLs", () => {
124
+ const message = {
125
+ ...baseMessage,
126
+ content: [
127
+ {
128
+ type: "image",
129
+ image: "DATA:IMAGE/JPEG;base64,/9j/4AAQSkZJRg==",
130
+ filename: "photo.jpg",
131
+ },
132
+ ],
133
+ } as unknown as AppendMessage;
134
+
135
+ const result = toCreateMessage(message);
136
+
137
+ expect(result.parts).toEqual([
138
+ {
139
+ type: "file",
140
+ url: "DATA:IMAGE/JPEG;base64,/9j/4AAQSkZJRg==",
141
+ filename: "photo.jpg",
142
+ mediaType: "image/jpeg",
143
+ },
144
+ ]);
145
+ });
146
+
147
+ it("uses attachment contentType for image attachments", () => {
148
+ const message = {
149
+ ...baseMessage,
150
+ content: [],
151
+ attachments: [
152
+ {
153
+ id: "some-id",
154
+ type: "image",
155
+ name: "photo.webp",
156
+ contentType: "image/webp",
157
+ status: { type: "complete" },
158
+ content: [
159
+ {
160
+ type: "image",
161
+ image: "https://cdn.example.com/photo",
162
+ },
163
+ ],
164
+ },
165
+ ],
166
+ } as unknown as AppendMessage;
167
+
168
+ const result = toCreateMessage(message);
169
+
170
+ expect(result.parts).toEqual([
171
+ {
172
+ type: "file",
173
+ url: "https://cdn.example.com/photo",
174
+ filename: "photo.webp",
175
+ mediaType: "image/webp",
176
+ },
177
+ ]);
178
+ });
179
+
180
+ it("falls back to image/png for images without a known media type", () => {
181
+ const message = {
182
+ ...baseMessage,
183
+ content: [
184
+ {
185
+ type: "image",
186
+ image: "https://cdn.example.com/photo",
187
+ },
188
+ ],
189
+ } as unknown as AppendMessage;
190
+
191
+ const result = toCreateMessage(message);
192
+
193
+ expect(result.parts).toEqual([
194
+ {
195
+ type: "file",
196
+ url: "https://cdn.example.com/photo",
197
+ mediaType: "image/png",
198
+ },
199
+ ]);
200
+ });
201
+
202
+ it("converts a data part in message content into a data-<name> part", () => {
203
+ const message = {
204
+ ...baseMessage,
205
+ content: [{ type: "data", name: "workflow", data: { field: 1 } }],
206
+ } as unknown as AppendMessage;
207
+
208
+ const result = toCreateMessage(message);
209
+
210
+ expect(result.parts).toEqual([
211
+ { type: "data-workflow", data: { field: 1 } },
212
+ ]);
213
+ });
214
+
215
+ it("forwards a data part id", () => {
216
+ const message = {
217
+ ...baseMessage,
218
+ content: [
219
+ { type: "data", name: "workflow", id: "p1", data: { field: 1 } },
220
+ ],
221
+ } as unknown as AppendMessage;
222
+
223
+ expect(toCreateMessage(message).parts).toEqual([
224
+ { type: "data-workflow", id: "p1", data: { field: 1 } },
225
+ ]);
226
+ });
227
+
228
+ it("converts a data part inside an attachment without throwing", () => {
229
+ const message = {
230
+ ...baseMessage,
231
+ content: [],
232
+ attachments: [
233
+ {
234
+ id: "some-id",
235
+ type: "document",
236
+ name: "some-name",
237
+ status: { type: "complete" },
238
+ content: [
239
+ {
240
+ type: "data",
241
+ name: "some-content-name",
242
+ data: { field: 1 },
243
+ },
244
+ ],
245
+ },
246
+ ],
247
+ } as unknown as AppendMessage;
248
+
249
+ const result = toCreateMessage(message);
250
+
251
+ expect(result.parts).toEqual([
252
+ { type: "data-some-content-name", data: { field: 1 } },
253
+ ]);
254
+ });
255
+
256
+ it("keeps converting attachment file content", () => {
257
+ const message = {
258
+ ...baseMessage,
259
+ content: [],
260
+ attachments: [
261
+ {
262
+ id: "some-id",
263
+ type: "document",
264
+ name: "invoice.pdf",
265
+ contentType: "application/pdf",
266
+ status: { type: "complete" },
267
+ content: [
268
+ {
269
+ type: "file",
270
+ data: "data:application/pdf;base64,JVBERi0xLjQ=",
271
+ mimeType: "application/pdf",
272
+ },
273
+ ],
274
+ },
275
+ ],
276
+ } as unknown as AppendMessage;
277
+
278
+ const result = toCreateMessage(message);
279
+
280
+ expect(result.parts).toEqual([
281
+ {
282
+ type: "file",
283
+ url: "data:application/pdf;base64,JVBERi0xLjQ=",
284
+ mediaType: "application/pdf",
285
+ filename: "invoice.pdf",
286
+ },
287
+ ]);
288
+ });
289
+
290
+ it("wraps bare base64 file data so the url survives convertToModelMessages", async () => {
291
+ const message = {
292
+ ...baseMessage,
293
+ content: [
294
+ { type: "file", data: "JVBERi0xLjQ=", mimeType: "application/pdf" },
295
+ ],
296
+ } as unknown as AppendMessage;
297
+
298
+ const result = toCreateMessage(message);
299
+
300
+ expect(result.parts).toEqual([
301
+ {
302
+ type: "file",
303
+ url: "data:application/pdf;base64,JVBERi0xLjQ=",
304
+ mediaType: "application/pdf",
305
+ },
306
+ ]);
307
+
308
+ // convertToModelMessages is async and passes `url` to an unguarded
309
+ // `new URL()`, so the bare payload rejects while the wrapped one resolves.
310
+ const { convertToModelMessages } = await import("ai");
311
+ await expect(
312
+ convertToModelMessages([{ ...result, id: "m1" } as never]),
313
+ ).resolves.toBeDefined();
314
+ await expect(
315
+ convertToModelMessages([
316
+ {
317
+ id: "m1",
318
+ role: "user",
319
+ parts: [
320
+ {
321
+ type: "file",
322
+ url: "JVBERi0xLjQ=",
323
+ mediaType: "application/pdf",
324
+ },
325
+ ],
326
+ } as never,
327
+ ]),
328
+ ).rejects.toThrow(/Invalid URL/);
329
+ });
330
+
331
+ it("sniffs the media type of a bare base64 image", () => {
332
+ const cases = [
333
+ { image: "/9j/4AAQSkZJRg==", mediaType: "image/jpeg" },
334
+ { image: "iVBORw0KGgoAAAANSUhEUg==", mediaType: "image/png" },
335
+ { image: "R0lGODlhAQABAA==", mediaType: "image/gif" },
336
+ { image: "UklGRiIAAABXRUJQVlA4", mediaType: "image/webp" },
337
+ ];
338
+
339
+ for (const { image, mediaType } of cases) {
340
+ const message = {
341
+ ...baseMessage,
342
+ content: [{ type: "image", image }],
343
+ } as unknown as AppendMessage;
344
+
345
+ expect(toCreateMessage(message).parts).toEqual([
346
+ { type: "file", url: `data:${mediaType};base64,${image}`, mediaType },
347
+ ]);
348
+ }
349
+ });
350
+
351
+ it("does not sniff a payload that is not base64", () => {
352
+ for (const image of [
353
+ "https://cdn.example.com/photo",
354
+ "blob:https://app.example/1-2-3",
355
+ "not base64 at all!!",
356
+ ]) {
357
+ const message = {
358
+ ...baseMessage,
359
+ content: [{ type: "image", image }],
360
+ } as unknown as AppendMessage;
361
+
362
+ expect(() => toCreateMessage(message)).not.toThrow();
363
+ expect(toCreateMessage(message).parts[0]).toMatchObject({
364
+ mediaType: "image/png",
365
+ });
366
+ }
367
+ });
368
+
369
+ it("reads the declared type of a non-base64 data url image", () => {
370
+ const message = {
371
+ ...baseMessage,
372
+ content: [
373
+ { type: "image", image: "data:image/svg+xml,%3Csvg%3E%3C/svg%3E" },
374
+ ],
375
+ } as unknown as AppendMessage;
376
+
377
+ expect(toCreateMessage(message).parts[0]).toMatchObject({
378
+ mediaType: "image/svg+xml",
379
+ url: "data:image/svg+xml,%3Csvg%3E%3C/svg%3E",
380
+ });
381
+ });
382
+
383
+ it("re-envelopes an image whose envelope disagrees with the resolved type", () => {
384
+ const message = {
385
+ ...baseMessage,
386
+ content: [
387
+ {
388
+ type: "image",
389
+ image: "data:application/octet-stream;base64,/9j/4AAQSkZJRg==",
390
+ },
391
+ ],
392
+ } as unknown as AppendMessage;
393
+
394
+ expect(toCreateMessage(message).parts[0]).toMatchObject({
395
+ mediaType: "image/jpeg",
396
+ url: "data:image/jpeg;base64,/9j/4AAQSkZJRg==",
397
+ });
398
+ });
399
+
400
+ it("keeps a good envelope when the declared file mime is empty", () => {
401
+ const message = {
402
+ ...baseMessage,
403
+ content: [
404
+ {
405
+ type: "file",
406
+ data: "data:application/octet-stream;base64,JVBERi0xLjQ=",
407
+ mimeType: "",
408
+ },
409
+ ],
410
+ } as unknown as AppendMessage;
411
+
412
+ expect(toCreateMessage(message).parts[0]).toMatchObject({
413
+ mediaType: "application/octet-stream",
414
+ url: "data:application/octet-stream;base64,JVBERi0xLjQ=",
415
+ });
416
+ });
417
+
418
+ it("reads the declared type of a non-base64 data url file", () => {
419
+ const message = {
420
+ ...baseMessage,
421
+ content: [{ type: "file", data: "data:text/plain,hello", mimeType: "" }],
422
+ } as unknown as AppendMessage;
423
+
424
+ expect(toCreateMessage(message).parts[0]).toMatchObject({
425
+ mediaType: "text/plain",
426
+ url: "data:text/plain,hello",
427
+ });
428
+ });
429
+
430
+ it("floors a bare base64 file payload with no declared mime", () => {
431
+ const message = {
432
+ ...baseMessage,
433
+ content: [{ type: "file", data: "JVBERi0xLjQ=", mimeType: "" }],
434
+ } as unknown as AppendMessage;
435
+
436
+ expect(toCreateMessage(message).parts[0]).toMatchObject({
437
+ mediaType: "application/octet-stream",
438
+ url: "data:application/octet-stream;base64,JVBERi0xLjQ=",
439
+ });
440
+ });
441
+
442
+ it("re-envelopes a file whose envelope disagrees with its declared type", () => {
443
+ const message = {
444
+ ...baseMessage,
445
+ content: [
446
+ {
447
+ type: "file",
448
+ data: "data:application/octet-stream;base64,JVBERi0xLjQ=",
449
+ mimeType: "application/pdf",
450
+ },
451
+ ],
452
+ } as unknown as AppendMessage;
453
+
454
+ expect(toCreateMessage(message).parts[0]).toMatchObject({
455
+ mediaType: "application/pdf",
456
+ url: "data:application/pdf;base64,JVBERi0xLjQ=",
457
+ });
458
+ });
459
+
460
+ it("falls back to image/png when the bytes match no known signature", () => {
461
+ const message = {
462
+ ...baseMessage,
463
+ content: [{ type: "image", image: "QUJD" }],
464
+ } as unknown as AppendMessage;
465
+
466
+ expect(toCreateMessage(message).parts).toEqual([
467
+ {
468
+ type: "file",
469
+ url: "data:image/png;base64,QUJD",
470
+ mediaType: "image/png",
471
+ },
472
+ ]);
473
+ });
474
+
475
+ it("wraps bare base64 image data using the resolved media type", () => {
476
+ const message = {
477
+ ...baseMessage,
478
+ content: [{ type: "image", image: "QUJD", contentType: "image/webp" }],
479
+ } as unknown as AppendMessage;
480
+
481
+ expect(toCreateMessage(message).parts).toEqual([
482
+ {
483
+ type: "file",
484
+ url: "data:image/webp;base64,QUJD",
485
+ mediaType: "image/webp",
486
+ },
487
+ ]);
488
+ });
489
+
490
+ it("leaves an id reference unwrapped rather than shipping it as base64", () => {
491
+ const message = {
492
+ ...baseMessage,
493
+ content: [
494
+ {
495
+ type: "file",
496
+ data: "file-abc123",
497
+ mimeType: "application/pdf",
498
+ sourceType: "id",
499
+ },
500
+ ],
501
+ } as unknown as AppendMessage;
502
+
503
+ expect(toCreateMessage(message).parts).toEqual([
504
+ { type: "file", url: "file-abc123", mediaType: "application/pdf" },
505
+ ]);
506
+ });
507
+
508
+ it("forwards blob and other parsable url schemes untouched", () => {
509
+ const message = {
510
+ ...baseMessage,
511
+ content: [
512
+ { type: "image", image: "blob:https://app.example/1-2-3" },
513
+ {
514
+ type: "file",
515
+ data: "blob:https://app.example/4-5-6",
516
+ mimeType: "application/pdf",
517
+ },
518
+ ],
519
+ } as unknown as AppendMessage;
520
+
521
+ const urls = toCreateMessage(message).parts.map((p) =>
522
+ "url" in p ? p.url : undefined,
523
+ );
524
+
525
+ expect(urls).toEqual([
526
+ "blob:https://app.example/1-2-3",
527
+ "blob:https://app.example/4-5-6",
528
+ ]);
529
+ });
530
+
531
+ it("converts an audio part into a file part with the format-derived media type", () => {
532
+ const message = {
533
+ ...baseMessage,
534
+ content: [{ type: "audio", audio: { data: "QUJD", format: "mp3" } }],
535
+ } as unknown as AppendMessage;
536
+
537
+ const result = toCreateMessage(message);
538
+
539
+ expect(result.parts).toEqual([
540
+ {
541
+ type: "file",
542
+ url: "data:audio/mp3;base64,QUJD",
543
+ mediaType: "audio/mp3",
544
+ },
545
+ ]);
546
+ });
547
+
548
+ it("converts a wav audio part", () => {
549
+ const message = {
550
+ ...baseMessage,
551
+ content: [{ type: "audio", audio: { data: "QUJD", format: "wav" } }],
552
+ } as unknown as AppendMessage;
553
+
554
+ const result = toCreateMessage(message);
555
+
556
+ expect(result.parts).toEqual([
557
+ {
558
+ type: "file",
559
+ url: "data:audio/wav;base64,QUJD",
560
+ mediaType: "audio/wav",
561
+ },
562
+ ]);
563
+ });
564
+
565
+ it("rebuilds the audio data URL envelope from the typed format", () => {
566
+ const message = {
567
+ ...baseMessage,
568
+ content: [
569
+ {
570
+ type: "audio",
571
+ audio: { data: "data:audio/mpeg;base64,QUJD", format: "mp3" },
572
+ },
573
+ ],
574
+ } as unknown as AppendMessage;
575
+
576
+ const result = toCreateMessage(message);
577
+
578
+ expect(result.parts).toEqual([
579
+ {
580
+ type: "file",
581
+ url: "data:audio/mp3;base64,QUJD",
582
+ mediaType: "audio/mp3",
583
+ },
584
+ ]);
585
+ });
586
+
587
+ it("forwards an http audio source instead of wrapping it in a data URL", () => {
588
+ const message = {
589
+ ...baseMessage,
590
+ content: [
591
+ {
592
+ type: "audio",
593
+ audio: { data: "https://cdn.example.com/memo.mp3", format: "mp3" },
594
+ },
595
+ ],
596
+ } as unknown as AppendMessage;
597
+
598
+ const result = toCreateMessage(message);
599
+
600
+ expect(result.parts).toEqual([
601
+ {
602
+ type: "file",
603
+ url: "https://cdn.example.com/memo.mp3",
604
+ mediaType: "audio/mp3",
605
+ },
606
+ ]);
607
+ });
608
+
609
+ it("keeps the attachment name on audio attachment content", () => {
610
+ const message = {
611
+ ...baseMessage,
612
+ content: [],
613
+ attachments: [
614
+ {
615
+ id: "some-id",
616
+ type: "audio",
617
+ name: "audio.mp3",
618
+ contentType: "audio/mp3",
619
+ status: { type: "complete" },
620
+ content: [{ type: "audio", audio: { data: "QUJD", format: "mp3" } }],
621
+ },
622
+ ],
623
+ } as unknown as AppendMessage;
624
+
625
+ const result = toCreateMessage(message);
626
+
627
+ expect(result.parts).toEqual([
628
+ {
629
+ type: "file",
630
+ url: "data:audio/mp3;base64,QUJD",
631
+ mediaType: "audio/mp3",
632
+ filename: "audio.mp3",
633
+ },
634
+ ]);
635
+ });
636
+ });
637
+
638
+ describe("toCreateMessage provider metadata", () => {
639
+ const providerMetadata = { google: { thoughtSignature: "signature" } };
640
+
641
+ it("preserves metadata on text, images and files", () => {
642
+ const message: AppendMessage = {
643
+ ...baseMessage,
644
+ createdAt: new Date(0),
645
+ attachments: [],
646
+ metadata: { custom: {} },
647
+ content: [
648
+ { type: "text", text: "hello", providerMetadata },
649
+ {
650
+ type: "image",
651
+ image: "data:image/png;base64,aW1n",
652
+ filename: "cat.png",
653
+ providerMetadata,
654
+ },
655
+ {
656
+ type: "file",
657
+ data: "cGRm",
658
+ mimeType: "application/pdf",
659
+ filename: "guide.pdf",
660
+ providerMetadata,
661
+ },
662
+ ],
663
+ };
664
+ expect(toCreateMessage(message).parts).toEqual([
665
+ { type: "text", text: "hello", providerMetadata },
666
+ {
667
+ type: "file",
668
+ url: "data:image/png;base64,aW1n",
669
+ mediaType: "image/png",
670
+ filename: "cat.png",
671
+ providerMetadata,
672
+ },
673
+ {
674
+ type: "file",
675
+ url: "data:application/pdf;base64,cGRm",
676
+ mediaType: "application/pdf",
677
+ filename: "guide.pdf",
678
+ providerMetadata,
679
+ },
680
+ ]);
681
+ });
682
+
683
+ it("preserves metadata from attachment content", () => {
684
+ const message: AppendMessage = {
685
+ ...baseMessage,
686
+ createdAt: new Date(0),
687
+ metadata: { custom: {} },
688
+ content: [],
689
+ attachments: [
690
+ {
691
+ id: "attachment-1",
692
+ type: "image",
693
+ name: "cat.png",
694
+ contentType: "image/png",
695
+ status: { type: "complete" },
696
+ content: [
697
+ {
698
+ type: "image",
699
+ image: "data:image/png;base64,aW1n",
700
+ providerMetadata,
701
+ },
702
+ ],
703
+ },
704
+ ],
705
+ };
706
+ expect(toCreateMessage(message).parts).toEqual([
707
+ {
708
+ type: "file",
709
+ url: "data:image/png;base64,aW1n",
710
+ mediaType: "image/png",
711
+ filename: "cat.png",
712
+ providerMetadata,
713
+ },
714
+ ]);
715
+ });
716
+ });