ai-retry 1.1.0 → 1.2.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
@@ -757,6 +757,46 @@ const retryableModel = createRetryable({
757
757
  });
758
758
  ```
759
759
 
760
+ #### Reset
761
+
762
+ By default, every new request starts with the base model, even if a previous request was retried with a different model. The `reset` option changes this behavior by making the last successfully retried model **sticky**, that means subsequent requests will continue using that model instead of switching back to the base model. The reset value controls how long the retry model stays sticky before resetting back to the base model.
763
+
764
+ | Value | Description |
765
+ |-------|-------------|
766
+ | `'after-request'` | Reset immediately after the next request (default) |
767
+ | `` `after-${N}-requests` `` | Keep the retry model for the next **N** requests, then reset |
768
+ | `` `after-${N}-seconds` `` | Keep the retry model for **N** seconds, then reset |
769
+
770
+ **Reset after each request (default)**
771
+
772
+ ```typescript
773
+ const retryableModel = createRetryable({
774
+ model: openai('gpt-4o-mini'),
775
+ retries: [anthropic('claude-sonnet-4-20250514')],
776
+ reset: 'after-request', // default — always start with the base model
777
+ });
778
+ ```
779
+
780
+ **Keep the retry model for N requests**
781
+
782
+ ```typescript
783
+ const retryableModel = createRetryable({
784
+ model: openai('gpt-4o-mini'),
785
+ retries: [anthropic('claude-sonnet-4-20250514')],
786
+ reset: 'after-5-requests', // use the retry model for 5 more requests before resetting
787
+ });
788
+ ```
789
+
790
+ **Keep the retry model for N seconds**
791
+
792
+ ```typescript
793
+ const retryableModel = createRetryable({
794
+ model: openai('gpt-4o-mini'),
795
+ retries: [anthropic('claude-sonnet-4-20250514')],
796
+ reset: 'after-30-seconds', // use the retry model for 30 seconds before resetting
797
+ });
798
+ ```
799
+
760
800
  ### Streaming
761
801
 
762
802
  Errors during streaming requests can occur in two ways:
@@ -777,6 +817,7 @@ interface RetryableModelOptions<MODEL extends LanguageModelV2 | EmbeddingModelV2
777
817
  model: MODEL;
778
818
  retries: Array<Retryable<MODEL> | MODEL>;
779
819
  disabled?: boolean | (() => boolean);
820
+ reset?: Reset;
780
821
  onError?: (context: RetryContext<MODEL>) => void;
781
822
  onRetry?: (context: RetryContext<MODEL>) => void;
782
823
  }
@@ -786,9 +827,25 @@ interface RetryableModelOptions<MODEL extends LanguageModelV2 | EmbeddingModelV2
786
827
  - `model`: The base model to use for the initial request.
787
828
  - `retries`: Array of retryables (functions, models, or retry objects) to attempt on failure.
788
829
  - `disabled`: Disable all retry logic. Can be a boolean or function returning boolean. Default: `false` (retries enabled).
830
+ - `reset`: Controls when to reset back to the base model after a successful retry. See [Reset](#reset) for details. Default: `'after-request'`.
789
831
  - `onError`: Callback invoked when an error occurs.
790
832
  - `onRetry`: Callback invoked before attempting a retry.
791
833
 
834
+ #### `Reset`
835
+
836
+ Controls when the sticky model resets back to the base model after a successful retry.
837
+
838
+ ```ts
839
+ type Reset =
840
+ | 'after-request'
841
+ | `after-${number}-requests`
842
+ | `after-${number}-seconds`;
843
+ ```
844
+
845
+ - `'after-request'` — reset immediately after the next request (default).
846
+ - `` `after-${N}-requests` `` — keep the retry model for the next N requests, then reset.
847
+ - `` `after-${N}-seconds` `` — keep the retry model for N seconds, then reset.
848
+
792
849
  #### `Retryable`
793
850
 
794
851
  A `Retryable` is a function that receives a `RetryContext` with the current error or result and model and all previous attempts.
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { C as RetryableModelOptions, S as Retryable, _ as Retry, a as GatewayLanguageModelId, b as RetryErrorAttempt, c as LanguageModelGenerate, d as LanguageModelStreamPart, f as ProviderOptions, g as Retries, h as ResolvedModel, i as EmbeddingModelRetryCallOptions, l as LanguageModelRetryCallOptions, m as ResolvableModel, n as EmbeddingModelCallOptions, o as LanguageModel, p as ResolvableLanguageModel, r as EmbeddingModelEmbed, s as LanguageModelCallOptions, t as EmbeddingModel, u as LanguageModelStream, v as RetryAttempt, w as RetryableOptions, x as RetryResultAttempt, y as RetryContext } from "./types-Bty5BU37.mjs";
1
+ import { C as Retryable, S as RetryResultAttempt, T as RetryableOptions, _ as Retries, a as GatewayLanguageModelId, b as RetryContext, c as LanguageModelGenerate, d as LanguageModelStreamPart, f as ProviderOptions, g as ResolvedModel, h as ResolvableModel, i as EmbeddingModelRetryCallOptions, l as LanguageModelRetryCallOptions, m as ResolvableLanguageModel, n as EmbeddingModelCallOptions, o as LanguageModel, p as Reset, r as EmbeddingModelEmbed, s as LanguageModelCallOptions, t as EmbeddingModel, u as LanguageModelStream, v as Retry, w as RetryableModelOptions, x as RetryErrorAttempt, y as RetryAttempt } from "./types-Dk5KMZMd.mjs";
2
2
  import * as _ai_sdk_provider0 from "@ai-sdk/provider";
3
3
 
4
4
  //#region src/create-retryable-model.d.ts
@@ -73,4 +73,4 @@ declare const isAbortError: (error: unknown) => boolean;
73
73
  */
74
74
  declare const isTimeoutError: (error: unknown) => boolean;
75
75
  //#endregion
76
- export { EmbeddingModel, EmbeddingModelCallOptions, EmbeddingModelEmbed, EmbeddingModelRetryCallOptions, GatewayLanguageModelId, LanguageModel, LanguageModelCallOptions, LanguageModelGenerate, LanguageModelRetryCallOptions, LanguageModelStream, LanguageModelStreamPart, ProviderOptions, ResolvableLanguageModel, ResolvableModel, ResolvedModel, Retries, Retry, RetryAttempt, RetryContext, RetryErrorAttempt, RetryResultAttempt, Retryable, RetryableModelOptions, RetryableOptions, createRetryable, getModelKey, isAbortError, isEmbeddingModel, isErrorAttempt, isGenerateResult, isLanguageModel, isModel, isObject, isResultAttempt, isStreamContentPart, isStreamResult, isString, isTimeoutError };
76
+ export { EmbeddingModel, EmbeddingModelCallOptions, EmbeddingModelEmbed, EmbeddingModelRetryCallOptions, GatewayLanguageModelId, LanguageModel, LanguageModelCallOptions, LanguageModelGenerate, LanguageModelRetryCallOptions, LanguageModelStream, LanguageModelStreamPart, ProviderOptions, Reset, ResolvableLanguageModel, ResolvableModel, ResolvedModel, Retries, Retry, RetryAttempt, RetryContext, RetryErrorAttempt, RetryResultAttempt, Retryable, RetryableModelOptions, RetryableOptions, createRetryable, getModelKey, isAbortError, isEmbeddingModel, isErrorAttempt, isGenerateResult, isLanguageModel, isModel, isObject, isResultAttempt, isStreamContentPart, isStreamResult, isString, isTimeoutError };
package/dist/index.mjs CHANGED
@@ -3,6 +3,86 @@ import { RetryError, gateway } from "ai";
3
3
  import { delay } from "@ai-sdk/provider-utils";
4
4
  import { getErrorMessage } from "@ai-sdk/provider";
5
5
 
6
+ //#region src/parse-reset.ts
7
+ /**
8
+ * Parses a `Reset` string into a structured object.
9
+ *
10
+ * `'after-request'` is treated as `{ type: 'requests', count: 0 }`,
11
+ * meaning the sticky model expires immediately (default behavior).
12
+ *
13
+ * @example
14
+ * parseReset(`after-request`); // { type: 'requests', count: 0 }
15
+ * parseReset(`after-5-requests`); // { type: 'requests', count: 5 }
16
+ * parseReset(`after-30-seconds`); // { type: 'seconds', count: 30 }
17
+ */
18
+ function parseReset(reset) {
19
+ if (reset === `after-request`) return {
20
+ type: `requests`,
21
+ count: 0
22
+ };
23
+ const requestsMatch = reset.match(/^after-(\d+)-requests$/);
24
+ if (requestsMatch) return {
25
+ type: `requests`,
26
+ count: Number.parseInt(requestsMatch[1], 10)
27
+ };
28
+ const secondsMatch = reset.match(/^after-(\d+)-seconds$/);
29
+ if (secondsMatch) return {
30
+ type: `seconds`,
31
+ count: Number.parseInt(secondsMatch[1], 10)
32
+ };
33
+ throw new Error(`Invalid reset option: ${reset}`);
34
+ }
35
+
36
+ //#endregion
37
+ //#region src/base-retryable-model.ts
38
+ var BaseRetryableModel = class {
39
+ baseModel;
40
+ currentModel;
41
+ options;
42
+ parsedReset;
43
+ /** The model that last succeeded via retry, used for subsequent requests. */
44
+ stickyState;
45
+ constructor(options) {
46
+ this.options = options;
47
+ this.baseModel = options.model;
48
+ this.currentModel = options.model;
49
+ this.parsedReset = parseReset(options.reset ?? `after-request`);
50
+ }
51
+ /**
52
+ * Determine which model to start the request with,
53
+ * considering the sticky model and reset policy.
54
+ */
55
+ resolveStartModel() {
56
+ if (!this.stickyState) return this.baseModel;
57
+ if (this.parsedReset.type === `requests`) {
58
+ if (this.stickyState.requestsRemaining > 0) {
59
+ this.stickyState.requestsRemaining--;
60
+ return this.stickyState.model;
61
+ }
62
+ } else if (Date.now() - this.stickyState.setAt < this.parsedReset.count * 1e3) return this.stickyState.model;
63
+ this.stickyState = void 0;
64
+ return this.baseModel;
65
+ }
66
+ /**
67
+ * After a successful request, update sticky model if a retry occurred.
68
+ */
69
+ updateStickyModel(startModel) {
70
+ if (this.currentModel !== startModel) this.stickyState = {
71
+ model: this.currentModel,
72
+ setAt: Date.now(),
73
+ requestsRemaining: this.parsedReset.type === `requests` ? this.parsedReset.count : 0
74
+ };
75
+ }
76
+ /**
77
+ * Check if retries are disabled
78
+ */
79
+ isDisabled() {
80
+ if (this.options.disabled === void 0) return false;
81
+ return typeof this.options.disabled === `function` ? this.options.disabled() : this.options.disabled;
82
+ }
83
+ };
84
+
85
+ //#endregion
6
86
  //#region src/calculate-exponential-backoff.ts
7
87
  /**
8
88
  * Calculates the exponential backoff delay.
@@ -100,11 +180,8 @@ function prepareRetryError(error, attempts) {
100
180
 
101
181
  //#endregion
102
182
  //#region src/retryable-embedding-model.ts
103
- var RetryableEmbeddingModel = class {
183
+ var RetryableEmbeddingModel = class extends BaseRetryableModel {
104
184
  specificationVersion = "v3";
105
- baseModel;
106
- currentModel;
107
- options;
108
185
  get modelId() {
109
186
  return this.currentModel.modelId;
110
187
  }
@@ -117,18 +194,6 @@ var RetryableEmbeddingModel = class {
117
194
  get supportsParallelCalls() {
118
195
  return this.currentModel.supportsParallelCalls;
119
196
  }
120
- constructor(options) {
121
- this.options = options;
122
- this.baseModel = options.model;
123
- this.currentModel = options.model;
124
- }
125
- /**
126
- * Check if retries are disabled
127
- */
128
- isDisabled() {
129
- if (this.options.disabled === void 0) return false;
130
- return typeof this.options.disabled === "function" ? this.options.disabled() : this.options.disabled;
131
- }
132
197
  /**
133
198
  * Get the retry call options overrides from a retry configuration.
134
199
  */
@@ -238,9 +303,10 @@ var RetryableEmbeddingModel = class {
238
303
  }
239
304
  async doEmbed(callOptions) {
240
305
  /**
241
- * Always start with the original model
306
+ * Resolve the starting model (base or sticky)
242
307
  */
243
- this.currentModel = this.baseModel;
308
+ const startModel = this.resolveStartModel();
309
+ this.currentModel = startModel;
244
310
  /**
245
311
  * If retries are disabled, bypass retry machinery entirely
246
312
  */
@@ -251,17 +317,15 @@ var RetryableEmbeddingModel = class {
251
317
  },
252
318
  callOptions
253
319
  });
320
+ this.updateStickyModel(startModel);
254
321
  return result;
255
322
  }
256
323
  };
257
324
 
258
325
  //#endregion
259
326
  //#region src/retryable-language-model.ts
260
- var RetryableLanguageModel = class {
327
+ var RetryableLanguageModel = class extends BaseRetryableModel {
261
328
  specificationVersion = "v3";
262
- baseModel;
263
- currentModel;
264
- options;
265
329
  get modelId() {
266
330
  return this.currentModel.modelId;
267
331
  }
@@ -271,18 +335,6 @@ var RetryableLanguageModel = class {
271
335
  get supportedUrls() {
272
336
  return this.currentModel.supportedUrls;
273
337
  }
274
- constructor(options) {
275
- this.options = options;
276
- this.baseModel = options.model;
277
- this.currentModel = options.model;
278
- }
279
- /**
280
- * Check if retries are disabled
281
- */
282
- isDisabled() {
283
- if (this.options.disabled === void 0) return false;
284
- return typeof this.options.disabled === "function" ? this.options.disabled() : this.options.disabled;
285
- }
286
338
  /**
287
339
  * Get the retry call options overrides from a retry configuration.
288
340
  */
@@ -446,9 +498,10 @@ var RetryableLanguageModel = class {
446
498
  }
447
499
  async doGenerate(callOptions) {
448
500
  /**
449
- * Always start with the original model
501
+ * Resolve the starting model (base or sticky)
450
502
  */
451
- this.currentModel = this.baseModel;
503
+ const startModel = this.resolveStartModel();
504
+ this.currentModel = startModel;
452
505
  /**
453
506
  * If retries are disabled, bypass retry machinery entirely
454
507
  */
@@ -459,13 +512,15 @@ var RetryableLanguageModel = class {
459
512
  },
460
513
  callOptions
461
514
  });
515
+ this.updateStickyModel(startModel);
462
516
  return result;
463
517
  }
464
518
  async doStream(callOptions) {
465
519
  /**
466
- * Always start with the original model
520
+ * Resolve the starting model (base or sticky)
467
521
  */
468
- this.currentModel = this.baseModel;
522
+ const startModel = this.resolveStartModel();
523
+ this.currentModel = startModel;
469
524
  /**
470
525
  * If retries are disabled, bypass retry machinery entirely
471
526
  */
@@ -479,6 +534,7 @@ var RetryableLanguageModel = class {
479
534
  },
480
535
  callOptions
481
536
  });
537
+ this.updateStickyModel(startModel);
482
538
  /**
483
539
  * Track the current retry model for computing call options in the stream handler
484
540
  */
@@ -554,6 +610,7 @@ var RetryableLanguageModel = class {
554
610
  await reader?.cancel();
555
611
  result = retriedResult.result;
556
612
  attempts = retriedResult.attempts;
613
+ this.updateStickyModel(startModel);
557
614
  } finally {
558
615
  reader?.releaseLock();
559
616
  }
@@ -1,4 +1,4 @@
1
- import { S as Retryable, p as ResolvableLanguageModel, t as EmbeddingModel, w as RetryableOptions } from "../types-Bty5BU37.mjs";
1
+ import { C as Retryable, T as RetryableOptions, m as ResolvableLanguageModel, t as EmbeddingModel } from "../types-Dk5KMZMd.mjs";
2
2
 
3
3
  //#region src/retryables/content-filter-triggered.d.ts
4
4
  /**
@@ -70,6 +70,12 @@ interface RetryableModelOptions<MODEL extends LanguageModel | EmbeddingModel> {
70
70
  model: MODEL;
71
71
  retries: Retries<MODEL>;
72
72
  disabled?: boolean | (() => boolean);
73
+ /**
74
+ * Controls when to reset back to the base model after a successful retry.
75
+ *
76
+ * @default 'after-request'
77
+ */
78
+ reset?: Reset;
73
79
  onError?: (context: RetryContext<MODEL>) => void;
74
80
  onRetry?: (context: RetryContext<MODEL>) => void;
75
81
  }
@@ -121,9 +127,17 @@ type Retry<MODEL extends ResolvableLanguageModel | EmbeddingModel> = {
121
127
  type Retryable<MODEL extends ResolvableLanguageModel | EmbeddingModel> = (context: RetryContext<MODEL>) => Retry<MODEL> | Promise<Retry<MODEL> | undefined> | undefined;
122
128
  type Retries<MODEL extends LanguageModel | EmbeddingModel> = Array<Retryable<ResolvableModel<MODEL>> | Retry<ResolvableModel<MODEL>> | ResolvableModel<MODEL>>;
123
129
  type RetryableOptions<MODEL extends ResolvableLanguageModel | EmbeddingModel> = Partial<Omit<Retry<MODEL>, 'model'>>;
130
+ /**
131
+ * Controls when to reset the sticky model back to the base model.
132
+ *
133
+ * - `'after-request'` — reset after each request (default, current behavior)
134
+ * - `` `after-${number}-requests` `` — use the retry model for the next N requests
135
+ * - `` `after-${number}-seconds` `` — use the retry model for the next N seconds
136
+ */
137
+ type Reset = 'after-request' | `after-${number}-requests` | `after-${number}-seconds`;
124
138
  type LanguageModelGenerate = Awaited<ReturnType<LanguageModel['doGenerate']>>;
125
139
  type LanguageModelStream = Awaited<ReturnType<LanguageModel['doStream']>>;
126
140
  type EmbeddingModelCallOptions = Parameters<EmbeddingModel['doEmbed']>[0];
127
141
  type EmbeddingModelEmbed = Awaited<ReturnType<EmbeddingModel['doEmbed']>>;
128
142
  //#endregion
129
- export { RetryableModelOptions as C, Retryable as S, Retry as _, GatewayLanguageModelId as a, RetryErrorAttempt as b, LanguageModelGenerate as c, LanguageModelStreamPart as d, ProviderOptions as f, Retries as g, ResolvedModel as h, EmbeddingModelRetryCallOptions as i, LanguageModelRetryCallOptions as l, ResolvableModel as m, EmbeddingModelCallOptions as n, LanguageModel as o, ResolvableLanguageModel as p, EmbeddingModelEmbed as r, LanguageModelCallOptions as s, EmbeddingModel as t, LanguageModelStream as u, RetryAttempt as v, RetryableOptions as w, RetryResultAttempt as x, RetryContext as y };
143
+ export { Retryable as C, RetryResultAttempt as S, RetryableOptions as T, Retries as _, GatewayLanguageModelId as a, RetryContext as b, LanguageModelGenerate as c, LanguageModelStreamPart as d, ProviderOptions as f, ResolvedModel as g, ResolvableModel as h, EmbeddingModelRetryCallOptions as i, LanguageModelRetryCallOptions as l, ResolvableLanguageModel as m, EmbeddingModelCallOptions as n, LanguageModel as o, Reset as p, EmbeddingModelEmbed as r, LanguageModelCallOptions as s, EmbeddingModel as t, LanguageModelStream as u, Retry as v, RetryableModelOptions as w, RetryErrorAttempt as x, RetryAttempt as y };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-retry",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "AI SDK Retry",
5
5
  "types": "./dist/index.d.mts",
6
6
  "type": "module",