ai-retry 0.4.1 → 0.5.1

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
@@ -21,9 +21,6 @@ It supports two types of retries:
21
21
 
22
22
  This library only supports AI SDK v5.
23
23
 
24
- > [!WARNING]
25
- > `ai-retry` is in an early stage and the API may change in future releases.
26
-
27
24
  ```bash
28
25
  npm install ai-retry
29
26
  ```
@@ -173,11 +170,10 @@ const retryable = createRetryable({
173
170
 
174
171
  #### Retry After Delay
175
172
 
176
- Handle retryable errors with delays and respect `retry-after` headers from rate-limited responses. This is useful for handling 429 (Too Many Requests) and 503 (Service Unavailable) errors.
177
-
178
- > [!NOTE]
179
- > If the response contains a [`retry-after`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Retry-After) header, it will be prioritized over the configured delay.
173
+ If an error is retryable, such as 429 (Too Many Requests) or 503 (Service Unavailable) errors, it will be retried after a delay.
174
+ The delay and exponential backoff can be configured. If the response contains a [`retry-after`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Retry-After) header, it will be prioritized over the configured delay.
180
175
 
176
+ Note that this retryable does not accept a model parameter, it will always retry the model from the latest failed attempt.
181
177
 
182
178
  ```typescript
183
179
  import { retryAfterDelay } from 'ai-retry/retryables';
@@ -190,19 +186,14 @@ const retryableModel = createRetryable({
190
186
 
191
187
  // Or retry with exponential backoff (2s, 4s, 8s)
192
188
  retryAfterDelay({ delay: 2000, backoffFactor: 2, maxAttempts: 3 }),
193
-
194
- // Or switch to a different model after delay
195
- retryAfterDelay(openai('gpt-4-mini'), { delay: 1000 }),
189
+
190
+ // Or retry only if the response contains a retry-after header
191
+ retryAfterDelay({ maxAttempts: 3 }),
196
192
  ],
197
193
  });
198
194
  ```
199
195
 
200
- **Options:**
201
- - `delay` (required): Delay in milliseconds before retrying
202
- - `backoffFactor` (optional): Multiplier for exponential backoff (delay × backoffFactor^attempt). If not provided, uses fixed delay.
203
- - `maxAttempts` (optional): Maximum number of retry attempts for this model
204
-
205
- 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` 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. If no headers are present, the configured delay or exponential backoff will be used.
196
+ 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.
206
197
 
207
198
  #### Fallbacks
208
199
 
@@ -284,22 +275,17 @@ try {
284
275
 
285
276
  #### Retry Delays
286
277
 
287
- You can add delays before retrying to handle rate limiting or give services time to recover. The delay respects abort signals, so requests can still be cancelled during the delay period.
278
+ You can delay retries with an optional exponential backoff. The delay respects abort signals, so requests can still be cancelled during the delay period.
288
279
 
289
280
  ```typescript
290
281
  const retryableModel = createRetryable({
291
282
  model: openai('gpt-4'),
292
283
  retries: [
293
- // Wait 1 second before retrying
294
- () => ({
295
- model: openai('gpt-4'),
296
- delay: 1_000
297
- }),
298
- // Wait 2 seconds before trying a different provider
299
- () => ({
300
- model: anthropic('claude-3-haiku-20240307'),
301
- delay: 2_000
302
- }),
284
+ // Retry model 3 times with fixed 2s delay
285
+ () => ({ model: openai('gpt-4'), delay: 2000, maxAttempts: 3 }),
286
+
287
+ // Or retry with exponential backoff (2s, 4s, 8s)
288
+ () => ({ model: openai('gpt-4'), delay: 2000, backoffFactor: 2, maxAttempts: 3 }),
303
289
  ],
304
290
  });
305
291
 
@@ -383,7 +369,7 @@ There are several built-in retryables:
383
369
  - [`contentFilterTriggered`](./src/retryables/content-filter-triggered.ts): Content filter was triggered based on the prompt or completion.
384
370
  - [`requestTimeout`](./src/retryables/request-timeout.ts): Request timeout occurred.
385
371
  - [`requestNotRetryable`](./src/retryables/request-not-retryable.ts): Request failed with a non-retryable error.
386
- - [`retryAfterDelay`](./src/retryables/retry-after-delay.ts): Retry with exponential backoff and respect `retry-after` headers for rate limiting.
372
+ - [`retryAfterDelay`](./src/retryables/retry-after-delay.ts): Retry with delay and exponential backoff and respect `retry-after` headers.
387
373
  - [`serviceOverloaded`](./src/retryables/service-overloaded.ts): Response with status code 529 (service overloaded).
388
374
  - Use this retryable to handle Anthropic's overloaded errors.
389
375
 
@@ -405,29 +391,32 @@ interface RetryableModelOptions<MODEL extends LanguageModelV2 | EmbeddingModelV2
405
391
  #### `Retryable`
406
392
 
407
393
  A `Retryable` is a function that receives a `RetryContext` with the current error or result and model and all previous attempts.
408
- It should evaluate the error/result and decide whether to retry by returning a `RetryModel` or to skip by returning `undefined`.
394
+ It should evaluate the error/result and decide whether to retry by returning a `Retry` or to skip by returning `undefined`.
409
395
 
410
396
  ```ts
411
397
  type Retryable = (
412
398
  context: RetryContext
413
- ) => RetryModel | Promise<RetryModel> | undefined;
399
+ ) => Retry | Promise<Retry> | undefined;
414
400
  ```
415
401
 
416
- #### `RetryModel`
402
+ #### `Retry`
417
403
 
418
- A `RetryModel` specifies the model to retry and optional settings like `maxAttempts` and `delay`.
404
+ A `Retry` specifies the model to retry and optional settings like `maxAttempts`, `delay` and `backoffFactor`.
419
405
 
420
406
  ```typescript
421
- interface RetryModel {
407
+ interface Retry {
422
408
  model: LanguageModelV2 | EmbeddingModelV2;
423
- maxAttempts?: number; // Maximum retry attempts per model (default: 1)
424
- delay?: number; // Delay in milliseconds before retrying
409
+ maxAttempts?: number; // Maximum retry attempts per model (default: 1)
410
+ delay?: number; // Delay in milliseconds before retrying
411
+ backoffFactor?: number; // Multiplier for exponential backoff
425
412
  }
426
413
  ```
427
414
 
428
415
  **Options:**
416
+ - `model`: The model to use for the retry attempt.
429
417
  - `maxAttempts`: Maximum number of times this model can be retried. Default is 1.
430
- - `delay`: Delay in milliseconds to wait before retrying. Useful for rate limiting or giving services time to recover. The delay respects abort signals from the request.
418
+ - `delay`: Delay in milliseconds to wait before retrying. The delay respects abort signals from the request.
419
+ - `backoffFactor`: Multiplier for exponential backoff (`delay × backoffFactor^attempt`). If not provided, uses fixed delay.
431
420
 
432
421
  #### `RetryContext`
433
422
 
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { EmbeddingModelV2, EmbeddingModelV2CallOptions, EmbeddingModelV2Embed, LanguageModelV2, LanguageModelV2Generate, LanguageModelV2Stream, Retries, RetryAttempt, RetryContext, RetryErrorAttempt, RetryModel, RetryResultAttempt, Retryable, RetryableModelOptions } from "./types-BrJaHkFh.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-DqwAmcZS.js";
2
2
  import * as _ai_sdk_provider0 from "@ai-sdk/provider";
3
- import { LanguageModelV2StreamPart } from "@ai-sdk/provider";
3
+ import { LanguageModelV2 as LanguageModelV2$1, LanguageModelV2StreamPart } from "@ai-sdk/provider";
4
4
 
5
5
  //#region src/create-retryable-model.d.ts
6
6
  declare function createRetryable<MODEL extends LanguageModelV2>(options: RetryableModelOptions<MODEL>): LanguageModelV2;
@@ -15,6 +15,9 @@ declare const getModelKey: (model: LanguageModelV2 | EmbeddingModelV2) => string
15
15
  //#region src/utils.d.ts
16
16
  declare const isObject: (value: unknown) => value is Record<string, unknown>;
17
17
  declare const isString: (value: unknown) => value is string;
18
+ declare const isModelV2: (model: unknown) => model is LanguageModelV2$1 | EmbeddingModelV2;
19
+ declare const isLanguageModelV2: (model: unknown) => model is LanguageModelV2$1;
20
+ declare const isEmbeddingModelV2: (model: unknown) => model is EmbeddingModelV2;
18
21
  declare const isStreamResult: (result: LanguageModelV2Generate | LanguageModelV2Stream) => result is LanguageModelV2Stream;
19
22
  declare const isGenerateResult: (result: LanguageModelV2Generate | LanguageModelV2Stream) => result is LanguageModelV2Generate;
20
23
  /**
@@ -64,4 +67,4 @@ declare const isStreamContentPart: (part: LanguageModelV2StreamPart) => part is
64
67
  rawValue: unknown;
65
68
  };
66
69
  //#endregion
67
- export { EmbeddingModelV2, EmbeddingModelV2CallOptions, EmbeddingModelV2Embed, LanguageModelV2, LanguageModelV2Generate, LanguageModelV2Stream, Retries, RetryAttempt, RetryContext, RetryErrorAttempt, RetryModel, RetryResultAttempt, Retryable, RetryableModelOptions, createRetryable, getModelKey, isErrorAttempt, isGenerateResult, isObject, isResultAttempt, isStreamContentPart, isStreamResult, isString };
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 };
package/dist/index.js CHANGED
@@ -1,8 +1,33 @@
1
- import { getModelKey, isErrorAttempt, isGenerateResult, isObject, isResultAttempt, isStreamContentPart, isStreamResult, isString } from "./utils-Dojn0elD.js";
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";
2
2
  import { delay } from "@ai-sdk/provider-utils";
3
3
  import { getErrorMessage } from "@ai-sdk/provider";
4
4
  import { RetryError } from "ai";
5
5
 
6
+ //#region src/calculate-exponential-backoff.ts
7
+ /**
8
+ * Calculates the exponential backoff delay.
9
+ */
10
+ function calculateExponentialBackoff(baseDelay, backoffFactor = 1, attempts) {
11
+ return baseDelay * Math.max(backoffFactor, 1) ** attempts;
12
+ }
13
+
14
+ //#endregion
15
+ //#region src/get-model-key.ts
16
+ /**
17
+ * Generate a unique key for a LanguageModelV2 instance.
18
+ */
19
+ const getModelKey = (model) => {
20
+ return `${model.provider}/${model.modelId}`;
21
+ };
22
+
23
+ //#endregion
24
+ //#region src/count-model-attempts.ts
25
+ function countModelAttempts(model, attempts) {
26
+ const modelKey = getModelKey(model);
27
+ return attempts.filter((a) => getModelKey(a.model) === modelKey).length;
28
+ }
29
+
30
+ //#endregion
6
31
  //#region src/find-retry-model.ts
7
32
  /**
8
33
  * Find the next model to retry with based on the retry context
@@ -18,10 +43,7 @@ async function findRetryModel(retries, context) {
18
43
  * Iterate through the applicable retryables to find a model to retry with
19
44
  */
20
45
  for (const retry of applicableRetries) {
21
- const retryModel = typeof retry === "function" ? await retry(context) : {
22
- model: retry,
23
- maxAttempts: 1
24
- };
46
+ const retryModel = typeof retry === "function" ? await retry(context) : { model: retry };
25
47
  if (retryModel) {
26
48
  /**
27
49
  * The model key uniquely identifies a model instance (provider + modelId)
@@ -97,17 +119,12 @@ var RetryableEmbeddingModel = class {
97
119
  * Skip on the first attempt since no previous attempt exists yet.
98
120
  */
99
121
  if (previousAttempt) {
100
- const currentAttempt = {
101
- ...previousAttempt,
102
- model: this.currentModel
103
- };
104
- /**
105
- * Create a shallow copy of the attempts for testing purposes
106
- */
107
- const updatedAttempts = [...attempts];
108
122
  const context = {
109
- current: currentAttempt,
110
- attempts: updatedAttempts
123
+ current: {
124
+ ...previousAttempt,
125
+ model: this.currentModel
126
+ },
127
+ attempts: [...attempts]
111
128
  };
112
129
  this.options.onRetry?.(context);
113
130
  }
@@ -119,7 +136,18 @@ var RetryableEmbeddingModel = class {
119
136
  } catch (error) {
120
137
  const { retryModel, attempt } = await this.handleError(error, attempts);
121
138
  attempts.push(attempt);
122
- if (retryModel.delay) await delay(retryModel.delay, { abortSignal: input.abortSignal });
139
+ if (retryModel.delay) {
140
+ /**
141
+ * Calculate exponential backoff delay based on the number of attempts for this specific model.
142
+ * The delay grows exponentially: baseDelay * backoffFactor^attempts
143
+ * Example: With delay=1000ms and backoffFactor=2:
144
+ * - Attempt 1: 1000ms
145
+ * - Attempt 2: 2000ms
146
+ * - Attempt 3: 4000ms
147
+ */
148
+ const modelAttemptsCount = countModelAttempts(retryModel.model, attempts);
149
+ await delay(calculateExponentialBackoff(retryModel.delay, retryModel.backoffFactor, modelAttemptsCount), { abortSignal: input.abortSignal });
150
+ }
123
151
  this.currentModel = retryModel.model;
124
152
  }
125
153
  }
@@ -208,17 +236,12 @@ var RetryableLanguageModel = class {
208
236
  * Skip on the first attempt since no previous attempt exists yet.
209
237
  */
210
238
  if (previousAttempt) {
211
- const currentAttempt = {
212
- ...previousAttempt,
213
- model: this.currentModel
214
- };
215
- /**
216
- * Create a shallow copy of the attempts for testing purposes
217
- */
218
- const updatedAttempts = [...attempts];
219
239
  const context = {
220
- current: currentAttempt,
221
- attempts: updatedAttempts
240
+ current: {
241
+ ...previousAttempt,
242
+ model: this.currentModel
243
+ },
244
+ attempts: [...attempts]
222
245
  };
223
246
  this.options.onRetry?.(context);
224
247
  }
@@ -234,7 +257,18 @@ var RetryableLanguageModel = class {
234
257
  const { retryModel, attempt } = await this.handleResult(result, attempts);
235
258
  attempts.push(attempt);
236
259
  if (retryModel) {
237
- if (retryModel.delay) await delay(retryModel.delay, { abortSignal: input.abortSignal });
260
+ if (retryModel.delay) {
261
+ /**
262
+ * Calculate exponential backoff delay based on the number of attempts for this specific model.
263
+ * The delay grows exponentially: baseDelay * backoffFactor^attempts
264
+ * Example: With delay=1000ms and backoffFactor=2:
265
+ * - Attempt 1: 1000ms
266
+ * - Attempt 2: 2000ms
267
+ * - Attempt 3: 4000ms
268
+ */
269
+ const modelAttemptsCount = countModelAttempts(retryModel.model, attempts);
270
+ await delay(calculateExponentialBackoff(retryModel.delay, retryModel.backoffFactor, modelAttemptsCount), { abortSignal: input.abortSignal });
271
+ }
238
272
  this.currentModel = retryModel.model;
239
273
  /**
240
274
  * Continue to the next iteration to retry
@@ -249,7 +283,14 @@ var RetryableLanguageModel = class {
249
283
  } catch (error) {
250
284
  const { retryModel, attempt } = await this.handleError(error, attempts);
251
285
  attempts.push(attempt);
252
- if (retryModel.delay) await delay(retryModel.delay, { abortSignal: input.abortSignal });
286
+ if (retryModel.delay) {
287
+ /**
288
+ * Calculate exponential backoff delay based on the number of attempts for this specific model.
289
+ * The delay grows exponentially: baseDelay * backoffFactor^attempts
290
+ */
291
+ const modelAttemptsCount = countModelAttempts(retryModel.model, attempts);
292
+ await delay(calculateExponentialBackoff(retryModel.delay, retryModel.backoffFactor, modelAttemptsCount), { abortSignal: input.abortSignal });
293
+ }
253
294
  this.currentModel = retryModel.model;
254
295
  }
255
296
  }
@@ -263,13 +304,9 @@ var RetryableLanguageModel = class {
263
304
  result,
264
305
  model: this.currentModel
265
306
  };
266
- /**
267
- * Save the current attempt
268
- */
269
- const updatedAttempts = [...attempts, resultAttempt];
270
307
  const context = {
271
308
  current: resultAttempt,
272
- attempts: updatedAttempts
309
+ attempts: [...attempts, resultAttempt]
273
310
  };
274
311
  return {
275
312
  retryModel: await findRetryModel(this.options.retries, context),
@@ -370,7 +407,14 @@ var RetryableLanguageModel = class {
370
407
  * Save the attempt
371
408
  */
372
409
  attempts.push(attempt);
373
- if (retryModel.delay) await delay(retryModel.delay, { abortSignal: options.abortSignal });
410
+ if (retryModel.delay) {
411
+ /**
412
+ * Calculate exponential backoff delay based on the number of attempts for this specific model.
413
+ * The delay grows exponentially: baseDelay * backoffFactor^attempts
414
+ */
415
+ const modelAttemptsCount = countModelAttempts(retryModel.model, attempts);
416
+ await delay(calculateExponentialBackoff(retryModel.delay, retryModel.backoffFactor, modelAttemptsCount), { abortSignal: options.abortSignal });
417
+ }
374
418
  this.currentModel = retryModel.model;
375
419
  /**
376
420
  * Retry the request by calling doStream again.
@@ -406,4 +450,4 @@ function createRetryable(options) {
406
450
  }
407
451
 
408
452
  //#endregion
409
- export { createRetryable, getModelKey, isErrorAttempt, isGenerateResult, isObject, isResultAttempt, isStreamContentPart, isStreamResult, isString };
453
+ export { createRetryable, getModelKey, isEmbeddingModelV2, isErrorAttempt, isGenerateResult, isLanguageModelV2, isModelV2, isObject, isResultAttempt, isStreamContentPart, isStreamResult, isString };
@@ -1,17 +1,17 @@
1
- import { EmbeddingModelV2, LanguageModelV2, RetryModel, Retryable } from "../types-BrJaHkFh.js";
1
+ import { h as RetryableOptions, i as LanguageModelV2, p as Retryable, t as EmbeddingModelV2 } from "../types-DqwAmcZS.js";
2
2
 
3
3
  //#region src/retryables/content-filter-triggered.d.ts
4
4
 
5
5
  /**
6
6
  * Fallback to a different model if the content filter was triggered.
7
7
  */
8
- declare function contentFilterTriggered<MODEL extends LanguageModelV2>(model: MODEL, options?: Omit<RetryModel<MODEL>, 'model'>): Retryable<MODEL>;
8
+ declare function contentFilterTriggered<MODEL extends LanguageModelV2>(model: MODEL, options?: RetryableOptions<MODEL>): Retryable<MODEL>;
9
9
  //#endregion
10
10
  //#region src/retryables/request-not-retryable.d.ts
11
11
  /**
12
12
  * Fallback to a different model if the error is non-retryable.
13
13
  */
14
- declare function requestNotRetryable<MODEL extends LanguageModelV2 | EmbeddingModelV2>(model: MODEL, options?: Omit<RetryModel<MODEL>, 'model'>): Retryable<MODEL>;
14
+ declare function requestNotRetryable<MODEL extends LanguageModelV2 | EmbeddingModelV2>(model: MODEL, options?: RetryableOptions<MODEL>): Retryable<MODEL>;
15
15
  //#endregion
16
16
  //#region src/retryables/request-timeout.d.ts
17
17
  /**
@@ -19,20 +19,15 @@ declare function requestNotRetryable<MODEL extends LanguageModelV2 | EmbeddingMo
19
19
  * Use in combination with the `abortSignal` option.
20
20
  * Works with both `LanguageModelV2` and `EmbeddingModelV2`.
21
21
  */
22
- declare function requestTimeout<MODEL extends LanguageModelV2 | EmbeddingModelV2>(model: MODEL, options?: Omit<RetryModel<MODEL>, 'model'>): Retryable<MODEL>;
22
+ declare function requestTimeout<MODEL extends LanguageModelV2 | EmbeddingModelV2>(model: MODEL, options?: RetryableOptions<MODEL>): Retryable<MODEL>;
23
23
  //#endregion
24
24
  //#region src/retryables/retry-after-delay.d.ts
25
- type RetryAfterDelayOptions<MODEL extends LanguageModelV2 | EmbeddingModelV2> = Omit<RetryModel<MODEL>, 'model' | 'delay'> & {
26
- delay: number;
27
- backoffFactor?: number;
28
- };
29
25
  /**
30
- * Retry with the same or a different model if the error is retryable with a delay.
26
+ * Retry the current failed attempt with the same model, if the error is retryable.
31
27
  * Uses the `Retry-After` or `Retry-After-Ms` headers if present.
32
- * Otherwise uses the specified `delay`, or exponential backoff if `backoffFactor` is provided.
28
+ * Otherwise uses the specified `delay` and `backoffFactor` if provided.
33
29
  */
34
- declare function retryAfterDelay<MODEL extends LanguageModelV2 | EmbeddingModelV2>(model: MODEL, options?: RetryAfterDelayOptions<MODEL>): Retryable<MODEL>;
35
- declare function retryAfterDelay<MODEL extends LanguageModelV2 | EmbeddingModelV2>(options: RetryAfterDelayOptions<MODEL>): Retryable<MODEL>;
30
+ declare function retryAfterDelay<MODEL extends LanguageModelV2 | EmbeddingModelV2>(options: RetryableOptions<MODEL>): Retryable<MODEL>;
36
31
  //#endregion
37
32
  //#region src/retryables/service-overloaded.d.ts
38
33
  /**
@@ -42,6 +37,6 @@ declare function retryAfterDelay<MODEL extends LanguageModelV2 | EmbeddingModelV
42
37
  * - Response with `type: "overloaded_error"`
43
38
  * - Response with a `message` containing "overloaded"
44
39
  */
45
- declare function serviceOverloaded<MODEL extends LanguageModelV2 | EmbeddingModelV2>(model: MODEL, options?: Omit<RetryModel<MODEL>, 'model'>): Retryable<MODEL>;
40
+ declare function serviceOverloaded<MODEL extends LanguageModelV2 | EmbeddingModelV2>(model: MODEL, options?: RetryableOptions<MODEL>): Retryable<MODEL>;
46
41
  //#endregion
47
42
  export { contentFilterTriggered, requestNotRetryable, requestTimeout, retryAfterDelay, serviceOverloaded };
@@ -1,4 +1,4 @@
1
- import { getModelKey, isErrorAttempt, isObject, isResultAttempt, isString } from "../utils-Dojn0elD.js";
1
+ import { n as isErrorAttempt, o as isObject, s as isResultAttempt, u as isString } from "../utils-BlCGaP0E.js";
2
2
  import { isAbortError } from "@ai-sdk/provider-utils";
3
3
  import { APICallError } from "ai";
4
4
 
@@ -69,15 +69,6 @@ function requestTimeout(model, options) {
69
69
  };
70
70
  }
71
71
 
72
- //#endregion
73
- //#region src/calculate-exponential-backoff.ts
74
- /**
75
- * Calculates the exponential backoff delay.
76
- */
77
- function calculateExponentialBackoff(baseDelay, backoffFactor, attempts) {
78
- return baseDelay * backoffFactor ** attempts;
79
- }
80
-
81
72
  //#endregion
82
73
  //#region src/parse-retry-headers.ts
83
74
  function parseRetryHeaders(headers) {
@@ -100,31 +91,28 @@ function parseRetryHeaders(headers) {
100
91
  //#endregion
101
92
  //#region src/retryables/retry-after-delay.ts
102
93
  const MAX_RETRY_AFTER_MS = 6e4;
103
- function retryAfterDelay(modelOrOptions, options) {
104
- const model = modelOrOptions && "delay" in modelOrOptions ? void 0 : modelOrOptions;
105
- const opts = modelOrOptions && "delay" in modelOrOptions ? modelOrOptions : options;
106
- if (!opts?.delay) throw new Error("retryAfterDelay: delay is required");
107
- const delay$1 = opts.delay;
108
- const backoffFactor = Math.max(opts.backoffFactor ?? 1, 1);
94
+ /**
95
+ * Retry the current failed attempt with the same model, if the error is retryable.
96
+ * Uses the `Retry-After` or `Retry-After-Ms` headers if present.
97
+ * Otherwise uses the specified `delay` and `backoffFactor` if provided.
98
+ */
99
+ function retryAfterDelay(options) {
109
100
  return (context) => {
110
- const { current, attempts } = context;
101
+ const { current } = context;
111
102
  if (isErrorAttempt(current)) {
112
103
  const { error } = current;
113
104
  if (APICallError.isInstance(error) && error.isRetryable === true) {
114
- const targetModel = model ?? current.model;
115
- const modelKey = getModelKey(targetModel);
116
- const modelAttempts = attempts.filter((a) => getModelKey(a.model) === modelKey);
105
+ const model = current.model;
117
106
  const headerDelay = parseRetryHeaders(error.responseHeaders);
118
107
  if (headerDelay !== null) return {
119
- model: targetModel,
108
+ model,
109
+ ...options,
120
110
  delay: Math.min(headerDelay, MAX_RETRY_AFTER_MS),
121
- maxAttempts: opts.maxAttempts
111
+ backoffFactor: 1
122
112
  };
123
- const calculatedDelay = calculateExponentialBackoff(delay$1, backoffFactor, modelAttempts.length);
124
113
  return {
125
- model: targetModel,
126
- delay: calculatedDelay,
127
- maxAttempts: opts.maxAttempts
114
+ model,
115
+ ...options
128
116
  };
129
117
  }
130
118
  }
@@ -1,11 +1,11 @@
1
- import { EmbeddingModelV2, LanguageModelV2 } from "@ai-sdk/provider";
1
+ import { EmbeddingModelV2, LanguageModelV2 as LanguageModelV2$1 } from "@ai-sdk/provider";
2
2
 
3
3
  //#region src/types.d.ts
4
4
  type EmbeddingModelV2$1<VALUE = any> = EmbeddingModelV2<VALUE>;
5
5
  /**
6
6
  * Options for creating a retryable model.
7
7
  */
8
- interface RetryableModelOptions<MODEL extends LanguageModelV2 | EmbeddingModelV2$1> {
8
+ interface RetryableModelOptions<MODEL extends LanguageModelV2$1 | EmbeddingModelV2$1> {
9
9
  model: MODEL;
10
10
  retries: Retries<MODEL>;
11
11
  onError?: (context: RetryContext<MODEL>) => void;
@@ -14,7 +14,7 @@ interface RetryableModelOptions<MODEL extends LanguageModelV2 | EmbeddingModelV2
14
14
  /**
15
15
  * The context provided to Retryables with the current attempt and all previous attempts.
16
16
  */
17
- type RetryContext<MODEL extends LanguageModelV2 | EmbeddingModelV2$1> = {
17
+ type RetryContext<MODEL extends LanguageModelV2$1 | EmbeddingModelV2$1> = {
18
18
  /**
19
19
  * Current attempt that caused the retry
20
20
  */
@@ -27,7 +27,7 @@ type RetryContext<MODEL extends LanguageModelV2 | EmbeddingModelV2$1> = {
27
27
  /**
28
28
  * A retry attempt with an error
29
29
  */
30
- type RetryErrorAttempt<MODEL extends LanguageModelV2 | EmbeddingModelV2$1> = {
30
+ type RetryErrorAttempt<MODEL extends LanguageModelV2$1 | EmbeddingModelV2$1> = {
31
31
  type: 'error';
32
32
  error: unknown;
33
33
  result?: undefined;
@@ -40,28 +40,30 @@ type RetryResultAttempt = {
40
40
  type: 'result';
41
41
  result: LanguageModelV2Generate;
42
42
  error?: undefined;
43
- model: LanguageModelV2;
43
+ model: LanguageModelV2$1;
44
44
  };
45
45
  /**
46
46
  * A retry attempt with either an error or a result and the model used
47
47
  */
48
- type RetryAttempt<MODEL extends LanguageModelV2 | EmbeddingModelV2$1> = RetryErrorAttempt<MODEL> | RetryResultAttempt;
48
+ type RetryAttempt<MODEL extends LanguageModelV2$1 | EmbeddingModelV2$1> = RetryErrorAttempt<MODEL> | RetryResultAttempt;
49
49
  /**
50
50
  * A model to retry with and the maximum number of attempts for that model.
51
51
  */
52
- type RetryModel<MODEL extends LanguageModelV2 | EmbeddingModelV2$1> = {
52
+ type Retry<MODEL extends LanguageModelV2$1 | EmbeddingModelV2$1> = {
53
53
  model: MODEL;
54
54
  maxAttempts?: number;
55
55
  delay?: number;
56
+ backoffFactor?: number;
56
57
  };
57
58
  /**
58
59
  * A function that determines whether to retry with a different model based on the current attempt and all previous attempts.
59
60
  */
60
- type Retryable<MODEL extends LanguageModelV2 | EmbeddingModelV2$1> = (context: RetryContext<MODEL>) => RetryModel<MODEL> | Promise<RetryModel<MODEL>> | undefined;
61
- type Retries<MODEL extends LanguageModelV2 | EmbeddingModelV2$1> = Array<Retryable<MODEL> | MODEL>;
62
- type LanguageModelV2Generate = Awaited<ReturnType<LanguageModelV2['doGenerate']>>;
63
- type LanguageModelV2Stream = Awaited<ReturnType<LanguageModelV2['doStream']>>;
61
+ type Retryable<MODEL extends LanguageModelV2$1 | EmbeddingModelV2$1> = (context: RetryContext<MODEL>) => Retry<MODEL> | Promise<Retry<MODEL>> | undefined;
62
+ type Retries<MODEL extends LanguageModelV2$1 | EmbeddingModelV2$1> = Array<Retryable<MODEL> | MODEL>;
63
+ type RetryableOptions<MODEL extends LanguageModelV2$1 | EmbeddingModelV2$1> = Partial<Omit<Retry<MODEL>, 'model'>>;
64
+ type LanguageModelV2Generate = Awaited<ReturnType<LanguageModelV2$1['doGenerate']>>;
65
+ type LanguageModelV2Stream = Awaited<ReturnType<LanguageModelV2$1['doStream']>>;
64
66
  type EmbeddingModelV2CallOptions<VALUE> = Parameters<EmbeddingModelV2$1<VALUE>['doEmbed']>[0];
65
67
  type EmbeddingModelV2Embed<VALUE> = Awaited<ReturnType<EmbeddingModelV2$1<VALUE>['doEmbed']>>;
66
68
  //#endregion
67
- export { EmbeddingModelV2$1 as EmbeddingModelV2, EmbeddingModelV2CallOptions, EmbeddingModelV2Embed, type LanguageModelV2, LanguageModelV2Generate, LanguageModelV2Stream, Retries, RetryAttempt, RetryContext, RetryErrorAttempt, RetryModel, RetryResultAttempt, Retryable, RetryableModelOptions };
69
+ export { LanguageModelV2Generate as a, Retry as c, RetryErrorAttempt as d, RetryResultAttempt as f, RetryableOptions as h, LanguageModelV2$1 as i, RetryAttempt as l, RetryableModelOptions as m, EmbeddingModelV2CallOptions as n, LanguageModelV2Stream as o, Retryable as p, EmbeddingModelV2Embed as r, Retries as s, EmbeddingModelV2$1 as t, RetryContext as u };
@@ -1,15 +1,9 @@
1
- //#region src/get-model-key.ts
2
- /**
3
- * Generate a unique key for a LanguageModelV2 instance.
4
- */
5
- const getModelKey = (model) => {
6
- return `${model.provider}/${model.modelId}`;
7
- };
8
-
9
- //#endregion
10
1
  //#region src/utils.ts
11
2
  const isObject = (value) => typeof value === "object" && value !== null;
12
3
  const isString = (value) => typeof value === "string";
4
+ const isModelV2 = (model) => isLanguageModelV2(model) || isEmbeddingModelV2(model);
5
+ const isLanguageModelV2 = (model) => isObject(model) && "provider" in model && "modelId" in model && "specificationVersion" in model && model.specificationVersion === "v2";
6
+ const isEmbeddingModelV2 = (model) => isObject(model) && "provider" in model && "modelId" in model && "specificationVersion" in model && model.specificationVersion === "v2";
13
7
  const isStreamResult = (result) => "stream" in result;
14
8
  const isGenerateResult = (result) => "content" in result;
15
9
  /**
@@ -34,4 +28,4 @@ const isStreamContentPart = (part) => {
34
28
  };
35
29
 
36
30
  //#endregion
37
- export { getModelKey, isErrorAttempt, isGenerateResult, isObject, isResultAttempt, isStreamContentPart, isStreamResult, isString };
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-retry",
3
- "version": "0.4.1",
3
+ "version": "0.5.1",
4
4
  "description": "AI SDK Retry",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -35,6 +35,7 @@
35
35
  "devDependencies": {
36
36
  "@ai-sdk/anthropic": "^2.0.18",
37
37
  "@ai-sdk/azure": "^2.0.30",
38
+ "@ai-sdk/groq": "^2.0.24",
38
39
  "@ai-sdk/openai": "^2.0.30",
39
40
  "@arethetypeswrong/cli": "^0.18.2",
40
41
  "@biomejs/biome": "^2.2.4",
@@ -45,7 +46,7 @@
45
46
  "msw": "^2.11.2",
46
47
  "pkg-pr-new": "^0.0.60",
47
48
  "publint": "^0.3.13",
48
- "tsdown": "^0.15.2",
49
+ "tsdown": "^0.15.9",
49
50
  "tsx": "^4.20.5",
50
51
  "typescript": "^5.9.2",
51
52
  "unbuild": "^3.6.1",