@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,457 @@
1
+ import {
2
+ isToolUIPart,
3
+ isReasoningFileUIPart,
4
+ isCustomContentUIPart,
5
+ getToolName,
6
+ type UIMessage,
7
+ } from "ai";
8
+ import {
9
+ createMessageConverter as unstable_createMessageConverter,
10
+ type useExternalMessageConverter,
11
+ } from "@assistant-ui/core/react";
12
+ import {
13
+ isMcpAppUri,
14
+ type ReasoningMessagePart,
15
+ type ToolCallMessagePart,
16
+ type TextMessagePart,
17
+ type DataMessagePart,
18
+ type PartProviderMetadata,
19
+ type SourceMessagePart,
20
+ type SourceProviderMetadata,
21
+ type FileMessagePart,
22
+ type ThreadMessageLike,
23
+ type McpAppMetadata,
24
+ type MessagePartStreamStatus,
25
+ } from "@assistant-ui/core";
26
+ import { stableStringifyToolArgs } from "@assistant-ui/core/internal";
27
+ import {
28
+ parsePartialJsonObject,
29
+ type ReadonlyJSONObject,
30
+ } from "assistant-stream/utils";
31
+ import { unwrapModelContentEnvelope } from "./modelContentEnvelope";
32
+
33
+ type MessageMetadata = ThreadMessageLike["metadata"];
34
+ export type AISDKMessageConverterMetadata =
35
+ useExternalMessageConverter.Metadata & {
36
+ toolArgsKeyOrderCache?: Map<string, Map<string, string[]>>;
37
+ toolLastInputCache?: Map<string, ReadonlyJSONObject>;
38
+ mcpAppMetadataCache?: Map<string, McpAppMetadata>;
39
+ /** Id of the currently-streaming message, flagged optimistic (#4037). */
40
+ optimisticMessageId?: string | undefined;
41
+ };
42
+
43
+ function stripClosingDelimiters(json: string): string {
44
+ return json.replace(/[}\]"]+$/, "");
45
+ }
46
+
47
+ const MCP_APP_METADATA_CACHE_MAX = 100;
48
+
49
+ function extractMcpAppMetadata(
50
+ part: unknown,
51
+ cache: Map<string, McpAppMetadata> | undefined,
52
+ ): McpAppMetadata | undefined {
53
+ if (!part || typeof part !== "object") return undefined;
54
+ const meta = (part as { callProviderMetadata?: unknown })
55
+ .callProviderMetadata;
56
+ const mcp =
57
+ meta && typeof meta === "object"
58
+ ? (meta as { mcp?: unknown }).mcp
59
+ : undefined;
60
+ const app =
61
+ mcp && typeof mcp === "object" ? (mcp as { app?: unknown }).app : undefined;
62
+ let a: Record<string, unknown>;
63
+ if (app && typeof app === "object") {
64
+ a = app as Record<string, unknown>;
65
+ } else {
66
+ // MCP-UI tools surface the pointer on result._meta: canonical nested
67
+ // `ui.resourceUri`, or the deprecated flat `"ui/resourceUri"` key.
68
+ const output = (part as { output?: unknown }).output;
69
+ const outMeta =
70
+ output && typeof output === "object"
71
+ ? (output as { _meta?: unknown })._meta
72
+ : undefined;
73
+ const ui =
74
+ outMeta && typeof outMeta === "object"
75
+ ? (outMeta as Record<string, unknown>)["ui"]
76
+ : undefined;
77
+ if (
78
+ ui &&
79
+ typeof ui === "object" &&
80
+ typeof (ui as Record<string, unknown>)["resourceUri"] === "string" &&
81
+ isMcpAppUri((ui as Record<string, unknown>)["resourceUri"] as string)
82
+ ) {
83
+ // Only the spec'd ui fields cross from the result body; serverId is a
84
+ // routing key and stays transport-derived via callProviderMetadata.
85
+ const uiMeta = ui as Record<string, unknown>;
86
+ a = {
87
+ resourceUri: uiMeta["resourceUri"],
88
+ ...(Array.isArray(uiMeta["visibility"])
89
+ ? { visibility: uiMeta["visibility"] }
90
+ : {}),
91
+ };
92
+ } else {
93
+ const flat =
94
+ outMeta && typeof outMeta === "object"
95
+ ? (outMeta as Record<string, unknown>)["ui/resourceUri"]
96
+ : undefined;
97
+ if (typeof flat !== "string" || !isMcpAppUri(flat)) return undefined;
98
+ a = { resourceUri: flat };
99
+ }
100
+ }
101
+ if (typeof a["resourceUri"] !== "string") return undefined;
102
+ if (!isMcpAppUri(a["resourceUri"])) return undefined;
103
+ const cacheKey = `${typeof a["serverId"] === "string" ? a["serverId"] : ""} ${a["resourceUri"]}`;
104
+ const cached = cache?.get(cacheKey);
105
+ if (cached) {
106
+ cache!.delete(cacheKey);
107
+ cache!.set(cacheKey, cached);
108
+ return cached;
109
+ }
110
+ const out: { -readonly [K in keyof McpAppMetadata]: McpAppMetadata[K] } = {
111
+ resourceUri: a["resourceUri"],
112
+ };
113
+ if (typeof a["mimeType"] === "string") out.mimeType = a["mimeType"];
114
+ if (Array.isArray(a["visibility"])) {
115
+ out.visibility = a["visibility"].filter(
116
+ (v): v is "model" | "app" => v === "model" || v === "app",
117
+ );
118
+ }
119
+ if (typeof a["serverId"] === "string" && a["serverId"].length > 0)
120
+ out.serverId = a["serverId"];
121
+ if (cache) {
122
+ if (cache.size >= MCP_APP_METADATA_CACHE_MAX) {
123
+ const oldest = cache.keys().next().value;
124
+ if (oldest !== undefined) cache.delete(oldest);
125
+ }
126
+ cache.set(cacheKey, out);
127
+ }
128
+ return out;
129
+ }
130
+
131
+ function getToolApprovalAndInterrupt(
132
+ part: {
133
+ approval?:
134
+ | {
135
+ id: string;
136
+ approved?: boolean;
137
+ reason?: string;
138
+ isAutomatic?: boolean;
139
+ }
140
+ | undefined;
141
+ },
142
+ toolStatus: { type: string; payload?: unknown } | undefined,
143
+ ): {
144
+ approval?: NonNullable<ToolCallMessagePart["approval"]>;
145
+ interrupt?: NonNullable<ToolCallMessagePart["interrupt"]>;
146
+ } {
147
+ if (part.approval && typeof part.approval.id === "string") {
148
+ const { id, approved, reason, isAutomatic } = part.approval;
149
+ return {
150
+ approval: {
151
+ id,
152
+ ...(typeof approved === "boolean" && { approved }),
153
+ ...(typeof reason === "string" && { reason }),
154
+ ...(isAutomatic === true && { isAutomatic: true }),
155
+ },
156
+ };
157
+ }
158
+
159
+ if (toolStatus?.type === "interrupt") {
160
+ return {
161
+ interrupt: toolStatus.payload as NonNullable<
162
+ ToolCallMessagePart["interrupt"]
163
+ >,
164
+ };
165
+ }
166
+
167
+ return {};
168
+ }
169
+
170
+ type MessageContent = Exclude<ThreadMessageLike["content"], string>;
171
+
172
+ const uiPartStateToStatus = (
173
+ state: "streaming" | "done" | undefined,
174
+ ): MessagePartStreamStatus | undefined => {
175
+ if (state === "streaming") return { type: "running" };
176
+ if (state === "done") return { type: "complete" };
177
+ return undefined;
178
+ };
179
+
180
+ function convertParts(
181
+ message: UIMessage,
182
+ metadata: AISDKMessageConverterMetadata,
183
+ ): MessageContent {
184
+ if (!message.parts || message.parts.length === 0) {
185
+ return [];
186
+ }
187
+
188
+ const converted = message.parts
189
+ .filter(
190
+ (p) =>
191
+ p.type !== "step-start" &&
192
+ (message.role !== "user" || p.type !== "file"),
193
+ )
194
+ .map((part) => {
195
+ if (part.type === "text") {
196
+ const status = uiPartStateToStatus(part.state);
197
+ return {
198
+ type: "text",
199
+ text: part.text,
200
+ ...(status != null ? { status } : undefined),
201
+ ...(part.providerMetadata != null
202
+ ? {
203
+ providerMetadata: part.providerMetadata as PartProviderMetadata,
204
+ }
205
+ : undefined),
206
+ } satisfies TextMessagePart;
207
+ }
208
+
209
+ if (part.type === "reasoning") {
210
+ const status = uiPartStateToStatus(part.state);
211
+ return {
212
+ type: "reasoning",
213
+ text: part.text,
214
+ ...(status != null ? { status } : undefined),
215
+ ...(part.providerMetadata != null
216
+ ? {
217
+ providerMetadata: part.providerMetadata as PartProviderMetadata,
218
+ }
219
+ : undefined),
220
+ } satisfies ReasoningMessagePart;
221
+ }
222
+
223
+ if (isToolUIPart(part)) {
224
+ const toolName = getToolName(part);
225
+ const toolCallId = part.toolCallId;
226
+ const argsKeyOrderCacheKey = `${message.id}:${toolCallId}`;
227
+
228
+ const rawInput = part.input as ReadonlyJSONObject | null | undefined;
229
+ let args: ReadonlyJSONObject;
230
+ if (
231
+ rawInput != null &&
232
+ typeof rawInput === "object" &&
233
+ !Array.isArray(rawInput)
234
+ ) {
235
+ args = rawInput;
236
+ metadata.toolLastInputCache?.set(argsKeyOrderCacheKey, args);
237
+ } else {
238
+ args = metadata.toolLastInputCache?.get(argsKeyOrderCacheKey) ?? {};
239
+ }
240
+
241
+ let result: unknown;
242
+ let modelContent: ToolCallMessagePart["modelContent"];
243
+ let isError = false;
244
+
245
+ if (part.state === "output-available") {
246
+ const unwrapped = unwrapModelContentEnvelope(part.output);
247
+ result = unwrapped.result;
248
+ modelContent = unwrapped.modelContent;
249
+ } else if (part.state === "output-error") {
250
+ isError = true;
251
+ result = { error: part.errorText };
252
+ } else if (part.state === "output-denied") {
253
+ isError = true;
254
+ result = {
255
+ error:
256
+ (part as { approval?: { reason?: string } }).approval?.reason ||
257
+ "Tool approval denied",
258
+ };
259
+ }
260
+
261
+ let argsText = stableStringifyToolArgs(
262
+ metadata.toolArgsKeyOrderCache,
263
+ argsKeyOrderCacheKey,
264
+ args,
265
+ );
266
+ if (part.state === "input-streaming") {
267
+ // strip closing delimiters added by the AI SDK's fix-json
268
+ argsText = stripClosingDelimiters(argsText);
269
+ // Re-parse so args carries the partial-JSON meta that marks which
270
+ // field is still mid-arrival, like every argsText-based runtime.
271
+ // The key-order cache appends new keys last, so the trailing field
272
+ // of the stripped text is the streaming frontier.
273
+ args = parsePartialJsonObject(argsText) ?? args;
274
+ } else {
275
+ metadata.toolArgsKeyOrderCache?.delete(argsKeyOrderCacheKey);
276
+ if (
277
+ part.state === "output-available" ||
278
+ part.state === "output-error" ||
279
+ part.state === "output-denied"
280
+ ) {
281
+ metadata.toolLastInputCache?.delete(argsKeyOrderCacheKey);
282
+ }
283
+ }
284
+
285
+ const toolStatus = metadata.toolStatuses?.[toolCallId];
286
+ const mcpApp = extractMcpAppMetadata(
287
+ part,
288
+ metadata.mcpAppMetadataCache,
289
+ );
290
+ return {
291
+ type: "tool-call",
292
+ toolName,
293
+ toolCallId,
294
+ argsText,
295
+ args,
296
+ result,
297
+ isError,
298
+ ...(modelContent !== undefined && { modelContent }),
299
+ ...(mcpApp && { mcp: { app: mcpApp } }),
300
+ ...(part.callProviderMetadata != null
301
+ ? {
302
+ providerMetadata:
303
+ part.callProviderMetadata as PartProviderMetadata,
304
+ }
305
+ : undefined),
306
+ ...getToolApprovalAndInterrupt(part, toolStatus),
307
+ } satisfies ToolCallMessagePart;
308
+ }
309
+
310
+ if (part.type === "source-url") {
311
+ return {
312
+ type: "source",
313
+ sourceType: "url",
314
+ id: part.sourceId,
315
+ url: part.url,
316
+ ...(part.title != null ? { title: part.title } : undefined),
317
+ ...(part.providerMetadata != null
318
+ ? {
319
+ providerMetadata:
320
+ part.providerMetadata as SourceProviderMetadata,
321
+ }
322
+ : undefined),
323
+ } satisfies SourceMessagePart;
324
+ }
325
+
326
+ if (part.type === "file") {
327
+ return {
328
+ type: "file",
329
+ data: part.url,
330
+ mimeType: part.mediaType,
331
+ ...(part.filename != null && { filename: part.filename }),
332
+ } satisfies FileMessagePart;
333
+ }
334
+
335
+ if (part.type === "source-document") {
336
+ return {
337
+ type: "source",
338
+ sourceType: "document",
339
+ id: part.sourceId,
340
+ title: part.title,
341
+ mediaType: part.mediaType,
342
+ ...(part.filename != null ? { filename: part.filename } : undefined),
343
+ ...(part.providerMetadata != null
344
+ ? {
345
+ providerMetadata:
346
+ part.providerMetadata as SourceProviderMetadata,
347
+ }
348
+ : undefined),
349
+ } satisfies SourceMessagePart;
350
+ }
351
+
352
+ if (part.type.startsWith("data-")) {
353
+ return {
354
+ type: "data",
355
+ name: part.type.substring(5),
356
+ data: (part as any).data,
357
+ } satisfies DataMessagePart;
358
+ }
359
+
360
+ if (isReasoningFileUIPart(part)) {
361
+ return {
362
+ type: "file",
363
+ data: part.url,
364
+ mimeType: part.mediaType,
365
+ } satisfies FileMessagePart;
366
+ }
367
+
368
+ if (isCustomContentUIPart(part)) {
369
+ return {
370
+ type: "data",
371
+ name: part.kind,
372
+ data: part.providerMetadata ?? null,
373
+ } satisfies DataMessagePart;
374
+ }
375
+
376
+ console.warn(`Unsupported message part type: ${part.type}`);
377
+ return null;
378
+ })
379
+ .filter(Boolean) as MessageContent[number][];
380
+
381
+ const seenToolCallIds = new Set<string>();
382
+ return converted.filter((part) => {
383
+ if (part.type === "tool-call" && part.toolCallId != null) {
384
+ if (seenToolCallIds.has(part.toolCallId)) return false;
385
+ seenToolCallIds.add(part.toolCallId);
386
+ }
387
+ return true;
388
+ });
389
+ }
390
+
391
+ export const AISDKMessageConverter = unstable_createMessageConverter(
392
+ (message: UIMessage, metadata: AISDKMessageConverterMetadata) => {
393
+ const createdAt = new Date();
394
+ const content = convertParts(message, metadata);
395
+
396
+ switch (message.role) {
397
+ case "user":
398
+ return {
399
+ role: "user",
400
+ id: message.id,
401
+ createdAt,
402
+ content,
403
+ attachments: message.parts
404
+ ?.filter((p) => p.type === "file")
405
+ .map((part, idx) => {
406
+ const mediaType = part.mediaType ?? "unknown/unknown";
407
+ const isImage = mediaType.startsWith("image/");
408
+ return {
409
+ id: idx.toString(),
410
+ type: isImage ? "image" : "file",
411
+ name: part.filename ?? "file",
412
+ content: [
413
+ isImage
414
+ ? {
415
+ type: "image",
416
+ image: part.url,
417
+ filename: part.filename!,
418
+ }
419
+ : {
420
+ type: "file",
421
+ filename: part.filename!,
422
+ data: part.url,
423
+ mimeType: mediaType,
424
+ },
425
+ ],
426
+ contentType: mediaType,
427
+ status: { type: "complete" as const },
428
+ };
429
+ }),
430
+ metadata: message.metadata as MessageMetadata,
431
+ };
432
+
433
+ case "system":
434
+ case "assistant": {
435
+ const timing = metadata.messageTiming?.[message.id];
436
+ const isOptimistic =
437
+ message.role === "assistant" &&
438
+ message.id === metadata.optimisticMessageId;
439
+ return {
440
+ role: message.role,
441
+ id: message.id,
442
+ createdAt,
443
+ content,
444
+ metadata: {
445
+ ...(message.metadata as MessageMetadata),
446
+ ...(timing && { timing }),
447
+ ...(isOptimistic && { isOptimistic: true }),
448
+ },
449
+ };
450
+ }
451
+
452
+ default:
453
+ console.warn(`Unsupported message role: ${message.role}`);
454
+ return [];
455
+ }
456
+ },
457
+ );
@@ -0,0 +1,41 @@
1
+ import type { ToolModelContentPart } from "assistant-stream";
2
+
3
+ const ENVELOPE_KEY = "__aui_modelContent";
4
+
5
+ export type ModelContentEnvelope<TResult = unknown> = {
6
+ readonly [ENVELOPE_KEY]: readonly ToolModelContentPart[];
7
+ readonly value: TResult;
8
+ };
9
+
10
+ export function isModelContentEnvelope<TResult = unknown>(
11
+ value: TResult | ModelContentEnvelope<TResult>,
12
+ ): value is ModelContentEnvelope<TResult> {
13
+ return (
14
+ value != null &&
15
+ typeof value === "object" &&
16
+ ENVELOPE_KEY in value &&
17
+ Array.isArray((value as Record<string, unknown>)[ENVELOPE_KEY])
18
+ );
19
+ }
20
+
21
+ export function wrapModelContentEnvelope<TResult>(
22
+ result: TResult,
23
+ modelContent: readonly ToolModelContentPart[],
24
+ ): ModelContentEnvelope<TResult> {
25
+ return { [ENVELOPE_KEY]: modelContent, value: result };
26
+ }
27
+
28
+ export function unwrapModelContentEnvelope<TResult>(
29
+ output: TResult | ModelContentEnvelope<TResult>,
30
+ ): {
31
+ result: TResult;
32
+ modelContent?: readonly ToolModelContentPart[];
33
+ } {
34
+ if (isModelContentEnvelope(output)) {
35
+ return {
36
+ result: output.value,
37
+ modelContent: output[ENVELOPE_KEY],
38
+ };
39
+ }
40
+ return { result: output };
41
+ }