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 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. This is also called a fallback model.
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` condition 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:
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-mini'),
111
- // Retryables are evaluated in order
109
+ model: openai('gpt-4'),
110
+ // Retryables are evaluated top-down in order
112
111
  retries: [
113
- // Dynamic retryable that matches only certain errors
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: openai('gpt-3.5-turbo') } // Retry with this model
117
- : undefined; // Skip to next retryable
116
+ ? { model: azure('gpt-4-mini') } // Retry
117
+ : undefined; // Skip
118
118
  },
119
119
 
120
- // Static retryable that always matches (fallback)
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 a 429 error, it will retry with `gpt-4`. In any other error case, it will fallback to `gpt-3.5-turbo`. If the order would be reversed, the static retryable would catch all errors first, and the dynamic retryable would never be reached.
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
- requestTimeout(azure('gpt-4-mini')), // Use faster model on timeout
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: 2000, maxAttempts: 3 }),
379
+ retryAfterDelay({ delay: 2_000, maxAttempts: 3 }),
347
380
 
348
381
  // Or retry with exponential backoff (2s, 4s, 8s)
349
- retryAfterDelay({ delay: 2000, backoffFactor: 2, maxAttempts: 3 }),
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: 2000, maxAttempts: 3 },
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: 2000, backoffFactor: 2, maxAttempts: 3 },
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-CfE400mD.js";
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-CfE400mD.js";
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
- * Works with both `LanguageModelV2` and `EmbeddingModelV2`.
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
- export { contentFilterTriggered, requestNotRetryable, requestTimeout, retryAfterDelay, serviceOverloaded };
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 };
@@ -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
- * Works with both `LanguageModelV2` and `EmbeddingModelV2`.
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 all retries are exhausted.
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
- export { contentFilterTriggered, requestNotRetryable, requestTimeout, retryAfterDelay, serviceOverloaded };
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-retry",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "AI SDK Retry",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",