@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,1058 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import {
3
+ defineMcpToolkit,
4
+ defineToolkit,
5
+ type ToolkitDefinition,
6
+ } from "@ulearning/ai-core/vue";
7
+ import { AISDKToolkit } from "./generativeTools";
8
+ import { wrapModelContentEnvelope } from "@ulearning/ai-stream/internal";
9
+
10
+ const mocks = vi.hoisted(() => ({
11
+ close: vi.fn(),
12
+ tools: vi.fn(),
13
+ createMCPClient: vi.fn(),
14
+ }));
15
+
16
+ vi.mock("@ai-sdk/mcp", () => ({
17
+ createMCPClient: mocks.createMCPClient,
18
+ }));
19
+
20
+ vi.mock("@ai-sdk/mcp/mcp-stdio", () => ({
21
+ Experimental_StdioMCPTransport: vi.fn((config) => ({
22
+ type: "stdio",
23
+ config,
24
+ })),
25
+ }));
26
+
27
+ const never = <T>() => new Promise<T>(() => {});
28
+
29
+ describe("AISDKToolkit.tools()", () => {
30
+ it("merges frontend tools with toolkit tools", async () => {
31
+ const toolSet = await new AISDKToolkit({
32
+ toolkit: {
33
+ serverTool: {
34
+ type: "backend",
35
+ description: "Server tool",
36
+ parameters: { type: "object", properties: {} },
37
+ execute: async () => "ok",
38
+ } as never,
39
+ },
40
+ }).tools({
41
+ frontend: {
42
+ clientTool: {
43
+ parameters: { type: "object", properties: {} },
44
+ },
45
+ },
46
+ });
47
+
48
+ expect(toolSet.clientTool).toBeDefined();
49
+ expect(toolSet.serverTool?.description).toBe("Server tool");
50
+ expect(toolSet.serverTool?.execute).toBeTypeOf("function");
51
+ });
52
+
53
+ it("keeps a flat toolkit tool named tools", async () => {
54
+ const toolSet = await new AISDKToolkit({
55
+ toolkit: {
56
+ tools: {
57
+ type: "backend",
58
+ description: "Actually a tool, not config",
59
+ parameters: { type: "object", properties: {} },
60
+ execute: async () => "ok",
61
+ } as never,
62
+ },
63
+ }).tools();
64
+
65
+ expect(toolSet.tools?.description).toBe("Actually a tool, not config");
66
+ expect(toolSet.tools?.execute).toBeTypeOf("function");
67
+ });
68
+
69
+ it("converts provider tools without an execute function", async () => {
70
+ const toolSet = await new AISDKToolkit({
71
+ toolkit: {
72
+ web_search: {
73
+ type: "provider",
74
+ providerId: "openai.web_search_preview",
75
+ args: { searchContextSize: "low" },
76
+ },
77
+ },
78
+ }).tools();
79
+
80
+ expect(toolSet.web_search).toMatchObject({
81
+ type: "provider",
82
+ id: "openai.web_search_preview",
83
+ args: { searchContextSize: "low" },
84
+ });
85
+ expect(toolSet.web_search).not.toHaveProperty("inputSchema");
86
+ expect(toolSet.web_search).not.toHaveProperty("execute");
87
+ });
88
+
89
+ it("forwards provider tool parameters and providerOptions when present", async () => {
90
+ const toolSet = await new AISDKToolkit({
91
+ toolkit: {
92
+ web_search: {
93
+ type: "provider",
94
+ providerId: "openai.web_search_preview",
95
+ args: { searchContextSize: "low" },
96
+ parameters: {
97
+ type: "object",
98
+ properties: {
99
+ query: { type: "string" },
100
+ },
101
+ required: ["query"],
102
+ },
103
+ providerOptions: {
104
+ openai: { rankingOptions: { scoreThreshold: 0.5 } },
105
+ },
106
+ },
107
+ },
108
+ }).tools();
109
+
110
+ expect(toolSet.web_search).toMatchObject({
111
+ type: "provider",
112
+ id: "openai.web_search_preview",
113
+ args: { searchContextSize: "low" },
114
+ providerOptions: {
115
+ openai: { rankingOptions: { scoreThreshold: 0.5 } },
116
+ },
117
+ });
118
+ expect(toolSet.web_search).toHaveProperty("inputSchema");
119
+ });
120
+
121
+ it("forwards explicit false supportsDeferredResults", async () => {
122
+ const toolSet = await new AISDKToolkit({
123
+ toolkit: {
124
+ web_search: {
125
+ type: "provider",
126
+ providerId: "openai.web_search_preview",
127
+ args: { searchContextSize: "low" },
128
+ supportsDeferredResults: false,
129
+ },
130
+ },
131
+ }).tools();
132
+
133
+ expect(toolSet.web_search).toMatchObject({
134
+ supportsDeferredResults: false,
135
+ });
136
+ });
137
+
138
+ it("converts unchanged provider and backend schemas once", async () => {
139
+ const toBackendJSONSchema = vi.fn(() => ({
140
+ type: "object" as const,
141
+ properties: {},
142
+ }));
143
+ const toProviderJSONSchema = vi.fn(() => ({
144
+ type: "object" as const,
145
+ properties: {},
146
+ }));
147
+ const toolkitDefinition = {
148
+ serverTool: {
149
+ type: "backend",
150
+ parameters: { toJSONSchema: toBackendJSONSchema },
151
+ execute: async () => "ok",
152
+ } as never,
153
+ providerTool: {
154
+ type: "provider",
155
+ providerId: "provider.tool",
156
+ args: {},
157
+ parameters: { toJSONSchema: toProviderJSONSchema },
158
+ } as never,
159
+ };
160
+ const firstToolkit = new AISDKToolkit({ toolkit: toolkitDefinition });
161
+ const secondToolkit = new AISDKToolkit({ toolkit: toolkitDefinition });
162
+
163
+ const first = await firstToolkit.tools();
164
+ const second = await secondToolkit.tools();
165
+
166
+ expect(toBackendJSONSchema).toHaveBeenCalledTimes(1);
167
+ expect(toProviderJSONSchema).toHaveBeenCalledTimes(1);
168
+ expect(second.serverTool).not.toBe(first.serverTool);
169
+ expect(second.providerTool).not.toBe(first.providerTool);
170
+ expect(second.serverTool!.inputSchema).not.toBe(
171
+ first.serverTool!.inputSchema,
172
+ );
173
+ expect(
174
+ (second.serverTool!.inputSchema as { jsonSchema: unknown }).jsonSchema,
175
+ ).toBe(
176
+ (first.serverTool!.inputSchema as { jsonSchema: unknown }).jsonSchema,
177
+ );
178
+ });
179
+
180
+ it("converts the current frontend tools on every call", async () => {
181
+ const toolkit = new AISDKToolkit({ toolkit: {} });
182
+
183
+ const first = await toolkit.tools({
184
+ frontend: {
185
+ firstClientTool: {
186
+ parameters: { type: "object", properties: {} },
187
+ },
188
+ },
189
+ });
190
+ const second = await toolkit.tools({
191
+ frontend: {
192
+ secondClientTool: {
193
+ parameters: { type: "object", properties: {} },
194
+ },
195
+ },
196
+ });
197
+
198
+ expect(first).toHaveProperty("firstClientTool");
199
+ expect(first).not.toHaveProperty("secondClientTool");
200
+ expect(second).toHaveProperty("secondClientTool");
201
+ expect(second).not.toHaveProperty("firstClientTool");
202
+ });
203
+
204
+ it("does not convert disabled static toolkit entries", async () => {
205
+ const toJSONSchema = vi.fn(() => ({
206
+ type: "object" as const,
207
+ properties: {},
208
+ }));
209
+ const toolkit = new AISDKToolkit({
210
+ toolkit: {
211
+ disabledServerTool: {
212
+ type: "backend",
213
+ disabled: true,
214
+ parameters: { toJSONSchema },
215
+ } as never,
216
+ disabledProviderTool: {
217
+ type: "provider",
218
+ disabled: true,
219
+ providerId: "provider.tool",
220
+ args: {},
221
+ parameters: { toJSONSchema },
222
+ } as never,
223
+ },
224
+ });
225
+
226
+ await expect(toolkit.tools()).resolves.toEqual({});
227
+ expect(toJSONSchema).not.toHaveBeenCalled();
228
+ });
229
+
230
+ it("reflects replaced toolkit entries on the next call", async () => {
231
+ const definition: ToolkitDefinition = {
232
+ serverTool: {
233
+ description: "Original tool",
234
+ parameters: { type: "object" as const, properties: {} },
235
+ execute: async () => "original",
236
+ },
237
+ };
238
+ const toolkit = new AISDKToolkit({ toolkit: defineToolkit(definition) });
239
+
240
+ const first = await toolkit.tools();
241
+ definition.serverTool = {
242
+ description: "Replacement tool",
243
+ parameters: { type: "object" as const, properties: {} },
244
+ execute: async () => "replacement",
245
+ };
246
+
247
+ const refreshed = await toolkit.tools();
248
+
249
+ expect(first.serverTool?.description).toBe("Original tool");
250
+ expect(refreshed.serverTool?.description).toBe("Replacement tool");
251
+ });
252
+
253
+ it("reconverts a replaced parameters object", async () => {
254
+ const firstConversion = vi.fn(() => ({
255
+ type: "object" as const,
256
+ properties: { first: { type: "string" } },
257
+ }));
258
+ const secondConversion = vi.fn(() => ({
259
+ type: "object" as const,
260
+ properties: { second: { type: "string" } },
261
+ }));
262
+ const definition: ToolkitDefinition = {
263
+ serverTool: {
264
+ parameters: { toJSONSchema: firstConversion },
265
+ execute: async () => "ok",
266
+ } as never,
267
+ };
268
+ const toolkit = new AISDKToolkit({ toolkit: defineToolkit(definition) });
269
+
270
+ await toolkit.tools();
271
+ definition.serverTool!.parameters = {
272
+ toJSONSchema: secondConversion,
273
+ } as never;
274
+ await toolkit.tools();
275
+
276
+ expect(firstConversion).toHaveBeenCalledOnce();
277
+ expect(secondConversion).toHaveBeenCalledOnce();
278
+ });
279
+
280
+ it("reflects in-place toolkit entry changes on the next call", async () => {
281
+ const definition: ToolkitDefinition = {
282
+ serverTool: {
283
+ description: "Original tool",
284
+ parameters: { type: "object" as const, properties: {} },
285
+ execute: async () => "ok",
286
+ },
287
+ };
288
+ const toolkit = new AISDKToolkit({ toolkit: defineToolkit(definition) });
289
+
290
+ expect((await toolkit.tools()).serverTool?.description).toBe(
291
+ "Original tool",
292
+ );
293
+
294
+ definition.serverTool!.description = "Updated tool";
295
+ expect((await toolkit.tools()).serverTool?.description).toBe(
296
+ "Updated tool",
297
+ );
298
+
299
+ definition.serverTool!.disabled = true;
300
+ expect(await toolkit.tools()).not.toHaveProperty("serverTool");
301
+ });
302
+
303
+ it("does not share mutable tool entries between calls", async () => {
304
+ const toolkit = new AISDKToolkit({
305
+ toolkit: defineToolkit({
306
+ serverTool: {
307
+ description: "Original tool",
308
+ parameters: { type: "object" as const, properties: {} },
309
+ execute: async () => "ok",
310
+ },
311
+ }),
312
+ });
313
+
314
+ const first = await toolkit.tools();
315
+ first.serverTool!.description = "Changed returned tool";
316
+
317
+ const second = await toolkit.tools();
318
+
319
+ expect(second.serverTool?.description).toBe("Original tool");
320
+ });
321
+ });
322
+
323
+ describe("AISDKToolkit", () => {
324
+ beforeEach(() => {
325
+ mocks.close.mockReset();
326
+ mocks.tools.mockReset();
327
+ mocks.createMCPClient.mockReset();
328
+ });
329
+
330
+ it("preserves prototype-named MCP tools", async () => {
331
+ const prototypeTool = { inputSchema: {} };
332
+ mocks.tools.mockResolvedValue(
333
+ Object.fromEntries([["__proto__", prototypeTool]]),
334
+ );
335
+ mocks.createMCPClient.mockResolvedValue({
336
+ tools: mocks.tools,
337
+ close: mocks.close,
338
+ });
339
+
340
+ const toolkit = new AISDKToolkit({
341
+ toolkit: {
342
+ docs: {
343
+ type: "mcp",
344
+ server: { type: "http", url: "http://localhost:3001/mcp" },
345
+ },
346
+ },
347
+ });
348
+
349
+ const tools = await toolkit.tools();
350
+ expect(Object.hasOwn(tools, "__proto__")).toBe(true);
351
+ expect(tools["__proto__"]).toBe(prototypeTool);
352
+ });
353
+
354
+ it("loads MCP tools through pooled clients", async () => {
355
+ mocks.tools.mockResolvedValue({ echo: { inputSchema: {} } });
356
+ mocks.createMCPClient.mockResolvedValue({
357
+ tools: mocks.tools,
358
+ close: mocks.close,
359
+ });
360
+
361
+ const toolkit = new AISDKToolkit({
362
+ toolkit: {
363
+ local: {
364
+ type: "mcp",
365
+ server: { type: "http", url: "http://localhost:3001/mcp" },
366
+ },
367
+ },
368
+ });
369
+
370
+ await expect(toolkit.tools()).resolves.toHaveProperty("echo");
371
+ await toolkit.tools();
372
+
373
+ expect(mocks.createMCPClient).toHaveBeenCalledTimes(1);
374
+ expect(mocks.createMCPClient).toHaveBeenCalledWith({
375
+ transport: {
376
+ type: "http",
377
+ url: "http://localhost:3001/mcp",
378
+ },
379
+ });
380
+ expect(mocks.tools).toHaveBeenCalledTimes(2);
381
+ });
382
+
383
+ it("does not connect disabled MCP toolkit entries", async () => {
384
+ const toolkit = new AISDKToolkit({
385
+ toolkit: {
386
+ local: {
387
+ type: "mcp",
388
+ server: { type: "http", url: "http://localhost:3001/mcp" },
389
+ disabled: true,
390
+ },
391
+ },
392
+ });
393
+
394
+ await expect(toolkit.tools()).resolves.toEqual({});
395
+
396
+ expect(mocks.createMCPClient).not.toHaveBeenCalled();
397
+ expect(mocks.tools).not.toHaveBeenCalled();
398
+ });
399
+
400
+ it("filters disabled MCP tools from enabled toolkit entries", async () => {
401
+ mocks.tools.mockResolvedValue({
402
+ publicSearch: { inputSchema: {} },
403
+ privateSearch: { inputSchema: {} },
404
+ });
405
+ mocks.createMCPClient.mockResolvedValue({
406
+ tools: mocks.tools,
407
+ close: mocks.close,
408
+ });
409
+
410
+ const toolkit = new AISDKToolkit({
411
+ toolkit: {
412
+ local: {
413
+ type: "mcp",
414
+ server: { type: "http", url: "http://localhost:3001/mcp" },
415
+ tools: {
416
+ privateSearch: {
417
+ disabled: true,
418
+ },
419
+ },
420
+ } as never,
421
+ },
422
+ });
423
+
424
+ await expect(toolkit.tools()).resolves.toEqual({
425
+ publicSearch: { inputSchema: {} },
426
+ });
427
+
428
+ expect(mocks.createMCPClient).toHaveBeenCalledTimes(1);
429
+ expect(mocks.tools).toHaveBeenCalledTimes(1);
430
+ });
431
+
432
+ it("closes pooled MCP clients", async () => {
433
+ mocks.tools.mockResolvedValue({});
434
+ mocks.createMCPClient.mockResolvedValue({
435
+ tools: mocks.tools,
436
+ close: mocks.close,
437
+ });
438
+
439
+ const toolkit = new AISDKToolkit({
440
+ toolkit: {
441
+ local: {
442
+ type: "mcp",
443
+ server: { type: "sse", url: "http://localhost:3001/sse" },
444
+ },
445
+ },
446
+ });
447
+
448
+ await toolkit.tools();
449
+ await toolkit.close();
450
+
451
+ expect(mocks.close).toHaveBeenCalledTimes(1);
452
+ });
453
+
454
+ it("clears pooled MCP clients even when initialization fails", async () => {
455
+ const error = new Error("connect failed");
456
+ const closeError = new Error("close failed");
457
+ const close = vi.fn().mockRejectedValue(closeError);
458
+ mocks.tools.mockResolvedValue({});
459
+ mocks.createMCPClient
460
+ .mockResolvedValueOnce({
461
+ tools: mocks.tools,
462
+ close,
463
+ })
464
+ .mockRejectedValueOnce(error);
465
+
466
+ const toolkit = new AISDKToolkit({
467
+ toolkit: {
468
+ first: {
469
+ type: "mcp",
470
+ server: { type: "http", url: "http://localhost:3001/mcp" },
471
+ },
472
+ second: {
473
+ type: "mcp",
474
+ server: { type: "http", url: "http://localhost:3002/mcp" },
475
+ },
476
+ },
477
+ });
478
+
479
+ const toolsPromise = toolkit.tools();
480
+ await expect(toolkit.close()).rejects.toMatchObject({
481
+ errors: [
482
+ {
483
+ message:
484
+ 'MCP toolkit entry "second" failed to connect: connect failed',
485
+ cause: error,
486
+ },
487
+ {
488
+ message: 'MCP toolkit entry "first" failed to close: close failed',
489
+ cause: closeError,
490
+ },
491
+ ],
492
+ });
493
+ await expect(toolsPromise).rejects.toMatchObject({
494
+ message: 'MCP toolkit entry "second" failed to connect: connect failed',
495
+ cause: error,
496
+ });
497
+ expect(close).toHaveBeenCalledTimes(1);
498
+
499
+ await expect(toolkit.close()).resolves.toBeUndefined();
500
+ });
501
+
502
+ it("includes the MCP toolkit entry name when closing a client fails", async () => {
503
+ const closeError = new Error("close failed");
504
+ mocks.tools.mockResolvedValue({});
505
+ mocks.createMCPClient.mockResolvedValue({
506
+ tools: mocks.tools,
507
+ close: vi.fn().mockRejectedValue(closeError),
508
+ });
509
+
510
+ const toolkit = new AISDKToolkit({
511
+ toolkit: {
512
+ github: {
513
+ type: "mcp",
514
+ server: { type: "http", url: "http://localhost:3001/mcp" },
515
+ },
516
+ },
517
+ });
518
+
519
+ await toolkit.tools();
520
+
521
+ await expect(toolkit.close()).rejects.toMatchObject({
522
+ message: 'MCP toolkit entry "github" failed to close: close failed',
523
+ cause: closeError,
524
+ });
525
+ });
526
+
527
+ it("evicts failed MCP client initialization so later calls can retry", async () => {
528
+ const error = new Error("connect failed");
529
+ mocks.createMCPClient.mockRejectedValueOnce(error).mockResolvedValueOnce({
530
+ tools: vi.fn().mockResolvedValue({ echo: { inputSchema: {} } }),
531
+ close: mocks.close,
532
+ });
533
+
534
+ const toolkit = new AISDKToolkit({
535
+ toolkit: {
536
+ local: {
537
+ type: "mcp",
538
+ server: { type: "http", url: "http://localhost:3001/mcp" },
539
+ },
540
+ },
541
+ });
542
+
543
+ await expect(toolkit.tools()).rejects.toMatchObject({
544
+ message: 'MCP toolkit entry "local" failed to connect: connect failed',
545
+ cause: error,
546
+ });
547
+ await expect(toolkit.tools()).resolves.toHaveProperty("echo");
548
+ expect(mocks.createMCPClient).toHaveBeenCalledTimes(2);
549
+ });
550
+
551
+ it("times out MCP client creation", async () => {
552
+ vi.useFakeTimers();
553
+ mocks.createMCPClient.mockReturnValue(never());
554
+
555
+ const toolkit = new AISDKToolkit({
556
+ toolkit: {
557
+ docs: {
558
+ type: "mcp",
559
+ server: {
560
+ type: "http",
561
+ url: "http://localhost:3001/mcp",
562
+ connectionTimeout: 10_000,
563
+ },
564
+ },
565
+ },
566
+ });
567
+
568
+ try {
569
+ const toolsPromise = toolkit.tools();
570
+ const expectedRejection = expect(toolsPromise).rejects.toThrow(
571
+ 'MCP toolkit entry "docs" timed out while connecting after 10000ms.',
572
+ );
573
+ await vi.advanceTimersByTimeAsync(10_000);
574
+ await expectedRejection;
575
+
576
+ mocks.createMCPClient.mockResolvedValueOnce({
577
+ tools: vi.fn().mockResolvedValue({ echo: { inputSchema: {} } }),
578
+ close: mocks.close,
579
+ });
580
+ await expect(toolkit.tools()).resolves.toHaveProperty("echo");
581
+ expect(mocks.createMCPClient).toHaveBeenCalledTimes(2);
582
+ } finally {
583
+ vi.useRealTimers();
584
+ }
585
+ });
586
+
587
+ it("closes an MCP client that resolves after its connection times out", async () => {
588
+ vi.useFakeTimers();
589
+ const close = vi.fn().mockResolvedValue(undefined);
590
+ let resolveClient!: (client: {
591
+ tools: typeof mocks.tools;
592
+ close: typeof close;
593
+ }) => void;
594
+ mocks.createMCPClient.mockReturnValue(
595
+ new Promise((resolve) => {
596
+ resolveClient = resolve;
597
+ }),
598
+ );
599
+
600
+ const toolkit = new AISDKToolkit({
601
+ toolkit: {
602
+ docs: {
603
+ type: "mcp",
604
+ server: {
605
+ type: "http",
606
+ url: "http://localhost:3001/mcp",
607
+ connectionTimeout: 10_000,
608
+ },
609
+ },
610
+ },
611
+ });
612
+
613
+ try {
614
+ const toolsPromise = toolkit.tools();
615
+ const expectedRejection = expect(toolsPromise).rejects.toThrow(
616
+ 'MCP toolkit entry "docs" timed out while connecting after 10000ms.',
617
+ );
618
+ await vi.advanceTimersByTimeAsync(10_000);
619
+ await expectedRejection;
620
+
621
+ resolveClient({ tools: mocks.tools, close });
622
+ await vi.waitFor(() => expect(close).toHaveBeenCalledTimes(1));
623
+ } finally {
624
+ vi.useRealTimers();
625
+ }
626
+ });
627
+
628
+ it("times out MCP tool listing", async () => {
629
+ vi.useFakeTimers();
630
+ const close = vi.fn().mockResolvedValue(undefined);
631
+ mocks.createMCPClient.mockResolvedValue({
632
+ tools: vi.fn(() => never()),
633
+ close,
634
+ });
635
+
636
+ const toolkit = new AISDKToolkit({
637
+ toolkit: {
638
+ docs: {
639
+ type: "mcp",
640
+ server: {
641
+ type: "http",
642
+ url: "http://localhost:3001/mcp",
643
+ connectionTimeout: 10_000,
644
+ },
645
+ },
646
+ },
647
+ });
648
+
649
+ try {
650
+ const toolsPromise = toolkit.tools();
651
+ const expectedRejection = expect(toolsPromise).rejects.toThrow(
652
+ 'MCP toolkit entry "docs" timed out while listing tools after 10000ms.',
653
+ );
654
+ await vi.advanceTimersByTimeAsync(10_000);
655
+ await expectedRejection;
656
+ expect(close).toHaveBeenCalledTimes(1);
657
+
658
+ mocks.createMCPClient.mockResolvedValueOnce({
659
+ tools: vi.fn().mockResolvedValue({ echo: { inputSchema: {} } }),
660
+ close: mocks.close,
661
+ });
662
+ await expect(toolkit.tools()).resolves.toHaveProperty("echo");
663
+ expect(mocks.createMCPClient).toHaveBeenCalledTimes(2);
664
+ } finally {
665
+ vi.useRealTimers();
666
+ }
667
+ });
668
+
669
+ it("does not evict a replacement client after an older listing timeout", async () => {
670
+ vi.useFakeTimers();
671
+ const oldClient = {
672
+ tools: vi.fn(() => never()),
673
+ close: vi.fn().mockResolvedValue(undefined),
674
+ };
675
+ const replacementClient = {
676
+ tools: vi.fn().mockResolvedValue({ echo: { inputSchema: {} } }),
677
+ close: vi.fn().mockResolvedValue(undefined),
678
+ };
679
+ mocks.createMCPClient
680
+ .mockResolvedValueOnce(oldClient)
681
+ .mockResolvedValue(replacementClient);
682
+
683
+ const toolkit = new AISDKToolkit({
684
+ toolkit: {
685
+ docs: {
686
+ type: "mcp",
687
+ server: {
688
+ type: "http",
689
+ url: "http://localhost:3001/mcp",
690
+ connectionTimeout: 100,
691
+ },
692
+ },
693
+ },
694
+ });
695
+
696
+ try {
697
+ const first = toolkit.tools();
698
+ const firstRejection = expect(first).rejects.toThrow(
699
+ /timed out while listing tools/,
700
+ );
701
+ await vi.advanceTimersByTimeAsync(50);
702
+
703
+ const second = toolkit.tools();
704
+ const secondRejection = expect(second).rejects.toThrow(
705
+ /timed out while listing tools/,
706
+ );
707
+ await vi.advanceTimersByTimeAsync(50);
708
+ await firstRejection;
709
+
710
+ await expect(toolkit.tools()).resolves.toHaveProperty("echo");
711
+ expect(mocks.createMCPClient).toHaveBeenCalledTimes(2);
712
+
713
+ await vi.advanceTimersByTimeAsync(50);
714
+ await secondRejection;
715
+
716
+ await expect(toolkit.tools()).resolves.toHaveProperty("echo");
717
+ expect(mocks.createMCPClient).toHaveBeenCalledTimes(2);
718
+ expect(oldClient.close).toHaveBeenCalledTimes(1);
719
+ } finally {
720
+ vi.useRealTimers();
721
+ }
722
+ });
723
+
724
+ it("includes the MCP toolkit entry name when listing tools fails", async () => {
725
+ const error = new Error("list failed");
726
+ mocks.tools.mockRejectedValue(error);
727
+ mocks.createMCPClient.mockResolvedValue({
728
+ tools: mocks.tools,
729
+ close: mocks.close,
730
+ });
731
+
732
+ const toolkit = new AISDKToolkit({
733
+ toolkit: {
734
+ docs: {
735
+ type: "mcp",
736
+ server: { type: "http", url: "http://localhost:3001/mcp" },
737
+ },
738
+ },
739
+ });
740
+
741
+ await expect(toolkit.tools()).rejects.toMatchObject({
742
+ message: 'MCP toolkit entry "docs" failed to list tools: list failed',
743
+ cause: error,
744
+ });
745
+ });
746
+
747
+ it("rejects duplicate MCP tool names", async () => {
748
+ mocks.createMCPClient
749
+ .mockResolvedValueOnce({
750
+ tools: vi.fn().mockResolvedValue({ echo: { inputSchema: {} } }),
751
+ close: mocks.close,
752
+ })
753
+ .mockResolvedValueOnce({
754
+ tools: vi.fn().mockResolvedValue({ echo: { inputSchema: {} } }),
755
+ close: mocks.close,
756
+ });
757
+
758
+ const toolkit = new AISDKToolkit({
759
+ toolkit: {
760
+ first: {
761
+ type: "mcp",
762
+ server: { type: "http", url: "http://localhost:3001/mcp" },
763
+ },
764
+ second: {
765
+ type: "mcp",
766
+ server: { type: "http", url: "http://localhost:3002/mcp" },
767
+ },
768
+ },
769
+ });
770
+
771
+ await expect(toolkit.tools()).rejects.toThrow(
772
+ /MCP tool name collision: "echo"/,
773
+ );
774
+ });
775
+
776
+ it("prefixes MCP tool names when configured", async () => {
777
+ const docsExecute = vi.fn().mockResolvedValue("docs result");
778
+ mocks.createMCPClient
779
+ .mockResolvedValueOnce({
780
+ tools: vi.fn().mockResolvedValue({
781
+ search: { inputSchema: {}, execute: docsExecute },
782
+ }),
783
+ close: mocks.close,
784
+ })
785
+ .mockResolvedValueOnce({
786
+ tools: vi.fn().mockResolvedValue({ search: { inputSchema: {} } }),
787
+ close: mocks.close,
788
+ });
789
+
790
+ const toolkit = new AISDKToolkit({
791
+ toolkit: defineMcpToolkit({
792
+ docs: {
793
+ server: { type: "http", url: "http://localhost:3001/mcp" },
794
+ prefix: "docs_",
795
+ },
796
+ github: {
797
+ server: { type: "http", url: "http://localhost:3002/mcp" },
798
+ prefix: "github_",
799
+ },
800
+ }),
801
+ });
802
+
803
+ const toolSet = await toolkit.tools();
804
+
805
+ expect(toolSet).toHaveProperty("docs_search");
806
+ expect(toolSet).toHaveProperty("github_search");
807
+ expect(toolSet).not.toHaveProperty("search");
808
+
809
+ expect(toolSet.docs_search?.execute).toBeTypeOf("function");
810
+ const executeOptions = {
811
+ toolCallId: "call-docs-search",
812
+ messages: [],
813
+ };
814
+ const execute = toolSet.docs_search?.execute as
815
+ | ((
816
+ args: { query: string },
817
+ options: typeof executeOptions,
818
+ ) => Promise<string>)
819
+ | undefined;
820
+ await expect(
821
+ execute?.({ query: "assistant-ui" }, executeOptions),
822
+ ).resolves.toBe("docs result");
823
+ expect(docsExecute).toHaveBeenCalledWith(
824
+ { query: "assistant-ui" },
825
+ executeOptions,
826
+ );
827
+ });
828
+
829
+ it("rejects MCP tool names that collide with toolkit tools", async () => {
830
+ mocks.tools.mockResolvedValue({ search: { inputSchema: {} } });
831
+ mocks.createMCPClient.mockResolvedValue({
832
+ tools: mocks.tools,
833
+ close: mocks.close,
834
+ });
835
+
836
+ const toolkit = new AISDKToolkit({
837
+ toolkit: {
838
+ docs: {
839
+ type: "mcp",
840
+ server: { type: "http", url: "http://localhost:3001/mcp" },
841
+ },
842
+ search: {
843
+ type: "backend",
844
+ parameters: { type: "object", properties: {} },
845
+ execute: async () => "local search",
846
+ } as never,
847
+ },
848
+ });
849
+
850
+ await expect(toolkit.tools()).rejects.toThrow(
851
+ 'MCP tool "search" from "docs" conflicts with toolkit tool "search"',
852
+ );
853
+ });
854
+
855
+ it("rejects MCP tool names that collide with provider tools", async () => {
856
+ mocks.tools.mockResolvedValue({ web_search: { inputSchema: {} } });
857
+ mocks.createMCPClient.mockResolvedValue({
858
+ tools: mocks.tools,
859
+ close: mocks.close,
860
+ });
861
+
862
+ const toolkit = new AISDKToolkit({
863
+ toolkit: {
864
+ docs: {
865
+ type: "mcp",
866
+ server: { type: "http", url: "http://localhost:3001/mcp" },
867
+ },
868
+ web_search: {
869
+ type: "provider",
870
+ providerId: "openai.web_search_preview",
871
+ args: { searchContextSize: "low" },
872
+ },
873
+ },
874
+ });
875
+
876
+ await expect(toolkit.tools()).rejects.toThrow(
877
+ 'MCP tool "web_search" from "docs" conflicts with provider tool "web_search"',
878
+ );
879
+ });
880
+
881
+ it("rejects MCP tool names that collide with uploaded frontend tools", async () => {
882
+ mocks.tools.mockResolvedValue({ clientTool: { inputSchema: {} } });
883
+ mocks.createMCPClient.mockResolvedValue({
884
+ tools: mocks.tools,
885
+ close: mocks.close,
886
+ });
887
+
888
+ const toolkit = new AISDKToolkit({
889
+ toolkit: {
890
+ docs: {
891
+ type: "mcp",
892
+ server: { type: "http", url: "http://localhost:3001/mcp" },
893
+ },
894
+ },
895
+ });
896
+
897
+ await expect(
898
+ toolkit.tools({
899
+ frontend: {
900
+ clientTool: {
901
+ parameters: { type: "object", properties: {} },
902
+ },
903
+ },
904
+ }),
905
+ ).rejects.toThrow(
906
+ 'MCP tool "clientTool" from "docs" conflicts with frontend tool "clientTool"',
907
+ );
908
+ });
909
+
910
+ it("ignores disabled MCP tools during name collision checks", async () => {
911
+ mocks.createMCPClient
912
+ .mockResolvedValueOnce({
913
+ tools: vi.fn().mockResolvedValue({ echo: { inputSchema: {} } }),
914
+ close: mocks.close,
915
+ })
916
+ .mockResolvedValueOnce({
917
+ tools: vi.fn().mockResolvedValue({ echo: { inputSchema: {} } }),
918
+ close: mocks.close,
919
+ });
920
+
921
+ const toolkit = new AISDKToolkit({
922
+ toolkit: {
923
+ first: {
924
+ type: "mcp",
925
+ server: { type: "http", url: "http://localhost:3001/mcp" },
926
+ tools: {
927
+ echo: {
928
+ disabled: true,
929
+ },
930
+ },
931
+ } as never,
932
+ second: {
933
+ type: "mcp",
934
+ server: { type: "http", url: "http://localhost:3002/mcp" },
935
+ },
936
+ },
937
+ });
938
+
939
+ await expect(toolkit.tools()).resolves.toEqual({
940
+ echo: { inputSchema: {} },
941
+ });
942
+ });
943
+
944
+ it("includes provider tools alongside MCP tools", async () => {
945
+ mocks.tools.mockResolvedValue({ echo: { inputSchema: {} } });
946
+ mocks.createMCPClient.mockResolvedValue({
947
+ tools: mocks.tools,
948
+ close: mocks.close,
949
+ });
950
+
951
+ const toolkit = new AISDKToolkit({
952
+ toolkit: {
953
+ local: {
954
+ type: "mcp",
955
+ server: { type: "http", url: "http://localhost:3001/mcp" },
956
+ },
957
+ web_search: {
958
+ type: "provider",
959
+ providerId: "openai.web_search_preview",
960
+ args: { searchContextSize: "low" },
961
+ supportsDeferredResults: false,
962
+ },
963
+ },
964
+ });
965
+
966
+ await expect(toolkit.tools()).resolves.toMatchObject({
967
+ echo: { inputSchema: {} },
968
+ web_search: {
969
+ type: "provider",
970
+ id: "openai.web_search_preview",
971
+ args: { searchContextSize: "low" },
972
+ supportsDeferredResults: false,
973
+ },
974
+ });
975
+ });
976
+ });
977
+
978
+ describe("AISDKToolkit toModelOutput", () => {
979
+ const createWeatherTools = (toModelOutput?: any) =>
980
+ new AISDKToolkit({
981
+ toolkit: {
982
+ get_weather: {
983
+ ...(toModelOutput && { toModelOutput }),
984
+ },
985
+ } as any,
986
+ }).tools();
987
+
988
+ it("adapts assistant-ui model content parts to the AI SDK tool output shape", async () => {
989
+ const tools = await createWeatherTools(({ output }: any) => [
990
+ { type: "text", text: `Weather card displayed: ${output.location}` },
991
+ ]);
992
+
993
+ const output = await tools.get_weather!.toModelOutput!({
994
+ toolCallId: "tc-weather",
995
+ input: {},
996
+ output: { location: "San Francisco" },
997
+ });
998
+
999
+ expect(output).toEqual({
1000
+ type: "content",
1001
+ value: [{ type: "text", text: "Weather card displayed: San Francisco" }],
1002
+ });
1003
+ });
1004
+
1005
+ it("uses stored model content envelopes without re-running the custom projector", async () => {
1006
+ let called = false;
1007
+ const tools = await createWeatherTools(() => {
1008
+ called = true;
1009
+ return [{ type: "text", text: "recomputed" }];
1010
+ });
1011
+
1012
+ const output = await tools.get_weather!.toModelOutput!({
1013
+ toolCallId: "tc-weather",
1014
+ input: {},
1015
+ output: wrapModelContentEnvelope({ location: "San Francisco" }, [
1016
+ { type: "text", text: "cached weather receipt" },
1017
+ ]),
1018
+ });
1019
+
1020
+ expect(called).toBe(false);
1021
+ expect(output).toEqual({
1022
+ type: "content",
1023
+ value: [{ type: "text", text: "cached weather receipt" }],
1024
+ });
1025
+ });
1026
+
1027
+ it("falls back to default model output when no custom projector is defined", async () => {
1028
+ const tools = await createWeatherTools();
1029
+
1030
+ const output = await tools.get_weather!.toModelOutput!({
1031
+ toolCallId: "tc-weather",
1032
+ input: {},
1033
+ output: { location: "San Francisco" },
1034
+ });
1035
+
1036
+ expect(output).toEqual({
1037
+ type: "json",
1038
+ value: { location: "San Francisco" },
1039
+ });
1040
+ });
1041
+
1042
+ it("uses stored model content envelopes when no custom projector is defined", async () => {
1043
+ const tools = await createWeatherTools();
1044
+
1045
+ const output = await tools.get_weather!.toModelOutput!({
1046
+ toolCallId: "tc-weather",
1047
+ input: {},
1048
+ output: wrapModelContentEnvelope({ location: "San Francisco" }, [
1049
+ { type: "text", text: "cached weather receipt" },
1050
+ ]),
1051
+ });
1052
+
1053
+ expect(output).toEqual({
1054
+ type: "content",
1055
+ value: [{ type: "text", text: "cached weather receipt" }],
1056
+ });
1057
+ });
1058
+ });