ai 6.0.32 → 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 (353) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/index.js +12 -2
  3. package/dist/index.js.map +1 -1
  4. package/dist/index.mjs +12 -2
  5. package/dist/index.mjs.map +1 -1
  6. package/dist/internal/index.js +1 -1
  7. package/dist/internal/index.mjs +1 -1
  8. package/docs/02-foundations/03-prompts.mdx +2 -2
  9. package/docs/03-ai-sdk-core/15-tools-and-tool-calling.mdx +1 -1
  10. package/docs/07-reference/01-ai-sdk-core/28-output.mdx +1 -1
  11. package/package.json +6 -4
  12. package/src/agent/agent.ts +116 -0
  13. package/src/agent/create-agent-ui-stream-response.test.ts +258 -0
  14. package/src/agent/create-agent-ui-stream-response.ts +50 -0
  15. package/src/agent/create-agent-ui-stream.ts +73 -0
  16. package/src/agent/index.ts +33 -0
  17. package/src/agent/infer-agent-tools.ts +7 -0
  18. package/src/agent/infer-agent-ui-message.test-d.ts +54 -0
  19. package/src/agent/infer-agent-ui-message.ts +11 -0
  20. package/src/agent/pipe-agent-ui-stream-to-response.ts +52 -0
  21. package/src/agent/tool-loop-agent-on-finish-callback.ts +31 -0
  22. package/src/agent/tool-loop-agent-on-step-finish-callback.ts +11 -0
  23. package/src/agent/tool-loop-agent-settings.ts +182 -0
  24. package/src/agent/tool-loop-agent.test-d.ts +114 -0
  25. package/src/agent/tool-loop-agent.test.ts +442 -0
  26. package/src/agent/tool-loop-agent.ts +114 -0
  27. package/src/embed/__snapshots__/embed-many.test.ts.snap +191 -0
  28. package/src/embed/__snapshots__/embed.test.ts.snap +81 -0
  29. package/src/embed/embed-many-result.ts +53 -0
  30. package/src/embed/embed-many.test.ts +653 -0
  31. package/src/embed/embed-many.ts +378 -0
  32. package/src/embed/embed-result.ts +50 -0
  33. package/src/embed/embed.test.ts +298 -0
  34. package/src/embed/embed.ts +211 -0
  35. package/src/embed/index.ts +4 -0
  36. package/src/error/index.ts +34 -0
  37. package/src/error/invalid-argument-error.ts +34 -0
  38. package/src/error/invalid-stream-part-error.ts +28 -0
  39. package/src/error/invalid-tool-approval-error.ts +26 -0
  40. package/src/error/invalid-tool-input-error.ts +33 -0
  41. package/src/error/no-image-generated-error.ts +39 -0
  42. package/src/error/no-object-generated-error.ts +70 -0
  43. package/src/error/no-output-generated-error.ts +26 -0
  44. package/src/error/no-speech-generated-error.ts +18 -0
  45. package/src/error/no-such-tool-error.ts +35 -0
  46. package/src/error/no-transcript-generated-error.ts +20 -0
  47. package/src/error/tool-call-not-found-for-approval-error.ts +32 -0
  48. package/src/error/tool-call-repair-error.ts +30 -0
  49. package/src/error/unsupported-model-version-error.ts +23 -0
  50. package/src/error/verify-no-object-generated-error.ts +27 -0
  51. package/src/generate-image/generate-image-result.ts +42 -0
  52. package/src/generate-image/generate-image.test.ts +1420 -0
  53. package/src/generate-image/generate-image.ts +360 -0
  54. package/src/generate-image/index.ts +18 -0
  55. package/src/generate-object/__snapshots__/generate-object.test.ts.snap +133 -0
  56. package/src/generate-object/__snapshots__/stream-object.test.ts.snap +297 -0
  57. package/src/generate-object/generate-object-result.ts +67 -0
  58. package/src/generate-object/generate-object.test-d.ts +49 -0
  59. package/src/generate-object/generate-object.test.ts +1191 -0
  60. package/src/generate-object/generate-object.ts +518 -0
  61. package/src/generate-object/index.ts +9 -0
  62. package/src/generate-object/inject-json-instruction.test.ts +181 -0
  63. package/src/generate-object/inject-json-instruction.ts +30 -0
  64. package/src/generate-object/output-strategy.ts +415 -0
  65. package/src/generate-object/parse-and-validate-object-result.ts +111 -0
  66. package/src/generate-object/repair-text.ts +12 -0
  67. package/src/generate-object/stream-object-result.ts +120 -0
  68. package/src/generate-object/stream-object.test-d.ts +74 -0
  69. package/src/generate-object/stream-object.test.ts +1950 -0
  70. package/src/generate-object/stream-object.ts +986 -0
  71. package/src/generate-object/validate-object-generation-input.ts +144 -0
  72. package/src/generate-speech/generate-speech-result.ts +30 -0
  73. package/src/generate-speech/generate-speech.test.ts +300 -0
  74. package/src/generate-speech/generate-speech.ts +190 -0
  75. package/src/generate-speech/generated-audio-file.ts +65 -0
  76. package/src/generate-speech/index.ts +3 -0
  77. package/src/generate-text/__snapshots__/generate-text.test.ts.snap +1872 -0
  78. package/src/generate-text/__snapshots__/stream-text.test.ts.snap +1255 -0
  79. package/src/generate-text/collect-tool-approvals.test.ts +553 -0
  80. package/src/generate-text/collect-tool-approvals.ts +116 -0
  81. package/src/generate-text/content-part.ts +25 -0
  82. package/src/generate-text/execute-tool-call.ts +129 -0
  83. package/src/generate-text/extract-reasoning-content.ts +17 -0
  84. package/src/generate-text/extract-text-content.ts +15 -0
  85. package/src/generate-text/generate-text-result.ts +168 -0
  86. package/src/generate-text/generate-text.test-d.ts +68 -0
  87. package/src/generate-text/generate-text.test.ts +7011 -0
  88. package/src/generate-text/generate-text.ts +1223 -0
  89. package/src/generate-text/generated-file.ts +70 -0
  90. package/src/generate-text/index.ts +57 -0
  91. package/src/generate-text/is-approval-needed.ts +29 -0
  92. package/src/generate-text/output-utils.ts +23 -0
  93. package/src/generate-text/output.test.ts +698 -0
  94. package/src/generate-text/output.ts +590 -0
  95. package/src/generate-text/parse-tool-call.test.ts +570 -0
  96. package/src/generate-text/parse-tool-call.ts +188 -0
  97. package/src/generate-text/prepare-step.ts +103 -0
  98. package/src/generate-text/prune-messages.test.ts +720 -0
  99. package/src/generate-text/prune-messages.ts +167 -0
  100. package/src/generate-text/reasoning-output.ts +20 -0
  101. package/src/generate-text/reasoning.ts +8 -0
  102. package/src/generate-text/response-message.ts +10 -0
  103. package/src/generate-text/run-tools-transformation.test.ts +1143 -0
  104. package/src/generate-text/run-tools-transformation.ts +420 -0
  105. package/src/generate-text/smooth-stream.test.ts +2101 -0
  106. package/src/generate-text/smooth-stream.ts +162 -0
  107. package/src/generate-text/step-result.ts +238 -0
  108. package/src/generate-text/stop-condition.ts +29 -0
  109. package/src/generate-text/stream-text-result.ts +463 -0
  110. package/src/generate-text/stream-text.test-d.ts +200 -0
  111. package/src/generate-text/stream-text.test.ts +19979 -0
  112. package/src/generate-text/stream-text.ts +2505 -0
  113. package/src/generate-text/to-response-messages.test.ts +922 -0
  114. package/src/generate-text/to-response-messages.ts +163 -0
  115. package/src/generate-text/tool-approval-request-output.ts +21 -0
  116. package/src/generate-text/tool-call-repair-function.ts +27 -0
  117. package/src/generate-text/tool-call.ts +47 -0
  118. package/src/generate-text/tool-error.ts +34 -0
  119. package/src/generate-text/tool-output-denied.ts +21 -0
  120. package/src/generate-text/tool-output.ts +7 -0
  121. package/src/generate-text/tool-result.ts +36 -0
  122. package/src/generate-text/tool-set.ts +14 -0
  123. package/src/global.ts +24 -0
  124. package/src/index.ts +50 -0
  125. package/src/logger/index.ts +6 -0
  126. package/src/logger/log-warnings.test.ts +351 -0
  127. package/src/logger/log-warnings.ts +119 -0
  128. package/src/middleware/__snapshots__/simulate-streaming-middleware.test.ts.snap +64 -0
  129. package/src/middleware/add-tool-input-examples-middleware.test.ts +476 -0
  130. package/src/middleware/add-tool-input-examples-middleware.ts +90 -0
  131. package/src/middleware/default-embedding-settings-middleware.test.ts +126 -0
  132. package/src/middleware/default-embedding-settings-middleware.ts +22 -0
  133. package/src/middleware/default-settings-middleware.test.ts +388 -0
  134. package/src/middleware/default-settings-middleware.ts +33 -0
  135. package/src/middleware/extract-json-middleware.test.ts +827 -0
  136. package/src/middleware/extract-json-middleware.ts +197 -0
  137. package/src/middleware/extract-reasoning-middleware.test.ts +1028 -0
  138. package/src/middleware/extract-reasoning-middleware.ts +238 -0
  139. package/src/middleware/index.ts +10 -0
  140. package/src/middleware/simulate-streaming-middleware.test.ts +911 -0
  141. package/src/middleware/simulate-streaming-middleware.ts +79 -0
  142. package/src/middleware/wrap-embedding-model.test.ts +358 -0
  143. package/src/middleware/wrap-embedding-model.ts +86 -0
  144. package/src/middleware/wrap-image-model.test.ts +423 -0
  145. package/src/middleware/wrap-image-model.ts +85 -0
  146. package/src/middleware/wrap-language-model.test.ts +518 -0
  147. package/src/middleware/wrap-language-model.ts +104 -0
  148. package/src/middleware/wrap-provider.test.ts +120 -0
  149. package/src/middleware/wrap-provider.ts +51 -0
  150. package/src/model/as-embedding-model-v3.test.ts +319 -0
  151. package/src/model/as-embedding-model-v3.ts +24 -0
  152. package/src/model/as-image-model-v3.test.ts +409 -0
  153. package/src/model/as-image-model-v3.ts +24 -0
  154. package/src/model/as-language-model-v3.test.ts +508 -0
  155. package/src/model/as-language-model-v3.ts +103 -0
  156. package/src/model/as-provider-v3.ts +36 -0
  157. package/src/model/as-speech-model-v3.test.ts +356 -0
  158. package/src/model/as-speech-model-v3.ts +24 -0
  159. package/src/model/as-transcription-model-v3.test.ts +529 -0
  160. package/src/model/as-transcription-model-v3.ts +24 -0
  161. package/src/model/resolve-model.test.ts +244 -0
  162. package/src/model/resolve-model.ts +126 -0
  163. package/src/prompt/call-settings.ts +148 -0
  164. package/src/prompt/content-part.ts +209 -0
  165. package/src/prompt/convert-to-language-model-prompt.test.ts +2018 -0
  166. package/src/prompt/convert-to-language-model-prompt.ts +442 -0
  167. package/src/prompt/create-tool-model-output.test.ts +508 -0
  168. package/src/prompt/create-tool-model-output.ts +34 -0
  169. package/src/prompt/data-content.test.ts +15 -0
  170. package/src/prompt/data-content.ts +134 -0
  171. package/src/prompt/index.ts +27 -0
  172. package/src/prompt/invalid-data-content-error.ts +29 -0
  173. package/src/prompt/invalid-message-role-error.ts +27 -0
  174. package/src/prompt/message-conversion-error.ts +28 -0
  175. package/src/prompt/message.ts +68 -0
  176. package/src/prompt/prepare-call-settings.test.ts +159 -0
  177. package/src/prompt/prepare-call-settings.ts +108 -0
  178. package/src/prompt/prepare-tools-and-tool-choice.test.ts +461 -0
  179. package/src/prompt/prepare-tools-and-tool-choice.ts +86 -0
  180. package/src/prompt/prompt.ts +43 -0
  181. package/src/prompt/split-data-url.ts +17 -0
  182. package/src/prompt/standardize-prompt.test.ts +82 -0
  183. package/src/prompt/standardize-prompt.ts +99 -0
  184. package/src/prompt/wrap-gateway-error.ts +29 -0
  185. package/src/registry/custom-provider.test.ts +211 -0
  186. package/src/registry/custom-provider.ts +155 -0
  187. package/src/registry/index.ts +7 -0
  188. package/src/registry/no-such-provider-error.ts +41 -0
  189. package/src/registry/provider-registry.test.ts +691 -0
  190. package/src/registry/provider-registry.ts +328 -0
  191. package/src/rerank/index.ts +2 -0
  192. package/src/rerank/rerank-result.ts +70 -0
  193. package/src/rerank/rerank.test.ts +516 -0
  194. package/src/rerank/rerank.ts +237 -0
  195. package/src/telemetry/assemble-operation-name.ts +21 -0
  196. package/src/telemetry/get-base-telemetry-attributes.ts +53 -0
  197. package/src/telemetry/get-tracer.ts +20 -0
  198. package/src/telemetry/noop-tracer.ts +69 -0
  199. package/src/telemetry/record-span.ts +63 -0
  200. package/src/telemetry/select-telemetry-attributes.ts +78 -0
  201. package/src/telemetry/select-temetry-attributes.test.ts +114 -0
  202. package/src/telemetry/stringify-for-telemetry.test.ts +114 -0
  203. package/src/telemetry/stringify-for-telemetry.ts +33 -0
  204. package/src/telemetry/telemetry-settings.ts +44 -0
  205. package/src/test/mock-embedding-model-v2.ts +35 -0
  206. package/src/test/mock-embedding-model-v3.ts +48 -0
  207. package/src/test/mock-image-model-v2.ts +28 -0
  208. package/src/test/mock-image-model-v3.ts +28 -0
  209. package/src/test/mock-language-model-v2.ts +72 -0
  210. package/src/test/mock-language-model-v3.ts +77 -0
  211. package/src/test/mock-provider-v2.ts +68 -0
  212. package/src/test/mock-provider-v3.ts +80 -0
  213. package/src/test/mock-reranking-model-v3.ts +25 -0
  214. package/src/test/mock-server-response.ts +69 -0
  215. package/src/test/mock-speech-model-v2.ts +24 -0
  216. package/src/test/mock-speech-model-v3.ts +24 -0
  217. package/src/test/mock-tracer.ts +156 -0
  218. package/src/test/mock-transcription-model-v2.ts +24 -0
  219. package/src/test/mock-transcription-model-v3.ts +24 -0
  220. package/src/test/mock-values.ts +4 -0
  221. package/src/test/not-implemented.ts +3 -0
  222. package/src/text-stream/create-text-stream-response.test.ts +38 -0
  223. package/src/text-stream/create-text-stream-response.ts +18 -0
  224. package/src/text-stream/index.ts +2 -0
  225. package/src/text-stream/pipe-text-stream-to-response.test.ts +38 -0
  226. package/src/text-stream/pipe-text-stream-to-response.ts +26 -0
  227. package/src/transcribe/index.ts +2 -0
  228. package/src/transcribe/transcribe-result.ts +60 -0
  229. package/src/transcribe/transcribe.test.ts +313 -0
  230. package/src/transcribe/transcribe.ts +173 -0
  231. package/src/types/embedding-model-middleware.ts +3 -0
  232. package/src/types/embedding-model.ts +18 -0
  233. package/src/types/image-model-middleware.ts +3 -0
  234. package/src/types/image-model-response-metadata.ts +16 -0
  235. package/src/types/image-model.ts +19 -0
  236. package/src/types/index.ts +29 -0
  237. package/src/types/json-value.ts +15 -0
  238. package/src/types/language-model-middleware.ts +3 -0
  239. package/src/types/language-model-request-metadata.ts +6 -0
  240. package/src/types/language-model-response-metadata.ts +21 -0
  241. package/src/types/language-model.ts +104 -0
  242. package/src/types/provider-metadata.ts +16 -0
  243. package/src/types/provider.ts +55 -0
  244. package/src/types/reranking-model.ts +6 -0
  245. package/src/types/speech-model-response-metadata.ts +21 -0
  246. package/src/types/speech-model.ts +6 -0
  247. package/src/types/transcription-model-response-metadata.ts +16 -0
  248. package/src/types/transcription-model.ts +9 -0
  249. package/src/types/usage.ts +200 -0
  250. package/src/types/warning.ts +7 -0
  251. package/src/ui/__snapshots__/append-response-messages.test.ts.snap +416 -0
  252. package/src/ui/__snapshots__/convert-to-model-messages.test.ts.snap +419 -0
  253. package/src/ui/__snapshots__/process-chat-text-response.test.ts.snap +142 -0
  254. package/src/ui/call-completion-api.ts +157 -0
  255. package/src/ui/chat-transport.ts +83 -0
  256. package/src/ui/chat.test-d.ts +233 -0
  257. package/src/ui/chat.test.ts +2695 -0
  258. package/src/ui/chat.ts +716 -0
  259. package/src/ui/convert-file-list-to-file-ui-parts.ts +36 -0
  260. package/src/ui/convert-to-model-messages.test.ts +2775 -0
  261. package/src/ui/convert-to-model-messages.ts +373 -0
  262. package/src/ui/default-chat-transport.ts +36 -0
  263. package/src/ui/direct-chat-transport.test.ts +446 -0
  264. package/src/ui/direct-chat-transport.ts +118 -0
  265. package/src/ui/http-chat-transport.test.ts +185 -0
  266. package/src/ui/http-chat-transport.ts +292 -0
  267. package/src/ui/index.ts +71 -0
  268. package/src/ui/last-assistant-message-is-complete-with-approval-responses.ts +44 -0
  269. package/src/ui/last-assistant-message-is-complete-with-tool-calls.test.ts +371 -0
  270. package/src/ui/last-assistant-message-is-complete-with-tool-calls.ts +39 -0
  271. package/src/ui/process-text-stream.test.ts +38 -0
  272. package/src/ui/process-text-stream.ts +16 -0
  273. package/src/ui/process-ui-message-stream.test.ts +8052 -0
  274. package/src/ui/process-ui-message-stream.ts +713 -0
  275. package/src/ui/text-stream-chat-transport.ts +23 -0
  276. package/src/ui/transform-text-to-ui-message-stream.test.ts +124 -0
  277. package/src/ui/transform-text-to-ui-message-stream.ts +27 -0
  278. package/src/ui/ui-messages.test.ts +48 -0
  279. package/src/ui/ui-messages.ts +534 -0
  280. package/src/ui/use-completion.ts +84 -0
  281. package/src/ui/validate-ui-messages.test.ts +1428 -0
  282. package/src/ui/validate-ui-messages.ts +476 -0
  283. package/src/ui-message-stream/create-ui-message-stream-response.test.ts +266 -0
  284. package/src/ui-message-stream/create-ui-message-stream-response.ts +32 -0
  285. package/src/ui-message-stream/create-ui-message-stream.test.ts +639 -0
  286. package/src/ui-message-stream/create-ui-message-stream.ts +124 -0
  287. package/src/ui-message-stream/get-response-ui-message-id.test.ts +55 -0
  288. package/src/ui-message-stream/get-response-ui-message-id.ts +24 -0
  289. package/src/ui-message-stream/handle-ui-message-stream-finish.test.ts +429 -0
  290. package/src/ui-message-stream/handle-ui-message-stream-finish.ts +135 -0
  291. package/src/ui-message-stream/index.ts +13 -0
  292. package/src/ui-message-stream/json-to-sse-transform-stream.ts +12 -0
  293. package/src/ui-message-stream/pipe-ui-message-stream-to-response.test.ts +90 -0
  294. package/src/ui-message-stream/pipe-ui-message-stream-to-response.ts +40 -0
  295. package/src/ui-message-stream/read-ui-message-stream.test.ts +122 -0
  296. package/src/ui-message-stream/read-ui-message-stream.ts +87 -0
  297. package/src/ui-message-stream/ui-message-chunks.test-d.ts +18 -0
  298. package/src/ui-message-stream/ui-message-chunks.ts +344 -0
  299. package/src/ui-message-stream/ui-message-stream-headers.ts +7 -0
  300. package/src/ui-message-stream/ui-message-stream-on-finish-callback.ts +32 -0
  301. package/src/ui-message-stream/ui-message-stream-response-init.ts +5 -0
  302. package/src/ui-message-stream/ui-message-stream-writer.ts +24 -0
  303. package/src/util/as-array.ts +3 -0
  304. package/src/util/async-iterable-stream.test.ts +241 -0
  305. package/src/util/async-iterable-stream.ts +94 -0
  306. package/src/util/consume-stream.ts +29 -0
  307. package/src/util/cosine-similarity.test.ts +57 -0
  308. package/src/util/cosine-similarity.ts +47 -0
  309. package/src/util/create-resolvable-promise.ts +30 -0
  310. package/src/util/create-stitchable-stream.test.ts +239 -0
  311. package/src/util/create-stitchable-stream.ts +112 -0
  312. package/src/util/data-url.ts +17 -0
  313. package/src/util/deep-partial.ts +84 -0
  314. package/src/util/detect-media-type.test.ts +670 -0
  315. package/src/util/detect-media-type.ts +184 -0
  316. package/src/util/download/download-function.ts +45 -0
  317. package/src/util/download/download.test.ts +69 -0
  318. package/src/util/download/download.ts +46 -0
  319. package/src/util/error-handler.ts +1 -0
  320. package/src/util/fix-json.test.ts +279 -0
  321. package/src/util/fix-json.ts +401 -0
  322. package/src/util/get-potential-start-index.test.ts +34 -0
  323. package/src/util/get-potential-start-index.ts +30 -0
  324. package/src/util/index.ts +11 -0
  325. package/src/util/is-deep-equal-data.test.ts +119 -0
  326. package/src/util/is-deep-equal-data.ts +48 -0
  327. package/src/util/is-non-empty-object.ts +5 -0
  328. package/src/util/job.ts +1 -0
  329. package/src/util/log-v2-compatibility-warning.ts +21 -0
  330. package/src/util/merge-abort-signals.test.ts +155 -0
  331. package/src/util/merge-abort-signals.ts +43 -0
  332. package/src/util/merge-objects.test.ts +118 -0
  333. package/src/util/merge-objects.ts +79 -0
  334. package/src/util/now.ts +4 -0
  335. package/src/util/parse-partial-json.test.ts +80 -0
  336. package/src/util/parse-partial-json.ts +30 -0
  337. package/src/util/prepare-headers.test.ts +51 -0
  338. package/src/util/prepare-headers.ts +14 -0
  339. package/src/util/prepare-retries.test.ts +10 -0
  340. package/src/util/prepare-retries.ts +47 -0
  341. package/src/util/retry-error.ts +41 -0
  342. package/src/util/retry-with-exponential-backoff.test.ts +446 -0
  343. package/src/util/retry-with-exponential-backoff.ts +154 -0
  344. package/src/util/serial-job-executor.test.ts +162 -0
  345. package/src/util/serial-job-executor.ts +36 -0
  346. package/src/util/simulate-readable-stream.test.ts +98 -0
  347. package/src/util/simulate-readable-stream.ts +39 -0
  348. package/src/util/split-array.test.ts +60 -0
  349. package/src/util/split-array.ts +20 -0
  350. package/src/util/value-of.ts +65 -0
  351. package/src/util/write-to-server-response.test.ts +266 -0
  352. package/src/util/write-to-server-response.ts +49 -0
  353. package/src/version.ts +5 -0
