ai 6.0.216 → 6.0.218
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 +21 -0
- package/dist/index.d.mts +19 -2
- package/dist/index.d.ts +19 -2
- package/dist/index.js +79 -106
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +90 -115
- package/dist/index.mjs.map +1 -1
- package/dist/internal/index.d.mts +1 -3
- package/dist/internal/index.d.ts +1 -3
- package/dist/internal/index.js +10 -58
- package/dist/internal/index.js.map +1 -1
- package/dist/internal/index.mjs +13 -59
- package/dist/internal/index.mjs.map +1 -1
- package/docs/02-foundations/02-providers-and-models.mdx +1 -0
- package/docs/03-ai-sdk-core/16-mcp-tools.mdx +28 -0
- package/docs/03-ai-sdk-core/38-video-generation.mdx +40 -0
- package/docs/03-ai-sdk-core/40-middleware.mdx +3 -3
- package/docs/07-reference/01-ai-sdk-core/13-generate-video.mdx +12 -0
- package/docs/07-reference/01-ai-sdk-core/23-create-mcp-client.mdx +7 -0
- package/package.json +4 -4
- package/src/generate-video/generate-video.ts +142 -64
- package/src/util/prepare-retries.ts +2 -4
- package/src/util/retry-with-exponential-backoff.ts +27 -95
|
@@ -171,6 +171,46 @@ const { video } = await generateVideo({
|
|
|
171
171
|
});
|
|
172
172
|
```
|
|
173
173
|
|
|
174
|
+
### First and Last Frame
|
|
175
|
+
|
|
176
|
+
Some video models support first-last-frame generation, where you provide the
|
|
177
|
+
starting and/or ending frames of the video. Use the `frameImages` option to pass
|
|
178
|
+
role-tagged images in a provider-agnostic way:
|
|
179
|
+
|
|
180
|
+
```tsx highlight={"5-16"}
|
|
181
|
+
const { video } = await generateVideo({
|
|
182
|
+
model: __VIDEO_MODEL__,
|
|
183
|
+
prompt: 'The cat walks across the scene and transforms into a dog by the end',
|
|
184
|
+
frameImages: [
|
|
185
|
+
{
|
|
186
|
+
image: 'https://example.com/first-frame.png',
|
|
187
|
+
frameType: 'first_frame',
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
image: 'https://example.com/last-frame.png',
|
|
191
|
+
frameType: 'last_frame',
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Reference Images
|
|
198
|
+
|
|
199
|
+
Some video models support reference-to-video generation, where you provide one or
|
|
200
|
+
more reference images that the model incorporates into the generated video. Use the `inputReferences` option to pass
|
|
201
|
+
these images in a provider-agnostic way:
|
|
202
|
+
|
|
203
|
+
```tsx highlight={"5-8"}
|
|
204
|
+
const { video } = await generateVideo({
|
|
205
|
+
model: __VIDEO_MODEL__,
|
|
206
|
+
prompt: 'The two characters meet in a bustling market',
|
|
207
|
+
inputReferences: [
|
|
208
|
+
'https://example.com/character-1.png',
|
|
209
|
+
'https://example.com/character-2.png',
|
|
210
|
+
],
|
|
211
|
+
});
|
|
212
|
+
```
|
|
213
|
+
|
|
174
214
|
### Providing a Seed
|
|
175
215
|
|
|
176
216
|
You can provide a seed to the `experimental_generateVideo` function to control the output of the video generation process.
|
|
@@ -271,15 +271,15 @@ Find more examples at this [link](https://github.com/minpeter/ai-sdk-tool-call-m
|
|
|
271
271
|
<Note>
|
|
272
272
|
Implementing language model middleware is advanced functionality and requires
|
|
273
273
|
a solid understanding of the [language model
|
|
274
|
-
specification](https://github.com/vercel/ai/blob/
|
|
274
|
+
specification](https://github.com/vercel/ai/blob/release-v6.0/packages/provider/src/language-model/v3/language-model-v3.ts).
|
|
275
275
|
</Note>
|
|
276
276
|
|
|
277
277
|
You can implement any of the following three function to modify the behavior of the language model:
|
|
278
278
|
|
|
279
279
|
1. `transformParams`: Transforms the parameters before they are passed to the language model, for both `doGenerate` and `doStream`.
|
|
280
|
-
2. `wrapGenerate`: Wraps the `doGenerate` method of the [language model](https://github.com/vercel/ai/blob/
|
|
280
|
+
2. `wrapGenerate`: Wraps the `doGenerate` method of the [language model](https://github.com/vercel/ai/blob/release-v6.0/packages/provider/src/language-model/v3/language-model-v3.ts).
|
|
281
281
|
You can modify the parameters, call the language model, and modify the result.
|
|
282
|
-
3. `wrapStream`: Wraps the `doStream` method of the [language model](https://github.com/vercel/ai/blob/
|
|
282
|
+
3. `wrapStream`: Wraps the `doStream` method of the [language model](https://github.com/vercel/ai/blob/release-v6.0/packages/provider/src/language-model/v3/language-model-v3.ts).
|
|
283
283
|
You can modify the parameters, call the language model, and modify the result.
|
|
284
284
|
|
|
285
285
|
Here are some examples of how to implement language model middleware:
|
|
@@ -110,6 +110,18 @@ console.log(videos);
|
|
|
110
110
|
isOptional: true,
|
|
111
111
|
description: 'Seed for the video generation.',
|
|
112
112
|
},
|
|
113
|
+
{
|
|
114
|
+
name: 'frameImages',
|
|
115
|
+
type: 'Array<{ image: DataContent; frameType: "first_frame" | "last_frame" }>',
|
|
116
|
+
isOptional: true,
|
|
117
|
+
description: 'Role-tagged image inputs for first-last-frame generation.',
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: 'inputReferences',
|
|
121
|
+
type: 'Array<DataContent>',
|
|
122
|
+
isOptional: true,
|
|
123
|
+
description: 'Reference image inputs for reference-to-video generation.',
|
|
124
|
+
},
|
|
113
125
|
{
|
|
114
126
|
name: 'generateAudio',
|
|
115
127
|
type: 'boolean',
|
|
@@ -146,6 +146,13 @@ It currently does not support accepting notifications from an MCP server, and cu
|
|
|
146
146
|
isOptional: true,
|
|
147
147
|
description: 'Handler for uncaught errors',
|
|
148
148
|
},
|
|
149
|
+
{
|
|
150
|
+
name: 'maxRetries',
|
|
151
|
+
type: 'number',
|
|
152
|
+
isOptional: true,
|
|
153
|
+
description:
|
|
154
|
+
'Maximum number of retries for transient MCP tool call failures. Set to 0 to disable retries. Defaults to 0. Retries are opt-in and apply to tools/call requests.',
|
|
155
|
+
},
|
|
149
156
|
{
|
|
150
157
|
name: 'capabilities',
|
|
151
158
|
type: 'ClientCapabilities',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.218",
|
|
4
4
|
"description": "AI SDK by Vercel - build apps like ChatGPT, Claude, Gemini, and more with a single interface for any model using the Vercel AI Gateway or go direct to OpenAI, Anthropic, Google, or any other model provider.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@opentelemetry/api": "^1.9.0",
|
|
48
|
-
"@ai-sdk/gateway": "3.0.
|
|
49
|
-
"@ai-sdk/provider": "3.0.
|
|
50
|
-
"@ai-sdk/provider-utils": "4.0.
|
|
48
|
+
"@ai-sdk/gateway": "3.0.142",
|
|
49
|
+
"@ai-sdk/provider": "3.0.13",
|
|
50
|
+
"@ai-sdk/provider-utils": "4.0.35"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@edge-runtime/vm": "^5.0.0",
|
|
@@ -2,6 +2,8 @@ import type {
|
|
|
2
2
|
Experimental_VideoModelV3,
|
|
3
3
|
Experimental_VideoModelV3CallOptions,
|
|
4
4
|
Experimental_VideoModelV3File,
|
|
5
|
+
Experimental_VideoModelV3FrameImage,
|
|
6
|
+
Experimental_VideoModelV3FrameType,
|
|
5
7
|
SharedV3ProviderMetadata,
|
|
6
8
|
} from '@ai-sdk/provider';
|
|
7
9
|
import {
|
|
@@ -30,6 +32,7 @@ import { prepareRetries } from '../util/prepare-retries';
|
|
|
30
32
|
import { VERSION } from '../version';
|
|
31
33
|
import type { GenerateVideoResult } from './generate-video-result';
|
|
32
34
|
import { splitDataUrl } from '../prompt/split-data-url';
|
|
35
|
+
import { convertDataContentToUint8Array } from '../prompt/data-content';
|
|
33
36
|
|
|
34
37
|
export type GenerateVideoPrompt =
|
|
35
38
|
| string
|
|
@@ -49,6 +52,8 @@ export type GenerateVideoPrompt =
|
|
|
49
52
|
* @param duration - Duration of the video in seconds.
|
|
50
53
|
* @param fps - Frames per second for the video.
|
|
51
54
|
* @param seed - Seed for the video generation.
|
|
55
|
+
* @param frameImages - Role-tagged image inputs for image-to-video and first-last-frame generation.
|
|
56
|
+
* @param inputReferences - Reference image inputs for reference-to-video generation.
|
|
52
57
|
* @param generateAudio - Whether the model should generate audio alongside the video.
|
|
53
58
|
* @param providerOptions - Additional provider-specific options that are passed through to the provider
|
|
54
59
|
* as body parameters.
|
|
@@ -70,6 +75,8 @@ export async function experimental_generateVideo({
|
|
|
70
75
|
duration,
|
|
71
76
|
fps,
|
|
72
77
|
seed,
|
|
78
|
+
frameImages,
|
|
79
|
+
inputReferences,
|
|
73
80
|
generateAudio,
|
|
74
81
|
providerOptions,
|
|
75
82
|
maxRetries: maxRetriesArg,
|
|
@@ -122,6 +129,26 @@ export async function experimental_generateVideo({
|
|
|
122
129
|
*/
|
|
123
130
|
seed?: number;
|
|
124
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Role-tagged image inputs for image-to-video and first-last-frame generation.
|
|
134
|
+
*/
|
|
135
|
+
frameImages?: Array<{
|
|
136
|
+
/**
|
|
137
|
+
* The image for this frame.
|
|
138
|
+
*/
|
|
139
|
+
image: DataContent;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Which frame this image represents.
|
|
143
|
+
*/
|
|
144
|
+
frameType: Experimental_VideoModelV3FrameType;
|
|
145
|
+
}>;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Reference image inputs for reference-to-video generation.
|
|
149
|
+
*/
|
|
150
|
+
inputReferences?: Array<DataContent>;
|
|
151
|
+
|
|
125
152
|
/**
|
|
126
153
|
* Whether the model should generate audio alongside the video.
|
|
127
154
|
*/
|
|
@@ -176,6 +203,55 @@ export async function experimental_generateVideo({
|
|
|
176
203
|
|
|
177
204
|
const { prompt, image } = normalizePrompt(promptArg);
|
|
178
205
|
|
|
206
|
+
const normalizedFrameImages:
|
|
207
|
+
| Array<Experimental_VideoModelV3FrameImage>
|
|
208
|
+
| undefined = frameImages?.map(frame => ({
|
|
209
|
+
image: normalizeImageData(frame.image),
|
|
210
|
+
frameType: frame.frameType,
|
|
211
|
+
}));
|
|
212
|
+
|
|
213
|
+
const normalizedInputReferences:
|
|
214
|
+
| Array<Experimental_VideoModelV3File>
|
|
215
|
+
| undefined = inputReferences?.map(reference =>
|
|
216
|
+
normalizeImageData(reference),
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const effectiveInputReferences =
|
|
220
|
+
normalizedFrameImages != null && normalizedFrameImages.length > 0
|
|
221
|
+
? undefined
|
|
222
|
+
: normalizedInputReferences;
|
|
223
|
+
|
|
224
|
+
const warnings: Array<Warning> = [];
|
|
225
|
+
|
|
226
|
+
if (
|
|
227
|
+
normalizedFrameImages != null &&
|
|
228
|
+
normalizedFrameImages.length > 0 &&
|
|
229
|
+
normalizedInputReferences != null &&
|
|
230
|
+
normalizedInputReferences.length > 0
|
|
231
|
+
) {
|
|
232
|
+
warnings.push({
|
|
233
|
+
type: 'other',
|
|
234
|
+
message:
|
|
235
|
+
'inputReferences were ignored because frameImages were provided; ' +
|
|
236
|
+
'frameImages and inputReferences cannot be combined.',
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const firstFrameImage = normalizedFrameImages?.find(
|
|
241
|
+
frame => frame.frameType === 'first_frame',
|
|
242
|
+
)?.image;
|
|
243
|
+
|
|
244
|
+
if (image != null && firstFrameImage != null) {
|
|
245
|
+
warnings.push({
|
|
246
|
+
type: 'other',
|
|
247
|
+
message:
|
|
248
|
+
'prompt.image was ignored because a first_frame frameImage was provided; ' +
|
|
249
|
+
'the first_frame frameImage takes precedence as the start image.',
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const resolvedImage = firstFrameImage ?? image;
|
|
254
|
+
|
|
179
255
|
const maxVideosPerCallWithDefault =
|
|
180
256
|
maxVideosPerCall ?? (await invokeModelMaxVideosPerCall(model)) ?? 1;
|
|
181
257
|
|
|
@@ -187,29 +263,31 @@ export async function experimental_generateVideo({
|
|
|
187
263
|
});
|
|
188
264
|
|
|
189
265
|
const results = await Promise.all(
|
|
190
|
-
callVideoCounts.map(
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
266
|
+
callVideoCounts.map(
|
|
267
|
+
async callVideoCount =>
|
|
268
|
+
await retry(() =>
|
|
269
|
+
model.doGenerate({
|
|
270
|
+
prompt,
|
|
271
|
+
n: callVideoCount,
|
|
272
|
+
aspectRatio,
|
|
273
|
+
resolution,
|
|
274
|
+
duration,
|
|
275
|
+
fps,
|
|
276
|
+
seed,
|
|
277
|
+
image: resolvedImage,
|
|
278
|
+
frameImages: normalizedFrameImages,
|
|
279
|
+
inputReferences: effectiveInputReferences,
|
|
280
|
+
generateAudio,
|
|
281
|
+
providerOptions: providerOptions ?? {},
|
|
282
|
+
headers: headersWithUserAgent,
|
|
283
|
+
abortSignal,
|
|
284
|
+
} satisfies Experimental_VideoModelV3CallOptions),
|
|
285
|
+
),
|
|
207
286
|
),
|
|
208
287
|
);
|
|
209
288
|
|
|
210
289
|
// collect result videos, warnings, and response metadata
|
|
211
290
|
const videos: Array<GeneratedFile> = [];
|
|
212
|
-
const warnings: Array<Warning> = [];
|
|
213
291
|
const responses: Array<VideoModelResponseMetadata> = [];
|
|
214
292
|
const providerMetadata: SharedV3ProviderMetadata = {};
|
|
215
293
|
|
|
@@ -345,59 +423,59 @@ function normalizePrompt(promptArg: GenerateVideoPrompt): {
|
|
|
345
423
|
};
|
|
346
424
|
}
|
|
347
425
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
detectMediaType({
|
|
373
|
-
data: bytes,
|
|
374
|
-
signatures: imageMediaTypeSignatures,
|
|
375
|
-
}) ?? 'image/png';
|
|
376
|
-
|
|
377
|
-
image = {
|
|
378
|
-
type: 'file',
|
|
379
|
-
mediaType,
|
|
380
|
-
data: bytes,
|
|
381
|
-
};
|
|
382
|
-
}
|
|
383
|
-
} else if (dataContent instanceof Uint8Array) {
|
|
384
|
-
const mediaType =
|
|
385
|
-
detectMediaType({
|
|
386
|
-
data: dataContent,
|
|
387
|
-
signatures: imageMediaTypeSignatures,
|
|
388
|
-
}) ?? 'image/png';
|
|
426
|
+
return {
|
|
427
|
+
prompt: promptArg.text,
|
|
428
|
+
image:
|
|
429
|
+
promptArg.image != null ? normalizeImageData(promptArg.image) : undefined,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Normalizes a {@link DataContent} image into a {@link Experimental_VideoModelV3File}.
|
|
435
|
+
* Accepts a URL string, a data URL, a base64 string, or binary image data.
|
|
436
|
+
*/
|
|
437
|
+
function normalizeImageData(
|
|
438
|
+
dataContent: DataContent,
|
|
439
|
+
): Experimental_VideoModelV3File {
|
|
440
|
+
if (typeof dataContent === 'string') {
|
|
441
|
+
if (
|
|
442
|
+
dataContent.startsWith('http://') ||
|
|
443
|
+
dataContent.startsWith('https://')
|
|
444
|
+
) {
|
|
445
|
+
return {
|
|
446
|
+
type: 'url',
|
|
447
|
+
url: dataContent,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
389
450
|
|
|
390
|
-
|
|
451
|
+
if (dataContent.startsWith('data:')) {
|
|
452
|
+
const { mediaType, base64Content } = splitDataUrl(dataContent);
|
|
453
|
+
return {
|
|
391
454
|
type: 'file',
|
|
392
|
-
mediaType,
|
|
393
|
-
data:
|
|
455
|
+
mediaType: mediaType ?? 'image/png',
|
|
456
|
+
data: convertBase64ToUint8Array(base64Content ?? ''),
|
|
394
457
|
};
|
|
395
458
|
}
|
|
459
|
+
|
|
460
|
+
const bytes = convertBase64ToUint8Array(dataContent);
|
|
461
|
+
return {
|
|
462
|
+
type: 'file',
|
|
463
|
+
mediaType:
|
|
464
|
+
detectMediaType({
|
|
465
|
+
data: bytes,
|
|
466
|
+
signatures: imageMediaTypeSignatures,
|
|
467
|
+
}) ?? 'image/png',
|
|
468
|
+
data: bytes,
|
|
469
|
+
};
|
|
396
470
|
}
|
|
397
471
|
|
|
472
|
+
const bytes = convertDataContentToUint8Array(dataContent);
|
|
398
473
|
return {
|
|
399
|
-
|
|
400
|
-
|
|
474
|
+
type: 'file',
|
|
475
|
+
mediaType:
|
|
476
|
+
detectMediaType({ data: bytes, signatures: imageMediaTypeSignatures }) ??
|
|
477
|
+
'image/png',
|
|
478
|
+
data: bytes,
|
|
401
479
|
};
|
|
402
480
|
}
|
|
403
481
|
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { InvalidArgumentError } from '../error/invalid-argument-error';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
type RetryFunction,
|
|
5
|
-
} from '../util/retry-with-exponential-backoff';
|
|
2
|
+
import type { RetryFunction } from '@ai-sdk/provider-utils';
|
|
3
|
+
import { retryWithExponentialBackoffRespectingRetryHeaders } from '../util/retry-with-exponential-backoff';
|
|
6
4
|
|
|
7
5
|
/**
|
|
8
6
|
* Validate and prepare retries.
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { APICallError } from '@ai-sdk/provider';
|
|
2
2
|
import { GatewayError } from '@ai-sdk/gateway';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
retryWithExponentialBackoff,
|
|
5
|
+
type RetryFunction,
|
|
6
|
+
} from '@ai-sdk/provider-utils';
|
|
4
7
|
import { RetryError } from './retry-error';
|
|
5
8
|
|
|
6
|
-
export type RetryFunction = <OUTPUT>(
|
|
7
|
-
fn: () => PromiseLike<OUTPUT>,
|
|
8
|
-
) => PromiseLike<OUTPUT>;
|
|
9
|
-
|
|
10
9
|
function getRetryDelayInMs({
|
|
11
10
|
error,
|
|
12
11
|
exponentialBackoffDelay,
|
|
@@ -62,98 +61,31 @@ function getRetryDelayInMs({
|
|
|
62
61
|
* while respecting rate limit headers (retry-after-ms and retry-after) if they are provided and reasonable (0-60 seconds).
|
|
63
62
|
* You can configure the maximum number of retries, the initial delay, and the backoff factor.
|
|
64
63
|
*/
|
|
65
|
-
export const retryWithExponentialBackoffRespectingRetryHeaders =
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
async <OUTPUT>(f: () => PromiseLike<OUTPUT>) =>
|
|
78
|
-
_retryWithExponentialBackoff(f, {
|
|
79
|
-
maxRetries,
|
|
80
|
-
delayInMs: initialDelayInMs,
|
|
81
|
-
backoffFactor,
|
|
82
|
-
abortSignal,
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
async function _retryWithExponentialBackoff<OUTPUT>(
|
|
86
|
-
f: () => PromiseLike<OUTPUT>,
|
|
87
|
-
{
|
|
64
|
+
export const retryWithExponentialBackoffRespectingRetryHeaders = ({
|
|
65
|
+
maxRetries = 2,
|
|
66
|
+
initialDelayInMs = 2000,
|
|
67
|
+
backoffFactor = 2,
|
|
68
|
+
abortSignal,
|
|
69
|
+
}: {
|
|
70
|
+
maxRetries?: number;
|
|
71
|
+
initialDelayInMs?: number;
|
|
72
|
+
backoffFactor?: number;
|
|
73
|
+
abortSignal?: AbortSignal;
|
|
74
|
+
} = {}): RetryFunction =>
|
|
75
|
+
retryWithExponentialBackoff({
|
|
88
76
|
maxRetries,
|
|
89
|
-
|
|
77
|
+
initialDelayInMs,
|
|
90
78
|
backoffFactor,
|
|
91
79
|
abortSignal,
|
|
92
|
-
|
|
93
|
-
maxRetries: number;
|
|
94
|
-
delayInMs: number;
|
|
95
|
-
backoffFactor: number;
|
|
96
|
-
abortSignal: AbortSignal | undefined;
|
|
97
|
-
},
|
|
98
|
-
errors: unknown[] = [],
|
|
99
|
-
): Promise<OUTPUT> {
|
|
100
|
-
try {
|
|
101
|
-
return await f();
|
|
102
|
-
} catch (error) {
|
|
103
|
-
if (isAbortError(error)) {
|
|
104
|
-
throw error; // don't retry when the request was aborted
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (maxRetries === 0) {
|
|
108
|
-
throw error; // don't wrap the error when retries are disabled
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const errorMessage = getErrorMessage(error);
|
|
112
|
-
const newErrors = [...errors, error];
|
|
113
|
-
const tryNumber = newErrors.length;
|
|
114
|
-
|
|
115
|
-
if (tryNumber > maxRetries) {
|
|
116
|
-
throw new RetryError({
|
|
117
|
-
message: `Failed after ${tryNumber} attempts. Last error: ${errorMessage}`,
|
|
118
|
-
reason: 'maxRetriesExceeded',
|
|
119
|
-
errors: newErrors,
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (
|
|
80
|
+
shouldRetry: error =>
|
|
124
81
|
error instanceof Error &&
|
|
125
82
|
((APICallError.isInstance(error) && error.isRetryable === true) ||
|
|
126
|
-
(GatewayError.isInstance(error) && error.isRetryable === true))
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
);
|
|
136
|
-
|
|
137
|
-
return _retryWithExponentialBackoff(
|
|
138
|
-
f,
|
|
139
|
-
{
|
|
140
|
-
maxRetries,
|
|
141
|
-
delayInMs: backoffFactor * delayInMs,
|
|
142
|
-
backoffFactor,
|
|
143
|
-
abortSignal,
|
|
144
|
-
},
|
|
145
|
-
newErrors,
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (tryNumber === 1) {
|
|
150
|
-
throw error; // don't wrap the error when a non-retryable error occurs on the first try
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
throw new RetryError({
|
|
154
|
-
message: `Failed after ${tryNumber} attempts with non-retryable error: '${errorMessage}'`,
|
|
155
|
-
reason: 'errorNotRetryable',
|
|
156
|
-
errors: newErrors,
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
}
|
|
83
|
+
(GatewayError.isInstance(error) && error.isRetryable === true)),
|
|
84
|
+
getDelayInMs: ({ error, exponentialBackoffDelay }) =>
|
|
85
|
+
getRetryDelayInMs({
|
|
86
|
+
error: error as APICallError | GatewayError,
|
|
87
|
+
exponentialBackoffDelay,
|
|
88
|
+
}),
|
|
89
|
+
createRetryError: ({ message, reason, errors }) =>
|
|
90
|
+
new RetryError({ message, reason, errors }),
|
|
91
|
+
});
|