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.
- package/CHANGELOG.md +16 -0
- package/dist/index.d.mts +50 -21
- package/dist/index.d.ts +50 -21
- package/dist/index.js +348 -286
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +280 -219
- package/dist/index.mjs.map +1 -1
- package/dist/internal/index.js +1 -1
- package/dist/internal/index.mjs +1 -1
- package/docs/02-foundations/03-prompts.mdx +2 -2
- package/docs/03-ai-sdk-core/15-tools-and-tool-calling.mdx +1 -1
- package/docs/07-reference/01-ai-sdk-core/28-output.mdx +1 -1
- package/docs/07-reference/05-ai-sdk-errors/ai-ui-message-stream-error.mdx +67 -0
- package/package.json +6 -4
- package/src/agent/agent.ts +116 -0
- package/src/agent/create-agent-ui-stream-response.test.ts +258 -0
- package/src/agent/create-agent-ui-stream-response.ts +50 -0
- package/src/agent/create-agent-ui-stream.ts +73 -0
- package/src/agent/index.ts +33 -0
- package/src/agent/infer-agent-tools.ts +7 -0
- package/src/agent/infer-agent-ui-message.test-d.ts +54 -0
- package/src/agent/infer-agent-ui-message.ts +11 -0
- package/src/agent/pipe-agent-ui-stream-to-response.ts +52 -0
- package/src/agent/tool-loop-agent-on-finish-callback.ts +31 -0
- package/src/agent/tool-loop-agent-on-step-finish-callback.ts +11 -0
- package/src/agent/tool-loop-agent-settings.ts +182 -0
- package/src/agent/tool-loop-agent.test-d.ts +114 -0
- package/src/agent/tool-loop-agent.test.ts +442 -0
- package/src/agent/tool-loop-agent.ts +114 -0
- package/src/embed/__snapshots__/embed-many.test.ts.snap +191 -0
- package/src/embed/__snapshots__/embed.test.ts.snap +81 -0
- package/src/embed/embed-many-result.ts +53 -0
- package/src/embed/embed-many.test.ts +653 -0
- package/src/embed/embed-many.ts +378 -0
- package/src/embed/embed-result.ts +50 -0
- package/src/embed/embed.test.ts +298 -0
- package/src/embed/embed.ts +211 -0
- package/src/embed/index.ts +4 -0
- package/src/error/index.ts +35 -0
- package/src/error/invalid-argument-error.ts +34 -0
- package/src/error/invalid-stream-part-error.ts +28 -0
- package/src/error/invalid-tool-approval-error.ts +26 -0
- package/src/error/invalid-tool-input-error.ts +33 -0
- package/src/error/no-image-generated-error.ts +39 -0
- package/src/error/no-object-generated-error.ts +70 -0
- package/src/error/no-output-generated-error.ts +26 -0
- package/src/error/no-speech-generated-error.ts +18 -0
- package/src/error/no-such-tool-error.ts +35 -0
- package/src/error/no-transcript-generated-error.ts +20 -0
- package/src/error/tool-call-not-found-for-approval-error.ts +32 -0
- package/src/error/tool-call-repair-error.ts +30 -0
- package/src/error/ui-message-stream-error.ts +48 -0
- package/src/error/unsupported-model-version-error.ts +23 -0
- package/src/error/verify-no-object-generated-error.ts +27 -0
- package/src/generate-image/generate-image-result.ts +42 -0
- package/src/generate-image/generate-image.test.ts +1420 -0
- package/src/generate-image/generate-image.ts +360 -0
- package/src/generate-image/index.ts +18 -0
- package/src/generate-object/__snapshots__/generate-object.test.ts.snap +133 -0
- package/src/generate-object/__snapshots__/stream-object.test.ts.snap +297 -0
- package/src/generate-object/generate-object-result.ts +67 -0
- package/src/generate-object/generate-object.test-d.ts +49 -0
- package/src/generate-object/generate-object.test.ts +1191 -0
- package/src/generate-object/generate-object.ts +518 -0
- package/src/generate-object/index.ts +9 -0
- package/src/generate-object/inject-json-instruction.test.ts +181 -0
- package/src/generate-object/inject-json-instruction.ts +30 -0
- package/src/generate-object/output-strategy.ts +415 -0
- package/src/generate-object/parse-and-validate-object-result.ts +111 -0
- package/src/generate-object/repair-text.ts +12 -0
- package/src/generate-object/stream-object-result.ts +120 -0
- package/src/generate-object/stream-object.test-d.ts +74 -0
- package/src/generate-object/stream-object.test.ts +1950 -0
- package/src/generate-object/stream-object.ts +986 -0
- package/src/generate-object/validate-object-generation-input.ts +144 -0
- package/src/generate-speech/generate-speech-result.ts +30 -0
- package/src/generate-speech/generate-speech.test.ts +300 -0
- package/src/generate-speech/generate-speech.ts +190 -0
- package/src/generate-speech/generated-audio-file.ts +65 -0
- package/src/generate-speech/index.ts +3 -0
- package/src/generate-text/__snapshots__/generate-text.test.ts.snap +1872 -0
- package/src/generate-text/__snapshots__/stream-text.test.ts.snap +1255 -0
- package/src/generate-text/collect-tool-approvals.test.ts +553 -0
- package/src/generate-text/collect-tool-approvals.ts +116 -0
- package/src/generate-text/content-part.ts +25 -0
- package/src/generate-text/execute-tool-call.ts +129 -0
- package/src/generate-text/extract-reasoning-content.ts +17 -0
- package/src/generate-text/extract-text-content.ts +15 -0
- package/src/generate-text/generate-text-result.ts +168 -0
- package/src/generate-text/generate-text.test-d.ts +68 -0
- package/src/generate-text/generate-text.test.ts +7011 -0
- package/src/generate-text/generate-text.ts +1223 -0
- package/src/generate-text/generated-file.ts +70 -0
- package/src/generate-text/index.ts +57 -0
- package/src/generate-text/is-approval-needed.ts +29 -0
- package/src/generate-text/output-utils.ts +23 -0
- package/src/generate-text/output.test.ts +698 -0
- package/src/generate-text/output.ts +590 -0
- package/src/generate-text/parse-tool-call.test.ts +570 -0
- package/src/generate-text/parse-tool-call.ts +188 -0
- package/src/generate-text/prepare-step.ts +103 -0
- package/src/generate-text/prune-messages.test.ts +720 -0
- package/src/generate-text/prune-messages.ts +167 -0
- package/src/generate-text/reasoning-output.ts +20 -0
- package/src/generate-text/reasoning.ts +8 -0
- package/src/generate-text/response-message.ts +10 -0
- package/src/generate-text/run-tools-transformation.test.ts +1143 -0
- package/src/generate-text/run-tools-transformation.ts +420 -0
- package/src/generate-text/smooth-stream.test.ts +2101 -0
- package/src/generate-text/smooth-stream.ts +162 -0
- package/src/generate-text/step-result.ts +238 -0
- package/src/generate-text/stop-condition.ts +29 -0
- package/src/generate-text/stream-text-result.ts +463 -0
- package/src/generate-text/stream-text.test-d.ts +200 -0
- package/src/generate-text/stream-text.test.ts +19979 -0
- package/src/generate-text/stream-text.ts +2505 -0
- package/src/generate-text/to-response-messages.test.ts +922 -0
- package/src/generate-text/to-response-messages.ts +163 -0
- package/src/generate-text/tool-approval-request-output.ts +21 -0
- package/src/generate-text/tool-call-repair-function.ts +27 -0
- package/src/generate-text/tool-call.ts +47 -0
- package/src/generate-text/tool-error.ts +34 -0
- package/src/generate-text/tool-output-denied.ts +21 -0
- package/src/generate-text/tool-output.ts +7 -0
- package/src/generate-text/tool-result.ts +36 -0
- package/src/generate-text/tool-set.ts +14 -0
- package/src/global.ts +24 -0
- package/src/index.ts +50 -0
- package/src/logger/index.ts +6 -0
- package/src/logger/log-warnings.test.ts +351 -0
- package/src/logger/log-warnings.ts +119 -0
- package/src/middleware/__snapshots__/simulate-streaming-middleware.test.ts.snap +64 -0
- package/src/middleware/add-tool-input-examples-middleware.test.ts +476 -0
- package/src/middleware/add-tool-input-examples-middleware.ts +90 -0
- package/src/middleware/default-embedding-settings-middleware.test.ts +126 -0
- package/src/middleware/default-embedding-settings-middleware.ts +22 -0
- package/src/middleware/default-settings-middleware.test.ts +388 -0
- package/src/middleware/default-settings-middleware.ts +33 -0
- package/src/middleware/extract-json-middleware.test.ts +827 -0
- package/src/middleware/extract-json-middleware.ts +197 -0
- package/src/middleware/extract-reasoning-middleware.test.ts +1028 -0
- package/src/middleware/extract-reasoning-middleware.ts +238 -0
- package/src/middleware/index.ts +10 -0
- package/src/middleware/simulate-streaming-middleware.test.ts +911 -0
- package/src/middleware/simulate-streaming-middleware.ts +79 -0
- package/src/middleware/wrap-embedding-model.test.ts +358 -0
- package/src/middleware/wrap-embedding-model.ts +86 -0
- package/src/middleware/wrap-image-model.test.ts +423 -0
- package/src/middleware/wrap-image-model.ts +85 -0
- package/src/middleware/wrap-language-model.test.ts +518 -0
- package/src/middleware/wrap-language-model.ts +104 -0
- package/src/middleware/wrap-provider.test.ts +120 -0
- package/src/middleware/wrap-provider.ts +51 -0
- package/src/model/as-embedding-model-v3.test.ts +319 -0
- package/src/model/as-embedding-model-v3.ts +24 -0
- package/src/model/as-image-model-v3.test.ts +409 -0
- package/src/model/as-image-model-v3.ts +24 -0
- package/src/model/as-language-model-v3.test.ts +508 -0
- package/src/model/as-language-model-v3.ts +103 -0
- package/src/model/as-provider-v3.ts +36 -0
- package/src/model/as-speech-model-v3.test.ts +356 -0
- package/src/model/as-speech-model-v3.ts +24 -0
- package/src/model/as-transcription-model-v3.test.ts +529 -0
- package/src/model/as-transcription-model-v3.ts +24 -0
- package/src/model/resolve-model.test.ts +244 -0
- package/src/model/resolve-model.ts +126 -0
- package/src/prompt/call-settings.ts +148 -0
- package/src/prompt/content-part.ts +209 -0
- package/src/prompt/convert-to-language-model-prompt.test.ts +2018 -0
- package/src/prompt/convert-to-language-model-prompt.ts +442 -0
- package/src/prompt/create-tool-model-output.test.ts +508 -0
- package/src/prompt/create-tool-model-output.ts +34 -0
- package/src/prompt/data-content.test.ts +15 -0
- package/src/prompt/data-content.ts +134 -0
- package/src/prompt/index.ts +27 -0
- package/src/prompt/invalid-data-content-error.ts +29 -0
- package/src/prompt/invalid-message-role-error.ts +27 -0
- package/src/prompt/message-conversion-error.ts +28 -0
- package/src/prompt/message.ts +68 -0
- package/src/prompt/prepare-call-settings.test.ts +159 -0
- package/src/prompt/prepare-call-settings.ts +108 -0
- package/src/prompt/prepare-tools-and-tool-choice.test.ts +461 -0
- package/src/prompt/prepare-tools-and-tool-choice.ts +86 -0
- package/src/prompt/prompt.ts +43 -0
- package/src/prompt/split-data-url.ts +17 -0
- package/src/prompt/standardize-prompt.test.ts +82 -0
- package/src/prompt/standardize-prompt.ts +99 -0
- package/src/prompt/wrap-gateway-error.ts +29 -0
- package/src/registry/custom-provider.test.ts +211 -0
- package/src/registry/custom-provider.ts +155 -0
- package/src/registry/index.ts +7 -0
- package/src/registry/no-such-provider-error.ts +41 -0
- package/src/registry/provider-registry.test.ts +691 -0
- package/src/registry/provider-registry.ts +328 -0
- package/src/rerank/index.ts +2 -0
- package/src/rerank/rerank-result.ts +70 -0
- package/src/rerank/rerank.test.ts +516 -0
- package/src/rerank/rerank.ts +237 -0
- package/src/telemetry/assemble-operation-name.ts +21 -0
- package/src/telemetry/get-base-telemetry-attributes.ts +53 -0
- package/src/telemetry/get-tracer.ts +20 -0
- package/src/telemetry/noop-tracer.ts +69 -0
- package/src/telemetry/record-span.ts +63 -0
- package/src/telemetry/select-telemetry-attributes.ts +78 -0
- package/src/telemetry/select-temetry-attributes.test.ts +114 -0
- package/src/telemetry/stringify-for-telemetry.test.ts +114 -0
- package/src/telemetry/stringify-for-telemetry.ts +33 -0
- package/src/telemetry/telemetry-settings.ts +44 -0
- package/src/test/mock-embedding-model-v2.ts +35 -0
- package/src/test/mock-embedding-model-v3.ts +48 -0
- package/src/test/mock-image-model-v2.ts +28 -0
- package/src/test/mock-image-model-v3.ts +28 -0
- package/src/test/mock-language-model-v2.ts +72 -0
- package/src/test/mock-language-model-v3.ts +77 -0
- package/src/test/mock-provider-v2.ts +68 -0
- package/src/test/mock-provider-v3.ts +80 -0
- package/src/test/mock-reranking-model-v3.ts +25 -0
- package/src/test/mock-server-response.ts +69 -0
- package/src/test/mock-speech-model-v2.ts +24 -0
- package/src/test/mock-speech-model-v3.ts +24 -0
- package/src/test/mock-tracer.ts +156 -0
- package/src/test/mock-transcription-model-v2.ts +24 -0
- package/src/test/mock-transcription-model-v3.ts +24 -0
- package/src/test/mock-values.ts +4 -0
- package/src/test/not-implemented.ts +3 -0
- package/src/text-stream/create-text-stream-response.test.ts +38 -0
- package/src/text-stream/create-text-stream-response.ts +18 -0
- package/src/text-stream/index.ts +2 -0
- package/src/text-stream/pipe-text-stream-to-response.test.ts +38 -0
- package/src/text-stream/pipe-text-stream-to-response.ts +26 -0
- package/src/transcribe/index.ts +2 -0
- package/src/transcribe/transcribe-result.ts +60 -0
- package/src/transcribe/transcribe.test.ts +313 -0
- package/src/transcribe/transcribe.ts +173 -0
- package/src/types/embedding-model-middleware.ts +3 -0
- package/src/types/embedding-model.ts +18 -0
- package/src/types/image-model-middleware.ts +3 -0
- package/src/types/image-model-response-metadata.ts +16 -0
- package/src/types/image-model.ts +19 -0
- package/src/types/index.ts +29 -0
- package/src/types/json-value.ts +15 -0
- package/src/types/language-model-middleware.ts +3 -0
- package/src/types/language-model-request-metadata.ts +6 -0
- package/src/types/language-model-response-metadata.ts +21 -0
- package/src/types/language-model.ts +104 -0
- package/src/types/provider-metadata.ts +16 -0
- package/src/types/provider.ts +55 -0
- package/src/types/reranking-model.ts +6 -0
- package/src/types/speech-model-response-metadata.ts +21 -0
- package/src/types/speech-model.ts +6 -0
- package/src/types/transcription-model-response-metadata.ts +16 -0
- package/src/types/transcription-model.ts +9 -0
- package/src/types/usage.ts +200 -0
- package/src/types/warning.ts +7 -0
- package/src/ui/__snapshots__/append-response-messages.test.ts.snap +416 -0
- package/src/ui/__snapshots__/convert-to-model-messages.test.ts.snap +419 -0
- package/src/ui/__snapshots__/process-chat-text-response.test.ts.snap +142 -0
- package/src/ui/call-completion-api.ts +157 -0
- package/src/ui/chat-transport.ts +83 -0
- package/src/ui/chat.test-d.ts +233 -0
- package/src/ui/chat.test.ts +2695 -0
- package/src/ui/chat.ts +716 -0
- package/src/ui/convert-file-list-to-file-ui-parts.ts +36 -0
- package/src/ui/convert-to-model-messages.test.ts +2775 -0
- package/src/ui/convert-to-model-messages.ts +373 -0
- package/src/ui/default-chat-transport.ts +36 -0
- package/src/ui/direct-chat-transport.test.ts +446 -0
- package/src/ui/direct-chat-transport.ts +118 -0
- package/src/ui/http-chat-transport.test.ts +185 -0
- package/src/ui/http-chat-transport.ts +292 -0
- package/src/ui/index.ts +71 -0
- package/src/ui/last-assistant-message-is-complete-with-approval-responses.ts +44 -0
- package/src/ui/last-assistant-message-is-complete-with-tool-calls.test.ts +371 -0
- package/src/ui/last-assistant-message-is-complete-with-tool-calls.ts +39 -0
- package/src/ui/process-text-stream.test.ts +38 -0
- package/src/ui/process-text-stream.ts +16 -0
- package/src/ui/process-ui-message-stream.test.ts +8294 -0
- package/src/ui/process-ui-message-stream.ts +761 -0
- package/src/ui/text-stream-chat-transport.ts +23 -0
- package/src/ui/transform-text-to-ui-message-stream.test.ts +124 -0
- package/src/ui/transform-text-to-ui-message-stream.ts +27 -0
- package/src/ui/ui-messages.test.ts +48 -0
- package/src/ui/ui-messages.ts +534 -0
- package/src/ui/use-completion.ts +84 -0
- package/src/ui/validate-ui-messages.test.ts +1428 -0
- package/src/ui/validate-ui-messages.ts +476 -0
- package/src/ui-message-stream/create-ui-message-stream-response.test.ts +266 -0
- package/src/ui-message-stream/create-ui-message-stream-response.ts +32 -0
- package/src/ui-message-stream/create-ui-message-stream.test.ts +639 -0
- package/src/ui-message-stream/create-ui-message-stream.ts +124 -0
- package/src/ui-message-stream/get-response-ui-message-id.test.ts +55 -0
- package/src/ui-message-stream/get-response-ui-message-id.ts +24 -0
- package/src/ui-message-stream/handle-ui-message-stream-finish.test.ts +429 -0
- package/src/ui-message-stream/handle-ui-message-stream-finish.ts +135 -0
- package/src/ui-message-stream/index.ts +13 -0
- package/src/ui-message-stream/json-to-sse-transform-stream.ts +12 -0
- package/src/ui-message-stream/pipe-ui-message-stream-to-response.test.ts +90 -0
- package/src/ui-message-stream/pipe-ui-message-stream-to-response.ts +40 -0
- package/src/ui-message-stream/read-ui-message-stream.test.ts +122 -0
- package/src/ui-message-stream/read-ui-message-stream.ts +87 -0
- package/src/ui-message-stream/ui-message-chunks.test-d.ts +18 -0
- package/src/ui-message-stream/ui-message-chunks.ts +344 -0
- package/src/ui-message-stream/ui-message-stream-headers.ts +7 -0
- package/src/ui-message-stream/ui-message-stream-on-finish-callback.ts +32 -0
- package/src/ui-message-stream/ui-message-stream-response-init.ts +5 -0
- package/src/ui-message-stream/ui-message-stream-writer.ts +24 -0
- package/src/util/as-array.ts +3 -0
- package/src/util/async-iterable-stream.test.ts +241 -0
- package/src/util/async-iterable-stream.ts +94 -0
- package/src/util/consume-stream.ts +29 -0
- package/src/util/cosine-similarity.test.ts +57 -0
- package/src/util/cosine-similarity.ts +47 -0
- package/src/util/create-resolvable-promise.ts +30 -0
- package/src/util/create-stitchable-stream.test.ts +239 -0
- package/src/util/create-stitchable-stream.ts +112 -0
- package/src/util/data-url.ts +17 -0
- package/src/util/deep-partial.ts +84 -0
- package/src/util/detect-media-type.test.ts +670 -0
- package/src/util/detect-media-type.ts +184 -0
- package/src/util/download/download-function.ts +45 -0
- package/src/util/download/download.test.ts +69 -0
- package/src/util/download/download.ts +46 -0
- package/src/util/error-handler.ts +1 -0
- package/src/util/fix-json.test.ts +279 -0
- package/src/util/fix-json.ts +401 -0
- package/src/util/get-potential-start-index.test.ts +34 -0
- package/src/util/get-potential-start-index.ts +30 -0
- package/src/util/index.ts +11 -0
- package/src/util/is-deep-equal-data.test.ts +119 -0
- package/src/util/is-deep-equal-data.ts +48 -0
- package/src/util/is-non-empty-object.ts +5 -0
- package/src/util/job.ts +1 -0
- package/src/util/log-v2-compatibility-warning.ts +21 -0
- package/src/util/merge-abort-signals.test.ts +155 -0
- package/src/util/merge-abort-signals.ts +43 -0
- package/src/util/merge-objects.test.ts +118 -0
- package/src/util/merge-objects.ts +79 -0
- package/src/util/now.ts +4 -0
- package/src/util/parse-partial-json.test.ts +80 -0
- package/src/util/parse-partial-json.ts +30 -0
- package/src/util/prepare-headers.test.ts +51 -0
- package/src/util/prepare-headers.ts +14 -0
- package/src/util/prepare-retries.test.ts +10 -0
- package/src/util/prepare-retries.ts +47 -0
- package/src/util/retry-error.ts +41 -0
- package/src/util/retry-with-exponential-backoff.test.ts +446 -0
- package/src/util/retry-with-exponential-backoff.ts +154 -0
- package/src/util/serial-job-executor.test.ts +162 -0
- package/src/util/serial-job-executor.ts +36 -0
- package/src/util/simulate-readable-stream.test.ts +98 -0
- package/src/util/simulate-readable-stream.ts +39 -0
- package/src/util/split-array.test.ts +60 -0
- package/src/util/split-array.ts +20 -0
- package/src/util/value-of.ts +65 -0
- package/src/util/write-to-server-response.test.ts +266 -0
- package/src/util/write-to-server-response.ts +49 -0
- 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
|
+
}
|