ai-retry 0.6.0 → 0.8.0
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/README.md +213 -100
- package/dist/index.d.ts +8 -4
- package/dist/index.js +16 -9
- package/dist/retryables/index.d.ts +2 -2
- package/dist/retryables/index.js +5 -3
- package/dist/{types-DhGbwiB4.d.ts → types-BuPozWMn.d.ts} +2 -1
- package/dist/{utils-BlCGaP0E.js → utils-DsvLGk6a.js} +5 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,7 +40,8 @@ import { createRetryable } from 'ai-retry';
|
|
|
40
40
|
|
|
41
41
|
// Create a retryable model
|
|
42
42
|
const retryableModel = createRetryable({
|
|
43
|
-
|
|
43
|
+
// Base model
|
|
44
|
+
model: openai('gpt-4-mini'),
|
|
44
45
|
retries: [
|
|
45
46
|
// Retry strategies and fallbacks...
|
|
46
47
|
],
|
|
@@ -74,7 +75,8 @@ import { createRetryable } from 'ai-retry';
|
|
|
74
75
|
|
|
75
76
|
// Create a retryable model
|
|
76
77
|
const retryableModel = createRetryable({
|
|
77
|
-
|
|
78
|
+
// Base model
|
|
79
|
+
model: openai.textEmbedding('text-embedding-3-large'),
|
|
78
80
|
retries: [
|
|
79
81
|
// Retry strategies and fallbacks...
|
|
80
82
|
],
|
|
@@ -89,6 +91,173 @@ const result = await embed({
|
|
|
89
91
|
console.log(result.embedding);
|
|
90
92
|
```
|
|
91
93
|
|
|
94
|
+
### Retryables
|
|
95
|
+
|
|
96
|
+
The objects passed to the `retries` are called retryables and control the retry behavior. We can distinguish between two types of retryables:
|
|
97
|
+
|
|
98
|
+
- **Static retryables** are simply models instances (language or embedding) that will always be used when an error occurs. They are also called fallback models.
|
|
99
|
+
- **Dynamic retryables** are functions that receive the current attempt context (error/result and previous attempts) and decide whether to retry with a different model based on custom logic.
|
|
100
|
+
|
|
101
|
+
You can think of the `retries` array as a big `if-else` block, where each dynamic retryable is an `if` branch that can match a certain error/result condition, and static retryables are the `else` branches that match all other conditions. The analogy is not perfect, because the order of retryables matters because `retries` are evaluated in order until one matches:
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
import { generateText, streamText } from 'ai';
|
|
105
|
+
import { createRetryable } from 'ai-retry';
|
|
106
|
+
|
|
107
|
+
const retryableModel = createRetryable({
|
|
108
|
+
// Base model
|
|
109
|
+
model: openai('gpt-4'),
|
|
110
|
+
// Retryables are evaluated top-down in order
|
|
111
|
+
retries: [
|
|
112
|
+
// Dynamic retryables act like if-branches:
|
|
113
|
+
// If error.code == 429 (too many requests) happens, retry with this model
|
|
114
|
+
(context) => {
|
|
115
|
+
return context.current.error.statusCode === 429
|
|
116
|
+
? { model: azure('gpt-4-mini') } // Retry
|
|
117
|
+
: undefined; // Skip
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
// If error.message ~= "service overloaded", retry with this model
|
|
121
|
+
(context) => {
|
|
122
|
+
return context.current.error.message.includes("service overloaded")
|
|
123
|
+
? { model: azure('gpt-4-mini') } // Retry
|
|
124
|
+
: undefined; // Skip
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
// Static retryables act like else branches:
|
|
128
|
+
// Else, always fallback to this model
|
|
129
|
+
anthropic('claude-3-haiku-20240307'),
|
|
130
|
+
// Same as:
|
|
131
|
+
// { model: anthropic('claude-3-haiku-20240307'), maxAttempts: 1 }
|
|
132
|
+
],
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
In this example, if the base model fails with code 429 or a service overloaded error, it will retry with `gpt-4-mini` on Azure. In any other error case, it will fallback to `claude-3-haiku-20240307` on Anthropic. If the order would be reversed, the static retryable would catch all errors first, and the dynamic retryable would never be reached.
|
|
137
|
+
|
|
138
|
+
#### Fallbacks
|
|
139
|
+
|
|
140
|
+
If you don't need precise error matching with custom logic and just want to fallback to different models on any error, you can simply provide a list of models.
|
|
141
|
+
|
|
142
|
+
> [!NOTE]
|
|
143
|
+
> Use the object syntax `{ model: openai('gpt-4') }` if you need to provide additional options like `maxAttempts`, `delay`, etc.
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
import { openai } from '@ai-sdk/openai';
|
|
147
|
+
import { generateText, streamText } from 'ai';
|
|
148
|
+
import { createRetryable } from 'ai-retry';
|
|
149
|
+
|
|
150
|
+
const retryableModel = createRetryable({
|
|
151
|
+
// Base model
|
|
152
|
+
model: openai('gpt-4-mini'),
|
|
153
|
+
// List of fallback models
|
|
154
|
+
retries: [
|
|
155
|
+
openai('gpt-3.5-turbo'), // Fallback for first error
|
|
156
|
+
// Same as:
|
|
157
|
+
// { model: openai('gpt-3.5-turbo'), maxAttempts: 1 },
|
|
158
|
+
|
|
159
|
+
anthropic('claude-3-haiku-20240307'), // Fallback for second error
|
|
160
|
+
// Same as:
|
|
161
|
+
// { model: anthropic('claude-3-haiku-20240307'), maxAttempts: 1 },
|
|
162
|
+
],
|
|
163
|
+
});
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
In this example, if the base model fails, it will retry with `gpt-3.5-turbo`. If that also fails, it will retry with `claude-3-haiku-20240307`. If that fails again, the whole retry process stops and a `RetryError` is thrown.
|
|
167
|
+
|
|
168
|
+
#### Custom
|
|
169
|
+
|
|
170
|
+
If you need more control over when to retry and which model to use, you can create your own custom retryable. This function is called with a context object containing the current attempt (error or result) and all previous attempts and needs to return a retry model or `undefined` to skip to the next retryable. The object you return from the retryable function is the same as the one you provide in the `retries` array.
|
|
171
|
+
|
|
172
|
+
> [!NOTE]
|
|
173
|
+
> You can return additional options like `maxAttempts`, `delay`, etc. along with the model.
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
import { anthropic } from '@ai-sdk/anthropic';
|
|
177
|
+
import { openai } from '@ai-sdk/openai';
|
|
178
|
+
import { APICallError } from 'ai';
|
|
179
|
+
import { createRetryable, isErrorAttempt } from 'ai-retry';
|
|
180
|
+
import type { Retryable } from 'ai-retry';
|
|
181
|
+
|
|
182
|
+
// Custom retryable that retries on rate limit errors (429)
|
|
183
|
+
const rateLimitRetry: Retryable = (context) => {
|
|
184
|
+
// Only handle error attempts
|
|
185
|
+
if (isErrorAttempt(context.current)) {
|
|
186
|
+
// Get the error from the current attempt
|
|
187
|
+
const { error } = context.current;
|
|
188
|
+
|
|
189
|
+
// Check for rate limit error
|
|
190
|
+
if (APICallError.isInstance(error) && error.statusCode === 429) {
|
|
191
|
+
// Retry with a different model
|
|
192
|
+
return { model: anthropic('claude-3-haiku-20240307') };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Skip to next retryable
|
|
197
|
+
return undefined;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const retryableModel = createRetryable({
|
|
201
|
+
// Base model
|
|
202
|
+
model: openai('gpt-4-mini'),
|
|
203
|
+
retries: [
|
|
204
|
+
// Use custom rate limit retryable
|
|
205
|
+
rateLimitRetry
|
|
206
|
+
|
|
207
|
+
// Other retryables...
|
|
208
|
+
],
|
|
209
|
+
});
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
In this example, if the base model fails with a 429 error, it will retry with `claude-3-haiku-20240307`. For any other error, it will skip to the next retryable (if any) or throw the original error.
|
|
213
|
+
|
|
214
|
+
#### All Retries Failed
|
|
215
|
+
|
|
216
|
+
If all retry attempts failed, a `RetryError` is thrown containing all individual errors.
|
|
217
|
+
If no retry was attempted (e.g. because all retryables returned `undefined`), the original error is thrown directly.
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
import { RetryError } from 'ai';
|
|
221
|
+
|
|
222
|
+
const retryableModel = createRetryable({
|
|
223
|
+
// Base model = first attempt
|
|
224
|
+
model: azure('gpt-4-mini'),
|
|
225
|
+
retries: [
|
|
226
|
+
// Fallback model 1 = Second attempt
|
|
227
|
+
openai('gpt-3.5-turbo'),
|
|
228
|
+
// Fallback model 2 = Third attempt
|
|
229
|
+
anthropic('claude-3-haiku-20240307')
|
|
230
|
+
],
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
const result = await generateText({
|
|
235
|
+
model: retryableModel,
|
|
236
|
+
prompt: 'Hello world!',
|
|
237
|
+
});
|
|
238
|
+
} catch (error) {
|
|
239
|
+
// RetryError is an official AI SDK error
|
|
240
|
+
if (error instanceof RetryError) {
|
|
241
|
+
console.error('All retry attempts failed:', error.errors);
|
|
242
|
+
} else {
|
|
243
|
+
console.error('Request failed:', error);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Errors are tracked per unique model (provider + modelId). That means on the first error, it will retry with `gpt-3.5-turbo`. If that also fails, it will retry with `claude-3-haiku-20240307`. If that fails again, the whole retry process stops and a `RetryError` is thrown.
|
|
249
|
+
|
|
250
|
+
### Built-in Retryables
|
|
251
|
+
|
|
252
|
+
There are several built-in dynamic retryables available for common use cases:
|
|
253
|
+
|
|
254
|
+
- [`contentFilterTriggered`](./src/retryables/content-filter-triggered.ts): Content filter was triggered based on the prompt or completion.
|
|
255
|
+
- [`requestTimeout`](./src/retryables/request-timeout.ts): Request timeout occurred.
|
|
256
|
+
- [`requestNotRetryable`](./src/retryables/request-not-retryable.ts): Request failed with a non-retryable error.
|
|
257
|
+
- [`retryAfterDelay`](./src/retryables/retry-after-delay.ts): Retry with delay and exponential backoff and respect `retry-after` headers.
|
|
258
|
+
- [`serviceOverloaded`](./src/retryables/service-overloaded.ts): Response with status code 529 (service overloaded).
|
|
259
|
+
- Use this retryable to handle Anthropic's overloaded errors.
|
|
260
|
+
|
|
92
261
|
#### Content Filter
|
|
93
262
|
|
|
94
263
|
Automatically switch to a different model when content filtering blocks your request.
|
|
@@ -114,20 +283,26 @@ Handle timeouts by switching to potentially faster models.
|
|
|
114
283
|
> [!NOTE]
|
|
115
284
|
> You need to use an `abortSignal` with a timeout on your request.
|
|
116
285
|
|
|
286
|
+
When a request times out, the `requestTimeout` retryable will automatically create a fresh abort signal for the retry attempt. This prevents the retry from immediately failing due to the already-aborted signal from the original request. If you do not provide a `timeout` value, a default of 60 seconds is used for the retry attempt.
|
|
287
|
+
|
|
117
288
|
```typescript
|
|
118
289
|
import { requestTimeout } from 'ai-retry/retryables';
|
|
119
290
|
|
|
120
291
|
const retryableModel = createRetryable({
|
|
121
292
|
model: azure('gpt-4'),
|
|
122
293
|
retries: [
|
|
123
|
-
|
|
294
|
+
// Defaults to 60 seconds timeout for the retry attempt
|
|
295
|
+
requestTimeout(azure('gpt-4-mini')),
|
|
296
|
+
|
|
297
|
+
// Or specify a custom timeout for the retry attempt
|
|
298
|
+
requestTimeout(azure('gpt-4-mini'), { timeout: 30_000 }),
|
|
124
299
|
],
|
|
125
300
|
});
|
|
126
301
|
|
|
127
302
|
const result = await generateText({
|
|
128
303
|
model: retryableModel,
|
|
129
304
|
prompt: 'Write a vegetarian lasagna recipe for 4 people.',
|
|
130
|
-
abortSignal: AbortSignal.timeout(60_000),
|
|
305
|
+
abortSignal: AbortSignal.timeout(60_000), // Original request timeout
|
|
131
306
|
});
|
|
132
307
|
```
|
|
133
308
|
|
|
@@ -182,10 +357,10 @@ const retryableModel = createRetryable({
|
|
|
182
357
|
model: openai('gpt-4'), // Base model
|
|
183
358
|
retries: [
|
|
184
359
|
// Retry base model 3 times with fixed 2s delay
|
|
185
|
-
retryAfterDelay({ delay:
|
|
360
|
+
retryAfterDelay({ delay: 2_000, maxAttempts: 3 }),
|
|
186
361
|
|
|
187
362
|
// Or retry with exponential backoff (2s, 4s, 8s)
|
|
188
|
-
retryAfterDelay({ delay:
|
|
363
|
+
retryAfterDelay({ delay: 2_000, backoffFactor: 2, maxAttempts: 3 }),
|
|
189
364
|
|
|
190
365
|
// Or retry only if the response contains a retry-after header
|
|
191
366
|
retryAfterDelay({ maxAttempts: 3 }),
|
|
@@ -195,82 +370,6 @@ const retryableModel = createRetryable({
|
|
|
195
370
|
|
|
196
371
|
By default, if a [`retry-after-ms`](https://learn.microsoft.com/en-us/azure/ai-foundry/openai/how-to/provisioned-get-started#what-should--i-do-when-i-receive-a-429-response) or [`retry-after`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Retry-After) header is present in the response, it will be prioritized over the configured delay. The delay from the header will be capped at 60 seconds for safety.
|
|
197
372
|
|
|
198
|
-
#### Fallbacks
|
|
199
|
-
|
|
200
|
-
If you always want to fallback to a different model on any error, you can simply provide a list of models.
|
|
201
|
-
|
|
202
|
-
```typescript
|
|
203
|
-
const retryableModel = createRetryable({
|
|
204
|
-
model: azure('gpt-4'),
|
|
205
|
-
retries: [
|
|
206
|
-
openai('gpt-4'),
|
|
207
|
-
anthropic('claude-3-haiku-20240307')
|
|
208
|
-
],
|
|
209
|
-
});
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
#### Custom
|
|
213
|
-
|
|
214
|
-
Create your own retryables for specific use cases:
|
|
215
|
-
|
|
216
|
-
```typescript
|
|
217
|
-
import { anthropic } from '@ai-sdk/anthropic';
|
|
218
|
-
import { openai } from '@ai-sdk/openai';
|
|
219
|
-
import { APICallError } from 'ai';
|
|
220
|
-
import { createRetryable, isErrorAttempt } from 'ai-retry';
|
|
221
|
-
import type { Retryable } from 'ai-retry';
|
|
222
|
-
|
|
223
|
-
const rateLimitRetry: Retryable = (context) => {
|
|
224
|
-
if (isErrorAttempt(context.current)) {
|
|
225
|
-
const { error } = context.current;
|
|
226
|
-
|
|
227
|
-
if (APICallError.isInstance(error) && error.statusCode === 429) {
|
|
228
|
-
return { model: anthropic('claude-3-haiku-20240307') };
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
return undefined;
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
const retryableModel = createRetryable({
|
|
236
|
-
model: openai('gpt-4'),
|
|
237
|
-
retries: [
|
|
238
|
-
rateLimitRetry
|
|
239
|
-
],
|
|
240
|
-
});
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
#### All Retries Failed
|
|
244
|
-
|
|
245
|
-
If all retry attempts failed, a `RetryError` is thrown containing all individual errors.
|
|
246
|
-
If no retry was attempted (e.g. because all retryables returned `undefined`), the original error is thrown directly.
|
|
247
|
-
|
|
248
|
-
```typescript
|
|
249
|
-
import { RetryError } from 'ai';
|
|
250
|
-
|
|
251
|
-
const retryableModel = createRetryable({
|
|
252
|
-
model: azure('gpt-4'),
|
|
253
|
-
retries: [
|
|
254
|
-
openai('gpt-4'),
|
|
255
|
-
anthropic('claude-3-haiku-20240307')
|
|
256
|
-
],
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
try {
|
|
260
|
-
const result = await generateText({
|
|
261
|
-
model: retryableModel,
|
|
262
|
-
prompt: 'Hello world!',
|
|
263
|
-
});
|
|
264
|
-
} catch (error) {
|
|
265
|
-
// RetryError is an official AI SDK error
|
|
266
|
-
if (error instanceof RetryError) {
|
|
267
|
-
console.error('All retry attempts failed:', error.errors);
|
|
268
|
-
} else {
|
|
269
|
-
console.error('Request failed:', error);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
```
|
|
273
|
-
|
|
274
373
|
### Options
|
|
275
374
|
|
|
276
375
|
#### Retry Delays
|
|
@@ -282,10 +381,10 @@ const retryableModel = createRetryable({
|
|
|
282
381
|
model: openai('gpt-4'),
|
|
283
382
|
retries: [
|
|
284
383
|
// Retry model 3 times with fixed 2s delay
|
|
285
|
-
|
|
384
|
+
{ model: openai('gpt-4'), delay: 2_000, maxAttempts: 3 },
|
|
286
385
|
|
|
287
386
|
// Or retry with exponential backoff (2s, 4s, 8s)
|
|
288
|
-
|
|
387
|
+
{ model: openai('gpt-4'), delay: 2_000, backoffFactor: 2, maxAttempts: 3 },
|
|
289
388
|
],
|
|
290
389
|
});
|
|
291
390
|
|
|
@@ -310,6 +409,30 @@ const retryableModel = createRetryable({
|
|
|
310
409
|
],
|
|
311
410
|
});
|
|
312
411
|
```
|
|
412
|
+
#### Timeouts
|
|
413
|
+
|
|
414
|
+
When a retry specifies a `timeout` value, a fresh `AbortSignal.timeout()` is created for that retry attempt, replacing any existing abort signal. This is essential when retrying after timeout errors, as the original abort signal would already be in an aborted state.
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
const retryableModel = createRetryable({
|
|
418
|
+
model: openai('gpt-4'),
|
|
419
|
+
retries: [
|
|
420
|
+
// Provide a fresh 30 second timeout for the retry
|
|
421
|
+
{
|
|
422
|
+
model: openai('gpt-3.5-turbo'),
|
|
423
|
+
timeout: 30_000
|
|
424
|
+
},
|
|
425
|
+
],
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// Even if the original request times out, the retry gets a fresh signal
|
|
429
|
+
const result = await generateText({
|
|
430
|
+
model: retryableModel,
|
|
431
|
+
prompt: 'Write a story',
|
|
432
|
+
// Original request timeout
|
|
433
|
+
abortSignal: AbortSignal.timeout(60_000),
|
|
434
|
+
});
|
|
435
|
+
```
|
|
313
436
|
|
|
314
437
|
#### Max Attempts
|
|
315
438
|
|
|
@@ -322,7 +445,7 @@ const retryableModel = createRetryable({
|
|
|
322
445
|
// Try this once
|
|
323
446
|
anthropic('claude-3-haiku-20240307'),
|
|
324
447
|
// Try this one more time (initial + 1 retry)
|
|
325
|
-
|
|
448
|
+
{ model: openai('gpt-4'), maxAttempts: 2 },
|
|
326
449
|
// Already tried, won't be retried again
|
|
327
450
|
anthropic('claude-3-haiku-20240307')
|
|
328
451
|
],
|
|
@@ -340,7 +463,7 @@ const retryableModel = createRetryable({
|
|
|
340
463
|
model: openai('gpt-5'),
|
|
341
464
|
retries: [
|
|
342
465
|
// Use different provider options for the retry
|
|
343
|
-
|
|
466
|
+
{
|
|
344
467
|
model: openai('gpt-4o-2024-08-06'),
|
|
345
468
|
providerOptions: {
|
|
346
469
|
openai: {
|
|
@@ -348,7 +471,7 @@ const retryableModel = createRetryable({
|
|
|
348
471
|
structuredOutputs: false,
|
|
349
472
|
},
|
|
350
473
|
},
|
|
351
|
-
}
|
|
474
|
+
},
|
|
352
475
|
],
|
|
353
476
|
});
|
|
354
477
|
|
|
@@ -396,18 +519,6 @@ Errors during streaming requests can occur in two ways:
|
|
|
396
519
|
|
|
397
520
|
In the second case, errors during stream processing will not always be retried, because the stream might have already emitted some actual content and the consumer might have processed it. Retrying will be stopped as soon as the first content chunk (e.g. types of `text-delta`, `tool-call`, etc.) is emitted. The type of chunks considered as content are the same as the ones that are passed to [onChunk()](https://github.com/vercel/ai/blob/1fe4bd4144bff927f5319d9d206e782a73979ccb/packages/ai/src/generate-text/stream-text.ts#L684-L697).
|
|
398
521
|
|
|
399
|
-
### Retryables
|
|
400
|
-
|
|
401
|
-
A retryable is a function that receives the current attempt and determines whether to retry with a different model based on the error/result and any previous attempts.
|
|
402
|
-
There are several built-in retryables:
|
|
403
|
-
|
|
404
|
-
- [`contentFilterTriggered`](./src/retryables/content-filter-triggered.ts): Content filter was triggered based on the prompt or completion.
|
|
405
|
-
- [`requestTimeout`](./src/retryables/request-timeout.ts): Request timeout occurred.
|
|
406
|
-
- [`requestNotRetryable`](./src/retryables/request-not-retryable.ts): Request failed with a non-retryable error.
|
|
407
|
-
- [`retryAfterDelay`](./src/retryables/retry-after-delay.ts): Retry with delay and exponential backoff and respect `retry-after` headers.
|
|
408
|
-
- [`serviceOverloaded`](./src/retryables/service-overloaded.ts): Response with status code 529 (service overloaded).
|
|
409
|
-
- Use this retryable to handle Anthropic's overloaded errors.
|
|
410
|
-
|
|
411
522
|
### API Reference
|
|
412
523
|
|
|
413
524
|
#### `createRetryable(options: RetryableModelOptions): LanguageModelV2 | EmbeddingModelV2`
|
|
@@ -436,7 +547,7 @@ type Retryable = (
|
|
|
436
547
|
|
|
437
548
|
#### `Retry`
|
|
438
549
|
|
|
439
|
-
A `Retry` specifies the model to retry and optional settings like `maxAttempts`, `delay`, `backoffFactor`, and `providerOptions`.
|
|
550
|
+
A `Retry` specifies the model to retry and optional settings like `maxAttempts`, `delay`, `backoffFactor`, `timeout`, and `providerOptions`.
|
|
440
551
|
|
|
441
552
|
```typescript
|
|
442
553
|
interface Retry {
|
|
@@ -444,6 +555,7 @@ interface Retry {
|
|
|
444
555
|
maxAttempts?: number; // Maximum retry attempts per model (default: 1)
|
|
445
556
|
delay?: number; // Delay in milliseconds before retrying
|
|
446
557
|
backoffFactor?: number; // Multiplier for exponential backoff
|
|
558
|
+
timeout?: number; // Timeout in milliseconds for the retry attempt
|
|
447
559
|
providerOptions?: ProviderOptions; // Provider-specific options for the retry
|
|
448
560
|
}
|
|
449
561
|
```
|
|
@@ -453,6 +565,7 @@ interface Retry {
|
|
|
453
565
|
- `maxAttempts`: Maximum number of times this model can be retried. Default is 1.
|
|
454
566
|
- `delay`: Delay in milliseconds to wait before retrying. The delay respects abort signals from the request.
|
|
455
567
|
- `backoffFactor`: Multiplier for exponential backoff (`delay × backoffFactor^attempt`). If not provided, uses fixed delay.
|
|
568
|
+
- `timeout`: Timeout in milliseconds for creating a fresh `AbortSignal.timeout()` for the retry attempt. This replaces any existing abort signal.
|
|
456
569
|
- `providerOptions`: Provider-specific options that override the original request's provider options during retry attempts.
|
|
457
570
|
|
|
458
571
|
#### `RetryContext`
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { a as LanguageModelV2Generate, c as Retry, d as RetryErrorAttempt, f as RetryResultAttempt, h as RetryableOptions, i as LanguageModelV2, l as RetryAttempt, m as RetryableModelOptions, n as EmbeddingModelV2CallOptions, o as LanguageModelV2Stream, p as Retryable, r as EmbeddingModelV2Embed, s as Retries, t as EmbeddingModelV2, u as RetryContext } from "./types-
|
|
1
|
+
import { a as LanguageModelV2Generate, c as Retry, d as RetryErrorAttempt, f as RetryResultAttempt, h as RetryableOptions, i as LanguageModelV2, l as RetryAttempt, m as RetryableModelOptions, n as EmbeddingModelV2CallOptions, o as LanguageModelV2Stream, p as Retryable, r as EmbeddingModelV2Embed, s as Retries, t as EmbeddingModelV2, u as RetryContext } from "./types-BuPozWMn.js";
|
|
2
2
|
import * as _ai_sdk_provider0 from "@ai-sdk/provider";
|
|
3
3
|
import { LanguageModelV2 as LanguageModelV2$1, LanguageModelV2StreamPart } from "@ai-sdk/provider";
|
|
4
4
|
|
|
5
5
|
//#region src/create-retryable-model.d.ts
|
|
6
|
-
declare function createRetryable<MODEL extends LanguageModelV2>(options: RetryableModelOptions<MODEL>): LanguageModelV2;
|
|
7
|
-
declare function createRetryable<MODEL extends EmbeddingModelV2>(options: RetryableModelOptions<MODEL>): EmbeddingModelV2;
|
|
6
|
+
declare function createRetryable<MODEL$1 extends LanguageModelV2>(options: RetryableModelOptions<MODEL$1>): LanguageModelV2;
|
|
7
|
+
declare function createRetryable<MODEL$1 extends EmbeddingModelV2>(options: RetryableModelOptions<MODEL$1>): EmbeddingModelV2;
|
|
8
8
|
//#endregion
|
|
9
9
|
//#region src/get-model-key.d.ts
|
|
10
10
|
/**
|
|
@@ -66,5 +66,9 @@ declare const isStreamContentPart: (part: LanguageModelV2StreamPart) => part is
|
|
|
66
66
|
type: "raw";
|
|
67
67
|
rawValue: unknown;
|
|
68
68
|
};
|
|
69
|
+
/**
|
|
70
|
+
* Type guard to check if a value is a Retry object (has a model property with a MODEL)
|
|
71
|
+
*/
|
|
72
|
+
declare const isRetry: <MODEL extends LanguageModelV2$1 | EmbeddingModelV2>(value: unknown) => value is Retry<MODEL>;
|
|
69
73
|
//#endregion
|
|
70
|
-
export { EmbeddingModelV2, EmbeddingModelV2CallOptions, EmbeddingModelV2Embed, LanguageModelV2, LanguageModelV2Generate, LanguageModelV2Stream, Retries, Retry, RetryAttempt, RetryContext, RetryErrorAttempt, RetryResultAttempt, Retryable, RetryableModelOptions, RetryableOptions, createRetryable, getModelKey, isEmbeddingModelV2, isErrorAttempt, isGenerateResult, isLanguageModelV2, isModelV2, isObject, isResultAttempt, isStreamContentPart, isStreamResult, isString };
|
|
74
|
+
export { EmbeddingModelV2, EmbeddingModelV2CallOptions, EmbeddingModelV2Embed, LanguageModelV2, LanguageModelV2Generate, LanguageModelV2Stream, Retries, Retry, RetryAttempt, RetryContext, RetryErrorAttempt, RetryResultAttempt, Retryable, RetryableModelOptions, RetryableOptions, createRetryable, getModelKey, isEmbeddingModelV2, isErrorAttempt, isGenerateResult, isLanguageModelV2, isModelV2, isObject, isResultAttempt, isRetry, isStreamContentPart, isStreamResult, isString };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as isModelV2, c as
|
|
1
|
+
import { a as isModelV2, c as isRetry, d as isString, i as isLanguageModelV2, l as isStreamContentPart, n as isErrorAttempt, o as isObject, r as isGenerateResult, s as isResultAttempt, t as isEmbeddingModelV2, u as isStreamResult } from "./utils-DsvLGk6a.js";
|
|
2
2
|
import { delay } from "@ai-sdk/provider-utils";
|
|
3
3
|
import { getErrorMessage } from "@ai-sdk/provider";
|
|
4
4
|
import { RetryError } from "ai";
|
|
@@ -35,15 +35,18 @@ function countModelAttempts(model, attempts) {
|
|
|
35
35
|
async function findRetryModel(retries, context) {
|
|
36
36
|
/**
|
|
37
37
|
* Filter retryables based on attempt type:
|
|
38
|
-
* - Result-based attempts: Only consider function retryables (skip plain models)
|
|
39
|
-
* - Error-based attempts: Consider all retryables (functions + plain models)
|
|
38
|
+
* - Result-based attempts: Only consider function retryables (skip plain models and static Retry objects)
|
|
39
|
+
* - Error-based attempts: Consider all retryables (functions + plain models + static Retry objects)
|
|
40
40
|
*/
|
|
41
41
|
const applicableRetries = isResultAttempt(context.current) ? retries.filter((retry) => typeof retry === "function") : retries;
|
|
42
42
|
/**
|
|
43
43
|
* Iterate through the applicable retryables to find a model to retry with
|
|
44
44
|
*/
|
|
45
45
|
for (const retry of applicableRetries) {
|
|
46
|
-
|
|
46
|
+
let retryModel;
|
|
47
|
+
if (typeof retry === "function") retryModel = await retry(context);
|
|
48
|
+
else if (isRetry(retry)) retryModel = retry;
|
|
49
|
+
else retryModel = { model: retry };
|
|
47
50
|
if (retryModel) {
|
|
48
51
|
/**
|
|
49
52
|
* The model key uniquely identifies a model instance (provider + modelId)
|
|
@@ -198,7 +201,8 @@ var RetryableEmbeddingModel = class {
|
|
|
198
201
|
fn: async (currentRetry) => {
|
|
199
202
|
const callOptions = {
|
|
200
203
|
...options,
|
|
201
|
-
providerOptions: currentRetry?.providerOptions ?? options.providerOptions
|
|
204
|
+
providerOptions: currentRetry?.providerOptions ?? options.providerOptions,
|
|
205
|
+
abortSignal: currentRetry?.timeout ? AbortSignal.timeout(currentRetry.timeout) : options.abortSignal
|
|
202
206
|
};
|
|
203
207
|
return this.currentModel.doEmbed(callOptions);
|
|
204
208
|
},
|
|
@@ -371,7 +375,8 @@ var RetryableLanguageModel = class {
|
|
|
371
375
|
fn: async (currentRetry) => {
|
|
372
376
|
const callOptions = {
|
|
373
377
|
...options,
|
|
374
|
-
providerOptions: currentRetry?.providerOptions ?? options.providerOptions
|
|
378
|
+
providerOptions: currentRetry?.providerOptions ?? options.providerOptions,
|
|
379
|
+
abortSignal: currentRetry?.timeout ? AbortSignal.timeout(currentRetry.timeout) : options.abortSignal
|
|
375
380
|
};
|
|
376
381
|
return this.currentModel.doGenerate(callOptions);
|
|
377
382
|
},
|
|
@@ -391,7 +396,8 @@ var RetryableLanguageModel = class {
|
|
|
391
396
|
fn: async (currentRetry) => {
|
|
392
397
|
const callOptions = {
|
|
393
398
|
...options,
|
|
394
|
-
providerOptions: currentRetry?.providerOptions ?? options.providerOptions
|
|
399
|
+
providerOptions: currentRetry?.providerOptions ?? options.providerOptions,
|
|
400
|
+
abortSignal: currentRetry?.timeout ? AbortSignal.timeout(currentRetry.timeout) : options.abortSignal
|
|
395
401
|
};
|
|
396
402
|
return this.currentModel.doStream(callOptions);
|
|
397
403
|
},
|
|
@@ -453,7 +459,8 @@ var RetryableLanguageModel = class {
|
|
|
453
459
|
fn: async () => {
|
|
454
460
|
const callOptions = {
|
|
455
461
|
...options,
|
|
456
|
-
providerOptions: retryModel.providerOptions ?? options.providerOptions
|
|
462
|
+
providerOptions: retryModel.providerOptions ?? options.providerOptions,
|
|
463
|
+
abortSignal: retryModel.timeout ? AbortSignal.timeout(retryModel.timeout) : options.abortSignal
|
|
457
464
|
};
|
|
458
465
|
return this.currentModel.doStream(callOptions);
|
|
459
466
|
},
|
|
@@ -485,4 +492,4 @@ function createRetryable(options) {
|
|
|
485
492
|
}
|
|
486
493
|
|
|
487
494
|
//#endregion
|
|
488
|
-
export { createRetryable, getModelKey, isEmbeddingModelV2, isErrorAttempt, isGenerateResult, isLanguageModelV2, isModelV2, isObject, isResultAttempt, isStreamContentPart, isStreamResult, isString };
|
|
495
|
+
export { createRetryable, getModelKey, isEmbeddingModelV2, isErrorAttempt, isGenerateResult, isLanguageModelV2, isModelV2, isObject, isResultAttempt, isRetry, isStreamContentPart, isStreamResult, isString };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { h as RetryableOptions, i as LanguageModelV2, p as Retryable, t as EmbeddingModelV2 } from "../types-
|
|
1
|
+
import { h as RetryableOptions, i as LanguageModelV2, p as Retryable, t as EmbeddingModelV2 } from "../types-BuPozWMn.js";
|
|
2
2
|
|
|
3
3
|
//#region src/retryables/content-filter-triggered.d.ts
|
|
4
4
|
|
|
@@ -17,7 +17,7 @@ declare function requestNotRetryable<MODEL extends LanguageModelV2 | EmbeddingMo
|
|
|
17
17
|
/**
|
|
18
18
|
* Fallback to a different model after a timeout/abort error.
|
|
19
19
|
* Use in combination with the `abortSignal` option.
|
|
20
|
-
*
|
|
20
|
+
* If no timeout is specified, a default of 60 seconds is used.
|
|
21
21
|
*/
|
|
22
22
|
declare function requestTimeout<MODEL extends LanguageModelV2 | EmbeddingModelV2>(model: MODEL, options?: RetryableOptions<MODEL>): Retryable<MODEL>;
|
|
23
23
|
//#endregion
|
package/dist/retryables/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as isErrorAttempt, o as isObject, s as isResultAttempt
|
|
1
|
+
import { d as isString, n as isErrorAttempt, o as isObject, s as isResultAttempt } from "../utils-DsvLGk6a.js";
|
|
2
2
|
import { isAbortError } from "@ai-sdk/provider-utils";
|
|
3
3
|
import { APICallError } from "ai";
|
|
4
4
|
|
|
@@ -51,18 +51,20 @@ function requestNotRetryable(model, options) {
|
|
|
51
51
|
/**
|
|
52
52
|
* Fallback to a different model after a timeout/abort error.
|
|
53
53
|
* Use in combination with the `abortSignal` option.
|
|
54
|
-
*
|
|
54
|
+
* If no timeout is specified, a default of 60 seconds is used.
|
|
55
55
|
*/
|
|
56
56
|
function requestTimeout(model, options) {
|
|
57
57
|
return (context) => {
|
|
58
58
|
const { current } = context;
|
|
59
59
|
if (isErrorAttempt(current)) {
|
|
60
60
|
/**
|
|
61
|
-
* Fallback to the specified model after
|
|
61
|
+
* Fallback to the specified model after a timeout/abort error.
|
|
62
|
+
* Provides a fresh timeout signal for the retry attempt.
|
|
62
63
|
*/
|
|
63
64
|
if (isAbortError(current.error)) return {
|
|
64
65
|
model,
|
|
65
66
|
maxAttempts: 1,
|
|
67
|
+
timeout: options?.timeout ?? 6e4,
|
|
66
68
|
...options
|
|
67
69
|
};
|
|
68
70
|
}
|
|
@@ -56,12 +56,13 @@ type Retry<MODEL extends LanguageModelV2$1 | EmbeddingModelV2$1> = {
|
|
|
56
56
|
delay?: number;
|
|
57
57
|
backoffFactor?: number;
|
|
58
58
|
providerOptions?: ProviderOptions;
|
|
59
|
+
timeout?: number;
|
|
59
60
|
};
|
|
60
61
|
/**
|
|
61
62
|
* A function that determines whether to retry with a different model based on the current attempt and all previous attempts.
|
|
62
63
|
*/
|
|
63
64
|
type Retryable<MODEL extends LanguageModelV2$1 | EmbeddingModelV2$1> = (context: RetryContext<MODEL>) => Retry<MODEL> | Promise<Retry<MODEL>> | undefined;
|
|
64
|
-
type Retries<MODEL extends LanguageModelV2$1 | EmbeddingModelV2$1> = Array<Retryable<MODEL> | MODEL>;
|
|
65
|
+
type Retries<MODEL extends LanguageModelV2$1 | EmbeddingModelV2$1> = Array<Retryable<MODEL> | Retry<MODEL> | MODEL>;
|
|
65
66
|
type RetryableOptions<MODEL extends LanguageModelV2$1 | EmbeddingModelV2$1> = Partial<Omit<Retry<MODEL>, 'model'>>;
|
|
66
67
|
type LanguageModelV2Generate = Awaited<ReturnType<LanguageModelV2$1['doGenerate']>>;
|
|
67
68
|
type LanguageModelV2Stream = Awaited<ReturnType<LanguageModelV2$1['doStream']>>;
|
|
@@ -26,6 +26,10 @@ function isResultAttempt(attempt) {
|
|
|
26
26
|
const isStreamContentPart = (part) => {
|
|
27
27
|
return part.type === "text-delta" || part.type === "reasoning-delta" || part.type === "source" || part.type === "tool-call" || part.type === "tool-result" || part.type === "tool-input-start" || part.type === "tool-input-delta" || part.type === "raw";
|
|
28
28
|
};
|
|
29
|
+
/**
|
|
30
|
+
* Type guard to check if a value is a Retry object (has a model property with a MODEL)
|
|
31
|
+
*/
|
|
32
|
+
const isRetry = (value) => isObject(value) && "model" in value && isModelV2(value.model);
|
|
29
33
|
|
|
30
34
|
//#endregion
|
|
31
|
-
export { isModelV2 as a,
|
|
35
|
+
export { isModelV2 as a, isRetry as c, isString as d, isLanguageModelV2 as i, isStreamContentPart as l, isErrorAttempt as n, isObject as o, isGenerateResult as r, isResultAttempt as s, isEmbeddingModelV2 as t, isStreamResult as u };
|