ai 6.0.33 → 6.0.34

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 (351) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/index.js +1 -1
  3. package/dist/index.mjs +1 -1
  4. package/dist/internal/index.js +1 -1
  5. package/dist/internal/index.mjs +1 -1
  6. package/docs/02-foundations/03-prompts.mdx +2 -2
  7. package/docs/03-ai-sdk-core/15-tools-and-tool-calling.mdx +1 -1
  8. package/docs/07-reference/01-ai-sdk-core/28-output.mdx +1 -1
  9. package/package.json +6 -4
  10. package/src/agent/agent.ts +116 -0
  11. package/src/agent/create-agent-ui-stream-response.test.ts +258 -0
  12. package/src/agent/create-agent-ui-stream-response.ts +50 -0
  13. package/src/agent/create-agent-ui-stream.ts +73 -0
  14. package/src/agent/index.ts +33 -0
  15. package/src/agent/infer-agent-tools.ts +7 -0
  16. package/src/agent/infer-agent-ui-message.test-d.ts +54 -0
  17. package/src/agent/infer-agent-ui-message.ts +11 -0
  18. package/src/agent/pipe-agent-ui-stream-to-response.ts +52 -0
  19. package/src/agent/tool-loop-agent-on-finish-callback.ts +31 -0
  20. package/src/agent/tool-loop-agent-on-step-finish-callback.ts +11 -0
  21. package/src/agent/tool-loop-agent-settings.ts +182 -0
  22. package/src/agent/tool-loop-agent.test-d.ts +114 -0
  23. package/src/agent/tool-loop-agent.test.ts +442 -0
  24. package/src/agent/tool-loop-agent.ts +114 -0
  25. package/src/embed/__snapshots__/embed-many.test.ts.snap +191 -0
  26. package/src/embed/__snapshots__/embed.test.ts.snap +81 -0
  27. package/src/embed/embed-many-result.ts +53 -0
  28. package/src/embed/embed-many.test.ts +653 -0
  29. package/src/embed/embed-many.ts +378 -0
  30. package/src/embed/embed-result.ts +50 -0
  31. package/src/embed/embed.test.ts +298 -0
  32. package/src/embed/embed.ts +211 -0
  33. package/src/embed/index.ts +4 -0
  34. package/src/error/index.ts +34 -0
  35. package/src/error/invalid-argument-error.ts +34 -0
  36. package/src/error/invalid-stream-part-error.ts +28 -0
  37. package/src/error/invalid-tool-approval-error.ts +26 -0
  38. package/src/error/invalid-tool-input-error.ts +33 -0
  39. package/src/error/no-image-generated-error.ts +39 -0
  40. package/src/error/no-object-generated-error.ts +70 -0
  41. package/src/error/no-output-generated-error.ts +26 -0
  42. package/src/error/no-speech-generated-error.ts +18 -0
  43. package/src/error/no-such-tool-error.ts +35 -0
  44. package/src/error/no-transcript-generated-error.ts +20 -0
  45. package/src/error/tool-call-not-found-for-approval-error.ts +32 -0
  46. package/src/error/tool-call-repair-error.ts +30 -0
  47. package/src/error/unsupported-model-version-error.ts +23 -0
  48. package/src/error/verify-no-object-generated-error.ts +27 -0
  49. package/src/generate-image/generate-image-result.ts +42 -0
  50. package/src/generate-image/generate-image.test.ts +1420 -0
  51. package/src/generate-image/generate-image.ts +360 -0
  52. package/src/generate-image/index.ts +18 -0
  53. package/src/generate-object/__snapshots__/generate-object.test.ts.snap +133 -0
  54. package/src/generate-object/__snapshots__/stream-object.test.ts.snap +297 -0
  55. package/src/generate-object/generate-object-result.ts +67 -0
  56. package/src/generate-object/generate-object.test-d.ts +49 -0
  57. package/src/generate-object/generate-object.test.ts +1191 -0
  58. package/src/generate-object/generate-object.ts +518 -0
  59. package/src/generate-object/index.ts +9 -0
  60. package/src/generate-object/inject-json-instruction.test.ts +181 -0
  61. package/src/generate-object/inject-json-instruction.ts +30 -0
  62. package/src/generate-object/output-strategy.ts +415 -0
  63. package/src/generate-object/parse-and-validate-object-result.ts +111 -0
  64. package/src/generate-object/repair-text.ts +12 -0
  65. package/src/generate-object/stream-object-result.ts +120 -0
  66. package/src/generate-object/stream-object.test-d.ts +74 -0
  67. package/src/generate-object/stream-object.test.ts +1950 -0
  68. package/src/generate-object/stream-object.ts +986 -0
  69. package/src/generate-object/validate-object-generation-input.ts +144 -0
  70. package/src/generate-speech/generate-speech-result.ts +30 -0
  71. package/src/generate-speech/generate-speech.test.ts +300 -0
  72. package/src/generate-speech/generate-speech.ts +190 -0
  73. package/src/generate-speech/generated-audio-file.ts +65 -0
  74. package/src/generate-speech/index.ts +3 -0
  75. package/src/generate-text/__snapshots__/generate-text.test.ts.snap +1872 -0
  76. package/src/generate-text/__snapshots__/stream-text.test.ts.snap +1255 -0
  77. package/src/generate-text/collect-tool-approvals.test.ts +553 -0
  78. package/src/generate-text/collect-tool-approvals.ts +116 -0
  79. package/src/generate-text/content-part.ts +25 -0
  80. package/src/generate-text/execute-tool-call.ts +129 -0
  81. package/src/generate-text/extract-reasoning-content.ts +17 -0
  82. package/src/generate-text/extract-text-content.ts +15 -0
  83. package/src/generate-text/generate-text-result.ts +168 -0
  84. package/src/generate-text/generate-text.test-d.ts +68 -0
  85. package/src/generate-text/generate-text.test.ts +7011 -0
  86. package/src/generate-text/generate-text.ts +1223 -0
  87. package/src/generate-text/generated-file.ts +70 -0
  88. package/src/generate-text/index.ts +57 -0
  89. package/src/generate-text/is-approval-needed.ts +29 -0
  90. package/src/generate-text/output-utils.ts +23 -0
  91. package/src/generate-text/output.test.ts +698 -0
  92. package/src/generate-text/output.ts +590 -0
  93. package/src/generate-text/parse-tool-call.test.ts +570 -0
  94. package/src/generate-text/parse-tool-call.ts +188 -0
  95. package/src/generate-text/prepare-step.ts +103 -0
  96. package/src/generate-text/prune-messages.test.ts +720 -0
  97. package/src/generate-text/prune-messages.ts +167 -0
  98. package/src/generate-text/reasoning-output.ts +20 -0
  99. package/src/generate-text/reasoning.ts +8 -0
  100. package/src/generate-text/response-message.ts +10 -0
  101. package/src/generate-text/run-tools-transformation.test.ts +1143 -0
  102. package/src/generate-text/run-tools-transformation.ts +420 -0
  103. package/src/generate-text/smooth-stream.test.ts +2101 -0
  104. package/src/generate-text/smooth-stream.ts +162 -0
  105. package/src/generate-text/step-result.ts +238 -0
  106. package/src/generate-text/stop-condition.ts +29 -0
  107. package/src/generate-text/stream-text-result.ts +463 -0
  108. package/src/generate-text/stream-text.test-d.ts +200 -0
  109. package/src/generate-text/stream-text.test.ts +19979 -0
  110. package/src/generate-text/stream-text.ts +2505 -0
  111. package/src/generate-text/to-response-messages.test.ts +922 -0
  112. package/src/generate-text/to-response-messages.ts +163 -0
  113. package/src/generate-text/tool-approval-request-output.ts +21 -0
  114. package/src/generate-text/tool-call-repair-function.ts +27 -0
  115. package/src/generate-text/tool-call.ts +47 -0
  116. package/src/generate-text/tool-error.ts +34 -0
  117. package/src/generate-text/tool-output-denied.ts +21 -0
  118. package/src/generate-text/tool-output.ts +7 -0
  119. package/src/generate-text/tool-result.ts +36 -0
  120. package/src/generate-text/tool-set.ts +14 -0
  121. package/src/global.ts +24 -0
  122. package/src/index.ts +50 -0
  123. package/src/logger/index.ts +6 -0
  124. package/src/logger/log-warnings.test.ts +351 -0
  125. package/src/logger/log-warnings.ts +119 -0
  126. package/src/middleware/__snapshots__/simulate-streaming-middleware.test.ts.snap +64 -0
  127. package/src/middleware/add-tool-input-examples-middleware.test.ts +476 -0
  128. package/src/middleware/add-tool-input-examples-middleware.ts +90 -0
  129. package/src/middleware/default-embedding-settings-middleware.test.ts +126 -0
  130. package/src/middleware/default-embedding-settings-middleware.ts +22 -0
  131. package/src/middleware/default-settings-middleware.test.ts +388 -0
  132. package/src/middleware/default-settings-middleware.ts +33 -0
  133. package/src/middleware/extract-json-middleware.test.ts +827 -0
  134. package/src/middleware/extract-json-middleware.ts +197 -0
  135. package/src/middleware/extract-reasoning-middleware.test.ts +1028 -0
  136. package/src/middleware/extract-reasoning-middleware.ts +238 -0
  137. package/src/middleware/index.ts +10 -0
  138. package/src/middleware/simulate-streaming-middleware.test.ts +911 -0
  139. package/src/middleware/simulate-streaming-middleware.ts +79 -0
  140. package/src/middleware/wrap-embedding-model.test.ts +358 -0
  141. package/src/middleware/wrap-embedding-model.ts +86 -0
  142. package/src/middleware/wrap-image-model.test.ts +423 -0
  143. package/src/middleware/wrap-image-model.ts +85 -0
  144. package/src/middleware/wrap-language-model.test.ts +518 -0
  145. package/src/middleware/wrap-language-model.ts +104 -0
  146. package/src/middleware/wrap-provider.test.ts +120 -0
  147. package/src/middleware/wrap-provider.ts +51 -0
  148. package/src/model/as-embedding-model-v3.test.ts +319 -0
  149. package/src/model/as-embedding-model-v3.ts +24 -0
  150. package/src/model/as-image-model-v3.test.ts +409 -0
  151. package/src/model/as-image-model-v3.ts +24 -0
  152. package/src/model/as-language-model-v3.test.ts +508 -0
  153. package/src/model/as-language-model-v3.ts +103 -0
  154. package/src/model/as-provider-v3.ts +36 -0
  155. package/src/model/as-speech-model-v3.test.ts +356 -0
  156. package/src/model/as-speech-model-v3.ts +24 -0
  157. package/src/model/as-transcription-model-v3.test.ts +529 -0
  158. package/src/model/as-transcription-model-v3.ts +24 -0
  159. package/src/model/resolve-model.test.ts +244 -0
  160. package/src/model/resolve-model.ts +126 -0
  161. package/src/prompt/call-settings.ts +148 -0
  162. package/src/prompt/content-part.ts +209 -0
  163. package/src/prompt/convert-to-language-model-prompt.test.ts +2018 -0
  164. package/src/prompt/convert-to-language-model-prompt.ts +442 -0
  165. package/src/prompt/create-tool-model-output.test.ts +508 -0
  166. package/src/prompt/create-tool-model-output.ts +34 -0
  167. package/src/prompt/data-content.test.ts +15 -0
  168. package/src/prompt/data-content.ts +134 -0
  169. package/src/prompt/index.ts +27 -0
  170. package/src/prompt/invalid-data-content-error.ts +29 -0
  171. package/src/prompt/invalid-message-role-error.ts +27 -0
  172. package/src/prompt/message-conversion-error.ts +28 -0
  173. package/src/prompt/message.ts +68 -0
  174. package/src/prompt/prepare-call-settings.test.ts +159 -0
  175. package/src/prompt/prepare-call-settings.ts +108 -0
  176. package/src/prompt/prepare-tools-and-tool-choice.test.ts +461 -0
  177. package/src/prompt/prepare-tools-and-tool-choice.ts +86 -0
  178. package/src/prompt/prompt.ts +43 -0
  179. package/src/prompt/split-data-url.ts +17 -0
  180. package/src/prompt/standardize-prompt.test.ts +82 -0
  181. package/src/prompt/standardize-prompt.ts +99 -0
  182. package/src/prompt/wrap-gateway-error.ts +29 -0
  183. package/src/registry/custom-provider.test.ts +211 -0
  184. package/src/registry/custom-provider.ts +155 -0
  185. package/src/registry/index.ts +7 -0
  186. package/src/registry/no-such-provider-error.ts +41 -0
  187. package/src/registry/provider-registry.test.ts +691 -0
  188. package/src/registry/provider-registry.ts +328 -0
  189. package/src/rerank/index.ts +2 -0
  190. package/src/rerank/rerank-result.ts +70 -0
  191. package/src/rerank/rerank.test.ts +516 -0
  192. package/src/rerank/rerank.ts +237 -0
  193. package/src/telemetry/assemble-operation-name.ts +21 -0
  194. package/src/telemetry/get-base-telemetry-attributes.ts +53 -0
  195. package/src/telemetry/get-tracer.ts +20 -0
  196. package/src/telemetry/noop-tracer.ts +69 -0
  197. package/src/telemetry/record-span.ts +63 -0
  198. package/src/telemetry/select-telemetry-attributes.ts +78 -0
  199. package/src/telemetry/select-temetry-attributes.test.ts +114 -0
  200. package/src/telemetry/stringify-for-telemetry.test.ts +114 -0
  201. package/src/telemetry/stringify-for-telemetry.ts +33 -0
  202. package/src/telemetry/telemetry-settings.ts +44 -0
  203. package/src/test/mock-embedding-model-v2.ts +35 -0
  204. package/src/test/mock-embedding-model-v3.ts +48 -0
  205. package/src/test/mock-image-model-v2.ts +28 -0
  206. package/src/test/mock-image-model-v3.ts +28 -0
  207. package/src/test/mock-language-model-v2.ts +72 -0
  208. package/src/test/mock-language-model-v3.ts +77 -0
  209. package/src/test/mock-provider-v2.ts +68 -0
  210. package/src/test/mock-provider-v3.ts +80 -0
  211. package/src/test/mock-reranking-model-v3.ts +25 -0
  212. package/src/test/mock-server-response.ts +69 -0
  213. package/src/test/mock-speech-model-v2.ts +24 -0
  214. package/src/test/mock-speech-model-v3.ts +24 -0
  215. package/src/test/mock-tracer.ts +156 -0
  216. package/src/test/mock-transcription-model-v2.ts +24 -0
  217. package/src/test/mock-transcription-model-v3.ts +24 -0
  218. package/src/test/mock-values.ts +4 -0
  219. package/src/test/not-implemented.ts +3 -0
  220. package/src/text-stream/create-text-stream-response.test.ts +38 -0
  221. package/src/text-stream/create-text-stream-response.ts +18 -0
  222. package/src/text-stream/index.ts +2 -0
  223. package/src/text-stream/pipe-text-stream-to-response.test.ts +38 -0
  224. package/src/text-stream/pipe-text-stream-to-response.ts +26 -0
  225. package/src/transcribe/index.ts +2 -0
  226. package/src/transcribe/transcribe-result.ts +60 -0
  227. package/src/transcribe/transcribe.test.ts +313 -0
  228. package/src/transcribe/transcribe.ts +173 -0
  229. package/src/types/embedding-model-middleware.ts +3 -0
  230. package/src/types/embedding-model.ts +18 -0
  231. package/src/types/image-model-middleware.ts +3 -0
  232. package/src/types/image-model-response-metadata.ts +16 -0
  233. package/src/types/image-model.ts +19 -0
  234. package/src/types/index.ts +29 -0
  235. package/src/types/json-value.ts +15 -0
  236. package/src/types/language-model-middleware.ts +3 -0
  237. package/src/types/language-model-request-metadata.ts +6 -0
  238. package/src/types/language-model-response-metadata.ts +21 -0
  239. package/src/types/language-model.ts +104 -0
  240. package/src/types/provider-metadata.ts +16 -0
  241. package/src/types/provider.ts +55 -0
  242. package/src/types/reranking-model.ts +6 -0
  243. package/src/types/speech-model-response-metadata.ts +21 -0
  244. package/src/types/speech-model.ts +6 -0
  245. package/src/types/transcription-model-response-metadata.ts +16 -0
  246. package/src/types/transcription-model.ts +9 -0
  247. package/src/types/usage.ts +200 -0
  248. package/src/types/warning.ts +7 -0
  249. package/src/ui/__snapshots__/append-response-messages.test.ts.snap +416 -0
  250. package/src/ui/__snapshots__/convert-to-model-messages.test.ts.snap +419 -0
  251. package/src/ui/__snapshots__/process-chat-text-response.test.ts.snap +142 -0
  252. package/src/ui/call-completion-api.ts +157 -0
  253. package/src/ui/chat-transport.ts +83 -0
  254. package/src/ui/chat.test-d.ts +233 -0
  255. package/src/ui/chat.test.ts +2695 -0
  256. package/src/ui/chat.ts +716 -0
  257. package/src/ui/convert-file-list-to-file-ui-parts.ts +36 -0
  258. package/src/ui/convert-to-model-messages.test.ts +2775 -0
  259. package/src/ui/convert-to-model-messages.ts +373 -0
  260. package/src/ui/default-chat-transport.ts +36 -0
  261. package/src/ui/direct-chat-transport.test.ts +446 -0
  262. package/src/ui/direct-chat-transport.ts +118 -0
  263. package/src/ui/http-chat-transport.test.ts +185 -0
  264. package/src/ui/http-chat-transport.ts +292 -0
  265. package/src/ui/index.ts +71 -0
  266. package/src/ui/last-assistant-message-is-complete-with-approval-responses.ts +44 -0
  267. package/src/ui/last-assistant-message-is-complete-with-tool-calls.test.ts +371 -0
  268. package/src/ui/last-assistant-message-is-complete-with-tool-calls.ts +39 -0
  269. package/src/ui/process-text-stream.test.ts +38 -0
  270. package/src/ui/process-text-stream.ts +16 -0
  271. package/src/ui/process-ui-message-stream.test.ts +8052 -0
  272. package/src/ui/process-ui-message-stream.ts +713 -0
  273. package/src/ui/text-stream-chat-transport.ts +23 -0
  274. package/src/ui/transform-text-to-ui-message-stream.test.ts +124 -0
  275. package/src/ui/transform-text-to-ui-message-stream.ts +27 -0
  276. package/src/ui/ui-messages.test.ts +48 -0
  277. package/src/ui/ui-messages.ts +534 -0
  278. package/src/ui/use-completion.ts +84 -0
  279. package/src/ui/validate-ui-messages.test.ts +1428 -0
  280. package/src/ui/validate-ui-messages.ts +476 -0
  281. package/src/ui-message-stream/create-ui-message-stream-response.test.ts +266 -0
  282. package/src/ui-message-stream/create-ui-message-stream-response.ts +32 -0
  283. package/src/ui-message-stream/create-ui-message-stream.test.ts +639 -0
  284. package/src/ui-message-stream/create-ui-message-stream.ts +124 -0
  285. package/src/ui-message-stream/get-response-ui-message-id.test.ts +55 -0
  286. package/src/ui-message-stream/get-response-ui-message-id.ts +24 -0
  287. package/src/ui-message-stream/handle-ui-message-stream-finish.test.ts +429 -0
  288. package/src/ui-message-stream/handle-ui-message-stream-finish.ts +135 -0
  289. package/src/ui-message-stream/index.ts +13 -0
  290. package/src/ui-message-stream/json-to-sse-transform-stream.ts +12 -0
  291. package/src/ui-message-stream/pipe-ui-message-stream-to-response.test.ts +90 -0
  292. package/src/ui-message-stream/pipe-ui-message-stream-to-response.ts +40 -0
  293. package/src/ui-message-stream/read-ui-message-stream.test.ts +122 -0
  294. package/src/ui-message-stream/read-ui-message-stream.ts +87 -0
  295. package/src/ui-message-stream/ui-message-chunks.test-d.ts +18 -0
  296. package/src/ui-message-stream/ui-message-chunks.ts +344 -0
  297. package/src/ui-message-stream/ui-message-stream-headers.ts +7 -0
  298. package/src/ui-message-stream/ui-message-stream-on-finish-callback.ts +32 -0
  299. package/src/ui-message-stream/ui-message-stream-response-init.ts +5 -0
  300. package/src/ui-message-stream/ui-message-stream-writer.ts +24 -0
  301. package/src/util/as-array.ts +3 -0
  302. package/src/util/async-iterable-stream.test.ts +241 -0
  303. package/src/util/async-iterable-stream.ts +94 -0
  304. package/src/util/consume-stream.ts +29 -0
  305. package/src/util/cosine-similarity.test.ts +57 -0
  306. package/src/util/cosine-similarity.ts +47 -0
  307. package/src/util/create-resolvable-promise.ts +30 -0
  308. package/src/util/create-stitchable-stream.test.ts +239 -0
  309. package/src/util/create-stitchable-stream.ts +112 -0
  310. package/src/util/data-url.ts +17 -0
  311. package/src/util/deep-partial.ts +84 -0
  312. package/src/util/detect-media-type.test.ts +670 -0
  313. package/src/util/detect-media-type.ts +184 -0
  314. package/src/util/download/download-function.ts +45 -0
  315. package/src/util/download/download.test.ts +69 -0
  316. package/src/util/download/download.ts +46 -0
  317. package/src/util/error-handler.ts +1 -0
  318. package/src/util/fix-json.test.ts +279 -0
  319. package/src/util/fix-json.ts +401 -0
  320. package/src/util/get-potential-start-index.test.ts +34 -0
  321. package/src/util/get-potential-start-index.ts +30 -0
  322. package/src/util/index.ts +11 -0
  323. package/src/util/is-deep-equal-data.test.ts +119 -0
  324. package/src/util/is-deep-equal-data.ts +48 -0
  325. package/src/util/is-non-empty-object.ts +5 -0
  326. package/src/util/job.ts +1 -0
  327. package/src/util/log-v2-compatibility-warning.ts +21 -0
  328. package/src/util/merge-abort-signals.test.ts +155 -0
  329. package/src/util/merge-abort-signals.ts +43 -0
  330. package/src/util/merge-objects.test.ts +118 -0
  331. package/src/util/merge-objects.ts +79 -0
  332. package/src/util/now.ts +4 -0
  333. package/src/util/parse-partial-json.test.ts +80 -0
  334. package/src/util/parse-partial-json.ts +30 -0
  335. package/src/util/prepare-headers.test.ts +51 -0
  336. package/src/util/prepare-headers.ts +14 -0
  337. package/src/util/prepare-retries.test.ts +10 -0
  338. package/src/util/prepare-retries.ts +47 -0
  339. package/src/util/retry-error.ts +41 -0
  340. package/src/util/retry-with-exponential-backoff.test.ts +446 -0
  341. package/src/util/retry-with-exponential-backoff.ts +154 -0
  342. package/src/util/serial-job-executor.test.ts +162 -0
  343. package/src/util/serial-job-executor.ts +36 -0
  344. package/src/util/simulate-readable-stream.test.ts +98 -0
  345. package/src/util/simulate-readable-stream.ts +39 -0
  346. package/src/util/split-array.test.ts +60 -0
  347. package/src/util/split-array.ts +20 -0
  348. package/src/util/value-of.ts +65 -0
  349. package/src/util/write-to-server-response.test.ts +266 -0
  350. package/src/util/write-to-server-response.ts +49 -0
  351. package/src/version.ts +5 -0
