ai 6.0.33 → 6.0.35

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 (357) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/index.d.mts +50 -21
  3. package/dist/index.d.ts +50 -21
  4. package/dist/index.js +348 -286
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +280 -219
  7. package/dist/index.mjs.map +1 -1
  8. package/dist/internal/index.js +1 -1
  9. package/dist/internal/index.mjs +1 -1
  10. package/docs/02-foundations/03-prompts.mdx +2 -2
  11. package/docs/03-ai-sdk-core/15-tools-and-tool-calling.mdx +1 -1
  12. package/docs/07-reference/01-ai-sdk-core/28-output.mdx +1 -1
  13. package/docs/07-reference/05-ai-sdk-errors/ai-ui-message-stream-error.mdx +67 -0
  14. package/package.json +6 -4
  15. package/src/agent/agent.ts +116 -0
  16. package/src/agent/create-agent-ui-stream-response.test.ts +258 -0
  17. package/src/agent/create-agent-ui-stream-response.ts +50 -0
  18. package/src/agent/create-agent-ui-stream.ts +73 -0
  19. package/src/agent/index.ts +33 -0
  20. package/src/agent/infer-agent-tools.ts +7 -0
  21. package/src/agent/infer-agent-ui-message.test-d.ts +54 -0
  22. package/src/agent/infer-agent-ui-message.ts +11 -0
  23. package/src/agent/pipe-agent-ui-stream-to-response.ts +52 -0
  24. package/src/agent/tool-loop-agent-on-finish-callback.ts +31 -0
  25. package/src/agent/tool-loop-agent-on-step-finish-callback.ts +11 -0
  26. package/src/agent/tool-loop-agent-settings.ts +182 -0
  27. package/src/agent/tool-loop-agent.test-d.ts +114 -0
  28. package/src/agent/tool-loop-agent.test.ts +442 -0
  29. package/src/agent/tool-loop-agent.ts +114 -0
  30. package/src/embed/__snapshots__/embed-many.test.ts.snap +191 -0
  31. package/src/embed/__snapshots__/embed.test.ts.snap +81 -0
  32. package/src/embed/embed-many-result.ts +53 -0
  33. package/src/embed/embed-many.test.ts +653 -0
  34. package/src/embed/embed-many.ts +378 -0
  35. package/src/embed/embed-result.ts +50 -0
  36. package/src/embed/embed.test.ts +298 -0
  37. package/src/embed/embed.ts +211 -0
  38. package/src/embed/index.ts +4 -0
  39. package/src/error/index.ts +35 -0
  40. package/src/error/invalid-argument-error.ts +34 -0
  41. package/src/error/invalid-stream-part-error.ts +28 -0
  42. package/src/error/invalid-tool-approval-error.ts +26 -0
  43. package/src/error/invalid-tool-input-error.ts +33 -0
  44. package/src/error/no-image-generated-error.ts +39 -0
  45. package/src/error/no-object-generated-error.ts +70 -0
  46. package/src/error/no-output-generated-error.ts +26 -0
  47. package/src/error/no-speech-generated-error.ts +18 -0
  48. package/src/error/no-such-tool-error.ts +35 -0
  49. package/src/error/no-transcript-generated-error.ts +20 -0
  50. package/src/error/tool-call-not-found-for-approval-error.ts +32 -0
  51. package/src/error/tool-call-repair-error.ts +30 -0
  52. package/src/error/ui-message-stream-error.ts +48 -0
  53. package/src/error/unsupported-model-version-error.ts +23 -0
  54. package/src/error/verify-no-object-generated-error.ts +27 -0
  55. package/src/generate-image/generate-image-result.ts +42 -0
  56. package/src/generate-image/generate-image.test.ts +1420 -0
  57. package/src/generate-image/generate-image.ts +360 -0
  58. package/src/generate-image/index.ts +18 -0
  59. package/src/generate-object/__snapshots__/generate-object.test.ts.snap +133 -0
  60. package/src/generate-object/__snapshots__/stream-object.test.ts.snap +297 -0
  61. package/src/generate-object/generate-object-result.ts +67 -0
  62. package/src/generate-object/generate-object.test-d.ts +49 -0
  63. package/src/generate-object/generate-object.test.ts +1191 -0
  64. package/src/generate-object/generate-object.ts +518 -0
  65. package/src/generate-object/index.ts +9 -0
  66. package/src/generate-object/inject-json-instruction.test.ts +181 -0
  67. package/src/generate-object/inject-json-instruction.ts +30 -0
  68. package/src/generate-object/output-strategy.ts +415 -0
  69. package/src/generate-object/parse-and-validate-object-result.ts +111 -0
  70. package/src/generate-object/repair-text.ts +12 -0
  71. package/src/generate-object/stream-object-result.ts +120 -0
  72. package/src/generate-object/stream-object.test-d.ts +74 -0
  73. package/src/generate-object/stream-object.test.ts +1950 -0
  74. package/src/generate-object/stream-object.ts +986 -0
  75. package/src/generate-object/validate-object-generation-input.ts +144 -0
  76. package/src/generate-speech/generate-speech-result.ts +30 -0
  77. package/src/generate-speech/generate-speech.test.ts +300 -0
  78. package/src/generate-speech/generate-speech.ts +190 -0
  79. package/src/generate-speech/generated-audio-file.ts +65 -0
  80. package/src/generate-speech/index.ts +3 -0
  81. package/src/generate-text/__snapshots__/generate-text.test.ts.snap +1872 -0
  82. package/src/generate-text/__snapshots__/stream-text.test.ts.snap +1255 -0
  83. package/src/generate-text/collect-tool-approvals.test.ts +553 -0
  84. package/src/generate-text/collect-tool-approvals.ts +116 -0
  85. package/src/generate-text/content-part.ts +25 -0
  86. package/src/generate-text/execute-tool-call.ts +129 -0
  87. package/src/generate-text/extract-reasoning-content.ts +17 -0
  88. package/src/generate-text/extract-text-content.ts +15 -0
  89. package/src/generate-text/generate-text-result.ts +168 -0
  90. package/src/generate-text/generate-text.test-d.ts +68 -0
  91. package/src/generate-text/generate-text.test.ts +7011 -0
  92. package/src/generate-text/generate-text.ts +1223 -0
  93. package/src/generate-text/generated-file.ts +70 -0
  94. package/src/generate-text/index.ts +57 -0
  95. package/src/generate-text/is-approval-needed.ts +29 -0
  96. package/src/generate-text/output-utils.ts +23 -0
  97. package/src/generate-text/output.test.ts +698 -0
  98. package/src/generate-text/output.ts +590 -0
  99. package/src/generate-text/parse-tool-call.test.ts +570 -0
  100. package/src/generate-text/parse-tool-call.ts +188 -0
  101. package/src/generate-text/prepare-step.ts +103 -0
  102. package/src/generate-text/prune-messages.test.ts +720 -0
  103. package/src/generate-text/prune-messages.ts +167 -0
  104. package/src/generate-text/reasoning-output.ts +20 -0
  105. package/src/generate-text/reasoning.ts +8 -0
  106. package/src/generate-text/response-message.ts +10 -0
  107. package/src/generate-text/run-tools-transformation.test.ts +1143 -0
  108. package/src/generate-text/run-tools-transformation.ts +420 -0
  109. package/src/generate-text/smooth-stream.test.ts +2101 -0
  110. package/src/generate-text/smooth-stream.ts +162 -0
  111. package/src/generate-text/step-result.ts +238 -0
  112. package/src/generate-text/stop-condition.ts +29 -0
  113. package/src/generate-text/stream-text-result.ts +463 -0
  114. package/src/generate-text/stream-text.test-d.ts +200 -0
  115. package/src/generate-text/stream-text.test.ts +19979 -0
  116. package/src/generate-text/stream-text.ts +2505 -0
  117. package/src/generate-text/to-response-messages.test.ts +922 -0
  118. package/src/generate-text/to-response-messages.ts +163 -0
  119. package/src/generate-text/tool-approval-request-output.ts +21 -0
  120. package/src/generate-text/tool-call-repair-function.ts +27 -0
  121. package/src/generate-text/tool-call.ts +47 -0
  122. package/src/generate-text/tool-error.ts +34 -0
  123. package/src/generate-text/tool-output-denied.ts +21 -0
  124. package/src/generate-text/tool-output.ts +7 -0
  125. package/src/generate-text/tool-result.ts +36 -0
  126. package/src/generate-text/tool-set.ts +14 -0
  127. package/src/global.ts +24 -0
  128. package/src/index.ts +50 -0
  129. package/src/logger/index.ts +6 -0
  130. package/src/logger/log-warnings.test.ts +351 -0
  131. package/src/logger/log-warnings.ts +119 -0
  132. package/src/middleware/__snapshots__/simulate-streaming-middleware.test.ts.snap +64 -0
  133. package/src/middleware/add-tool-input-examples-middleware.test.ts +476 -0
  134. package/src/middleware/add-tool-input-examples-middleware.ts +90 -0
  135. package/src/middleware/default-embedding-settings-middleware.test.ts +126 -0
  136. package/src/middleware/default-embedding-settings-middleware.ts +22 -0
  137. package/src/middleware/default-settings-middleware.test.ts +388 -0
  138. package/src/middleware/default-settings-middleware.ts +33 -0
  139. package/src/middleware/extract-json-middleware.test.ts +827 -0
  140. package/src/middleware/extract-json-middleware.ts +197 -0
  141. package/src/middleware/extract-reasoning-middleware.test.ts +1028 -0
  142. package/src/middleware/extract-reasoning-middleware.ts +238 -0
  143. package/src/middleware/index.ts +10 -0
  144. package/src/middleware/simulate-streaming-middleware.test.ts +911 -0
  145. package/src/middleware/simulate-streaming-middleware.ts +79 -0
  146. package/src/middleware/wrap-embedding-model.test.ts +358 -0
  147. package/src/middleware/wrap-embedding-model.ts +86 -0
  148. package/src/middleware/wrap-image-model.test.ts +423 -0
  149. package/src/middleware/wrap-image-model.ts +85 -0
  150. package/src/middleware/wrap-language-model.test.ts +518 -0
  151. package/src/middleware/wrap-language-model.ts +104 -0
  152. package/src/middleware/wrap-provider.test.ts +120 -0
  153. package/src/middleware/wrap-provider.ts +51 -0
  154. package/src/model/as-embedding-model-v3.test.ts +319 -0
  155. package/src/model/as-embedding-model-v3.ts +24 -0
  156. package/src/model/as-image-model-v3.test.ts +409 -0
  157. package/src/model/as-image-model-v3.ts +24 -0
  158. package/src/model/as-language-model-v3.test.ts +508 -0
  159. package/src/model/as-language-model-v3.ts +103 -0
  160. package/src/model/as-provider-v3.ts +36 -0
  161. package/src/model/as-speech-model-v3.test.ts +356 -0
  162. package/src/model/as-speech-model-v3.ts +24 -0
  163. package/src/model/as-transcription-model-v3.test.ts +529 -0
  164. package/src/model/as-transcription-model-v3.ts +24 -0
  165. package/src/model/resolve-model.test.ts +244 -0
  166. package/src/model/resolve-model.ts +126 -0
  167. package/src/prompt/call-settings.ts +148 -0
  168. package/src/prompt/content-part.ts +209 -0
  169. package/src/prompt/convert-to-language-model-prompt.test.ts +2018 -0
  170. package/src/prompt/convert-to-language-model-prompt.ts +442 -0
  171. package/src/prompt/create-tool-model-output.test.ts +508 -0
  172. package/src/prompt/create-tool-model-output.ts +34 -0
  173. package/src/prompt/data-content.test.ts +15 -0
  174. package/src/prompt/data-content.ts +134 -0
  175. package/src/prompt/index.ts +27 -0
  176. package/src/prompt/invalid-data-content-error.ts +29 -0
  177. package/src/prompt/invalid-message-role-error.ts +27 -0
  178. package/src/prompt/message-conversion-error.ts +28 -0
  179. package/src/prompt/message.ts +68 -0
  180. package/src/prompt/prepare-call-settings.test.ts +159 -0
  181. package/src/prompt/prepare-call-settings.ts +108 -0
  182. package/src/prompt/prepare-tools-and-tool-choice.test.ts +461 -0
  183. package/src/prompt/prepare-tools-and-tool-choice.ts +86 -0
  184. package/src/prompt/prompt.ts +43 -0
  185. package/src/prompt/split-data-url.ts +17 -0
  186. package/src/prompt/standardize-prompt.test.ts +82 -0
  187. package/src/prompt/standardize-prompt.ts +99 -0
  188. package/src/prompt/wrap-gateway-error.ts +29 -0
  189. package/src/registry/custom-provider.test.ts +211 -0
  190. package/src/registry/custom-provider.ts +155 -0
  191. package/src/registry/index.ts +7 -0
  192. package/src/registry/no-such-provider-error.ts +41 -0
  193. package/src/registry/provider-registry.test.ts +691 -0
  194. package/src/registry/provider-registry.ts +328 -0
  195. package/src/rerank/index.ts +2 -0
  196. package/src/rerank/rerank-result.ts +70 -0
  197. package/src/rerank/rerank.test.ts +516 -0
  198. package/src/rerank/rerank.ts +237 -0
  199. package/src/telemetry/assemble-operation-name.ts +21 -0
  200. package/src/telemetry/get-base-telemetry-attributes.ts +53 -0
  201. package/src/telemetry/get-tracer.ts +20 -0
  202. package/src/telemetry/noop-tracer.ts +69 -0
  203. package/src/telemetry/record-span.ts +63 -0
  204. package/src/telemetry/select-telemetry-attributes.ts +78 -0
  205. package/src/telemetry/select-temetry-attributes.test.ts +114 -0
  206. package/src/telemetry/stringify-for-telemetry.test.ts +114 -0
  207. package/src/telemetry/stringify-for-telemetry.ts +33 -0
  208. package/src/telemetry/telemetry-settings.ts +44 -0
  209. package/src/test/mock-embedding-model-v2.ts +35 -0
  210. package/src/test/mock-embedding-model-v3.ts +48 -0
  211. package/src/test/mock-image-model-v2.ts +28 -0
  212. package/src/test/mock-image-model-v3.ts +28 -0
  213. package/src/test/mock-language-model-v2.ts +72 -0
  214. package/src/test/mock-language-model-v3.ts +77 -0
  215. package/src/test/mock-provider-v2.ts +68 -0
  216. package/src/test/mock-provider-v3.ts +80 -0
  217. package/src/test/mock-reranking-model-v3.ts +25 -0
  218. package/src/test/mock-server-response.ts +69 -0
  219. package/src/test/mock-speech-model-v2.ts +24 -0
  220. package/src/test/mock-speech-model-v3.ts +24 -0
  221. package/src/test/mock-tracer.ts +156 -0
  222. package/src/test/mock-transcription-model-v2.ts +24 -0
  223. package/src/test/mock-transcription-model-v3.ts +24 -0
  224. package/src/test/mock-values.ts +4 -0
  225. package/src/test/not-implemented.ts +3 -0
  226. package/src/text-stream/create-text-stream-response.test.ts +38 -0
  227. package/src/text-stream/create-text-stream-response.ts +18 -0
  228. package/src/text-stream/index.ts +2 -0
  229. package/src/text-stream/pipe-text-stream-to-response.test.ts +38 -0
  230. package/src/text-stream/pipe-text-stream-to-response.ts +26 -0
  231. package/src/transcribe/index.ts +2 -0
  232. package/src/transcribe/transcribe-result.ts +60 -0
  233. package/src/transcribe/transcribe.test.ts +313 -0
  234. package/src/transcribe/transcribe.ts +173 -0
  235. package/src/types/embedding-model-middleware.ts +3 -0
  236. package/src/types/embedding-model.ts +18 -0
  237. package/src/types/image-model-middleware.ts +3 -0
  238. package/src/types/image-model-response-metadata.ts +16 -0
  239. package/src/types/image-model.ts +19 -0
  240. package/src/types/index.ts +29 -0
  241. package/src/types/json-value.ts +15 -0
  242. package/src/types/language-model-middleware.ts +3 -0
  243. package/src/types/language-model-request-metadata.ts +6 -0
  244. package/src/types/language-model-response-metadata.ts +21 -0
  245. package/src/types/language-model.ts +104 -0
  246. package/src/types/provider-metadata.ts +16 -0
  247. package/src/types/provider.ts +55 -0
  248. package/src/types/reranking-model.ts +6 -0
  249. package/src/types/speech-model-response-metadata.ts +21 -0
  250. package/src/types/speech-model.ts +6 -0
  251. package/src/types/transcription-model-response-metadata.ts +16 -0
  252. package/src/types/transcription-model.ts +9 -0
  253. package/src/types/usage.ts +200 -0
  254. package/src/types/warning.ts +7 -0
  255. package/src/ui/__snapshots__/append-response-messages.test.ts.snap +416 -0
  256. package/src/ui/__snapshots__/convert-to-model-messages.test.ts.snap +419 -0
  257. package/src/ui/__snapshots__/process-chat-text-response.test.ts.snap +142 -0
  258. package/src/ui/call-completion-api.ts +157 -0
  259. package/src/ui/chat-transport.ts +83 -0
  260. package/src/ui/chat.test-d.ts +233 -0
  261. package/src/ui/chat.test.ts +2695 -0
  262. package/src/ui/chat.ts +716 -0
  263. package/src/ui/convert-file-list-to-file-ui-parts.ts +36 -0
  264. package/src/ui/convert-to-model-messages.test.ts +2775 -0
  265. package/src/ui/convert-to-model-messages.ts +373 -0
  266. package/src/ui/default-chat-transport.ts +36 -0
  267. package/src/ui/direct-chat-transport.test.ts +446 -0
  268. package/src/ui/direct-chat-transport.ts +118 -0
  269. package/src/ui/http-chat-transport.test.ts +185 -0
  270. package/src/ui/http-chat-transport.ts +292 -0
  271. package/src/ui/index.ts +71 -0
  272. package/src/ui/last-assistant-message-is-complete-with-approval-responses.ts +44 -0
  273. package/src/ui/last-assistant-message-is-complete-with-tool-calls.test.ts +371 -0
  274. package/src/ui/last-assistant-message-is-complete-with-tool-calls.ts +39 -0
  275. package/src/ui/process-text-stream.test.ts +38 -0
  276. package/src/ui/process-text-stream.ts +16 -0
  277. package/src/ui/process-ui-message-stream.test.ts +8294 -0
  278. package/src/ui/process-ui-message-stream.ts +761 -0
  279. package/src/ui/text-stream-chat-transport.ts +23 -0
  280. package/src/ui/transform-text-to-ui-message-stream.test.ts +124 -0
  281. package/src/ui/transform-text-to-ui-message-stream.ts +27 -0
  282. package/src/ui/ui-messages.test.ts +48 -0
  283. package/src/ui/ui-messages.ts +534 -0
  284. package/src/ui/use-completion.ts +84 -0
  285. package/src/ui/validate-ui-messages.test.ts +1428 -0
  286. package/src/ui/validate-ui-messages.ts +476 -0
  287. package/src/ui-message-stream/create-ui-message-stream-response.test.ts +266 -0
  288. package/src/ui-message-stream/create-ui-message-stream-response.ts +32 -0
  289. package/src/ui-message-stream/create-ui-message-stream.test.ts +639 -0
  290. package/src/ui-message-stream/create-ui-message-stream.ts +124 -0
  291. package/src/ui-message-stream/get-response-ui-message-id.test.ts +55 -0
  292. package/src/ui-message-stream/get-response-ui-message-id.ts +24 -0
  293. package/src/ui-message-stream/handle-ui-message-stream-finish.test.ts +429 -0
  294. package/src/ui-message-stream/handle-ui-message-stream-finish.ts +135 -0
  295. package/src/ui-message-stream/index.ts +13 -0
  296. package/src/ui-message-stream/json-to-sse-transform-stream.ts +12 -0
  297. package/src/ui-message-stream/pipe-ui-message-stream-to-response.test.ts +90 -0
  298. package/src/ui-message-stream/pipe-ui-message-stream-to-response.ts +40 -0
  299. package/src/ui-message-stream/read-ui-message-stream.test.ts +122 -0
  300. package/src/ui-message-stream/read-ui-message-stream.ts +87 -0
  301. package/src/ui-message-stream/ui-message-chunks.test-d.ts +18 -0
  302. package/src/ui-message-stream/ui-message-chunks.ts +344 -0
  303. package/src/ui-message-stream/ui-message-stream-headers.ts +7 -0
  304. package/src/ui-message-stream/ui-message-stream-on-finish-callback.ts +32 -0
  305. package/src/ui-message-stream/ui-message-stream-response-init.ts +5 -0
  306. package/src/ui-message-stream/ui-message-stream-writer.ts +24 -0
  307. package/src/util/as-array.ts +3 -0
  308. package/src/util/async-iterable-stream.test.ts +241 -0
  309. package/src/util/async-iterable-stream.ts +94 -0
  310. package/src/util/consume-stream.ts +29 -0
  311. package/src/util/cosine-similarity.test.ts +57 -0
  312. package/src/util/cosine-similarity.ts +47 -0
  313. package/src/util/create-resolvable-promise.ts +30 -0
  314. package/src/util/create-stitchable-stream.test.ts +239 -0
  315. package/src/util/create-stitchable-stream.ts +112 -0
  316. package/src/util/data-url.ts +17 -0
  317. package/src/util/deep-partial.ts +84 -0
  318. package/src/util/detect-media-type.test.ts +670 -0
  319. package/src/util/detect-media-type.ts +184 -0
  320. package/src/util/download/download-function.ts +45 -0
  321. package/src/util/download/download.test.ts +69 -0
  322. package/src/util/download/download.ts +46 -0
  323. package/src/util/error-handler.ts +1 -0
  324. package/src/util/fix-json.test.ts +279 -0
  325. package/src/util/fix-json.ts +401 -0
  326. package/src/util/get-potential-start-index.test.ts +34 -0
  327. package/src/util/get-potential-start-index.ts +30 -0
  328. package/src/util/index.ts +11 -0
  329. package/src/util/is-deep-equal-data.test.ts +119 -0
  330. package/src/util/is-deep-equal-data.ts +48 -0
  331. package/src/util/is-non-empty-object.ts +5 -0
  332. package/src/util/job.ts +1 -0
  333. package/src/util/log-v2-compatibility-warning.ts +21 -0
  334. package/src/util/merge-abort-signals.test.ts +155 -0
  335. package/src/util/merge-abort-signals.ts +43 -0
  336. package/src/util/merge-objects.test.ts +118 -0
  337. package/src/util/merge-objects.ts +79 -0
  338. package/src/util/now.ts +4 -0
  339. package/src/util/parse-partial-json.test.ts +80 -0
  340. package/src/util/parse-partial-json.ts +30 -0
  341. package/src/util/prepare-headers.test.ts +51 -0
  342. package/src/util/prepare-headers.ts +14 -0
  343. package/src/util/prepare-retries.test.ts +10 -0
  344. package/src/util/prepare-retries.ts +47 -0
  345. package/src/util/retry-error.ts +41 -0
  346. package/src/util/retry-with-exponential-backoff.test.ts +446 -0
  347. package/src/util/retry-with-exponential-backoff.ts +154 -0
  348. package/src/util/serial-job-executor.test.ts +162 -0
  349. package/src/util/serial-job-executor.ts +36 -0
  350. package/src/util/simulate-readable-stream.test.ts +98 -0
  351. package/src/util/simulate-readable-stream.ts +39 -0
  352. package/src/util/split-array.test.ts +60 -0
  353. package/src/util/split-array.ts +20 -0
  354. package/src/util/value-of.ts +65 -0
  355. package/src/util/write-to-server-response.test.ts +266 -0
  356. package/src/util/write-to-server-response.ts +49 -0
  357. package/src/version.ts +5 -0
