ai-retry 0.7.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 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
+ },
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
118
125
  },
119
126
 
120
- // Static retryable that always matches (fallback)
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
 
@@ -275,20 +283,26 @@ Handle timeouts by switching to potentially faster models.
275
283
  > [!NOTE]
276
284
  > You need to use an `abortSignal` with a timeout on your request.
277
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
+
278
288
  ```typescript
279
289
  import { requestTimeout } from 'ai-retry/retryables';
280
290
 
281
291
  const retryableModel = createRetryable({
282
292
  model: azure('gpt-4'),
283
293
  retries: [
284
- requestTimeout(azure('gpt-4-mini')), // Use faster model on timeout
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 }),
285
299
  ],
286
300
  });
287
301
 
288
302
  const result = await generateText({
289
303
  model: retryableModel,
290
304
  prompt: 'Write a vegetarian lasagna recipe for 4 people.',
291
- abortSignal: AbortSignal.timeout(60_000),
305
+ abortSignal: AbortSignal.timeout(60_000), // Original request timeout
292
306
  });
293
307
  ```
294
308
 
@@ -343,10 +357,10 @@ const retryableModel = createRetryable({
343
357
  model: openai('gpt-4'), // Base model
344
358
  retries: [
345
359
  // Retry base model 3 times with fixed 2s delay
346
- retryAfterDelay({ delay: 2000, maxAttempts: 3 }),
360
+ retryAfterDelay({ delay: 2_000, maxAttempts: 3 }),
347
361
 
348
362
  // Or retry with exponential backoff (2s, 4s, 8s)
349
- retryAfterDelay({ delay: 2000, backoffFactor: 2, maxAttempts: 3 }),
363
+ retryAfterDelay({ delay: 2_000, backoffFactor: 2, maxAttempts: 3 }),
350
364
 
351
365
  // Or retry only if the response contains a retry-after header
352
366
  retryAfterDelay({ maxAttempts: 3 }),
@@ -367,10 +381,10 @@ const retryableModel = createRetryable({
367
381
  model: openai('gpt-4'),
368
382
  retries: [
369
383
  // Retry model 3 times with fixed 2s delay
370
- { model: openai('gpt-4'), delay: 2000, maxAttempts: 3 },
384
+ { model: openai('gpt-4'), delay: 2_000, maxAttempts: 3 },
371
385
 
372
386
  // Or retry with exponential backoff (2s, 4s, 8s)
373
- { model: openai('gpt-4'), delay: 2000, backoffFactor: 2, maxAttempts: 3 },
387
+ { model: openai('gpt-4'), delay: 2_000, backoffFactor: 2, maxAttempts: 3 },
374
388
  ],
375
389
  });
376
390
 
@@ -395,6 +409,30 @@ const retryableModel = createRetryable({
395
409
  ],
396
410
  });
397
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
+ ```
398
436
 
399
437
  #### Max Attempts
400
438
 
@@ -509,7 +547,7 @@ type Retryable = (
509
547
 
510
548
  #### `Retry`
511
549
 
512
- 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`.
513
551
 
514
552
  ```typescript
515
553
  interface Retry {
@@ -517,6 +555,7 @@ interface Retry {
517
555
  maxAttempts?: number; // Maximum retry attempts per model (default: 1)
518
556
  delay?: number; // Delay in milliseconds before retrying
519
557
  backoffFactor?: number; // Multiplier for exponential backoff
558
+ timeout?: number; // Timeout in milliseconds for the retry attempt
520
559
  providerOptions?: ProviderOptions; // Provider-specific options for the retry
521
560
  }
522
561
  ```
@@ -526,6 +565,7 @@ interface Retry {
526
565
  - `maxAttempts`: Maximum number of times this model can be retried. Default is 1.
527
566
  - `delay`: Delay in milliseconds to wait before retrying. The delay respects abort signals from the request.
528
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.
529
569
  - `providerOptions`: Provider-specific options that override the original request's provider options during retry attempts.
530
570
 
531
571
  #### `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
@@ -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
  }
@@ -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.8.0",
4
4
  "description": "AI SDK Retry",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",