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 CHANGED
@@ -40,7 +40,8 @@ import { createRetryable } from 'ai-retry';
40
40
 
41
41
  // Create a retryable model
42
42
  const retryableModel = createRetryable({
43
- model: openai('gpt-4-mini'), // Base model
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
- model: openai.textEmbedding('text-embedding-3-large'), // Base model
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
- 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 }),
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: 2000, maxAttempts: 3 }),
360
+ retryAfterDelay({ delay: 2_000, maxAttempts: 3 }),
186
361
 
187
362
  // Or retry with exponential backoff (2s, 4s, 8s)
188
- retryAfterDelay({ delay: 2000, backoffFactor: 2, maxAttempts: 3 }),
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
- () => ({ model: openai('gpt-4'), delay: 2000, maxAttempts: 3 }),
384
+ { model: openai('gpt-4'), delay: 2_000, maxAttempts: 3 },
286
385
 
287
386
  // Or retry with exponential backoff (2s, 4s, 8s)
288
- () => ({ model: openai('gpt-4'), delay: 2000, backoffFactor: 2, maxAttempts: 3 }),
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
- () => ({ model: openai('gpt-4'), maxAttempts: 2 }),
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-DhGbwiB4.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
 
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 isStreamContentPart, i as isLanguageModelV2, l as isStreamResult, n as isErrorAttempt, o as isObject, r as isGenerateResult, s as isResultAttempt, t as isEmbeddingModelV2, u as isString } from "./utils-BlCGaP0E.js";
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
- const retryModel = typeof retry === "function" ? await retry(context) : { model: retry };
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-DhGbwiB4.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
@@ -1,4 +1,4 @@
1
- import { n as isErrorAttempt, o as isObject, s as isResultAttempt, u as isString } from "../utils-BlCGaP0E.js";
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
- * 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,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, isStreamContentPart as c, isLanguageModelV2 as i, isStreamResult as l, isErrorAttempt as n, isObject as o, isGenerateResult as r, isResultAttempt as s, isEmbeddingModelV2 as t, isString as u };
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-retry",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "AI SDK Retry",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",