ai-retry 0.7.0 → 0.9.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 +76 -17
- package/dist/index.d.ts +1 -1
- package/dist/index.js +8 -4
- package/dist/retryables/index.d.ts +10 -3
- package/dist/retryables/index.js +25 -3
- package/dist/{types-CfE400mD.d.ts → types-BuPozWMn.d.ts} +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -95,29 +95,37 @@ console.log(result.embedding);
|
|
|
95
95
|
|
|
96
96
|
The objects passed to the `retries` are called retryables and control the retry behavior. We can distinguish between two types of retryables:
|
|
97
97
|
|
|
98
|
-
- **Static retryables** are simply models instances (language or embedding) that will always be used when an error occurs.
|
|
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
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
100
|
|
|
101
|
-
You can think of `retries` as a big `if-else` block, where each dynamic retryable is an `if`
|
|
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
102
|
|
|
103
103
|
```typescript
|
|
104
|
-
import { openai } from '@ai-sdk/openai';
|
|
105
104
|
import { generateText, streamText } from 'ai';
|
|
106
105
|
import { createRetryable } from 'ai-retry';
|
|
107
106
|
|
|
108
107
|
const retryableModel = createRetryable({
|
|
109
108
|
// Base model
|
|
110
|
-
model: openai('gpt-4
|
|
111
|
-
// Retryables are evaluated in order
|
|
109
|
+
model: openai('gpt-4'),
|
|
110
|
+
// Retryables are evaluated top-down in order
|
|
112
111
|
retries: [
|
|
113
|
-
// Dynamic
|
|
112
|
+
// Dynamic retryables act like if-branches:
|
|
113
|
+
// If error.code == 429 (too many requests) happens, retry with this model
|
|
114
114
|
(context) => {
|
|
115
115
|
return context.current.error.statusCode === 429
|
|
116
|
-
? { model:
|
|
117
|
-
: undefined;
|
|
116
|
+
? { model: azure('gpt-4-mini') } // Retry
|
|
117
|
+
: undefined; // Skip
|
|
118
118
|
},
|
|
119
119
|
|
|
120
|
-
//
|
|
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
|
|
121
129
|
anthropic('claude-3-haiku-20240307'),
|
|
122
130
|
// Same as:
|
|
123
131
|
// { model: anthropic('claude-3-haiku-20240307'), maxAttempts: 1 }
|
|
@@ -125,7 +133,7 @@ const retryableModel = createRetryable({
|
|
|
125
133
|
});
|
|
126
134
|
```
|
|
127
135
|
|
|
128
|
-
In this example, if the base model fails with
|
|
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.
|
|
129
137
|
|
|
130
138
|
#### Fallbacks
|
|
131
139
|
|
|
@@ -243,12 +251,16 @@ Errors are tracked per unique model (provider + modelId). That means on the firs
|
|
|
243
251
|
|
|
244
252
|
There are several built-in dynamic retryables available for common use cases:
|
|
245
253
|
|
|
254
|
+
> [!TIP]
|
|
255
|
+
> You are missing a retryable for your use case? [Open an issue](https://github.com/zirkelc/ai-retry/issues/new) and let's discuss it!
|
|
256
|
+
|
|
246
257
|
- [`contentFilterTriggered`](./src/retryables/content-filter-triggered.ts): Content filter was triggered based on the prompt or completion.
|
|
247
258
|
- [`requestTimeout`](./src/retryables/request-timeout.ts): Request timeout occurred.
|
|
248
259
|
- [`requestNotRetryable`](./src/retryables/request-not-retryable.ts): Request failed with a non-retryable error.
|
|
249
260
|
- [`retryAfterDelay`](./src/retryables/retry-after-delay.ts): Retry with delay and exponential backoff and respect `retry-after` headers.
|
|
250
261
|
- [`serviceOverloaded`](./src/retryables/service-overloaded.ts): Response with status code 529 (service overloaded).
|
|
251
262
|
- Use this retryable to handle Anthropic's overloaded errors.
|
|
263
|
+
- [`serviceUnavailable`](./src/retryables/service-unavailable.ts): Response with status code 503 (service unavailable).
|
|
252
264
|
|
|
253
265
|
#### Content Filter
|
|
254
266
|
|
|
@@ -275,20 +287,26 @@ Handle timeouts by switching to potentially faster models.
|
|
|
275
287
|
> [!NOTE]
|
|
276
288
|
> You need to use an `abortSignal` with a timeout on your request.
|
|
277
289
|
|
|
290
|
+
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.
|
|
291
|
+
|
|
278
292
|
```typescript
|
|
279
293
|
import { requestTimeout } from 'ai-retry/retryables';
|
|
280
294
|
|
|
281
295
|
const retryableModel = createRetryable({
|
|
282
296
|
model: azure('gpt-4'),
|
|
283
297
|
retries: [
|
|
284
|
-
|
|
298
|
+
// Defaults to 60 seconds timeout for the retry attempt
|
|
299
|
+
requestTimeout(azure('gpt-4-mini')),
|
|
300
|
+
|
|
301
|
+
// Or specify a custom timeout for the retry attempt
|
|
302
|
+
requestTimeout(azure('gpt-4-mini'), { timeout: 30_000 }),
|
|
285
303
|
],
|
|
286
304
|
});
|
|
287
305
|
|
|
288
306
|
const result = await generateText({
|
|
289
307
|
model: retryableModel,
|
|
290
308
|
prompt: 'Write a vegetarian lasagna recipe for 4 people.',
|
|
291
|
-
abortSignal: AbortSignal.timeout(60_000),
|
|
309
|
+
abortSignal: AbortSignal.timeout(60_000), // Original request timeout
|
|
292
310
|
});
|
|
293
311
|
```
|
|
294
312
|
|
|
@@ -310,6 +328,21 @@ const retryableModel = createRetryable({
|
|
|
310
328
|
});
|
|
311
329
|
```
|
|
312
330
|
|
|
331
|
+
#### Service Unavailable
|
|
332
|
+
|
|
333
|
+
Handle service unavailable errors (status code 503) by switching to a different provider.
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
import { serviceUnavailable } from 'ai-retry/retryables';
|
|
337
|
+
|
|
338
|
+
const retryableModel = createRetryable({
|
|
339
|
+
model: azure('gpt-4'),
|
|
340
|
+
retries: [
|
|
341
|
+
serviceUnavailable(openai('gpt-4')), // Switch to OpenAI if Azure is unavailable
|
|
342
|
+
],
|
|
343
|
+
});
|
|
344
|
+
```
|
|
345
|
+
|
|
313
346
|
#### Request Not Retryable
|
|
314
347
|
|
|
315
348
|
Handle cases where the base model fails with a non-retryable error.
|
|
@@ -343,10 +376,10 @@ const retryableModel = createRetryable({
|
|
|
343
376
|
model: openai('gpt-4'), // Base model
|
|
344
377
|
retries: [
|
|
345
378
|
// Retry base model 3 times with fixed 2s delay
|
|
346
|
-
retryAfterDelay({ delay:
|
|
379
|
+
retryAfterDelay({ delay: 2_000, maxAttempts: 3 }),
|
|
347
380
|
|
|
348
381
|
// Or retry with exponential backoff (2s, 4s, 8s)
|
|
349
|
-
retryAfterDelay({ delay:
|
|
382
|
+
retryAfterDelay({ delay: 2_000, backoffFactor: 2, maxAttempts: 3 }),
|
|
350
383
|
|
|
351
384
|
// Or retry only if the response contains a retry-after header
|
|
352
385
|
retryAfterDelay({ maxAttempts: 3 }),
|
|
@@ -367,10 +400,10 @@ const retryableModel = createRetryable({
|
|
|
367
400
|
model: openai('gpt-4'),
|
|
368
401
|
retries: [
|
|
369
402
|
// Retry model 3 times with fixed 2s delay
|
|
370
|
-
{ model: openai('gpt-4'), delay:
|
|
403
|
+
{ model: openai('gpt-4'), delay: 2_000, maxAttempts: 3 },
|
|
371
404
|
|
|
372
405
|
// Or retry with exponential backoff (2s, 4s, 8s)
|
|
373
|
-
{ model: openai('gpt-4'), delay:
|
|
406
|
+
{ model: openai('gpt-4'), delay: 2_000, backoffFactor: 2, maxAttempts: 3 },
|
|
374
407
|
],
|
|
375
408
|
});
|
|
376
409
|
|
|
@@ -395,6 +428,30 @@ const retryableModel = createRetryable({
|
|
|
395
428
|
],
|
|
396
429
|
});
|
|
397
430
|
```
|
|
431
|
+
#### Timeouts
|
|
432
|
+
|
|
433
|
+
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.
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
const retryableModel = createRetryable({
|
|
437
|
+
model: openai('gpt-4'),
|
|
438
|
+
retries: [
|
|
439
|
+
// Provide a fresh 30 second timeout for the retry
|
|
440
|
+
{
|
|
441
|
+
model: openai('gpt-3.5-turbo'),
|
|
442
|
+
timeout: 30_000
|
|
443
|
+
},
|
|
444
|
+
],
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// Even if the original request times out, the retry gets a fresh signal
|
|
448
|
+
const result = await generateText({
|
|
449
|
+
model: retryableModel,
|
|
450
|
+
prompt: 'Write a story',
|
|
451
|
+
// Original request timeout
|
|
452
|
+
abortSignal: AbortSignal.timeout(60_000),
|
|
453
|
+
});
|
|
454
|
+
```
|
|
398
455
|
|
|
399
456
|
#### Max Attempts
|
|
400
457
|
|
|
@@ -509,7 +566,7 @@ type Retryable = (
|
|
|
509
566
|
|
|
510
567
|
#### `Retry`
|
|
511
568
|
|
|
512
|
-
A `Retry` specifies the model to retry and optional settings like `maxAttempts`, `delay`, `backoffFactor`, and `providerOptions`.
|
|
569
|
+
A `Retry` specifies the model to retry and optional settings like `maxAttempts`, `delay`, `backoffFactor`, `timeout`, and `providerOptions`.
|
|
513
570
|
|
|
514
571
|
```typescript
|
|
515
572
|
interface Retry {
|
|
@@ -517,6 +574,7 @@ interface Retry {
|
|
|
517
574
|
maxAttempts?: number; // Maximum retry attempts per model (default: 1)
|
|
518
575
|
delay?: number; // Delay in milliseconds before retrying
|
|
519
576
|
backoffFactor?: number; // Multiplier for exponential backoff
|
|
577
|
+
timeout?: number; // Timeout in milliseconds for the retry attempt
|
|
520
578
|
providerOptions?: ProviderOptions; // Provider-specific options for the retry
|
|
521
579
|
}
|
|
522
580
|
```
|
|
@@ -526,6 +584,7 @@ interface Retry {
|
|
|
526
584
|
- `maxAttempts`: Maximum number of times this model can be retried. Default is 1.
|
|
527
585
|
- `delay`: Delay in milliseconds to wait before retrying. The delay respects abort signals from the request.
|
|
528
586
|
- `backoffFactor`: Multiplier for exponential backoff (`delay × backoffFactor^attempt`). If not provided, uses fixed delay.
|
|
587
|
+
- `timeout`: Timeout in milliseconds for creating a fresh `AbortSignal.timeout()` for the retry attempt. This replaces any existing abort signal.
|
|
529
588
|
- `providerOptions`: Provider-specific options that override the original request's provider options during retry attempts.
|
|
530
589
|
|
|
531
590
|
#### `RetryContext`
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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
|
|
package/dist/index.js
CHANGED
|
@@ -201,7 +201,8 @@ var RetryableEmbeddingModel = class {
|
|
|
201
201
|
fn: async (currentRetry) => {
|
|
202
202
|
const callOptions = {
|
|
203
203
|
...options,
|
|
204
|
-
providerOptions: currentRetry?.providerOptions ?? options.providerOptions
|
|
204
|
+
providerOptions: currentRetry?.providerOptions ?? options.providerOptions,
|
|
205
|
+
abortSignal: currentRetry?.timeout ? AbortSignal.timeout(currentRetry.timeout) : options.abortSignal
|
|
205
206
|
};
|
|
206
207
|
return this.currentModel.doEmbed(callOptions);
|
|
207
208
|
},
|
|
@@ -374,7 +375,8 @@ var RetryableLanguageModel = class {
|
|
|
374
375
|
fn: async (currentRetry) => {
|
|
375
376
|
const callOptions = {
|
|
376
377
|
...options,
|
|
377
|
-
providerOptions: currentRetry?.providerOptions ?? options.providerOptions
|
|
378
|
+
providerOptions: currentRetry?.providerOptions ?? options.providerOptions,
|
|
379
|
+
abortSignal: currentRetry?.timeout ? AbortSignal.timeout(currentRetry.timeout) : options.abortSignal
|
|
378
380
|
};
|
|
379
381
|
return this.currentModel.doGenerate(callOptions);
|
|
380
382
|
},
|
|
@@ -394,7 +396,8 @@ var RetryableLanguageModel = class {
|
|
|
394
396
|
fn: async (currentRetry) => {
|
|
395
397
|
const callOptions = {
|
|
396
398
|
...options,
|
|
397
|
-
providerOptions: currentRetry?.providerOptions ?? options.providerOptions
|
|
399
|
+
providerOptions: currentRetry?.providerOptions ?? options.providerOptions,
|
|
400
|
+
abortSignal: currentRetry?.timeout ? AbortSignal.timeout(currentRetry.timeout) : options.abortSignal
|
|
398
401
|
};
|
|
399
402
|
return this.currentModel.doStream(callOptions);
|
|
400
403
|
},
|
|
@@ -456,7 +459,8 @@ var RetryableLanguageModel = class {
|
|
|
456
459
|
fn: async () => {
|
|
457
460
|
const callOptions = {
|
|
458
461
|
...options,
|
|
459
|
-
providerOptions: retryModel.providerOptions ?? options.providerOptions
|
|
462
|
+
providerOptions: retryModel.providerOptions ?? options.providerOptions,
|
|
463
|
+
abortSignal: retryModel.timeout ? AbortSignal.timeout(retryModel.timeout) : options.abortSignal
|
|
460
464
|
};
|
|
461
465
|
return this.currentModel.doStream(callOptions);
|
|
462
466
|
},
|
|
@@ -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
|
|
@@ -39,4 +39,11 @@ declare function retryAfterDelay<MODEL extends LanguageModelV2 | EmbeddingModelV
|
|
|
39
39
|
*/
|
|
40
40
|
declare function serviceOverloaded<MODEL extends LanguageModelV2 | EmbeddingModelV2>(model: MODEL, options?: RetryableOptions<MODEL>): Retryable<MODEL>;
|
|
41
41
|
//#endregion
|
|
42
|
-
|
|
42
|
+
//#region src/retryables/service-unavailable.d.ts
|
|
43
|
+
/**
|
|
44
|
+
* Fallback to a different model if the provider returns a service unavailable error.
|
|
45
|
+
* This retryable handles HTTP status code 503 (Service Unavailable).
|
|
46
|
+
*/
|
|
47
|
+
declare function serviceUnavailable<MODEL extends LanguageModelV2 | EmbeddingModelV2>(model: MODEL, options?: RetryableOptions<MODEL>): Retryable<MODEL>;
|
|
48
|
+
//#endregion
|
|
49
|
+
export { contentFilterTriggered, requestNotRetryable, requestTimeout, retryAfterDelay, serviceOverloaded, serviceUnavailable };
|
package/dist/retryables/index.js
CHANGED
|
@@ -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
|
}
|
|
@@ -150,4 +152,24 @@ function serviceOverloaded(model, options) {
|
|
|
150
152
|
}
|
|
151
153
|
|
|
152
154
|
//#endregion
|
|
153
|
-
|
|
155
|
+
//#region src/retryables/service-unavailable.ts
|
|
156
|
+
/**
|
|
157
|
+
* Fallback to a different model if the provider returns a service unavailable error.
|
|
158
|
+
* This retryable handles HTTP status code 503 (Service Unavailable).
|
|
159
|
+
*/
|
|
160
|
+
function serviceUnavailable(model, options) {
|
|
161
|
+
return (context) => {
|
|
162
|
+
const { current } = context;
|
|
163
|
+
if (isErrorAttempt(current)) {
|
|
164
|
+
const { error } = current;
|
|
165
|
+
if (APICallError.isInstance(error) && error.statusCode === 503) return {
|
|
166
|
+
model,
|
|
167
|
+
maxAttempts: 1,
|
|
168
|
+
...options
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
//#endregion
|
|
175
|
+
export { contentFilterTriggered, requestNotRetryable, requestTimeout, retryAfterDelay, serviceOverloaded, serviceUnavailable };
|
|
@@ -56,6 +56,7 @@ 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.
|