@@ -0,0 +1,446 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { APICallError } from '@ai-sdk/provider';
3
+ import { retryWithExponentialBackoffRespectingRetryHeaders } from './retry-with-exponential-backoff';
4
+
5
+ describe('retryWithExponentialBackoffRespectingRetryHeaders', () => {
6
+ beforeEach(() => {
7
+ vi.useFakeTimers();
8
+ });
9
+
10
+ afterEach(() => {
11
+ vi.useRealTimers();
12
+ });
13
+
14
+ it('should use rate limit header delay when present and reasonable', async () => {
15
+ let attempt = 0;
16
+ const retryAfterMs = 3000;
17
+
18
+ const fn = vi.fn().mockImplementation(async () => {
19
+ attempt++;
20
+ if (attempt === 1) {
21
+ throw new APICallError({
22
+ message: 'Rate limited',
23
+ url: 'https://api.example.com',
24
+ requestBodyValues: {},
25
+ isRetryable: true,
26
+ data: undefined,
27
+ responseHeaders: {
28
+ 'retry-after-ms': retryAfterMs.toString(),
29
+ },
30
+ });
31
+ }
32
+ return 'success';
33
+ });
34
+
35
+ const promise = retryWithExponentialBackoffRespectingRetryHeaders()(fn);
36
+
37
+ // Should use rate limit delay (3000ms)
38
+ await vi.advanceTimersByTimeAsync(retryAfterMs - 100);
39
+ expect(fn).toHaveBeenCalledTimes(1);
40
+
41
+ await vi.advanceTimersByTimeAsync(200);
42
+ expect(fn).toHaveBeenCalledTimes(2);
43
+
44
+ const result = await promise;
45
+ expect(result).toBe('success');
46
+ });
47
+
48
+ it('should parse retry-after header in seconds', async () => {
49
+ let attempt = 0;
50
+ const retryAfterSeconds = 5;
51
+
52
+ const fn = vi.fn().mockImplementation(async () => {
53
+ attempt++;
54
+ if (attempt === 1) {
55
+ throw new APICallError({
56
+ message: 'Rate limited',
57
+ url: 'https://api.example.com',
58
+ requestBodyValues: {},
59
+ isRetryable: true,
60
+ data: undefined,
61
+ responseHeaders: {
62
+ 'retry-after': retryAfterSeconds.toString(),
63
+ },
64
+ });
65
+ }
66
+ return 'success';
67
+ });
68
+
69
+ const promise = retryWithExponentialBackoffRespectingRetryHeaders()(fn);
70
+
71
+ // Fast-forward to just before the retry delay
72
+ await vi.advanceTimersByTimeAsync(retryAfterSeconds * 1000 - 100);
73
+ expect(fn).toHaveBeenCalledTimes(1);
74
+
75
+ // Fast-forward past the retry delay
76
+ await vi.advanceTimersByTimeAsync(200);
77
+ expect(fn).toHaveBeenCalledTimes(2);
78
+
79
+ const result = await promise;
80
+ expect(result).toBe('success');
81
+ });
82
+
83
+ it('should use exponential backoff when rate limit delay is too long', async () => {
84
+ let attempt = 0;
85
+ const retryAfterMs = 70000; // 70 seconds - too long
86
+ const initialDelay = 2000; // Default exponential backoff
87
+
88
+ const fn = vi.fn().mockImplementation(async () => {
89
+ attempt++;
90
+ if (attempt === 1) {
91
+ throw new APICallError({
92
+ message: 'Rate limited',
93
+ url: 'https://api.example.com',
94
+ requestBodyValues: {},
95
+ isRetryable: true,
96
+ data: undefined,
97
+ responseHeaders: {
98
+ 'retry-after-ms': retryAfterMs.toString(),
99
+ },
100
+ });
101
+ }
102
+ return 'success';
103
+ });
104
+
105
+ const promise = retryWithExponentialBackoffRespectingRetryHeaders({
106
+ initialDelayInMs: initialDelay,
107
+ })(fn);
108
+
109
+ // Should use exponential backoff delay (2000ms) not the rate limit (70000ms)
110
+ await vi.advanceTimersByTimeAsync(initialDelay - 100);
111
+ expect(fn).toHaveBeenCalledTimes(1);
112
+
113
+ await vi.advanceTimersByTimeAsync(200);
114
+ expect(fn).toHaveBeenCalledTimes(2);
115
+
116
+ const result = await promise;
117
+ expect(result).toBe('success');
118
+ });
119
+
120
+ it('should fall back to exponential backoff when no rate limit headers', async () => {
121
+ let attempt = 0;
122
+ const initialDelay = 2000;
123
+
124
+ const fn = vi.fn().mockImplementation(async () => {
125
+ attempt++;
126
+ if (attempt === 1) {
127
+ throw new APICallError({
128
+ message: 'Temporary error',
129
+ url: 'https://api.example.com',
130
+ requestBodyValues: {},
131
+ isRetryable: true,
132
+ data: undefined,
133
+ responseHeaders: {},
134
+ });
135
+ }
136
+ return 'success';
137
+ });
138
+
139
+ const promise = retryWithExponentialBackoffRespectingRetryHeaders({
140
+ initialDelayInMs: initialDelay,
141
+ })(fn);
142
+
143
+ // Fast-forward to just before the initial delay
144
+ await vi.advanceTimersByTimeAsync(initialDelay - 100);
145
+ expect(fn).toHaveBeenCalledTimes(1);
146
+
147
+ // Fast-forward past the initial delay
148
+ await vi.advanceTimersByTimeAsync(200);
149
+ expect(fn).toHaveBeenCalledTimes(2);
150
+
151
+ const result = await promise;
152
+ expect(result).toBe('success');
153
+ });
154
+
155
+ it('should handle invalid rate limit header values', async () => {
156
+ let attempt = 0;
157
+ const initialDelay = 2000;
158
+
159
+ const fn = vi.fn().mockImplementation(async () => {
160
+ attempt++;
161
+ if (attempt === 1) {
162
+ throw new APICallError({
163
+ message: 'Rate limited',
164
+ url: 'https://api.example.com',
165
+ requestBodyValues: {},
166
+ isRetryable: true,
167
+ data: undefined,
168
+ responseHeaders: {
169
+ 'retry-after-ms': 'invalid',
170
+ 'retry-after': 'not-a-number',
171
+ },
172
+ });
173
+ }
174
+ return 'success';
175
+ });
176
+
177
+ const promise = retryWithExponentialBackoffRespectingRetryHeaders({
178
+ initialDelayInMs: initialDelay,
179
+ })(fn);
180
+
181
+ // Should fall back to exponential backoff delay
182
+ await vi.advanceTimersByTimeAsync(initialDelay - 100);
183
+ expect(fn).toHaveBeenCalledTimes(1);
184
+
185
+ await vi.advanceTimersByTimeAsync(200);
186
+ expect(fn).toHaveBeenCalledTimes(2);
187
+
188
+ const result = await promise;
189
+ expect(result).toBe('success');
190
+ });
191
+
192
+ describe('with mocked provider responses', () => {
193
+ it('should handle Anthropic 429 response with retry-after-ms header', async () => {
194
+ let attempt = 0;
195
+ const delayMs = 5000;
196
+
197
+ const fn = vi.fn().mockImplementation(async () => {
198
+ attempt++;
199
+ if (attempt === 1) {
200
+ // Simulate actual Anthropic 429 response with retry-after-ms
201
+ throw new APICallError({
202
+ message: 'Rate limit exceeded',
203
+ url: 'https://api.anthropic.com/v1/messages',
204
+ requestBodyValues: {},
205
+ statusCode: 429,
206
+ isRetryable: true,
207
+ data: {
208
+ error: {
209
+ type: 'rate_limit_error',
210
+ message: 'Rate limit exceeded',
211
+ },
212
+ },
213
+ responseHeaders: {
214
+ 'retry-after-ms': delayMs.toString(),
215
+ 'x-request-id': 'req_123456',
216
+ },
217
+ });
218
+ }
219
+ return { content: 'Hello from Claude!' };
220
+ });
221
+
222
+ const promise = retryWithExponentialBackoffRespectingRetryHeaders()(fn);
223
+
224
+ // Should use the delay from retry-after-ms header
225
+ await vi.advanceTimersByTimeAsync(delayMs - 100);
226
+ expect(fn).toHaveBeenCalledTimes(1);
227
+
228
+ await vi.advanceTimersByTimeAsync(200);
229
+ expect(fn).toHaveBeenCalledTimes(2);
230
+
231
+ const result = await promise;
232
+ expect(result).toEqual({ content: 'Hello from Claude!' });
233
+ });
234
+
235
+ it('should handle OpenAI 429 response with retry-after header', async () => {
236
+ let attempt = 0;
237
+ const delaySeconds = 30; // 30 seconds
238
+
239
+ const fn = vi.fn().mockImplementation(async () => {
240
+ attempt++;
241
+ if (attempt === 1) {
242
+ // Simulate actual OpenAI 429 response with retry-after
243
+ throw new APICallError({
244
+ message: 'Rate limit reached for requests',
245
+ url: 'https://api.openai.com/v1/chat/completions',
246
+ requestBodyValues: {},
247
+ statusCode: 429,
248
+ isRetryable: true,
249
+ data: {
250
+ error: {
251
+ message: 'Rate limit reached for requests',
252
+ type: 'requests',
253
+ param: null,
254
+ code: 'rate_limit_exceeded',
255
+ },
256
+ },
257
+ responseHeaders: {
258
+ 'retry-after': delaySeconds.toString(),
259
+ 'x-request-id': 'req_abcdef123456',
260
+ },
261
+ });
262
+ }
263
+ return { choices: [{ message: { content: 'Hello from GPT!' } }] };
264
+ });
265
+
266
+ const promise = retryWithExponentialBackoffRespectingRetryHeaders()(fn);
267
+
268
+ // Should use the delay from retry-after header (30 seconds)
269
+ await vi.advanceTimersByTimeAsync(delaySeconds * 1000 - 100);
270
+ expect(fn).toHaveBeenCalledTimes(1);
271
+
272
+ await vi.advanceTimersByTimeAsync(200);
273
+ expect(fn).toHaveBeenCalledTimes(2);
274
+
275
+ const result = await promise;
276
+ expect(result).toEqual({
277
+ choices: [{ message: { content: 'Hello from GPT!' } }],
278
+ });
279
+ });
280
+
281
+ it('should handle multiple retries with exponential backoff progression', async () => {
282
+ let attempt = 0;
283
+ const baseTime = 1700000000000;
284
+
285
+ vi.setSystemTime(baseTime);
286
+
287
+ const fn = vi.fn().mockImplementation(async () => {
288
+ attempt++;
289
+ if (attempt === 1) {
290
+ // First attempt: 5 second rate limit delay
291
+ throw new APICallError({
292
+ message: 'Rate limited',
293
+ url: 'https://api.anthropic.com/v1/messages',
294
+ requestBodyValues: {},
295
+ statusCode: 429,
296
+ isRetryable: true,
297
+ data: undefined,
298
+ responseHeaders: {
299
+ 'retry-after-ms': '5000',
300
+ },
301
+ });
302
+ } else if (attempt === 2) {
303
+ // Second attempt: 2 second rate limit delay, but exponential backoff is 4 seconds
304
+ throw new APICallError({
305
+ message: 'Rate limited',
306
+ url: 'https://api.anthropic.com/v1/messages',
307
+ requestBodyValues: {},
308
+ statusCode: 429,
309
+ isRetryable: true,
310
+ data: undefined,
311
+ responseHeaders: {
312
+ 'retry-after-ms': '2000',
313
+ },
314
+ });
315
+ }
316
+ return { content: 'Success after retries!' };
317
+ });
318
+
319
+ const promise = retryWithExponentialBackoffRespectingRetryHeaders({
320
+ maxRetries: 3,
321
+ })(fn);
322
+
323
+ // First retry - uses rate limit delay (5000ms)
324
+ await vi.advanceTimersByTimeAsync(5000);
325
+ expect(fn).toHaveBeenCalledTimes(2);
326
+
327
+ // Second retry - uses exponential backoff (4000ms) which is > rate limit delay (2000ms)
328
+ await vi.advanceTimersByTimeAsync(4000);
329
+ expect(fn).toHaveBeenCalledTimes(3);
330
+
331
+ const result = await promise;
332
+ expect(result).toEqual({ content: 'Success after retries!' });
333
+ });
334
+
335
+ it('should prefer retry-after-ms over retry-after when both present', async () => {
336
+ let attempt = 0;
337
+
338
+ const fn = vi.fn().mockImplementation(async () => {
339
+ attempt++;
340
+ if (attempt === 1) {
341
+ throw new APICallError({
342
+ message: 'Rate limited',
343
+ url: 'https://api.example.com/v1/messages',
344
+ requestBodyValues: {},
345
+ statusCode: 429,
346
+ isRetryable: true,
347
+ data: undefined,
348
+ responseHeaders: {
349
+ 'retry-after-ms': '3000', // 3 seconds - should use this
350
+ 'retry-after': '10', // 10 seconds - should ignore
351
+ },
352
+ });
353
+ }
354
+ return 'success';
355
+ });
356
+
357
+ const promise = retryWithExponentialBackoffRespectingRetryHeaders()(fn);
358
+
359
+ // Should use 3 second delay from retry-after-ms
360
+ await vi.advanceTimersByTimeAsync(3000);
361
+ expect(fn).toHaveBeenCalledTimes(2);
362
+
363
+ const result = await promise;
364
+ expect(result).toBe('success');
365
+ });
366
+
367
+ it('should handle retry-after header with HTTP date format', async () => {
368
+ let attempt = 0;
369
+ const baseTime = 1700000000000;
370
+ const delayMs = 5000;
371
+
372
+ vi.setSystemTime(baseTime);
373
+
374
+ const fn = vi.fn().mockImplementation(async () => {
375
+ attempt++;
376
+ if (attempt === 1) {
377
+ const futureDate = new Date(baseTime + delayMs).toUTCString();
378
+ throw new APICallError({
379
+ message: 'Rate limit exceeded',
380
+ url: 'https://api.example.com/v1/endpoint',
381
+ requestBodyValues: {},
382
+ statusCode: 429,
383
+ isRetryable: true,
384
+ data: undefined,
385
+ responseHeaders: {
386
+ 'retry-after': futureDate,
387
+ },
388
+ });
389
+ }
390
+ return { data: 'success' };
391
+ });
392
+
393
+ const promise = retryWithExponentialBackoffRespectingRetryHeaders()(fn);
394
+
395
+ await vi.advanceTimersByTimeAsync(0);
396
+ expect(fn).toHaveBeenCalledTimes(1);
397
+
398
+ // Should wait for 5 seconds
399
+ await vi.advanceTimersByTimeAsync(delayMs - 100);
400
+ expect(fn).toHaveBeenCalledTimes(1);
401
+
402
+ await vi.advanceTimersByTimeAsync(200);
403
+ expect(fn).toHaveBeenCalledTimes(2);
404
+
405
+ const result = await promise;
406
+ expect(result).toEqual({ data: 'success' });
407
+ });
408
+
409
+ it('should fall back to exponential backoff when rate limit delay is negative', async () => {
410
+ let attempt = 0;
411
+ const initialDelay = 2000;
412
+
413
+ const fn = vi.fn().mockImplementation(async () => {
414
+ attempt++;
415
+ if (attempt === 1) {
416
+ throw new APICallError({
417
+ message: 'Rate limited',
418
+ url: 'https://api.example.com',
419
+ requestBodyValues: {},
420
+ statusCode: 429,
421
+ isRetryable: true,
422
+ data: undefined,
423
+ responseHeaders: {
424
+ 'retry-after-ms': '-1000', // Negative value
425
+ },
426
+ });
427
+ }
428
+ return 'success';
429
+ });
430
+
431
+ const promise = retryWithExponentialBackoffRespectingRetryHeaders({
432
+ initialDelayInMs: initialDelay,
433
+ })(fn);
434
+
435
+ // Should use exponential backoff delay (2000ms) not the negative rate limit
436
+ await vi.advanceTimersByTimeAsync(initialDelay - 100);
437
+ expect(fn).toHaveBeenCalledTimes(1);
438
+
439
+ await vi.advanceTimersByTimeAsync(200);
440
+ expect(fn).toHaveBeenCalledTimes(2);
441
+
442
+ const result = await promise;
443
+ expect(result).toBe('success');
444
+ });
445
+ });
446
+ });
@@ -0,0 +1,154 @@
1
+ import { APICallError } from '@ai-sdk/provider';
2
+ import { delay, getErrorMessage, isAbortError } from '@ai-sdk/provider-utils';
3
+ import { RetryError } from './retry-error';
4
+
5
+ export type RetryFunction = <OUTPUT>(
6
+ fn: () => PromiseLike<OUTPUT>,
7
+ ) => PromiseLike<OUTPUT>;
8
+
9
+ function getRetryDelayInMs({
10
+ error,
11
+ exponentialBackoffDelay,
12
+ }: {
13
+ error: APICallError;
14
+ exponentialBackoffDelay: number;
15
+ }): number {
16
+ const headers = error.responseHeaders;
17
+
18
+ if (!headers) return exponentialBackoffDelay;
19
+
20
+ let ms: number | undefined;
21
+
22
+ // retry-ms is more precise than retry-after and used by e.g. OpenAI
23
+ const retryAfterMs = headers['retry-after-ms'];
24
+ if (retryAfterMs) {
25
+ const timeoutMs = parseFloat(retryAfterMs);
26
+ if (!Number.isNaN(timeoutMs)) {
27
+ ms = timeoutMs;
28
+ }
29
+ }
30
+
31
+ // About the Retry-After header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
32
+ const retryAfter = headers['retry-after'];
33
+ if (retryAfter && ms === undefined) {
34
+ const timeoutSeconds = parseFloat(retryAfter);
35
+ if (!Number.isNaN(timeoutSeconds)) {
36
+ ms = timeoutSeconds * 1000;
37
+ } else {
38
+ ms = Date.parse(retryAfter) - Date.now();
39
+ }
40
+ }
41
+
42
+ // check that the delay is reasonable:
43
+ if (
44
+ ms != null &&
45
+ !Number.isNaN(ms) &&
46
+ 0 <= ms &&
47
+ (ms < 60 * 1000 || ms < exponentialBackoffDelay)
48
+ ) {
49
+ return ms;
50
+ }
51
+
52
+ return exponentialBackoffDelay;
53
+ }
54
+
55
+ /**
56
+ The `retryWithExponentialBackoffRespectingRetryHeaders` strategy retries a failed API call with an exponential backoff,
57
+ while respecting rate limit headers (retry-after-ms and retry-after) if they are provided and reasonable (0-60 seconds).
58
+ You can configure the maximum number of retries, the initial delay, and the backoff factor.
59
+ */
60
+ export const retryWithExponentialBackoffRespectingRetryHeaders =
61
+ ({
62
+ maxRetries = 2,
63
+ initialDelayInMs = 2000,
64
+ backoffFactor = 2,
65
+ abortSignal,
66
+ }: {
67
+ maxRetries?: number;
68
+ initialDelayInMs?: number;
69
+ backoffFactor?: number;
70
+ abortSignal?: AbortSignal;
71
+ } = {}): RetryFunction =>
72
+ async <OUTPUT>(f: () => PromiseLike<OUTPUT>) =>
73
+ _retryWithExponentialBackoff(f, {
74
+ maxRetries,
75
+ delayInMs: initialDelayInMs,
76
+ backoffFactor,
77
+ abortSignal,
78
+ });
79
+
80
+ async function _retryWithExponentialBackoff<OUTPUT>(
81
+ f: () => PromiseLike<OUTPUT>,
82
+ {
83
+ maxRetries,
84
+ delayInMs,
85
+ backoffFactor,
86
+ abortSignal,
87
+ }: {
88
+ maxRetries: number;
89
+ delayInMs: number;
90
+ backoffFactor: number;
91
+ abortSignal: AbortSignal | undefined;
92
+ },
93
+ errors: unknown[] = [],
94
+ ): Promise<OUTPUT> {
95
+ try {
96
+ return await f();
97
+ } catch (error) {
98
+ if (isAbortError(error)) {
99
+ throw error; // don't retry when the request was aborted
100
+ }
101
+
102
+ if (maxRetries === 0) {
103
+ throw error; // don't wrap the error when retries are disabled
104
+ }
105
+
106
+ const errorMessage = getErrorMessage(error);
107
+ const newErrors = [...errors, error];
108
+ const tryNumber = newErrors.length;
109
+
110
+ if (tryNumber > maxRetries) {
111
+ throw new RetryError({
112
+ message: `Failed after ${tryNumber} attempts. Last error: ${errorMessage}`,
113
+ reason: 'maxRetriesExceeded',
114
+ errors: newErrors,
115
+ });
116
+ }
117
+
118
+ if (
119
+ error instanceof Error &&
120
+ APICallError.isInstance(error) &&
121
+ error.isRetryable === true &&
122
+ tryNumber <= maxRetries
123
+ ) {
124
+ await delay(
125
+ getRetryDelayInMs({
126
+ error,
127
+ exponentialBackoffDelay: delayInMs,
128
+ }),
129
+ { abortSignal },
130
+ );
131
+
132
+ return _retryWithExponentialBackoff(
133
+ f,
134
+ {
135
+ maxRetries,
136
+ delayInMs: backoffFactor * delayInMs,
137
+ backoffFactor,
138
+ abortSignal,
139
+ },
140
+ newErrors,
141
+ );
142
+ }
143
+
144
+ if (tryNumber === 1) {
145
+ throw error; // don't wrap the error when a non-retryable error occurs on the first try
146
+ }
147
+
148
+ throw new RetryError({
149
+ message: `Failed after ${tryNumber} attempts with non-retryable error: '${errorMessage}'`,
150
+ reason: 'errorNotRetryable',
151
+ errors: newErrors,
152
+ });
153
+ }
154
+ }