@@ -0,0 +1,1420 @@
1
+ import { ImageModelV3, ImageModelV3ProviderMetadata } from '@ai-sdk/provider';
2
+ import {
3
+ convertBase64ToUint8Array,
4
+ convertUint8ArrayToBase64,
5
+ } from '@ai-sdk/provider-utils';
6
+ import {
7
+ afterEach,
8
+ beforeEach,
9
+ describe,
10
+ expect,
11
+ it,
12
+ test,
13
+ vi,
14
+ vitest,
15
+ } from 'vitest';
16
+ import * as logWarningsModule from '../logger/log-warnings';
17
+ import { MockImageModelV3 } from '../test/mock-image-model-v3';
18
+ import { Warning } from '../types/warning';
19
+ import { generateImage } from './generate-image';
20
+
21
+ const prompt = 'sunny day at the beach';
22
+ const testDate = new Date(2024, 0, 1);
23
+
24
+ const pngBase64 =
25
+ 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg=='; // 1x1 transparent PNG
26
+ const jpegBase64 =
27
+ '/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAb/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k='; // 1x1 black JPEG
28
+ const gifBase64 = 'R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs='; // 1x1 transparent GIF
29
+
30
+ vi.mock('../version', () => {
31
+ return {
32
+ VERSION: '0.0.0-test',
33
+ };
34
+ });
35
+
36
+ const createMockResponse = (options: {
37
+ images: string[] | Uint8Array[];
38
+ warnings?: Warning[];
39
+ timestamp?: Date;
40
+ modelId?: string;
41
+ providerMetaData?: ImageModelV3ProviderMetadata;
42
+ headers?: Record<string, string>;
43
+ }) => ({
44
+ images: options.images,
45
+ warnings: options.warnings ?? [],
46
+ providerMetadata: options.providerMetaData ?? {
47
+ testProvider: {
48
+ images: options.images.map(() => null),
49
+ },
50
+ },
51
+ response: {
52
+ timestamp: options.timestamp ?? new Date(),
53
+ modelId: options.modelId ?? 'test-model-id',
54
+ headers: options.headers ?? {},
55
+ },
56
+ });
57
+
58
+ describe('generateImage', () => {
59
+ let logWarningsSpy: ReturnType<typeof vitest.spyOn>;
60
+
61
+ beforeEach(() => {
62
+ logWarningsSpy = vitest
63
+ .spyOn(logWarningsModule, 'logWarnings')
64
+ .mockImplementation(() => {});
65
+ });
66
+
67
+ afterEach(() => {
68
+ logWarningsSpy.mockRestore();
69
+ });
70
+
71
+ it('should send args to doGenerate', async () => {
72
+ const abortController = new AbortController();
73
+ const abortSignal = abortController.signal;
74
+
75
+ let capturedArgs!: Parameters<ImageModelV3['doGenerate']>[0];
76
+
77
+ await generateImage({
78
+ model: new MockImageModelV3({
79
+ doGenerate: async args => {
80
+ capturedArgs = args;
81
+ return createMockResponse({
82
+ images: [pngBase64],
83
+ });
84
+ },
85
+ }),
86
+ prompt: {
87
+ text: prompt,
88
+ images: [pngBase64],
89
+ mask: pngBase64,
90
+ },
91
+ size: '1024x1024',
92
+ aspectRatio: '16:9',
93
+ seed: 12345,
94
+ providerOptions: {
95
+ 'mock-provider': {
96
+ style: 'vivid',
97
+ },
98
+ },
99
+ headers: {
100
+ 'custom-request-header': 'request-header-value',
101
+ },
102
+ abortSignal,
103
+ });
104
+
105
+ expect(capturedArgs).toStrictEqual({
106
+ n: 1,
107
+ prompt,
108
+ mask: {
109
+ type: 'file',
110
+ data: convertBase64ToUint8Array(pngBase64),
111
+ mediaType: 'image/png',
112
+ },
113
+ files: [
114
+ {
115
+ type: 'file',
116
+ data: convertBase64ToUint8Array(pngBase64),
117
+ mediaType: 'image/png',
118
+ },
119
+ ],
120
+ size: '1024x1024',
121
+ aspectRatio: '16:9',
122
+ seed: 12345,
123
+ providerOptions: { 'mock-provider': { style: 'vivid' } },
124
+ headers: {
125
+ 'custom-request-header': 'request-header-value',
126
+ 'user-agent': 'ai/0.0.0-test',
127
+ },
128
+ abortSignal,
129
+ });
130
+ });
131
+
132
+ it('should return warnings', async () => {
133
+ const result = await generateImage({
134
+ model: new MockImageModelV3({
135
+ doGenerate: async () =>
136
+ createMockResponse({
137
+ images: [pngBase64],
138
+ warnings: [
139
+ {
140
+ type: 'other',
141
+ message: 'Setting is not supported',
142
+ },
143
+ ],
144
+ }),
145
+ }),
146
+ prompt,
147
+ });
148
+
149
+ expect(result.warnings).toStrictEqual([
150
+ {
151
+ type: 'other',
152
+ message: 'Setting is not supported',
153
+ },
154
+ ]);
155
+ });
156
+
157
+ it('should call logWarnings with the correct warnings', async () => {
158
+ const expectedWarnings: Warning[] = [
159
+ {
160
+ type: 'other',
161
+ message: 'Setting is not supported',
162
+ },
163
+ {
164
+ type: 'unsupported',
165
+ feature: 'size',
166
+ details: 'Size parameter not supported',
167
+ },
168
+ ];
169
+
170
+ await generateImage({
171
+ model: new MockImageModelV3({
172
+ doGenerate: async () =>
173
+ createMockResponse({
174
+ images: [pngBase64],
175
+ warnings: expectedWarnings,
176
+ }),
177
+ }),
178
+ prompt,
179
+ });
180
+
181
+ expect(logWarningsSpy).toHaveBeenCalledOnce();
182
+ expect(logWarningsSpy).toHaveBeenCalledWith({
183
+ warnings: expectedWarnings,
184
+ provider: 'mock-provider',
185
+ model: 'mock-model-id',
186
+ });
187
+ });
188
+
189
+ it('should call logWarnings with aggregated warnings from multiple calls', async () => {
190
+ const warning1: Warning = {
191
+ type: 'other',
192
+ message: 'Warning from call 1',
193
+ };
194
+ const warning2: Warning = {
195
+ type: 'other',
196
+ message: 'Warning from call 2',
197
+ };
198
+ const expectedAggregatedWarnings = [warning1, warning2];
199
+
200
+ let callCount = 0;
201
+
202
+ await generateImage({
203
+ model: new MockImageModelV3({
204
+ maxImagesPerCall: 1,
205
+ doGenerate: async () => {
206
+ switch (callCount++) {
207
+ case 0:
208
+ return createMockResponse({
209
+ images: [pngBase64],
210
+ warnings: [warning1],
211
+ });
212
+ case 1:
213
+ return createMockResponse({
214
+ images: [jpegBase64],
215
+ warnings: [warning2],
216
+ });
217
+ default:
218
+ throw new Error('Unexpected call');
219
+ }
220
+ },
221
+ }),
222
+ prompt,
223
+ n: 2,
224
+ });
225
+
226
+ expect(logWarningsSpy).toHaveBeenCalledOnce();
227
+ expect(logWarningsSpy).toHaveBeenCalledWith({
228
+ warnings: expectedAggregatedWarnings,
229
+ provider: 'mock-provider',
230
+ model: 'mock-model-id',
231
+ });
232
+ });
233
+
234
+ it('should call logWarnings with empty array when no warnings are present', async () => {
235
+ await generateImage({
236
+ model: new MockImageModelV3({
237
+ doGenerate: async () =>
238
+ createMockResponse({
239
+ images: [pngBase64],
240
+ warnings: [], // no warnings
241
+ }),
242
+ }),
243
+ prompt,
244
+ });
245
+
246
+ expect(logWarningsSpy).toHaveBeenCalledOnce();
247
+ expect(logWarningsSpy).toHaveBeenCalledWith({
248
+ warnings: [],
249
+ provider: 'mock-provider',
250
+ model: 'mock-model-id',
251
+ });
252
+ });
253
+
254
+ describe('base64 image data', () => {
255
+ it('should return generated images with correct mime types', async () => {
256
+ const result = await generateImage({
257
+ model: new MockImageModelV3({
258
+ doGenerate: async () =>
259
+ createMockResponse({
260
+ images: [pngBase64, jpegBase64],
261
+ }),
262
+ }),
263
+ prompt,
264
+ });
265
+
266
+ expect(
267
+ result.images.map(image => ({
268
+ base64: image.base64,
269
+ uint8Array: image.uint8Array,
270
+ mediaType: image.mediaType,
271
+ })),
272
+ ).toStrictEqual([
273
+ {
274
+ base64: pngBase64,
275
+ uint8Array: convertBase64ToUint8Array(pngBase64),
276
+ mediaType: 'image/png',
277
+ },
278
+ {
279
+ base64: jpegBase64,
280
+ uint8Array: convertBase64ToUint8Array(jpegBase64),
281
+ mediaType: 'image/jpeg',
282
+ },
283
+ ]);
284
+ });
285
+
286
+ it('should return the first image with correct mime type', async () => {
287
+ const result = await generateImage({
288
+ model: new MockImageModelV3({
289
+ doGenerate: async () =>
290
+ createMockResponse({
291
+ images: [pngBase64, jpegBase64],
292
+ }),
293
+ }),
294
+ prompt,
295
+ });
296
+
297
+ expect({
298
+ base64: result.image.base64,
299
+ uint8Array: result.image.uint8Array,
300
+ mediaType: result.image.mediaType,
301
+ }).toStrictEqual({
302
+ base64: pngBase64,
303
+ uint8Array: convertBase64ToUint8Array(pngBase64),
304
+ mediaType: 'image/png',
305
+ });
306
+ });
307
+ });
308
+
309
+ describe('uint8array image data', () => {
310
+ it('should return generated images', async () => {
311
+ const uint8ArrayImages = [
312
+ convertBase64ToUint8Array(pngBase64),
313
+ convertBase64ToUint8Array(jpegBase64),
314
+ ];
315
+
316
+ const result = await generateImage({
317
+ model: new MockImageModelV3({
318
+ doGenerate: async () =>
319
+ createMockResponse({
320
+ images: uint8ArrayImages,
321
+ }),
322
+ }),
323
+ prompt,
324
+ });
325
+
326
+ expect(
327
+ result.images.map(image => ({
328
+ base64: image.base64,
329
+ uint8Array: image.uint8Array,
330
+ })),
331
+ ).toStrictEqual([
332
+ {
333
+ base64: convertUint8ArrayToBase64(uint8ArrayImages[0]),
334
+ uint8Array: uint8ArrayImages[0],
335
+ },
336
+ {
337
+ base64: convertUint8ArrayToBase64(uint8ArrayImages[1]),
338
+ uint8Array: uint8ArrayImages[1],
339
+ },
340
+ ]);
341
+ });
342
+ });
343
+
344
+ describe('when several calls are required', () => {
345
+ it('should generate images', async () => {
346
+ const base64Images = [pngBase64, jpegBase64, gifBase64];
347
+
348
+ let callCount = 0;
349
+
350
+ const result = await generateImage({
351
+ model: new MockImageModelV3({
352
+ maxImagesPerCall: 2,
353
+ doGenerate: async options => {
354
+ switch (callCount++) {
355
+ case 0:
356
+ expect(options).toStrictEqual({
357
+ prompt,
358
+ files: undefined,
359
+ mask: undefined,
360
+ n: 2,
361
+ seed: 12345,
362
+ size: '1024x1024',
363
+ aspectRatio: '16:9',
364
+ providerOptions: {
365
+ 'mock-provider': { style: 'vivid' },
366
+ },
367
+ headers: {
368
+ 'custom-request-header': 'request-header-value',
369
+ 'user-agent': 'ai/0.0.0-test',
370
+ },
371
+ abortSignal: undefined,
372
+ });
373
+ return createMockResponse({
374
+ images: base64Images.slice(0, 2),
375
+ });
376
+ case 1:
377
+ expect(options).toStrictEqual({
378
+ prompt,
379
+ files: undefined,
380
+ mask: undefined,
381
+ n: 1,
382
+ seed: 12345,
383
+ size: '1024x1024',
384
+ aspectRatio: '16:9',
385
+ providerOptions: { 'mock-provider': { style: 'vivid' } },
386
+ headers: {
387
+ 'custom-request-header': 'request-header-value',
388
+ 'user-agent': 'ai/0.0.0-test',
389
+ },
390
+ abortSignal: undefined,
391
+ });
392
+ return createMockResponse({
393
+ images: base64Images.slice(2),
394
+ });
395
+ default:
396
+ throw new Error('Unexpected call');
397
+ }
398
+ },
399
+ }),
400
+ prompt,
401
+ n: 3,
402
+ size: '1024x1024',
403
+ aspectRatio: '16:9',
404
+ seed: 12345,
405
+ providerOptions: { 'mock-provider': { style: 'vivid' } },
406
+ headers: {
407
+ 'custom-request-header': 'request-header-value',
408
+ },
409
+ });
410
+
411
+ expect(result.images.map(image => image.base64)).toStrictEqual(
412
+ base64Images,
413
+ );
414
+ });
415
+
416
+ it('should aggregate warnings', async () => {
417
+ const base64Images = [pngBase64, jpegBase64, gifBase64];
418
+
419
+ let callCount = 0;
420
+
421
+ const result = await generateImage({
422
+ model: new MockImageModelV3({
423
+ maxImagesPerCall: 2,
424
+ doGenerate: async options => {
425
+ switch (callCount++) {
426
+ case 0:
427
+ expect(options).toStrictEqual({
428
+ prompt,
429
+ files: undefined,
430
+ mask: undefined,
431
+ n: 2,
432
+ seed: 12345,
433
+ size: '1024x1024',
434
+ aspectRatio: '16:9',
435
+ providerOptions: { 'mock-provider': { style: 'vivid' } },
436
+ headers: {
437
+ 'custom-request-header': 'request-header-value',
438
+ 'user-agent': 'ai/0.0.0-test',
439
+ },
440
+ abortSignal: undefined,
441
+ });
442
+ return createMockResponse({
443
+ images: base64Images.slice(0, 2),
444
+ warnings: [{ type: 'other', message: '1' }],
445
+ });
446
+ case 1:
447
+ expect(options).toStrictEqual({
448
+ prompt,
449
+ files: undefined,
450
+ mask: undefined,
451
+ n: 1,
452
+ seed: 12345,
453
+ size: '1024x1024',
454
+ aspectRatio: '16:9',
455
+ providerOptions: { 'mock-provider': { style: 'vivid' } },
456
+ headers: {
457
+ 'custom-request-header': 'request-header-value',
458
+ 'user-agent': 'ai/0.0.0-test',
459
+ },
460
+ abortSignal: undefined,
461
+ });
462
+ return createMockResponse({
463
+ images: base64Images.slice(2),
464
+ warnings: [{ type: 'other', message: '2' }],
465
+ });
466
+ default:
467
+ throw new Error('Unexpected call');
468
+ }
469
+ },
470
+ }),
471
+ prompt,
472
+ n: 3,
473
+ size: '1024x1024',
474
+ aspectRatio: '16:9',
475
+ seed: 12345,
476
+ providerOptions: { 'mock-provider': { style: 'vivid' } },
477
+ headers: {
478
+ 'custom-request-header': 'request-header-value',
479
+ },
480
+ });
481
+
482
+ expect(result.warnings).toStrictEqual([
483
+ { type: 'other', message: '1' },
484
+ { type: 'other', message: '2' },
485
+ ]);
486
+ });
487
+
488
+ test.each([
489
+ ['sync method', () => 2],
490
+ ['async method', async () => 2],
491
+ ])(
492
+ 'should generate with maxImagesPerCall = %s',
493
+ async (_, maxImagesPerCall) => {
494
+ const base64Images = [pngBase64, jpegBase64, gifBase64];
495
+
496
+ let callCount = 0;
497
+ const maxImagesPerCallMock = vitest.fn(maxImagesPerCall);
498
+
499
+ const result = await generateImage({
500
+ model: new MockImageModelV3({
501
+ maxImagesPerCall: maxImagesPerCallMock,
502
+ doGenerate: async options => {
503
+ switch (callCount++) {
504
+ case 0:
505
+ expect(options).toStrictEqual({
506
+ prompt,
507
+ files: undefined,
508
+ mask: undefined,
509
+ n: 2,
510
+ seed: 12345,
511
+ size: '1024x1024',
512
+ aspectRatio: '16:9',
513
+ providerOptions: {
514
+ 'mock-provider': { style: 'vivid' },
515
+ },
516
+ headers: {
517
+ 'custom-request-header': 'request-header-value',
518
+ 'user-agent': 'ai/0.0.0-test',
519
+ },
520
+ abortSignal: undefined,
521
+ });
522
+ return createMockResponse({
523
+ images: base64Images.slice(0, 2),
524
+ });
525
+ case 1:
526
+ expect(options).toStrictEqual({
527
+ prompt,
528
+ files: undefined,
529
+ mask: undefined,
530
+ n: 1,
531
+ seed: 12345,
532
+ size: '1024x1024',
533
+ aspectRatio: '16:9',
534
+ providerOptions: { 'mock-provider': { style: 'vivid' } },
535
+ headers: {
536
+ 'custom-request-header': 'request-header-value',
537
+ 'user-agent': 'ai/0.0.0-test',
538
+ },
539
+ abortSignal: undefined,
540
+ });
541
+ return createMockResponse({
542
+ images: base64Images.slice(2),
543
+ });
544
+ default:
545
+ throw new Error('Unexpected call');
546
+ }
547
+ },
548
+ }),
549
+ prompt,
550
+ n: 3,
551
+ size: '1024x1024',
552
+ aspectRatio: '16:9',
553
+ seed: 12345,
554
+ providerOptions: { 'mock-provider': { style: 'vivid' } },
555
+ headers: {
556
+ 'custom-request-header': 'request-header-value',
557
+ },
558
+ });
559
+
560
+ expect(result.images.map(image => image.base64)).toStrictEqual(
561
+ base64Images,
562
+ );
563
+ expect(maxImagesPerCallMock).toHaveBeenCalledTimes(1);
564
+ expect(maxImagesPerCallMock).toHaveBeenCalledWith({
565
+ modelId: 'mock-model-id',
566
+ });
567
+ },
568
+ );
569
+ });
570
+
571
+ describe('error handling', () => {
572
+ it('should throw NoImageGeneratedError when no images are returned', async () => {
573
+ await expect(
574
+ generateImage({
575
+ model: new MockImageModelV3({
576
+ doGenerate: async () =>
577
+ createMockResponse({
578
+ images: [],
579
+ timestamp: testDate,
580
+ }),
581
+ }),
582
+ prompt,
583
+ }),
584
+ ).rejects.toMatchObject({
585
+ name: 'AI_NoImageGeneratedError',
586
+ message: 'No image generated.',
587
+ responses: [
588
+ {
589
+ timestamp: testDate,
590
+ modelId: expect.any(String),
591
+ },
592
+ ],
593
+ });
594
+ });
595
+
596
+ it('should include response headers in error when no images generated', async () => {
597
+ await expect(
598
+ generateImage({
599
+ model: new MockImageModelV3({
600
+ doGenerate: async () =>
601
+ createMockResponse({
602
+ images: [],
603
+ timestamp: testDate,
604
+ headers: {
605
+ 'custom-response-header': 'response-header-value',
606
+ 'user-agent': 'ai/0.0.0-test',
607
+ },
608
+ }),
609
+ }),
610
+ prompt,
611
+ }),
612
+ ).rejects.toMatchObject({
613
+ name: 'AI_NoImageGeneratedError',
614
+ message: 'No image generated.',
615
+ responses: [
616
+ {
617
+ timestamp: testDate,
618
+ modelId: expect.any(String),
619
+ headers: {
620
+ 'custom-response-header': 'response-header-value',
621
+ 'user-agent': 'ai/0.0.0-test',
622
+ },
623
+ },
624
+ ],
625
+ });
626
+ });
627
+ });
628
+
629
+ it('should return response metadata', async () => {
630
+ const testHeaders = { 'x-test': 'value' };
631
+
632
+ const result = await generateImage({
633
+ model: new MockImageModelV3({
634
+ doGenerate: async () =>
635
+ createMockResponse({
636
+ images: [pngBase64],
637
+ timestamp: testDate,
638
+ modelId: 'test-model',
639
+ headers: testHeaders,
640
+ }),
641
+ }),
642
+ prompt,
643
+ });
644
+
645
+ expect(result.responses).toStrictEqual([
646
+ {
647
+ timestamp: testDate,
648
+ modelId: 'test-model',
649
+ headers: testHeaders,
650
+ },
651
+ ]);
652
+ });
653
+
654
+ it('should return provider metadata', async () => {
655
+ const result = await generateImage({
656
+ model: new MockImageModelV3({
657
+ doGenerate: async () =>
658
+ createMockResponse({
659
+ images: [pngBase64, pngBase64],
660
+ timestamp: testDate,
661
+ modelId: 'test-model',
662
+ providerMetaData: {
663
+ testProvider: {
664
+ images: [{ revisedPrompt: 'test-revised-prompt' }, null],
665
+ },
666
+ },
667
+ headers: {},
668
+ }),
669
+ }),
670
+ prompt,
671
+ });
672
+
673
+ expect(result.providerMetadata).toStrictEqual({
674
+ testProvider: {
675
+ images: [{ revisedPrompt: 'test-revised-prompt' }, null],
676
+ },
677
+ });
678
+ });
679
+
680
+ it('should expose empty usage when provider does not report usage', async () => {
681
+ const result = await generateImage({
682
+ model: new MockImageModelV3({
683
+ doGenerate: async () =>
684
+ createMockResponse({
685
+ images: [pngBase64],
686
+ }),
687
+ }),
688
+ prompt,
689
+ });
690
+
691
+ expect(result.usage).toStrictEqual({
692
+ inputTokens: undefined,
693
+ outputTokens: undefined,
694
+ totalTokens: undefined,
695
+ });
696
+ });
697
+
698
+ it('should aggregate usage across multiple provider calls', async () => {
699
+ let callCount = 0;
700
+
701
+ const result = await generateImage({
702
+ model: new MockImageModelV3({
703
+ maxImagesPerCall: 1,
704
+ doGenerate: async () => {
705
+ switch (callCount++) {
706
+ case 0:
707
+ return {
708
+ images: [pngBase64],
709
+ warnings: [],
710
+ providerMetadata: {
711
+ testProvider: { images: [null] },
712
+ },
713
+ response: {
714
+ timestamp: new Date(),
715
+ modelId: 'mock-model-id',
716
+ headers: {},
717
+ },
718
+ usage: {
719
+ inputTokens: 10,
720
+ outputTokens: 0,
721
+ totalTokens: 10,
722
+ },
723
+ };
724
+ case 1:
725
+ return {
726
+ images: [jpegBase64],
727
+ warnings: [],
728
+ providerMetadata: {
729
+ testProvider: { images: [null] },
730
+ },
731
+ response: {
732
+ timestamp: new Date(),
733
+ modelId: 'mock-model-id',
734
+ headers: {},
735
+ },
736
+ usage: {
737
+ inputTokens: 5,
738
+ outputTokens: 0,
739
+ totalTokens: 5,
740
+ },
741
+ };
742
+ default:
743
+ throw new Error('Unexpected call');
744
+ }
745
+ },
746
+ }),
747
+ prompt,
748
+ n: 2,
749
+ });
750
+
751
+ expect(result.images.map(image => image.base64)).toStrictEqual([
752
+ pngBase64,
753
+ jpegBase64,
754
+ ]);
755
+ expect(result.usage).toStrictEqual({
756
+ inputTokens: 15,
757
+ outputTokens: 0,
758
+ totalTokens: 15,
759
+ });
760
+ });
761
+ describe('provider metadata merging', () => {
762
+ it('should merge provider metadata from multiple calls', async () => {
763
+ let callCount = 0;
764
+
765
+ const result = await generateImage({
766
+ model: new MockImageModelV3({
767
+ maxImagesPerCall: 1,
768
+ doGenerate: async () => {
769
+ switch (callCount++) {
770
+ case 0:
771
+ return createMockResponse({
772
+ images: [pngBase64],
773
+ providerMetaData: {
774
+ testProvider: {
775
+ images: [{ revisedPrompt: 'prompt-1' }],
776
+ },
777
+ },
778
+ });
779
+ case 1:
780
+ return createMockResponse({
781
+ images: [jpegBase64],
782
+ providerMetaData: {
783
+ testProvider: {
784
+ images: [{ revisedPrompt: 'prompt-2' }],
785
+ },
786
+ },
787
+ });
788
+ default:
789
+ throw new Error('Unexpected call');
790
+ }
791
+ },
792
+ }),
793
+ prompt,
794
+ n: 2,
795
+ });
796
+
797
+ expect(result.providerMetadata).toStrictEqual({
798
+ testProvider: {
799
+ images: [
800
+ { revisedPrompt: 'prompt-1' },
801
+ { revisedPrompt: 'prompt-2' },
802
+ ],
803
+ },
804
+ });
805
+ });
806
+
807
+ it('should merge non-image provider metadata fields', async () => {
808
+ let callCount = 0;
809
+
810
+ const result = await generateImage({
811
+ model: new MockImageModelV3({
812
+ maxImagesPerCall: 1,
813
+ doGenerate: async () => {
814
+ switch (callCount++) {
815
+ case 0:
816
+ return createMockResponse({
817
+ images: [pngBase64],
818
+ providerMetaData: {
819
+ gateway: {
820
+ images: [],
821
+ routing: { provider: 'test1' },
822
+ cost: '0.01',
823
+ },
824
+ },
825
+ });
826
+ case 1:
827
+ return createMockResponse({
828
+ images: [jpegBase64],
829
+ providerMetaData: {
830
+ gateway: {
831
+ images: [],
832
+ routing: { provider: 'test2' },
833
+ generationId: 'gen-123',
834
+ },
835
+ },
836
+ });
837
+ default:
838
+ throw new Error('Unexpected call');
839
+ }
840
+ },
841
+ }),
842
+ prompt,
843
+ n: 2,
844
+ });
845
+
846
+ expect(result.providerMetadata.gateway).toStrictEqual({
847
+ routing: { provider: 'test2' },
848
+ generationId: 'gen-123',
849
+ cost: '0.01',
850
+ });
851
+ });
852
+
853
+ it('should drop empty images array for gateway provider', async () => {
854
+ const result = await generateImage({
855
+ model: new MockImageModelV3({
856
+ doGenerate: async () =>
857
+ createMockResponse({
858
+ images: [pngBase64],
859
+ providerMetaData: {
860
+ gateway: {
861
+ images: [],
862
+ routing: { provider: 'vertex' },
863
+ cost: '0.04',
864
+ },
865
+ },
866
+ }),
867
+ }),
868
+ prompt,
869
+ });
870
+
871
+ expect(result.providerMetadata.gateway).toStrictEqual({
872
+ routing: { provider: 'vertex' },
873
+ cost: '0.04',
874
+ });
875
+ expect(result.providerMetadata.gateway).not.toHaveProperty('images');
876
+ });
877
+
878
+ it('should not drop empty images array for non-gateway providers', async () => {
879
+ const result = await generateImage({
880
+ model: new MockImageModelV3({
881
+ doGenerate: async () =>
882
+ createMockResponse({
883
+ images: [pngBase64],
884
+ providerMetaData: {
885
+ openai: {
886
+ images: [],
887
+ usage: { tokens: 100 },
888
+ },
889
+ },
890
+ }),
891
+ }),
892
+ prompt,
893
+ });
894
+
895
+ expect(result.providerMetadata.openai).toStrictEqual({
896
+ images: [],
897
+ });
898
+ });
899
+
900
+ it('should handle provider metadata without images field', async () => {
901
+ const result = await generateImage({
902
+ model: new MockImageModelV3({
903
+ doGenerate: async () => {
904
+ const response: Awaited<ReturnType<ImageModelV3['doGenerate']>> = {
905
+ images: [pngBase64],
906
+ warnings: [],
907
+ providerMetadata: {
908
+ gateway: {
909
+ routing: { provider: 'vertex' },
910
+ cost: '0.04',
911
+ },
912
+ } as unknown as ImageModelV3ProviderMetadata,
913
+ response: {
914
+ timestamp: new Date(),
915
+ modelId: 'test-model-id',
916
+ headers: {},
917
+ },
918
+ };
919
+ return response;
920
+ },
921
+ }),
922
+ prompt,
923
+ });
924
+
925
+ expect(result.providerMetadata.gateway).toStrictEqual({
926
+ routing: { provider: 'vertex' },
927
+ cost: '0.04',
928
+ });
929
+ expect(result.providerMetadata.gateway).not.toHaveProperty('images');
930
+ });
931
+
932
+ it('should handle undefined providerMetadata', async () => {
933
+ const result = await generateImage({
934
+ model: new MockImageModelV3({
935
+ doGenerate: async () => ({
936
+ images: [pngBase64],
937
+ warnings: [],
938
+ providerMetadata: undefined,
939
+ response: {
940
+ timestamp: new Date(),
941
+ modelId: 'test-model-id',
942
+ headers: {},
943
+ },
944
+ }),
945
+ }),
946
+ prompt,
947
+ });
948
+
949
+ expect(result.providerMetadata).toStrictEqual({});
950
+ });
951
+
952
+ it('should merge multiple providers from same call', async () => {
953
+ const result = await generateImage({
954
+ model: new MockImageModelV3({
955
+ maxImagesPerCall: 2,
956
+ doGenerate: async () => ({
957
+ images: [pngBase64, jpegBase64],
958
+ warnings: [],
959
+ providerMetadata: {
960
+ vertex: {
961
+ images: [
962
+ { revisedPrompt: 'revised-1' },
963
+ { revisedPrompt: 'revised-2' },
964
+ ],
965
+ },
966
+ gateway: {
967
+ images: [],
968
+ routing: { provider: 'vertex' },
969
+ cost: '0.08',
970
+ },
971
+ },
972
+ response: {
973
+ timestamp: new Date(),
974
+ modelId: 'test-model-id',
975
+ headers: {},
976
+ },
977
+ }),
978
+ }),
979
+ prompt,
980
+ n: 2,
981
+ });
982
+
983
+ expect(result.providerMetadata).toStrictEqual({
984
+ vertex: {
985
+ images: [
986
+ { revisedPrompt: 'revised-1' },
987
+ { revisedPrompt: 'revised-2' },
988
+ ],
989
+ },
990
+ gateway: {
991
+ routing: { provider: 'vertex' },
992
+ cost: '0.08',
993
+ },
994
+ });
995
+ });
996
+
997
+ it('should merge multiple providers across multiple calls', async () => {
998
+ let callCount = 0;
999
+
1000
+ const result = await generateImage({
1001
+ model: new MockImageModelV3({
1002
+ maxImagesPerCall: 1,
1003
+ doGenerate: async () => {
1004
+ switch (callCount++) {
1005
+ case 0:
1006
+ return createMockResponse({
1007
+ images: [pngBase64],
1008
+ providerMetaData: {
1009
+ vertex: {
1010
+ images: [{ revisedPrompt: 'revised-1' }],
1011
+ },
1012
+ gateway: {
1013
+ images: [],
1014
+ routing: { provider: 'vertex' },
1015
+ },
1016
+ },
1017
+ });
1018
+ case 1:
1019
+ return createMockResponse({
1020
+ images: [jpegBase64],
1021
+ providerMetaData: {
1022
+ vertex: {
1023
+ images: [{ revisedPrompt: 'revised-2' }],
1024
+ },
1025
+ gateway: {
1026
+ images: [],
1027
+ cost: '0.08',
1028
+ },
1029
+ },
1030
+ });
1031
+ default:
1032
+ throw new Error('Unexpected call');
1033
+ }
1034
+ },
1035
+ }),
1036
+ prompt,
1037
+ n: 2,
1038
+ });
1039
+
1040
+ expect(result.providerMetadata).toStrictEqual({
1041
+ vertex: {
1042
+ images: [
1043
+ { revisedPrompt: 'revised-1' },
1044
+ { revisedPrompt: 'revised-2' },
1045
+ ],
1046
+ },
1047
+ gateway: {
1048
+ routing: { provider: 'vertex' },
1049
+ cost: '0.08',
1050
+ },
1051
+ });
1052
+ });
1053
+
1054
+ it('should preserve null values in images array', async () => {
1055
+ const result = await generateImage({
1056
+ model: new MockImageModelV3({
1057
+ maxImagesPerCall: 2,
1058
+ doGenerate: async () => ({
1059
+ images: [pngBase64, jpegBase64],
1060
+ warnings: [],
1061
+ providerMetadata: {
1062
+ openai: {
1063
+ images: [{ revisedPrompt: 'revised' }, null],
1064
+ },
1065
+ },
1066
+ response: {
1067
+ timestamp: new Date(),
1068
+ modelId: 'test-model-id',
1069
+ headers: {},
1070
+ },
1071
+ }),
1072
+ }),
1073
+ prompt,
1074
+ n: 2,
1075
+ });
1076
+
1077
+ expect(result.providerMetadata.openai).toStrictEqual({
1078
+ images: [{ revisedPrompt: 'revised' }, null],
1079
+ });
1080
+ });
1081
+
1082
+ it('should handle complex nested metadata structures', async () => {
1083
+ const result = await generateImage({
1084
+ model: new MockImageModelV3({
1085
+ doGenerate: async () =>
1086
+ createMockResponse({
1087
+ images: [pngBase64],
1088
+ providerMetaData: {
1089
+ gateway: {
1090
+ images: [],
1091
+ routing: {
1092
+ provider: 'vertex',
1093
+ attempts: [
1094
+ { provider: 'openai', success: false },
1095
+ { provider: 'vertex', success: true },
1096
+ ],
1097
+ },
1098
+ cost: '0.04',
1099
+ marketCost: '0.06',
1100
+ generationId: 'gen-abc-123',
1101
+ },
1102
+ },
1103
+ }),
1104
+ }),
1105
+ prompt,
1106
+ });
1107
+
1108
+ expect(result.providerMetadata.gateway).toStrictEqual({
1109
+ routing: {
1110
+ provider: 'vertex',
1111
+ attempts: [
1112
+ { provider: 'openai', success: false },
1113
+ { provider: 'vertex', success: true },
1114
+ ],
1115
+ },
1116
+ cost: '0.04',
1117
+ marketCost: '0.06',
1118
+ generationId: 'gen-abc-123',
1119
+ });
1120
+ });
1121
+
1122
+ it('should handle empty gateway images across multiple calls', async () => {
1123
+ let callCount = 0;
1124
+
1125
+ const result = await generateImage({
1126
+ model: new MockImageModelV3({
1127
+ maxImagesPerCall: 1,
1128
+ doGenerate: async () => {
1129
+ switch (callCount++) {
1130
+ case 0:
1131
+ return createMockResponse({
1132
+ images: [pngBase64],
1133
+ providerMetaData: {
1134
+ gateway: {
1135
+ images: [],
1136
+ routing: { provider: 'vertex' },
1137
+ },
1138
+ },
1139
+ });
1140
+ case 1:
1141
+ return createMockResponse({
1142
+ images: [jpegBase64],
1143
+ providerMetaData: {
1144
+ gateway: {
1145
+ images: [],
1146
+ cost: '0.04',
1147
+ },
1148
+ },
1149
+ });
1150
+ default:
1151
+ throw new Error('Unexpected call');
1152
+ }
1153
+ },
1154
+ }),
1155
+ prompt,
1156
+ n: 2,
1157
+ });
1158
+
1159
+ expect(result.providerMetadata.gateway).toStrictEqual({
1160
+ routing: { provider: 'vertex' },
1161
+ cost: '0.04',
1162
+ });
1163
+ expect(result.providerMetadata.gateway).not.toHaveProperty('images');
1164
+ });
1165
+
1166
+ it('should keep images array for gateway if non-empty', async () => {
1167
+ const result = await generateImage({
1168
+ model: new MockImageModelV3({
1169
+ doGenerate: async () =>
1170
+ createMockResponse({
1171
+ images: [pngBase64],
1172
+ providerMetaData: {
1173
+ gateway: {
1174
+ images: [{ metadata: 'value' }],
1175
+ routing: { provider: 'vertex' },
1176
+ cost: '0.04',
1177
+ },
1178
+ },
1179
+ }),
1180
+ }),
1181
+ prompt,
1182
+ });
1183
+
1184
+ expect(result.providerMetadata.gateway).toStrictEqual({
1185
+ images: [{ metadata: 'value' }],
1186
+ routing: { provider: 'vertex' },
1187
+ cost: '0.04',
1188
+ });
1189
+ });
1190
+ });
1191
+ });
1192
+
1193
+ describe('data URL handling', () => {
1194
+ it('should handle data URL with media type in prompt images', async () => {
1195
+ const dataUrl = `data:image/png;base64,${pngBase64}`;
1196
+
1197
+ let capturedArgs!: Parameters<ImageModelV3['doGenerate']>[0];
1198
+
1199
+ await generateImage({
1200
+ model: new MockImageModelV3({
1201
+ doGenerate: async args => {
1202
+ capturedArgs = args;
1203
+ return createMockResponse({
1204
+ images: [pngBase64],
1205
+ });
1206
+ },
1207
+ }),
1208
+ prompt: {
1209
+ text: prompt,
1210
+ images: [dataUrl],
1211
+ },
1212
+ });
1213
+
1214
+ expect(capturedArgs.files).toStrictEqual([
1215
+ {
1216
+ type: 'file',
1217
+ data: convertBase64ToUint8Array(pngBase64),
1218
+ mediaType: 'image/png',
1219
+ },
1220
+ ]);
1221
+ });
1222
+
1223
+ it('should handle data URL with jpeg media type', async () => {
1224
+ const dataUrl = `data:image/jpeg;base64,${jpegBase64}`;
1225
+
1226
+ let capturedArgs!: Parameters<ImageModelV3['doGenerate']>[0];
1227
+
1228
+ await generateImage({
1229
+ model: new MockImageModelV3({
1230
+ doGenerate: async args => {
1231
+ capturedArgs = args;
1232
+ return createMockResponse({
1233
+ images: [pngBase64],
1234
+ });
1235
+ },
1236
+ }),
1237
+ prompt: {
1238
+ text: prompt,
1239
+ images: [dataUrl],
1240
+ },
1241
+ });
1242
+
1243
+ expect(capturedArgs.files).toStrictEqual([
1244
+ {
1245
+ type: 'file',
1246
+ data: convertBase64ToUint8Array(jpegBase64),
1247
+ mediaType: 'image/jpeg',
1248
+ },
1249
+ ]);
1250
+ });
1251
+
1252
+ it('should handle data URL as mask', async () => {
1253
+ const dataUrl = `data:image/png;base64,${pngBase64}`;
1254
+
1255
+ let capturedArgs!: Parameters<ImageModelV3['doGenerate']>[0];
1256
+
1257
+ await generateImage({
1258
+ model: new MockImageModelV3({
1259
+ doGenerate: async args => {
1260
+ capturedArgs = args;
1261
+ return createMockResponse({
1262
+ images: [pngBase64],
1263
+ });
1264
+ },
1265
+ }),
1266
+ prompt: {
1267
+ text: prompt,
1268
+ images: [pngBase64],
1269
+ mask: dataUrl,
1270
+ },
1271
+ });
1272
+
1273
+ expect(capturedArgs.mask).toStrictEqual({
1274
+ type: 'file',
1275
+ data: convertBase64ToUint8Array(pngBase64),
1276
+ mediaType: 'image/png',
1277
+ });
1278
+ });
1279
+
1280
+ it('should detect media type from data when data URL has no media type', async () => {
1281
+ // Data URL with minimal header (no explicit media type before semicolon)
1282
+ const dataUrl = `data:;base64,${pngBase64}`;
1283
+
1284
+ let capturedArgs!: Parameters<ImageModelV3['doGenerate']>[0];
1285
+
1286
+ await generateImage({
1287
+ model: new MockImageModelV3({
1288
+ doGenerate: async args => {
1289
+ capturedArgs = args;
1290
+ return createMockResponse({
1291
+ images: [pngBase64],
1292
+ });
1293
+ },
1294
+ }),
1295
+ prompt: {
1296
+ text: prompt,
1297
+ images: [dataUrl],
1298
+ },
1299
+ });
1300
+
1301
+ // Should detect PNG from the actual image data
1302
+ expect(capturedArgs.files).toStrictEqual([
1303
+ {
1304
+ type: 'file',
1305
+ data: convertBase64ToUint8Array(pngBase64),
1306
+ mediaType: 'image/png',
1307
+ },
1308
+ ]);
1309
+ });
1310
+
1311
+ it('should handle multiple data URLs in prompt images', async () => {
1312
+ const pngDataUrl = `data:image/png;base64,${pngBase64}`;
1313
+ const jpegDataUrl = `data:image/jpeg;base64,${jpegBase64}`;
1314
+
1315
+ let capturedArgs!: Parameters<ImageModelV3['doGenerate']>[0];
1316
+
1317
+ await generateImage({
1318
+ model: new MockImageModelV3({
1319
+ doGenerate: async args => {
1320
+ capturedArgs = args;
1321
+ return createMockResponse({
1322
+ images: [pngBase64],
1323
+ });
1324
+ },
1325
+ }),
1326
+ prompt: {
1327
+ text: prompt,
1328
+ images: [pngDataUrl, jpegDataUrl],
1329
+ },
1330
+ });
1331
+
1332
+ expect(capturedArgs.files).toStrictEqual([
1333
+ {
1334
+ type: 'file',
1335
+ data: convertBase64ToUint8Array(pngBase64),
1336
+ mediaType: 'image/png',
1337
+ },
1338
+ {
1339
+ type: 'file',
1340
+ data: convertBase64ToUint8Array(jpegBase64),
1341
+ mediaType: 'image/jpeg',
1342
+ },
1343
+ ]);
1344
+ });
1345
+
1346
+ it('should handle mix of data URLs and base64 strings', async () => {
1347
+ const pngDataUrl = `data:image/png;base64,${pngBase64}`;
1348
+
1349
+ let capturedArgs!: Parameters<ImageModelV3['doGenerate']>[0];
1350
+
1351
+ await generateImage({
1352
+ model: new MockImageModelV3({
1353
+ doGenerate: async args => {
1354
+ capturedArgs = args;
1355
+ return createMockResponse({
1356
+ images: [pngBase64],
1357
+ });
1358
+ },
1359
+ }),
1360
+ prompt: {
1361
+ text: prompt,
1362
+ images: [pngDataUrl, jpegBase64],
1363
+ },
1364
+ });
1365
+
1366
+ expect(capturedArgs.files).toStrictEqual([
1367
+ {
1368
+ type: 'file',
1369
+ data: convertBase64ToUint8Array(pngBase64),
1370
+ mediaType: 'image/png',
1371
+ },
1372
+ {
1373
+ type: 'file',
1374
+ data: convertBase64ToUint8Array(jpegBase64),
1375
+ mediaType: 'image/jpeg',
1376
+ },
1377
+ ]);
1378
+ });
1379
+ });
1380
+
1381
+ describe('deprecated APIs', () => {
1382
+ it('experimental_generateImage should still work', async () => {
1383
+ // Import the deprecated export
1384
+ const { experimental_generateImage } = await import('./index');
1385
+
1386
+ const result = await experimental_generateImage({
1387
+ model: new MockImageModelV3({
1388
+ doGenerate: async () =>
1389
+ createMockResponse({
1390
+ images: [pngBase64],
1391
+ }),
1392
+ }),
1393
+ prompt,
1394
+ });
1395
+
1396
+ expect(result.images).toHaveLength(1);
1397
+ expect(result.image.base64).toBe(pngBase64);
1398
+ });
1399
+
1400
+ it('Experimental_GenerateImageResult type should be exported', async () => {
1401
+ // Import the deprecated exports
1402
+ const { experimental_generateImage } = await import('./index');
1403
+ type ResultType = import('./index').Experimental_GenerateImageResult;
1404
+
1405
+ const result: ResultType = await experimental_generateImage({
1406
+ model: new MockImageModelV3({
1407
+ doGenerate: async () =>
1408
+ createMockResponse({
1409
+ images: [pngBase64],
1410
+ }),
1411
+ }),
1412
+ prompt,
1413
+ });
1414
+
1415
+ // Type assertions to verify the shape is correct
1416
+ expect(result.images).toBeDefined();
1417
+ expect(result.image).toBeDefined();
1418
+ expect(result.warnings).toBeDefined();
1419
+ });
1420
+ });