@@ -0,0 +1,2775 @@
1
+ import { ModelMessage } from '@ai-sdk/provider-utils';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { convertToModelMessages } from './convert-to-model-messages';
4
+ import { UIMessage } from './ui-messages';
5
+
6
+ describe('convertToModelMessages', () => {
7
+ describe('system message', () => {
8
+ it('should convert a simple system message', async () => {
9
+ const result = await convertToModelMessages([
10
+ {
11
+ role: 'system',
12
+ parts: [{ text: 'System message', type: 'text' }],
13
+ },
14
+ ]);
15
+
16
+ expect(result).toEqual([{ role: 'system', content: 'System message' }]);
17
+ });
18
+
19
+ it('should convert a system message with provider metadata', async () => {
20
+ const result = await convertToModelMessages([
21
+ {
22
+ role: 'system',
23
+ parts: [
24
+ {
25
+ text: 'System message with metadata',
26
+ type: 'text',
27
+ providerMetadata: { testProvider: { systemSignature: 'abc123' } },
28
+ },
29
+ ],
30
+ },
31
+ ]);
32
+
33
+ expect(result).toEqual([
34
+ {
35
+ role: 'system',
36
+ content: 'System message with metadata',
37
+ providerOptions: { testProvider: { systemSignature: 'abc123' } },
38
+ },
39
+ ]);
40
+ });
41
+
42
+ it('should merge provider metadata from multiple text parts in system message', async () => {
43
+ const result = await convertToModelMessages([
44
+ {
45
+ role: 'system',
46
+ parts: [
47
+ {
48
+ text: 'Part 1',
49
+ type: 'text',
50
+ providerMetadata: { provider1: { key1: 'value1' } },
51
+ },
52
+ {
53
+ text: ' Part 2',
54
+ type: 'text',
55
+ providerMetadata: { provider2: { key2: 'value2' } },
56
+ },
57
+ ],
58
+ },
59
+ ]);
60
+
61
+ expect(result).toEqual([
62
+ {
63
+ role: 'system',
64
+ content: 'Part 1 Part 2',
65
+ providerOptions: {
66
+ provider1: { key1: 'value1' },
67
+ provider2: { key2: 'value2' },
68
+ },
69
+ },
70
+ ]);
71
+ });
72
+
73
+ it('should convert a system message with Anthropic cache control metadata', async () => {
74
+ const SYSTEM_PROMPT = 'You are a helpful assistant.';
75
+
76
+ const systemMessage = {
77
+ id: 'system',
78
+ role: 'system' as const,
79
+ parts: [
80
+ {
81
+ type: 'text' as const,
82
+ text: SYSTEM_PROMPT,
83
+ providerMetadata: {
84
+ anthropic: {
85
+ cacheControl: { type: 'ephemeral' },
86
+ },
87
+ },
88
+ },
89
+ ],
90
+ };
91
+
92
+ const result = await convertToModelMessages([systemMessage]);
93
+
94
+ expect(result).toEqual([
95
+ {
96
+ role: 'system',
97
+ content: SYSTEM_PROMPT,
98
+ providerOptions: {
99
+ anthropic: {
100
+ cacheControl: { type: 'ephemeral' },
101
+ },
102
+ },
103
+ },
104
+ ]);
105
+ });
106
+ });
107
+
108
+ describe('user message', () => {
109
+ it('should convert a simple user message', async () => {
110
+ const result = await convertToModelMessages([
111
+ {
112
+ role: 'user',
113
+ parts: [{ text: 'Hello, AI!', type: 'text' }],
114
+ },
115
+ ]);
116
+
117
+ expect(result).toMatchInlineSnapshot(`
118
+ [
119
+ {
120
+ "content": [
121
+ {
122
+ "text": "Hello, AI!",
123
+ "type": "text",
124
+ },
125
+ ],
126
+ "role": "user",
127
+ },
128
+ ]
129
+ `);
130
+ });
131
+
132
+ it('should convert a simple user message with provider metadata', async () => {
133
+ const result = await convertToModelMessages([
134
+ {
135
+ role: 'user',
136
+ parts: [
137
+ {
138
+ text: 'Hello, AI!',
139
+ type: 'text',
140
+ providerMetadata: { testProvider: { signature: '1234567890' } },
141
+ },
142
+ ],
143
+ },
144
+ ]);
145
+
146
+ expect(result).toMatchInlineSnapshot(`
147
+ [
148
+ {
149
+ "content": [
150
+ {
151
+ "providerOptions": {
152
+ "testProvider": {
153
+ "signature": "1234567890",
154
+ },
155
+ },
156
+ "text": "Hello, AI!",
157
+ "type": "text",
158
+ },
159
+ ],
160
+ "role": "user",
161
+ },
162
+ ]
163
+ `);
164
+ });
165
+
166
+ it('should handle user message file parts', async () => {
167
+ const result = await convertToModelMessages([
168
+ {
169
+ role: 'user',
170
+ parts: [
171
+ {
172
+ type: 'file',
173
+ mediaType: 'image/jpeg',
174
+ url: 'https://example.com/image.jpg',
175
+ },
176
+ { type: 'text', text: 'Check this image' },
177
+ ],
178
+ },
179
+ ]);
180
+
181
+ expect(result).toEqual([
182
+ {
183
+ role: 'user',
184
+ content: [
185
+ {
186
+ type: 'file',
187
+ mediaType: 'image/jpeg',
188
+ data: 'https://example.com/image.jpg',
189
+ },
190
+ { type: 'text', text: 'Check this image' },
191
+ ],
192
+ },
193
+ ]);
194
+ });
195
+
196
+ it('should handle user message file parts with provider metadata', async () => {
197
+ const result = await convertToModelMessages([
198
+ {
199
+ role: 'user',
200
+ parts: [
201
+ {
202
+ type: 'file',
203
+ mediaType: 'image/jpeg',
204
+ url: 'https://example.com/image.jpg',
205
+ providerMetadata: { testProvider: { signature: '1234567890' } },
206
+ },
207
+ { type: 'text', text: 'Check this image' },
208
+ ],
209
+ },
210
+ ]);
211
+
212
+ expect(result).toEqual([
213
+ {
214
+ role: 'user',
215
+ content: [
216
+ {
217
+ type: 'file',
218
+ mediaType: 'image/jpeg',
219
+ data: 'https://example.com/image.jpg',
220
+ providerOptions: { testProvider: { signature: '1234567890' } },
221
+ },
222
+ { type: 'text', text: 'Check this image' },
223
+ ],
224
+ },
225
+ ]);
226
+ });
227
+
228
+ it('should include filename for user file parts when provided', async () => {
229
+ const result = await convertToModelMessages([
230
+ {
231
+ role: 'user',
232
+ parts: [
233
+ {
234
+ type: 'file',
235
+ mediaType: 'image/jpeg',
236
+ url: 'https://example.com/image.jpg',
237
+ filename: 'image.jpg',
238
+ },
239
+ ],
240
+ },
241
+ ]);
242
+
243
+ expect(result).toEqual([
244
+ {
245
+ role: 'user',
246
+ content: [
247
+ {
248
+ type: 'file',
249
+ mediaType: 'image/jpeg',
250
+ data: 'https://example.com/image.jpg',
251
+ filename: 'image.jpg',
252
+ },
253
+ ],
254
+ },
255
+ ]);
256
+ });
257
+ });
258
+
259
+ it('should not include filename for user file parts when not provided', async () => {
260
+ const result = await convertToModelMessages([
261
+ {
262
+ role: 'user',
263
+ parts: [
264
+ {
265
+ type: 'file',
266
+ mediaType: 'image/jpeg',
267
+ url: 'https://example.com/image.jpg',
268
+ },
269
+ ],
270
+ },
271
+ ]);
272
+
273
+ expect(result).toEqual([
274
+ {
275
+ role: 'user',
276
+ content: [
277
+ {
278
+ type: 'file',
279
+ mediaType: 'image/jpeg',
280
+ data: 'https://example.com/image.jpg',
281
+ },
282
+ ],
283
+ },
284
+ ]);
285
+ });
286
+
287
+ describe('assistant message', () => {
288
+ it('should convert a simple assistant text message', async () => {
289
+ const result = await convertToModelMessages([
290
+ {
291
+ role: 'assistant',
292
+ parts: [{ type: 'text', text: 'Hello, human!', state: 'done' }],
293
+ },
294
+ ]);
295
+
296
+ expect(result).toEqual([
297
+ {
298
+ role: 'assistant',
299
+ content: [{ type: 'text', text: 'Hello, human!' }],
300
+ },
301
+ ]);
302
+ });
303
+
304
+ it('should convert a simple assistant text message with provider metadata', async () => {
305
+ const result = await convertToModelMessages([
306
+ {
307
+ role: 'assistant',
308
+ parts: [
309
+ {
310
+ type: 'text',
311
+ text: 'Hello, human!',
312
+ state: 'done',
313
+ providerMetadata: { testProvider: { signature: '1234567890' } },
314
+ },
315
+ ],
316
+ },
317
+ ]);
318
+
319
+ expect(result).toMatchInlineSnapshot(`
320
+ [
321
+ {
322
+ "content": [
323
+ {
324
+ "providerOptions": {
325
+ "testProvider": {
326
+ "signature": "1234567890",
327
+ },
328
+ },
329
+ "text": "Hello, human!",
330
+ "type": "text",
331
+ },
332
+ ],
333
+ "role": "assistant",
334
+ },
335
+ ]
336
+ `);
337
+ });
338
+
339
+ it('should convert an assistant message with reasoning', async () => {
340
+ const result = await convertToModelMessages([
341
+ {
342
+ role: 'assistant',
343
+ parts: [
344
+ {
345
+ type: 'reasoning',
346
+ text: 'Thinking...',
347
+ providerMetadata: {
348
+ testProvider: {
349
+ signature: '1234567890',
350
+ },
351
+ },
352
+ state: 'done',
353
+ },
354
+ {
355
+ type: 'reasoning',
356
+ text: 'redacted-data',
357
+ providerMetadata: {
358
+ testProvider: { isRedacted: true },
359
+ },
360
+ state: 'done',
361
+ },
362
+ { type: 'text', text: 'Hello, human!', state: 'done' },
363
+ ],
364
+ },
365
+ ]);
366
+
367
+ expect(result).toEqual([
368
+ {
369
+ role: 'assistant',
370
+ content: [
371
+ {
372
+ type: 'reasoning',
373
+ text: 'Thinking...',
374
+ providerOptions: { testProvider: { signature: '1234567890' } },
375
+ },
376
+ {
377
+ type: 'reasoning',
378
+ text: 'redacted-data',
379
+ providerOptions: { testProvider: { isRedacted: true } },
380
+ },
381
+ { type: 'text', text: 'Hello, human!' },
382
+ ],
383
+ },
384
+ ] satisfies ModelMessage[]);
385
+ });
386
+
387
+ it('should convert an assistant message with file parts', async () => {
388
+ const result = await convertToModelMessages([
389
+ {
390
+ role: 'assistant',
391
+ parts: [
392
+ {
393
+ type: 'file',
394
+ mediaType: 'image/png',
395
+ url: 'data:image/png;base64,dGVzdA==',
396
+ },
397
+ ],
398
+ },
399
+ ]);
400
+
401
+ expect(result).toEqual([
402
+ {
403
+ role: 'assistant',
404
+ content: [
405
+ {
406
+ type: 'file',
407
+ mediaType: 'image/png',
408
+ data: 'data:image/png;base64,dGVzdA==',
409
+ },
410
+ ],
411
+ },
412
+ ] satisfies ModelMessage[]);
413
+ });
414
+
415
+ it('should include filename for assistant file parts when provided', async () => {
416
+ const result = await convertToModelMessages([
417
+ {
418
+ role: 'assistant',
419
+ parts: [
420
+ {
421
+ type: 'file',
422
+ mediaType: 'image/png',
423
+ url: 'data:image/png;base64,dGVzdA==',
424
+ filename: 'test.png',
425
+ },
426
+ ],
427
+ },
428
+ ]);
429
+
430
+ expect(result).toEqual([
431
+ {
432
+ role: 'assistant',
433
+ content: [
434
+ {
435
+ type: 'file',
436
+ mediaType: 'image/png',
437
+ data: 'data:image/png;base64,dGVzdA==',
438
+ filename: 'test.png',
439
+ },
440
+ ],
441
+ },
442
+ ] as unknown as ModelMessage[]);
443
+ });
444
+
445
+ it('should handle assistant message with tool output available', async () => {
446
+ const result = await convertToModelMessages([
447
+ {
448
+ role: 'assistant',
449
+ parts: [
450
+ { type: 'step-start' },
451
+ {
452
+ type: 'text',
453
+ text: 'Let me calculate that for you.',
454
+ state: 'done',
455
+ },
456
+ {
457
+ type: 'tool-calculator',
458
+ state: 'output-available',
459
+ toolCallId: 'call1',
460
+ input: { operation: 'add', numbers: [1, 2] },
461
+ output: '3',
462
+ callProviderMetadata: {
463
+ testProvider: {
464
+ signature: '1234567890',
465
+ },
466
+ },
467
+ },
468
+ ],
469
+ },
470
+ ]);
471
+
472
+ expect(result).toMatchInlineSnapshot(`
473
+ [
474
+ {
475
+ "content": [
476
+ {
477
+ "text": "Let me calculate that for you.",
478
+ "type": "text",
479
+ },
480
+ {
481
+ "input": {
482
+ "numbers": [
483
+ 1,
484
+ 2,
485
+ ],
486
+ "operation": "add",
487
+ },
488
+ "providerExecuted": undefined,
489
+ "providerOptions": {
490
+ "testProvider": {
491
+ "signature": "1234567890",
492
+ },
493
+ },
494
+ "toolCallId": "call1",
495
+ "toolName": "calculator",
496
+ "type": "tool-call",
497
+ },
498
+ ],
499
+ "role": "assistant",
500
+ },
501
+ {
502
+ "content": [
503
+ {
504
+ "output": {
505
+ "type": "text",
506
+ "value": "3",
507
+ },
508
+ "providerOptions": {
509
+ "testProvider": {
510
+ "signature": "1234567890",
511
+ },
512
+ },
513
+ "toolCallId": "call1",
514
+ "toolName": "calculator",
515
+ "type": "tool-result",
516
+ },
517
+ ],
518
+ "role": "tool",
519
+ },
520
+ ]
521
+ `);
522
+ });
523
+
524
+ describe('tool output error', () => {
525
+ it('should handle assistant message with tool output error that has raw input', async () => {
526
+ const result = await convertToModelMessages([
527
+ {
528
+ role: 'assistant',
529
+ parts: [
530
+ { type: 'step-start' },
531
+ {
532
+ type: 'text',
533
+ text: 'Let me calculate that for you.',
534
+ state: 'done',
535
+ },
536
+ {
537
+ type: 'tool-calculator',
538
+ state: 'output-error',
539
+ toolCallId: 'call1',
540
+ errorText: 'Error: Invalid input',
541
+ input: undefined,
542
+ rawInput: { operation: 'add', numbers: [1, 2] },
543
+ },
544
+ ],
545
+ },
546
+ ]);
547
+
548
+ expect(result).toMatchInlineSnapshot(`
549
+ [
550
+ {
551
+ "content": [
552
+ {
553
+ "text": "Let me calculate that for you.",
554
+ "type": "text",
555
+ },
556
+ {
557
+ "input": {
558
+ "numbers": [
559
+ 1,
560
+ 2,
561
+ ],
562
+ "operation": "add",
563
+ },
564
+ "providerExecuted": undefined,
565
+ "toolCallId": "call1",
566
+ "toolName": "calculator",
567
+ "type": "tool-call",
568
+ },
569
+ ],
570
+ "role": "assistant",
571
+ },
572
+ {
573
+ "content": [
574
+ {
575
+ "output": {
576
+ "type": "error-text",
577
+ "value": "Error: Invalid input",
578
+ },
579
+ "toolCallId": "call1",
580
+ "toolName": "calculator",
581
+ "type": "tool-result",
582
+ },
583
+ ],
584
+ "role": "tool",
585
+ },
586
+ ]
587
+ `);
588
+ });
589
+
590
+ it('should handle assistant message with tool output error that has no raw input', async () => {
591
+ const result = await convertToModelMessages([
592
+ {
593
+ role: 'assistant',
594
+ parts: [
595
+ { type: 'step-start' },
596
+ {
597
+ type: 'text',
598
+ text: 'Let me calculate that for you.',
599
+ state: 'done',
600
+ },
601
+ {
602
+ type: 'tool-calculator',
603
+ state: 'output-error',
604
+ toolCallId: 'call1',
605
+ input: { operation: 'add', numbers: [1, 2] },
606
+ errorText: 'Error: Invalid input',
607
+ },
608
+ ],
609
+ },
610
+ ]);
611
+
612
+ expect(result).toMatchInlineSnapshot(`
613
+ [
614
+ {
615
+ "content": [
616
+ {
617
+ "text": "Let me calculate that for you.",
618
+ "type": "text",
619
+ },
620
+ {
621
+ "input": {
622
+ "numbers": [
623
+ 1,
624
+ 2,
625
+ ],
626
+ "operation": "add",
627
+ },
628
+ "providerExecuted": undefined,
629
+ "toolCallId": "call1",
630
+ "toolName": "calculator",
631
+ "type": "tool-call",
632
+ },
633
+ ],
634
+ "role": "assistant",
635
+ },
636
+ {
637
+ "content": [
638
+ {
639
+ "output": {
640
+ "type": "error-text",
641
+ "value": "Error: Invalid input",
642
+ },
643
+ "toolCallId": "call1",
644
+ "toolName": "calculator",
645
+ "type": "tool-result",
646
+ },
647
+ ],
648
+ "role": "tool",
649
+ },
650
+ ]
651
+ `);
652
+ });
653
+ });
654
+
655
+ it('should handle assistant message with provider-executed tool output available', async () => {
656
+ const result = await convertToModelMessages([
657
+ {
658
+ role: 'assistant',
659
+ parts: [
660
+ { type: 'step-start' },
661
+ {
662
+ type: 'text',
663
+ text: 'Let me calculate that for you.',
664
+ state: 'done',
665
+ },
666
+ {
667
+ type: 'tool-calculator',
668
+ state: 'output-available',
669
+ toolCallId: 'call1',
670
+ input: { operation: 'add', numbers: [1, 2] },
671
+ output: '3',
672
+ providerExecuted: true,
673
+ },
674
+ ],
675
+ },
676
+ ]);
677
+
678
+ expect(result).toMatchInlineSnapshot(`
679
+ [
680
+ {
681
+ "content": [
682
+ {
683
+ "text": "Let me calculate that for you.",
684
+ "type": "text",
685
+ },
686
+ {
687
+ "input": {
688
+ "numbers": [
689
+ 1,
690
+ 2,
691
+ ],
692
+ "operation": "add",
693
+ },
694
+ "providerExecuted": true,
695
+ "toolCallId": "call1",
696
+ "toolName": "calculator",
697
+ "type": "tool-call",
698
+ },
699
+ {
700
+ "output": {
701
+ "type": "text",
702
+ "value": "3",
703
+ },
704
+ "toolCallId": "call1",
705
+ "toolName": "calculator",
706
+ "type": "tool-result",
707
+ },
708
+ ],
709
+ "role": "assistant",
710
+ },
711
+ ]
712
+ `);
713
+ });
714
+
715
+ it('should handle assistant message with provider-executed tool output error', async () => {
716
+ const result = await convertToModelMessages([
717
+ {
718
+ role: 'assistant',
719
+ parts: [
720
+ { type: 'step-start' },
721
+ {
722
+ type: 'text',
723
+ text: 'Let me calculate that for you.',
724
+ state: 'done',
725
+ },
726
+ {
727
+ type: 'tool-calculator',
728
+ state: 'output-error',
729
+ toolCallId: 'call1',
730
+ input: { operation: 'add', numbers: [1, 2] },
731
+ errorText: 'Error: Invalid input',
732
+ providerExecuted: true,
733
+ },
734
+ ],
735
+ },
736
+ ]);
737
+
738
+ expect(result).toMatchInlineSnapshot(`
739
+ [
740
+ {
741
+ "content": [
742
+ {
743
+ "text": "Let me calculate that for you.",
744
+ "type": "text",
745
+ },
746
+ {
747
+ "input": {
748
+ "numbers": [
749
+ 1,
750
+ 2,
751
+ ],
752
+ "operation": "add",
753
+ },
754
+ "providerExecuted": true,
755
+ "toolCallId": "call1",
756
+ "toolName": "calculator",
757
+ "type": "tool-call",
758
+ },
759
+ {
760
+ "output": {
761
+ "type": "error-json",
762
+ "value": "Error: Invalid input",
763
+ },
764
+ "toolCallId": "call1",
765
+ "toolName": "calculator",
766
+ "type": "tool-result",
767
+ },
768
+ ],
769
+ "role": "assistant",
770
+ },
771
+ ]
772
+ `);
773
+ });
774
+
775
+ it('should propagate provider metadata to provider-executed tool-result', async () => {
776
+ const result = await convertToModelMessages([
777
+ {
778
+ role: 'assistant',
779
+ parts: [
780
+ { type: 'step-start' },
781
+ {
782
+ type: 'tool-calculator',
783
+ state: 'output-available',
784
+ toolCallId: 'call1',
785
+ input: { operation: 'multiply', numbers: [3, 4] },
786
+ output: '12',
787
+ providerExecuted: true,
788
+ callProviderMetadata: {
789
+ testProvider: {
790
+ executionTime: 75,
791
+ },
792
+ },
793
+ },
794
+ ],
795
+ },
796
+ ]);
797
+
798
+ expect(result).toMatchInlineSnapshot(`
799
+ [
800
+ {
801
+ "content": [
802
+ {
803
+ "input": {
804
+ "numbers": [
805
+ 3,
806
+ 4,
807
+ ],
808
+ "operation": "multiply",
809
+ },
810
+ "providerExecuted": true,
811
+ "providerOptions": {
812
+ "testProvider": {
813
+ "executionTime": 75,
814
+ },
815
+ },
816
+ "toolCallId": "call1",
817
+ "toolName": "calculator",
818
+ "type": "tool-call",
819
+ },
820
+ {
821
+ "output": {
822
+ "type": "text",
823
+ "value": "12",
824
+ },
825
+ "providerOptions": {
826
+ "testProvider": {
827
+ "executionTime": 75,
828
+ },
829
+ },
830
+ "toolCallId": "call1",
831
+ "toolName": "calculator",
832
+ "type": "tool-result",
833
+ },
834
+ ],
835
+ "role": "assistant",
836
+ },
837
+ ]
838
+ `);
839
+ });
840
+
841
+ it('should handle assistant message with tool invocations that have multi-part responses', async () => {
842
+ const result = await convertToModelMessages([
843
+ {
844
+ role: 'assistant',
845
+ parts: [
846
+ { type: 'step-start' },
847
+ {
848
+ type: 'text',
849
+ text: 'Let me calculate that for you.',
850
+ state: 'done',
851
+ },
852
+ {
853
+ type: 'tool-screenshot',
854
+ state: 'output-available',
855
+ toolCallId: 'call1',
856
+ input: {},
857
+ output: 'imgbase64',
858
+ },
859
+ ],
860
+ },
861
+ ]);
862
+
863
+ expect(result).toMatchSnapshot();
864
+ });
865
+
866
+ it('should handle conversation with an assistant message that has empty tool invocations', async () => {
867
+ const result = await convertToModelMessages([
868
+ {
869
+ role: 'user',
870
+ parts: [{ type: 'text', text: 'text1' }],
871
+ },
872
+ {
873
+ role: 'assistant',
874
+ parts: [{ type: 'text', text: 'text2', state: 'done' }],
875
+ },
876
+ ]);
877
+
878
+ expect(result).toMatchSnapshot();
879
+ });
880
+
881
+ it('should handle conversation with multiple tool invocations that have step information', async () => {
882
+ const result = await convertToModelMessages([
883
+ {
884
+ role: 'assistant',
885
+ parts: [
886
+ { type: 'step-start' },
887
+ { type: 'text', text: 'response', state: 'done' },
888
+ {
889
+ type: 'tool-screenshot',
890
+ state: 'output-available',
891
+ toolCallId: 'call-1',
892
+ input: { value: 'value-1' },
893
+ output: 'result-1',
894
+ },
895
+ { type: 'step-start' },
896
+ {
897
+ type: 'tool-screenshot',
898
+ state: 'output-available',
899
+ toolCallId: 'call-2',
900
+ input: { value: 'value-2' },
901
+ output: 'result-2',
902
+ },
903
+ {
904
+ type: 'tool-screenshot',
905
+ state: 'output-available',
906
+ toolCallId: 'call-3',
907
+ input: { value: 'value-3' },
908
+ output: 'result-3',
909
+ },
910
+ { type: 'step-start' },
911
+ {
912
+ type: 'tool-screenshot',
913
+ state: 'output-available',
914
+ toolCallId: 'call-4',
915
+ input: { value: 'value-4' },
916
+ output: 'result-4',
917
+ },
918
+ ],
919
+ },
920
+ ]);
921
+
922
+ expect(result).toMatchSnapshot();
923
+ });
924
+
925
+ it('should handle conversation with mix of tool invocations and text', async () => {
926
+ const result = await convertToModelMessages([
927
+ {
928
+ role: 'assistant',
929
+ parts: [
930
+ { type: 'step-start' },
931
+ { type: 'text', text: 'i am gonna use tool1', state: 'done' },
932
+ {
933
+ type: 'tool-screenshot',
934
+ state: 'output-available',
935
+ toolCallId: 'call-1',
936
+ input: { value: 'value-1' },
937
+ output: 'result-1',
938
+ },
939
+ { type: 'step-start' },
940
+ {
941
+ type: 'text',
942
+ text: 'i am gonna use tool2 and tool3',
943
+ state: 'done',
944
+ },
945
+ {
946
+ type: 'tool-screenshot',
947
+ state: 'output-available',
948
+ toolCallId: 'call-2',
949
+ input: { value: 'value-2' },
950
+ output: 'result-2',
951
+ },
952
+ {
953
+ type: 'tool-screenshot',
954
+ state: 'output-available',
955
+ toolCallId: 'call-3',
956
+ input: { value: 'value-3' },
957
+ output: 'result-3',
958
+ },
959
+ { type: 'step-start' },
960
+ {
961
+ type: 'tool-screenshot',
962
+ state: 'output-available',
963
+ toolCallId: 'call-4',
964
+ input: { value: 'value-4' },
965
+ output: 'result-4',
966
+ },
967
+ { type: 'step-start' },
968
+ { type: 'text', text: 'final response', state: 'done' },
969
+ ],
970
+ },
971
+ ]);
972
+
973
+ expect(result).toMatchSnapshot();
974
+ });
975
+ });
976
+
977
+ describe('multiple messages', () => {
978
+ it('should handle a conversation with multiple messages', async () => {
979
+ const result = await convertToModelMessages([
980
+ {
981
+ role: 'user',
982
+ parts: [{ type: 'text', text: "What's the weather like?" }],
983
+ },
984
+ {
985
+ role: 'assistant',
986
+ parts: [
987
+ { type: 'text', text: "I'll check that for you.", state: 'done' },
988
+ ],
989
+ },
990
+ {
991
+ role: 'user',
992
+ parts: [{ type: 'text', text: 'Thanks!' }],
993
+ },
994
+ ]);
995
+
996
+ expect(result).toMatchInlineSnapshot(`
997
+ [
998
+ {
999
+ "content": [
1000
+ {
1001
+ "text": "What's the weather like?",
1002
+ "type": "text",
1003
+ },
1004
+ ],
1005
+ "role": "user",
1006
+ },
1007
+ {
1008
+ "content": [
1009
+ {
1010
+ "text": "I'll check that for you.",
1011
+ "type": "text",
1012
+ },
1013
+ ],
1014
+ "role": "assistant",
1015
+ },
1016
+ {
1017
+ "content": [
1018
+ {
1019
+ "text": "Thanks!",
1020
+ "type": "text",
1021
+ },
1022
+ ],
1023
+ "role": "user",
1024
+ },
1025
+ ]
1026
+ `);
1027
+ });
1028
+
1029
+ it('should handle conversation with multiple tool invocations and user message at the end', async () => {
1030
+ const result = await convertToModelMessages([
1031
+ {
1032
+ role: 'assistant',
1033
+ parts: [
1034
+ { type: 'step-start' },
1035
+ {
1036
+ type: 'tool-screenshot',
1037
+ state: 'output-available',
1038
+ toolCallId: 'call-1',
1039
+ input: { value: 'value-1' },
1040
+ output: 'result-1',
1041
+ },
1042
+ { type: 'step-start' },
1043
+ {
1044
+ type: 'tool-screenshot',
1045
+ state: 'output-available',
1046
+ toolCallId: 'call-2',
1047
+ input: { value: 'value-2' },
1048
+ output: 'result-2',
1049
+ },
1050
+ {
1051
+ type: 'tool-screenshot',
1052
+ state: 'output-available',
1053
+ toolCallId: 'call-3',
1054
+ input: { value: 'value-3' },
1055
+ output: 'result-3',
1056
+ },
1057
+ { type: 'step-start' },
1058
+ {
1059
+ type: 'tool-screenshot',
1060
+ state: 'output-available',
1061
+ toolCallId: 'call-4',
1062
+ input: { value: 'value-4' },
1063
+ output: 'result-4',
1064
+ },
1065
+ { type: 'step-start' },
1066
+ { type: 'text', text: 'response', state: 'done' },
1067
+ ],
1068
+ },
1069
+ {
1070
+ role: 'user',
1071
+ parts: [{ type: 'text', text: 'Thanks!' }],
1072
+ },
1073
+ ]);
1074
+
1075
+ expect(result).toMatchSnapshot();
1076
+ });
1077
+ });
1078
+
1079
+ describe('error handling', () => {
1080
+ it('should throw an error for unhandled roles', async () => {
1081
+ expect(async () => {
1082
+ await convertToModelMessages([
1083
+ {
1084
+ role: 'unknown' as any,
1085
+ parts: [{ text: 'unknown role message', type: 'text' }],
1086
+ },
1087
+ ]);
1088
+ }).rejects.toThrow('Unsupported role: unknown');
1089
+ });
1090
+ });
1091
+
1092
+ describe('when ignoring incomplete tool calls', () => {
1093
+ it('should handle conversation with multiple tool invocations and user message at the end', async () => {
1094
+ const result = await convertToModelMessages(
1095
+ [
1096
+ {
1097
+ role: 'assistant',
1098
+ parts: [
1099
+ { type: 'step-start' },
1100
+ {
1101
+ type: 'tool-screenshot',
1102
+ state: 'output-available',
1103
+ toolCallId: 'call-1',
1104
+ input: { value: 'value-1' },
1105
+ output: 'result-1',
1106
+ },
1107
+ { type: 'step-start' },
1108
+ {
1109
+ type: 'tool-screenshot',
1110
+ state: 'input-streaming',
1111
+ toolCallId: 'call-2',
1112
+ input: { value: 'value-2' },
1113
+ },
1114
+ {
1115
+ type: 'tool-screenshot',
1116
+ state: 'input-available',
1117
+ toolCallId: 'call-3',
1118
+ input: { value: 'value-3' },
1119
+ },
1120
+ {
1121
+ type: 'dynamic-tool',
1122
+ toolName: 'tool-screenshot2',
1123
+ state: 'input-available',
1124
+ toolCallId: 'call-3',
1125
+ input: { value: 'value-3' },
1126
+ },
1127
+ { type: 'text', text: 'response', state: 'done' },
1128
+ ],
1129
+ },
1130
+ {
1131
+ role: 'user',
1132
+ parts: [{ type: 'text', text: 'Thanks!' }],
1133
+ },
1134
+ ],
1135
+ { ignoreIncompleteToolCalls: true },
1136
+ );
1137
+
1138
+ expect(result).toMatchInlineSnapshot(`
1139
+ [
1140
+ {
1141
+ "content": [
1142
+ {
1143
+ "input": {
1144
+ "value": "value-1",
1145
+ },
1146
+ "providerExecuted": undefined,
1147
+ "toolCallId": "call-1",
1148
+ "toolName": "screenshot",
1149
+ "type": "tool-call",
1150
+ },
1151
+ ],
1152
+ "role": "assistant",
1153
+ },
1154
+ {
1155
+ "content": [
1156
+ {
1157
+ "output": {
1158
+ "type": "text",
1159
+ "value": "result-1",
1160
+ },
1161
+ "toolCallId": "call-1",
1162
+ "toolName": "screenshot",
1163
+ "type": "tool-result",
1164
+ },
1165
+ ],
1166
+ "role": "tool",
1167
+ },
1168
+ {
1169
+ "content": [
1170
+ {
1171
+ "text": "response",
1172
+ "type": "text",
1173
+ },
1174
+ ],
1175
+ "role": "assistant",
1176
+ },
1177
+ {
1178
+ "content": [
1179
+ {
1180
+ "text": "Thanks!",
1181
+ "type": "text",
1182
+ },
1183
+ ],
1184
+ "role": "user",
1185
+ },
1186
+ ]
1187
+ `);
1188
+ });
1189
+ });
1190
+
1191
+ describe('when converting dynamic tool invocations', () => {
1192
+ it('should convert a dynamic tool invocation', async () => {
1193
+ const result = await convertToModelMessages(
1194
+ [
1195
+ {
1196
+ role: 'assistant',
1197
+ parts: [
1198
+ { type: 'step-start' },
1199
+ {
1200
+ type: 'dynamic-tool',
1201
+ toolName: 'screenshot',
1202
+ state: 'output-available',
1203
+ toolCallId: 'call-1',
1204
+ input: { value: 'value-1' },
1205
+ output: 'result-1',
1206
+ },
1207
+ ],
1208
+ },
1209
+ {
1210
+ role: 'user',
1211
+ parts: [{ type: 'text', text: 'Thanks!' }],
1212
+ },
1213
+ ],
1214
+ { ignoreIncompleteToolCalls: true },
1215
+ );
1216
+
1217
+ expect(result).toMatchInlineSnapshot(`
1218
+ [
1219
+ {
1220
+ "content": [
1221
+ {
1222
+ "input": {
1223
+ "value": "value-1",
1224
+ },
1225
+ "providerExecuted": undefined,
1226
+ "toolCallId": "call-1",
1227
+ "toolName": "screenshot",
1228
+ "type": "tool-call",
1229
+ },
1230
+ ],
1231
+ "role": "assistant",
1232
+ },
1233
+ {
1234
+ "content": [
1235
+ {
1236
+ "output": {
1237
+ "type": "text",
1238
+ "value": "result-1",
1239
+ },
1240
+ "toolCallId": "call-1",
1241
+ "toolName": "screenshot",
1242
+ "type": "tool-result",
1243
+ },
1244
+ ],
1245
+ "role": "tool",
1246
+ },
1247
+ {
1248
+ "content": [
1249
+ {
1250
+ "text": "Thanks!",
1251
+ "type": "text",
1252
+ },
1253
+ ],
1254
+ "role": "user",
1255
+ },
1256
+ ]
1257
+ `);
1258
+ });
1259
+ });
1260
+
1261
+ describe('when converting provider-executed dynamic tool invocations', () => {
1262
+ it('should convert a provider-executed dynamic tool invocation', async () => {
1263
+ const result = await convertToModelMessages(
1264
+ [
1265
+ {
1266
+ role: 'assistant',
1267
+ parts: [
1268
+ { type: 'step-start' },
1269
+ {
1270
+ type: 'dynamic-tool',
1271
+ toolName: 'screenshot',
1272
+ state: 'output-available',
1273
+ toolCallId: 'call-1',
1274
+ input: { value: 'value-1' },
1275
+ output: 'result-1',
1276
+ providerExecuted: true,
1277
+ callProviderMetadata: {
1278
+ 'test-provider': {
1279
+ 'key-a': 'test-value-1',
1280
+ 'key-b': 'test-value-2',
1281
+ },
1282
+ },
1283
+ },
1284
+ ],
1285
+ },
1286
+ {
1287
+ role: 'user',
1288
+ parts: [{ type: 'text', text: 'Thanks!' }],
1289
+ },
1290
+ ],
1291
+ { ignoreIncompleteToolCalls: true },
1292
+ );
1293
+
1294
+ expect(result).toMatchInlineSnapshot(`
1295
+ [
1296
+ {
1297
+ "content": [
1298
+ {
1299
+ "input": {
1300
+ "value": "value-1",
1301
+ },
1302
+ "providerExecuted": true,
1303
+ "providerOptions": {
1304
+ "test-provider": {
1305
+ "key-a": "test-value-1",
1306
+ "key-b": "test-value-2",
1307
+ },
1308
+ },
1309
+ "toolCallId": "call-1",
1310
+ "toolName": "screenshot",
1311
+ "type": "tool-call",
1312
+ },
1313
+ {
1314
+ "output": {
1315
+ "type": "text",
1316
+ "value": "result-1",
1317
+ },
1318
+ "providerOptions": {
1319
+ "test-provider": {
1320
+ "key-a": "test-value-1",
1321
+ "key-b": "test-value-2",
1322
+ },
1323
+ },
1324
+ "toolCallId": "call-1",
1325
+ "toolName": "screenshot",
1326
+ "type": "tool-result",
1327
+ },
1328
+ ],
1329
+ "role": "assistant",
1330
+ },
1331
+ {
1332
+ "content": [
1333
+ {
1334
+ "text": "Thanks!",
1335
+ "type": "text",
1336
+ },
1337
+ ],
1338
+ "role": "user",
1339
+ },
1340
+ ]
1341
+ `);
1342
+ });
1343
+ });
1344
+
1345
+ describe('when converting tool approval request responses', () => {
1346
+ it('should convert an approved tool approval request (static tool)', async () => {
1347
+ const result = await convertToModelMessages([
1348
+ {
1349
+ parts: [
1350
+ {
1351
+ text: 'What is the weather in Tokyo?',
1352
+ type: 'text',
1353
+ },
1354
+ ],
1355
+ role: 'user',
1356
+ },
1357
+ {
1358
+ parts: [
1359
+ {
1360
+ type: 'step-start',
1361
+ },
1362
+ {
1363
+ approval: {
1364
+ approved: true,
1365
+ id: 'approval-1',
1366
+ reason: undefined,
1367
+ },
1368
+ input: {
1369
+ city: 'Tokyo',
1370
+ },
1371
+ state: 'approval-responded',
1372
+ toolCallId: 'call-1',
1373
+ type: 'tool-weather',
1374
+ },
1375
+ ],
1376
+ role: 'assistant',
1377
+ },
1378
+ ]);
1379
+
1380
+ expect(result).toMatchInlineSnapshot(`
1381
+ [
1382
+ {
1383
+ "content": [
1384
+ {
1385
+ "text": "What is the weather in Tokyo?",
1386
+ "type": "text",
1387
+ },
1388
+ ],
1389
+ "role": "user",
1390
+ },
1391
+ {
1392
+ "content": [
1393
+ {
1394
+ "input": {
1395
+ "city": "Tokyo",
1396
+ },
1397
+ "providerExecuted": undefined,
1398
+ "toolCallId": "call-1",
1399
+ "toolName": "weather",
1400
+ "type": "tool-call",
1401
+ },
1402
+ {
1403
+ "approvalId": "approval-1",
1404
+ "toolCallId": "call-1",
1405
+ "type": "tool-approval-request",
1406
+ },
1407
+ ],
1408
+ "role": "assistant",
1409
+ },
1410
+ {
1411
+ "content": [
1412
+ {
1413
+ "approvalId": "approval-1",
1414
+ "approved": true,
1415
+ "providerExecuted": undefined,
1416
+ "reason": undefined,
1417
+ "type": "tool-approval-response",
1418
+ },
1419
+ ],
1420
+ "role": "tool",
1421
+ },
1422
+ ]
1423
+ `);
1424
+ });
1425
+
1426
+ it('should convert an approved tool approval request (dynamic tool)', async () => {
1427
+ const result = await convertToModelMessages([
1428
+ {
1429
+ parts: [
1430
+ {
1431
+ text: 'What is the weather in Tokyo?',
1432
+ type: 'text',
1433
+ },
1434
+ ],
1435
+ role: 'user',
1436
+ },
1437
+ {
1438
+ parts: [
1439
+ {
1440
+ type: 'step-start',
1441
+ },
1442
+ {
1443
+ approval: {
1444
+ approved: true,
1445
+ id: 'approval-1',
1446
+ reason: undefined,
1447
+ },
1448
+ input: {
1449
+ city: 'Tokyo',
1450
+ },
1451
+ state: 'approval-responded',
1452
+ toolCallId: 'call-1',
1453
+ type: 'dynamic-tool',
1454
+ toolName: 'weather',
1455
+ },
1456
+ ],
1457
+ role: 'assistant',
1458
+ },
1459
+ ]);
1460
+
1461
+ expect(result).toMatchInlineSnapshot(`
1462
+ [
1463
+ {
1464
+ "content": [
1465
+ {
1466
+ "text": "What is the weather in Tokyo?",
1467
+ "type": "text",
1468
+ },
1469
+ ],
1470
+ "role": "user",
1471
+ },
1472
+ {
1473
+ "content": [
1474
+ {
1475
+ "input": {
1476
+ "city": "Tokyo",
1477
+ },
1478
+ "providerExecuted": undefined,
1479
+ "toolCallId": "call-1",
1480
+ "toolName": "weather",
1481
+ "type": "tool-call",
1482
+ },
1483
+ {
1484
+ "approvalId": "approval-1",
1485
+ "toolCallId": "call-1",
1486
+ "type": "tool-approval-request",
1487
+ },
1488
+ ],
1489
+ "role": "assistant",
1490
+ },
1491
+ {
1492
+ "content": [
1493
+ {
1494
+ "approvalId": "approval-1",
1495
+ "approved": true,
1496
+ "providerExecuted": undefined,
1497
+ "reason": undefined,
1498
+ "type": "tool-approval-response",
1499
+ },
1500
+ ],
1501
+ "role": "tool",
1502
+ },
1503
+ ]
1504
+ `);
1505
+ });
1506
+
1507
+ it('should convert a denied tool approval request and follow up text (static tool)', async () => {
1508
+ const result = await convertToModelMessages([
1509
+ {
1510
+ parts: [
1511
+ {
1512
+ text: 'What is the weather in Tokyo?',
1513
+ type: 'text',
1514
+ },
1515
+ ],
1516
+ role: 'user',
1517
+ },
1518
+ {
1519
+ parts: [
1520
+ {
1521
+ type: 'step-start',
1522
+ },
1523
+ {
1524
+ approval: {
1525
+ approved: false,
1526
+ id: 'approval-1',
1527
+ reason: "I don't want to approve this",
1528
+ },
1529
+ input: {
1530
+ city: 'Tokyo',
1531
+ },
1532
+ state: 'approval-responded',
1533
+ toolCallId: 'call-1',
1534
+ type: 'tool-weather',
1535
+ },
1536
+ { type: 'step-start' },
1537
+ {
1538
+ type: 'text',
1539
+ text: 'I was not able to retrieve the weather.',
1540
+ state: 'done',
1541
+ },
1542
+ ],
1543
+ role: 'assistant',
1544
+ },
1545
+ ]);
1546
+
1547
+ expect(result).toMatchInlineSnapshot(`
1548
+ [
1549
+ {
1550
+ "content": [
1551
+ {
1552
+ "text": "What is the weather in Tokyo?",
1553
+ "type": "text",
1554
+ },
1555
+ ],
1556
+ "role": "user",
1557
+ },
1558
+ {
1559
+ "content": [
1560
+ {
1561
+ "input": {
1562
+ "city": "Tokyo",
1563
+ },
1564
+ "providerExecuted": undefined,
1565
+ "toolCallId": "call-1",
1566
+ "toolName": "weather",
1567
+ "type": "tool-call",
1568
+ },
1569
+ {
1570
+ "approvalId": "approval-1",
1571
+ "toolCallId": "call-1",
1572
+ "type": "tool-approval-request",
1573
+ },
1574
+ ],
1575
+ "role": "assistant",
1576
+ },
1577
+ {
1578
+ "content": [
1579
+ {
1580
+ "approvalId": "approval-1",
1581
+ "approved": false,
1582
+ "providerExecuted": undefined,
1583
+ "reason": "I don't want to approve this",
1584
+ "type": "tool-approval-response",
1585
+ },
1586
+ ],
1587
+ "role": "tool",
1588
+ },
1589
+ {
1590
+ "content": [
1591
+ {
1592
+ "text": "I was not able to retrieve the weather.",
1593
+ "type": "text",
1594
+ },
1595
+ ],
1596
+ "role": "assistant",
1597
+ },
1598
+ ]
1599
+ `);
1600
+ });
1601
+
1602
+ it('should convert a denied tool approval request and follow up text (dynamic tool)', async () => {
1603
+ const result = await convertToModelMessages([
1604
+ {
1605
+ parts: [
1606
+ {
1607
+ text: 'What is the weather in Tokyo?',
1608
+ type: 'text',
1609
+ },
1610
+ ],
1611
+ role: 'user',
1612
+ },
1613
+ {
1614
+ parts: [
1615
+ {
1616
+ type: 'step-start',
1617
+ },
1618
+ {
1619
+ approval: {
1620
+ approved: false,
1621
+ id: 'approval-1',
1622
+ reason: "I don't want to approve this",
1623
+ },
1624
+ input: {
1625
+ city: 'Tokyo',
1626
+ },
1627
+ state: 'approval-responded',
1628
+ toolCallId: 'call-1',
1629
+ type: 'dynamic-tool',
1630
+ toolName: 'weather',
1631
+ },
1632
+ { type: 'step-start' },
1633
+ {
1634
+ type: 'text',
1635
+ text: 'I was not able to retrieve the weather.',
1636
+ state: 'done',
1637
+ },
1638
+ ],
1639
+ role: 'assistant',
1640
+ },
1641
+ ]);
1642
+
1643
+ expect(result).toMatchInlineSnapshot(`
1644
+ [
1645
+ {
1646
+ "content": [
1647
+ {
1648
+ "text": "What is the weather in Tokyo?",
1649
+ "type": "text",
1650
+ },
1651
+ ],
1652
+ "role": "user",
1653
+ },
1654
+ {
1655
+ "content": [
1656
+ {
1657
+ "input": {
1658
+ "city": "Tokyo",
1659
+ },
1660
+ "providerExecuted": undefined,
1661
+ "toolCallId": "call-1",
1662
+ "toolName": "weather",
1663
+ "type": "tool-call",
1664
+ },
1665
+ {
1666
+ "approvalId": "approval-1",
1667
+ "toolCallId": "call-1",
1668
+ "type": "tool-approval-request",
1669
+ },
1670
+ ],
1671
+ "role": "assistant",
1672
+ },
1673
+ {
1674
+ "content": [
1675
+ {
1676
+ "approvalId": "approval-1",
1677
+ "approved": false,
1678
+ "providerExecuted": undefined,
1679
+ "reason": "I don't want to approve this",
1680
+ "type": "tool-approval-response",
1681
+ },
1682
+ ],
1683
+ "role": "tool",
1684
+ },
1685
+ {
1686
+ "content": [
1687
+ {
1688
+ "text": "I was not able to retrieve the weather.",
1689
+ "type": "text",
1690
+ },
1691
+ ],
1692
+ "role": "assistant",
1693
+ },
1694
+ ]
1695
+ `);
1696
+ });
1697
+
1698
+ it('should convert tool output denied (static tool)', async () => {
1699
+ const result = await convertToModelMessages([
1700
+ {
1701
+ parts: [
1702
+ {
1703
+ text: 'What is the weather in Tokyo?',
1704
+ type: 'text',
1705
+ },
1706
+ ],
1707
+ role: 'user',
1708
+ },
1709
+ {
1710
+ parts: [
1711
+ {
1712
+ type: 'step-start',
1713
+ },
1714
+ {
1715
+ approval: {
1716
+ approved: false,
1717
+ id: 'approval-1',
1718
+ reason: "I don't want to approve this",
1719
+ },
1720
+ input: {
1721
+ city: 'Tokyo',
1722
+ },
1723
+ state: 'output-denied',
1724
+ toolCallId: 'call-1',
1725
+ type: 'tool-weather',
1726
+ },
1727
+ ],
1728
+ role: 'assistant',
1729
+ },
1730
+ ]);
1731
+
1732
+ expect(result).toMatchInlineSnapshot(`
1733
+ [
1734
+ {
1735
+ "content": [
1736
+ {
1737
+ "text": "What is the weather in Tokyo?",
1738
+ "type": "text",
1739
+ },
1740
+ ],
1741
+ "role": "user",
1742
+ },
1743
+ {
1744
+ "content": [
1745
+ {
1746
+ "input": {
1747
+ "city": "Tokyo",
1748
+ },
1749
+ "providerExecuted": undefined,
1750
+ "toolCallId": "call-1",
1751
+ "toolName": "weather",
1752
+ "type": "tool-call",
1753
+ },
1754
+ {
1755
+ "approvalId": "approval-1",
1756
+ "toolCallId": "call-1",
1757
+ "type": "tool-approval-request",
1758
+ },
1759
+ ],
1760
+ "role": "assistant",
1761
+ },
1762
+ {
1763
+ "content": [
1764
+ {
1765
+ "approvalId": "approval-1",
1766
+ "approved": false,
1767
+ "providerExecuted": undefined,
1768
+ "reason": "I don't want to approve this",
1769
+ "type": "tool-approval-response",
1770
+ },
1771
+ {
1772
+ "output": {
1773
+ "type": "error-text",
1774
+ "value": "I don't want to approve this",
1775
+ },
1776
+ "toolCallId": "call-1",
1777
+ "toolName": "weather",
1778
+ "type": "tool-result",
1779
+ },
1780
+ ],
1781
+ "role": "tool",
1782
+ },
1783
+ ]
1784
+ `);
1785
+ });
1786
+
1787
+ it('should convert tool output denied (dynamic tool)', async () => {
1788
+ const result = await convertToModelMessages([
1789
+ {
1790
+ parts: [
1791
+ {
1792
+ text: 'What is the weather in Tokyo?',
1793
+ type: 'text',
1794
+ },
1795
+ ],
1796
+ role: 'user',
1797
+ },
1798
+ {
1799
+ parts: [
1800
+ {
1801
+ type: 'step-start',
1802
+ },
1803
+ {
1804
+ approval: {
1805
+ approved: false,
1806
+ id: 'approval-1',
1807
+ reason: "I don't want to approve this",
1808
+ },
1809
+ input: {
1810
+ city: 'Tokyo',
1811
+ },
1812
+ state: 'output-denied',
1813
+ toolCallId: 'call-1',
1814
+ type: 'dynamic-tool',
1815
+ toolName: 'weather',
1816
+ },
1817
+ ],
1818
+ role: 'assistant',
1819
+ },
1820
+ ]);
1821
+
1822
+ expect(result).toMatchInlineSnapshot(`
1823
+ [
1824
+ {
1825
+ "content": [
1826
+ {
1827
+ "text": "What is the weather in Tokyo?",
1828
+ "type": "text",
1829
+ },
1830
+ ],
1831
+ "role": "user",
1832
+ },
1833
+ {
1834
+ "content": [
1835
+ {
1836
+ "input": {
1837
+ "city": "Tokyo",
1838
+ },
1839
+ "providerExecuted": undefined,
1840
+ "toolCallId": "call-1",
1841
+ "toolName": "weather",
1842
+ "type": "tool-call",
1843
+ },
1844
+ {
1845
+ "approvalId": "approval-1",
1846
+ "toolCallId": "call-1",
1847
+ "type": "tool-approval-request",
1848
+ },
1849
+ ],
1850
+ "role": "assistant",
1851
+ },
1852
+ {
1853
+ "content": [
1854
+ {
1855
+ "approvalId": "approval-1",
1856
+ "approved": false,
1857
+ "providerExecuted": undefined,
1858
+ "reason": "I don't want to approve this",
1859
+ "type": "tool-approval-response",
1860
+ },
1861
+ {
1862
+ "output": {
1863
+ "type": "error-text",
1864
+ "value": "I don't want to approve this",
1865
+ },
1866
+ "toolCallId": "call-1",
1867
+ "toolName": "weather",
1868
+ "type": "tool-result",
1869
+ },
1870
+ ],
1871
+ "role": "tool",
1872
+ },
1873
+ ]
1874
+ `);
1875
+ });
1876
+
1877
+ it('should convert tool output result with approval and follow up text (static tool)', async () => {
1878
+ const result = await convertToModelMessages([
1879
+ {
1880
+ parts: [
1881
+ {
1882
+ text: 'What is the weather in Tokyo?',
1883
+ type: 'text',
1884
+ },
1885
+ ],
1886
+ role: 'user',
1887
+ },
1888
+ {
1889
+ parts: [
1890
+ { type: 'step-start' },
1891
+ {
1892
+ approval: {
1893
+ approved: true,
1894
+ id: 'approval-1',
1895
+ },
1896
+ input: {
1897
+ city: 'Tokyo',
1898
+ },
1899
+ output: {
1900
+ weather: 'Sunny',
1901
+ temperature: '20°C',
1902
+ },
1903
+ state: 'output-available',
1904
+ toolCallId: 'call-1',
1905
+ type: 'tool-weather',
1906
+ },
1907
+ { type: 'step-start' },
1908
+ {
1909
+ type: 'text',
1910
+ text: 'The weather in Tokyo is sunny.',
1911
+ state: 'done',
1912
+ },
1913
+ ],
1914
+ role: 'assistant',
1915
+ },
1916
+ ]);
1917
+
1918
+ expect(result).toMatchInlineSnapshot(`
1919
+ [
1920
+ {
1921
+ "content": [
1922
+ {
1923
+ "text": "What is the weather in Tokyo?",
1924
+ "type": "text",
1925
+ },
1926
+ ],
1927
+ "role": "user",
1928
+ },
1929
+ {
1930
+ "content": [
1931
+ {
1932
+ "input": {
1933
+ "city": "Tokyo",
1934
+ },
1935
+ "providerExecuted": undefined,
1936
+ "toolCallId": "call-1",
1937
+ "toolName": "weather",
1938
+ "type": "tool-call",
1939
+ },
1940
+ {
1941
+ "approvalId": "approval-1",
1942
+ "toolCallId": "call-1",
1943
+ "type": "tool-approval-request",
1944
+ },
1945
+ ],
1946
+ "role": "assistant",
1947
+ },
1948
+ {
1949
+ "content": [
1950
+ {
1951
+ "approvalId": "approval-1",
1952
+ "approved": true,
1953
+ "providerExecuted": undefined,
1954
+ "reason": undefined,
1955
+ "type": "tool-approval-response",
1956
+ },
1957
+ {
1958
+ "output": {
1959
+ "type": "json",
1960
+ "value": {
1961
+ "temperature": "20°C",
1962
+ "weather": "Sunny",
1963
+ },
1964
+ },
1965
+ "toolCallId": "call-1",
1966
+ "toolName": "weather",
1967
+ "type": "tool-result",
1968
+ },
1969
+ ],
1970
+ "role": "tool",
1971
+ },
1972
+ {
1973
+ "content": [
1974
+ {
1975
+ "text": "The weather in Tokyo is sunny.",
1976
+ "type": "text",
1977
+ },
1978
+ ],
1979
+ "role": "assistant",
1980
+ },
1981
+ ]
1982
+ `);
1983
+ });
1984
+
1985
+ it('should convert tool error result with approval and follow up text (static tool)', async () => {
1986
+ const result = await convertToModelMessages([
1987
+ {
1988
+ parts: [
1989
+ {
1990
+ text: 'What is the weather in Tokyo?',
1991
+ type: 'text',
1992
+ },
1993
+ ],
1994
+ role: 'user',
1995
+ },
1996
+ {
1997
+ parts: [
1998
+ { type: 'step-start' },
1999
+ {
2000
+ approval: {
2001
+ approved: true,
2002
+ id: 'approval-1',
2003
+ },
2004
+ input: {
2005
+ city: 'Tokyo',
2006
+ },
2007
+ errorText: 'Error: Fetching weather data failed',
2008
+ state: 'output-error',
2009
+ toolCallId: 'call-1',
2010
+ type: 'tool-weather',
2011
+ },
2012
+ { type: 'step-start' },
2013
+ {
2014
+ type: 'text',
2015
+ text: 'The weather in Tokyo is sunny.',
2016
+ state: 'done',
2017
+ },
2018
+ ],
2019
+ role: 'assistant',
2020
+ },
2021
+ ]);
2022
+
2023
+ expect(result).toMatchInlineSnapshot(`
2024
+ [
2025
+ {
2026
+ "content": [
2027
+ {
2028
+ "text": "What is the weather in Tokyo?",
2029
+ "type": "text",
2030
+ },
2031
+ ],
2032
+ "role": "user",
2033
+ },
2034
+ {
2035
+ "content": [
2036
+ {
2037
+ "input": {
2038
+ "city": "Tokyo",
2039
+ },
2040
+ "providerExecuted": undefined,
2041
+ "toolCallId": "call-1",
2042
+ "toolName": "weather",
2043
+ "type": "tool-call",
2044
+ },
2045
+ {
2046
+ "approvalId": "approval-1",
2047
+ "toolCallId": "call-1",
2048
+ "type": "tool-approval-request",
2049
+ },
2050
+ ],
2051
+ "role": "assistant",
2052
+ },
2053
+ {
2054
+ "content": [
2055
+ {
2056
+ "approvalId": "approval-1",
2057
+ "approved": true,
2058
+ "providerExecuted": undefined,
2059
+ "reason": undefined,
2060
+ "type": "tool-approval-response",
2061
+ },
2062
+ {
2063
+ "output": {
2064
+ "type": "error-text",
2065
+ "value": "Error: Fetching weather data failed",
2066
+ },
2067
+ "toolCallId": "call-1",
2068
+ "toolName": "weather",
2069
+ "type": "tool-result",
2070
+ },
2071
+ ],
2072
+ "role": "tool",
2073
+ },
2074
+ {
2075
+ "content": [
2076
+ {
2077
+ "text": "The weather in Tokyo is sunny.",
2078
+ "type": "text",
2079
+ },
2080
+ ],
2081
+ "role": "assistant",
2082
+ },
2083
+ ]
2084
+ `);
2085
+ });
2086
+ });
2087
+
2088
+ describe('data part conversion', () => {
2089
+ describe('in user messages', () => {
2090
+ it('should convert data parts to text when converter provided', async () => {
2091
+ const result = await convertToModelMessages<
2092
+ UIMessage<unknown, { url: { url: string; content: string } }>
2093
+ >(
2094
+ [
2095
+ {
2096
+ role: 'user',
2097
+ parts: [
2098
+ {
2099
+ type: 'data-url',
2100
+ data: { url: 'https://example.com', content: 'Article text' },
2101
+ },
2102
+ ],
2103
+ },
2104
+ ],
2105
+ {
2106
+ convertDataPart: part => ({
2107
+ type: 'text',
2108
+ text: `\n\n[${part.data.url}]\n${part.data.content}`,
2109
+ }),
2110
+ },
2111
+ );
2112
+
2113
+ expect(result).toMatchInlineSnapshot(`
2114
+ [
2115
+ {
2116
+ "content": [
2117
+ {
2118
+ "text": "
2119
+
2120
+ [https://example.com]
2121
+ Article text",
2122
+ "type": "text",
2123
+ },
2124
+ ],
2125
+ "role": "user",
2126
+ },
2127
+ ]
2128
+ `);
2129
+ });
2130
+
2131
+ it('should skip data parts when no converter provided', async () => {
2132
+ const result = await convertToModelMessages([
2133
+ {
2134
+ role: 'user',
2135
+ parts: [
2136
+ { type: 'text', text: 'Hello' },
2137
+ { type: 'data-url', data: { url: 'https://example.com' } },
2138
+ ],
2139
+ },
2140
+ ]);
2141
+
2142
+ expect(result).toMatchInlineSnapshot(`
2143
+ [
2144
+ {
2145
+ "content": [
2146
+ {
2147
+ "text": "Hello",
2148
+ "type": "text",
2149
+ },
2150
+ ],
2151
+ "role": "user",
2152
+ },
2153
+ ]
2154
+ `);
2155
+ });
2156
+
2157
+ it('should selectively convert data parts', async () => {
2158
+ const result = await convertToModelMessages<
2159
+ UIMessage<
2160
+ unknown,
2161
+ {
2162
+ url: { url: string };
2163
+ 'ui-state': { enabled: boolean };
2164
+ }
2165
+ >
2166
+ >(
2167
+ [
2168
+ {
2169
+ role: 'user',
2170
+ parts: [
2171
+ { type: 'data-url', data: { url: 'https://example.com' } },
2172
+ { type: 'data-ui-state', data: { enabled: true } },
2173
+ ],
2174
+ },
2175
+ ],
2176
+ {
2177
+ convertDataPart: part => {
2178
+ // Include URLs, skip UI state
2179
+ if (part.type === 'data-url') {
2180
+ return { type: 'text', text: part.data.url };
2181
+ }
2182
+ },
2183
+ },
2184
+ );
2185
+
2186
+ expect(result).toMatchInlineSnapshot(`
2187
+ [
2188
+ {
2189
+ "content": [
2190
+ {
2191
+ "text": "https://example.com",
2192
+ "type": "text",
2193
+ },
2194
+ ],
2195
+ "role": "user",
2196
+ },
2197
+ ]
2198
+ `);
2199
+ });
2200
+
2201
+ it('should convert data parts to file parts', async () => {
2202
+ const result = await convertToModelMessages<
2203
+ UIMessage<
2204
+ unknown,
2205
+ {
2206
+ attachment: { mediaType: string; filename: string; data: string };
2207
+ }
2208
+ >
2209
+ >(
2210
+ [
2211
+ {
2212
+ role: 'user',
2213
+ parts: [
2214
+ { type: 'text', text: 'Check this file' },
2215
+ {
2216
+ type: 'data-attachment',
2217
+ data: {
2218
+ mediaType: 'application/pdf',
2219
+ filename: 'document.pdf',
2220
+ data: 'base64data',
2221
+ },
2222
+ },
2223
+ ],
2224
+ },
2225
+ ],
2226
+ {
2227
+ convertDataPart: part => {
2228
+ if (part.type === 'data-attachment') {
2229
+ return {
2230
+ type: 'file',
2231
+ mediaType: part.data.mediaType,
2232
+ filename: part.data.filename,
2233
+ data: part.data.data,
2234
+ };
2235
+ }
2236
+ },
2237
+ },
2238
+ );
2239
+
2240
+ expect(result).toMatchInlineSnapshot(`
2241
+ [
2242
+ {
2243
+ "content": [
2244
+ {
2245
+ "text": "Check this file",
2246
+ "type": "text",
2247
+ },
2248
+ {
2249
+ "data": "base64data",
2250
+ "filename": "document.pdf",
2251
+ "mediaType": "application/pdf",
2252
+ "type": "file",
2253
+ },
2254
+ ],
2255
+ "role": "user",
2256
+ },
2257
+ ]
2258
+ `);
2259
+ });
2260
+
2261
+ it('should handle multiple data parts of different types', async () => {
2262
+ const result = await convertToModelMessages<
2263
+ UIMessage<
2264
+ never,
2265
+ {
2266
+ url: { url: string; title: string };
2267
+ code: { code: string; language: string };
2268
+ note: { text: string };
2269
+ }
2270
+ >
2271
+ >(
2272
+ [
2273
+ {
2274
+ role: 'user',
2275
+ parts: [
2276
+ { type: 'text', text: 'Review these:' },
2277
+ {
2278
+ type: 'data-url',
2279
+ data: { url: 'https://example.com', title: 'Example' },
2280
+ },
2281
+ {
2282
+ type: 'data-code',
2283
+ data: { code: 'console.log("test")', language: 'javascript' },
2284
+ },
2285
+ {
2286
+ type: 'data-note',
2287
+ data: { text: 'Internal note' },
2288
+ },
2289
+ ],
2290
+ },
2291
+ ],
2292
+ {
2293
+ convertDataPart: part => {
2294
+ switch (part.type) {
2295
+ case 'data-url':
2296
+ return {
2297
+ type: 'text',
2298
+ text: `[${part.data.title}](${part.data.url})`,
2299
+ };
2300
+ case 'data-code':
2301
+ return {
2302
+ type: 'text',
2303
+ text: `\`\`\`${part.data.language}\n${part.data.code}\n\`\`\``,
2304
+ };
2305
+ }
2306
+ },
2307
+ },
2308
+ );
2309
+
2310
+ expect(result).toMatchInlineSnapshot(`
2311
+ [
2312
+ {
2313
+ "content": [
2314
+ {
2315
+ "text": "Review these:",
2316
+ "type": "text",
2317
+ },
2318
+ {
2319
+ "text": "[Example](https://example.com)",
2320
+ "type": "text",
2321
+ },
2322
+ {
2323
+ "text": "\`\`\`javascript
2324
+ console.log("test")
2325
+ \`\`\`",
2326
+ "type": "text",
2327
+ },
2328
+ ],
2329
+ "role": "user",
2330
+ },
2331
+ ]
2332
+ `);
2333
+ });
2334
+
2335
+ it('should work with messages that have no data parts', async () => {
2336
+ const result = await convertToModelMessages(
2337
+ [
2338
+ {
2339
+ role: 'user',
2340
+ parts: [
2341
+ { type: 'text', text: 'Hello' },
2342
+ {
2343
+ type: 'file',
2344
+ mediaType: 'image/png',
2345
+ url: 'https://example.com/image.png',
2346
+ },
2347
+ ],
2348
+ },
2349
+ ],
2350
+ {
2351
+ convertDataPart: () => ({ type: 'text', text: 'converted' }),
2352
+ },
2353
+ );
2354
+
2355
+ expect(result).toMatchInlineSnapshot(`
2356
+ [
2357
+ {
2358
+ "content": [
2359
+ {
2360
+ "text": "Hello",
2361
+ "type": "text",
2362
+ },
2363
+ {
2364
+ "data": "https://example.com/image.png",
2365
+ "filename": undefined,
2366
+ "mediaType": "image/png",
2367
+ "type": "file",
2368
+ },
2369
+ ],
2370
+ "role": "user",
2371
+ },
2372
+ ]
2373
+ `);
2374
+ });
2375
+
2376
+ it('should preserve order of parts including converted data parts', async () => {
2377
+ const result = await convertToModelMessages<
2378
+ UIMessage<unknown, { tag: { value: string } }>
2379
+ >(
2380
+ [
2381
+ {
2382
+ role: 'user',
2383
+ parts: [
2384
+ { type: 'text', text: 'First' },
2385
+ { type: 'data-tag', data: { value: 'tag1' } },
2386
+ { type: 'text', text: 'Second' },
2387
+ { type: 'data-tag', data: { value: 'tag2' } },
2388
+ { type: 'text', text: 'Third' },
2389
+ ],
2390
+ },
2391
+ ],
2392
+ {
2393
+ convertDataPart: part => ({
2394
+ type: 'text',
2395
+ text: `[${part.data.value}]`,
2396
+ }),
2397
+ },
2398
+ );
2399
+
2400
+ expect(result).toMatchInlineSnapshot(`
2401
+ [
2402
+ {
2403
+ "content": [
2404
+ {
2405
+ "text": "First",
2406
+ "type": "text",
2407
+ },
2408
+ {
2409
+ "text": "[tag1]",
2410
+ "type": "text",
2411
+ },
2412
+ {
2413
+ "text": "Second",
2414
+ "type": "text",
2415
+ },
2416
+ {
2417
+ "text": "[tag2]",
2418
+ "type": "text",
2419
+ },
2420
+ {
2421
+ "text": "Third",
2422
+ "type": "text",
2423
+ },
2424
+ ],
2425
+ "role": "user",
2426
+ },
2427
+ ]
2428
+ `);
2429
+ });
2430
+ });
2431
+
2432
+ describe('in assistant messages', () => {
2433
+ it('should convert data parts to text when converter provided', async () => {
2434
+ const result = await convertToModelMessages<
2435
+ UIMessage<unknown, { url: { url: string; content: string } }>
2436
+ >(
2437
+ [
2438
+ {
2439
+ role: 'assistant',
2440
+ parts: [
2441
+ {
2442
+ type: 'data-url',
2443
+ data: { url: 'https://example.com', content: 'Article text' },
2444
+ },
2445
+ ],
2446
+ },
2447
+ ],
2448
+ {
2449
+ convertDataPart: part => ({
2450
+ type: 'text',
2451
+ text: `\n\n[${part.data.url}]\n${part.data.content}`,
2452
+ }),
2453
+ },
2454
+ );
2455
+
2456
+ expect(result).toMatchInlineSnapshot(`
2457
+ [
2458
+ {
2459
+ "content": [
2460
+ {
2461
+ "text": "
2462
+
2463
+ [https://example.com]
2464
+ Article text",
2465
+ "type": "text",
2466
+ },
2467
+ ],
2468
+ "role": "assistant",
2469
+ },
2470
+ ]
2471
+ `);
2472
+ });
2473
+
2474
+ it('should skip data parts when no converter provided', async () => {
2475
+ const result = await convertToModelMessages([
2476
+ {
2477
+ role: 'assistant',
2478
+ parts: [
2479
+ { type: 'text', text: 'Hello' },
2480
+ { type: 'data-url', data: { url: 'https://example.com' } },
2481
+ ],
2482
+ },
2483
+ ]);
2484
+
2485
+ expect(result).toMatchInlineSnapshot(`
2486
+ [
2487
+ {
2488
+ "content": [
2489
+ {
2490
+ "text": "Hello",
2491
+ "type": "text",
2492
+ },
2493
+ ],
2494
+ "role": "assistant",
2495
+ },
2496
+ ]
2497
+ `);
2498
+ });
2499
+
2500
+ it('should selectively convert data parts', async () => {
2501
+ const result = await convertToModelMessages<
2502
+ UIMessage<
2503
+ unknown,
2504
+ {
2505
+ url: { url: string };
2506
+ 'ui-state': { enabled: boolean };
2507
+ }
2508
+ >
2509
+ >(
2510
+ [
2511
+ {
2512
+ role: 'assistant',
2513
+ parts: [
2514
+ { type: 'data-url', data: { url: 'https://example.com' } },
2515
+ { type: 'data-ui-state', data: { enabled: true } },
2516
+ ],
2517
+ },
2518
+ ],
2519
+ {
2520
+ convertDataPart: part => {
2521
+ // Include URLs, skip UI state
2522
+ if (part.type === 'data-url') {
2523
+ return { type: 'text', text: part.data.url };
2524
+ }
2525
+ },
2526
+ },
2527
+ );
2528
+
2529
+ expect(result).toMatchInlineSnapshot(`
2530
+ [
2531
+ {
2532
+ "content": [
2533
+ {
2534
+ "text": "https://example.com",
2535
+ "type": "text",
2536
+ },
2537
+ ],
2538
+ "role": "assistant",
2539
+ },
2540
+ ]
2541
+ `);
2542
+ });
2543
+
2544
+ it('should convert data parts to file parts', async () => {
2545
+ const result = await convertToModelMessages<
2546
+ UIMessage<
2547
+ unknown,
2548
+ {
2549
+ attachment: { mediaType: string; filename: string; data: string };
2550
+ }
2551
+ >
2552
+ >(
2553
+ [
2554
+ {
2555
+ role: 'assistant',
2556
+ parts: [
2557
+ { type: 'text', text: 'Check this file' },
2558
+ {
2559
+ type: 'data-attachment',
2560
+ data: {
2561
+ mediaType: 'application/pdf',
2562
+ filename: 'document.pdf',
2563
+ data: 'base64data',
2564
+ },
2565
+ },
2566
+ ],
2567
+ },
2568
+ ],
2569
+ {
2570
+ convertDataPart: part => {
2571
+ if (part.type === 'data-attachment') {
2572
+ return {
2573
+ type: 'file',
2574
+ mediaType: part.data.mediaType,
2575
+ filename: part.data.filename,
2576
+ data: part.data.data,
2577
+ };
2578
+ }
2579
+ },
2580
+ },
2581
+ );
2582
+
2583
+ expect(result).toMatchInlineSnapshot(`
2584
+ [
2585
+ {
2586
+ "content": [
2587
+ {
2588
+ "text": "Check this file",
2589
+ "type": "text",
2590
+ },
2591
+ {
2592
+ "data": "base64data",
2593
+ "filename": "document.pdf",
2594
+ "mediaType": "application/pdf",
2595
+ "type": "file",
2596
+ },
2597
+ ],
2598
+ "role": "assistant",
2599
+ },
2600
+ ]
2601
+ `);
2602
+ });
2603
+
2604
+ it('should handle multiple data parts of different types', async () => {
2605
+ const result = await convertToModelMessages<
2606
+ UIMessage<
2607
+ never,
2608
+ {
2609
+ url: { url: string; title: string };
2610
+ code: { code: string; language: string };
2611
+ note: { text: string };
2612
+ }
2613
+ >
2614
+ >(
2615
+ [
2616
+ {
2617
+ role: 'assistant',
2618
+ parts: [
2619
+ { type: 'text', text: 'Review these:' },
2620
+ {
2621
+ type: 'data-url',
2622
+ data: { url: 'https://example.com', title: 'Example' },
2623
+ },
2624
+ {
2625
+ type: 'data-code',
2626
+ data: { code: 'console.log("test")', language: 'javascript' },
2627
+ },
2628
+ {
2629
+ type: 'data-note',
2630
+ data: { text: 'Internal note' },
2631
+ },
2632
+ ],
2633
+ },
2634
+ ],
2635
+ {
2636
+ convertDataPart: part => {
2637
+ switch (part.type) {
2638
+ case 'data-url':
2639
+ return {
2640
+ type: 'text',
2641
+ text: `[${part.data.title}](${part.data.url})`,
2642
+ };
2643
+ case 'data-code':
2644
+ return {
2645
+ type: 'text',
2646
+ text: `\`\`\`${part.data.language}\n${part.data.code}\n\`\`\``,
2647
+ };
2648
+ }
2649
+ },
2650
+ },
2651
+ );
2652
+
2653
+ expect(result).toMatchInlineSnapshot(`
2654
+ [
2655
+ {
2656
+ "content": [
2657
+ {
2658
+ "text": "Review these:",
2659
+ "type": "text",
2660
+ },
2661
+ {
2662
+ "text": "[Example](https://example.com)",
2663
+ "type": "text",
2664
+ },
2665
+ {
2666
+ "text": "\`\`\`javascript
2667
+ console.log("test")
2668
+ \`\`\`",
2669
+ "type": "text",
2670
+ },
2671
+ ],
2672
+ "role": "assistant",
2673
+ },
2674
+ ]
2675
+ `);
2676
+ });
2677
+
2678
+ it('should work with messages that have no data parts', async () => {
2679
+ const result = await convertToModelMessages(
2680
+ [
2681
+ {
2682
+ role: 'assistant',
2683
+ parts: [
2684
+ { type: 'text', text: 'Hello' },
2685
+ {
2686
+ type: 'file',
2687
+ mediaType: 'image/png',
2688
+ url: 'https://example.com/image.png',
2689
+ },
2690
+ ],
2691
+ },
2692
+ ],
2693
+ {
2694
+ convertDataPart: () => ({ type: 'text', text: 'converted' }),
2695
+ },
2696
+ );
2697
+
2698
+ expect(result).toMatchInlineSnapshot(`
2699
+ [
2700
+ {
2701
+ "content": [
2702
+ {
2703
+ "text": "Hello",
2704
+ "type": "text",
2705
+ },
2706
+ {
2707
+ "data": "https://example.com/image.png",
2708
+ "filename": undefined,
2709
+ "mediaType": "image/png",
2710
+ "type": "file",
2711
+ },
2712
+ ],
2713
+ "role": "assistant",
2714
+ },
2715
+ ]
2716
+ `);
2717
+ });
2718
+
2719
+ it('should preserve order of parts including converted data parts', async () => {
2720
+ const result = await convertToModelMessages<
2721
+ UIMessage<unknown, { tag: { value: string } }>
2722
+ >(
2723
+ [
2724
+ {
2725
+ role: 'assistant',
2726
+ parts: [
2727
+ { type: 'text', text: 'First' },
2728
+ { type: 'data-tag', data: { value: 'tag1' } },
2729
+ { type: 'text', text: 'Second' },
2730
+ { type: 'data-tag', data: { value: 'tag2' } },
2731
+ { type: 'text', text: 'Third' },
2732
+ ],
2733
+ },
2734
+ ],
2735
+ {
2736
+ convertDataPart: part => ({
2737
+ type: 'text',
2738
+ text: `[${part.data.value}]`,
2739
+ }),
2740
+ },
2741
+ );
2742
+
2743
+ expect(result).toMatchInlineSnapshot(`
2744
+ [
2745
+ {
2746
+ "content": [
2747
+ {
2748
+ "text": "First",
2749
+ "type": "text",
2750
+ },
2751
+ {
2752
+ "text": "[tag1]",
2753
+ "type": "text",
2754
+ },
2755
+ {
2756
+ "text": "Second",
2757
+ "type": "text",
2758
+ },
2759
+ {
2760
+ "text": "[tag2]",
2761
+ "type": "text",
2762
+ },
2763
+ {
2764
+ "text": "Third",
2765
+ "type": "text",
2766
+ },
2767
+ ],
2768
+ "role": "assistant",
2769
+ },
2770
+ ]
2771
+ `);
2772
+ });
2773
+ });
2774
+ });
2775
+ });