ai-retry 0.0.1-alpha.3 → 0.0.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
@@ -1,20 +1,22 @@
1
- ### ai-retry
1
+ # ai-retry: Retry and fallback mechanisms for AI SDK
2
2
 
3
- Intelligent retry and fallback mechanisms for AI SDK models. Automatically handle API failures, content filtering, timeouts, and schema mismatches by switching between different AI models.
3
+ Automatically handle API failures, content filtering and timeouts by switching between different AI models.
4
4
 
5
- #### How?
5
+ `ai-retry` wraps the provided base model with a set of retry conditions (retryables). When a request fails with an error or the response is not satisfying, it iterates through the given retryables to find a suitable fallback model. It automatically tracks which models have been tried and how many attempts have been made to prevent infinite loops.
6
6
 
7
- `ai-retry` wraps the provided base model with a set of retry conditions (retryables). When a request fails due to specific errors (like content filtering or timeouts), it iterates through the given retryables to find a suitable fallback model. It automatically tracks which models have been tried and how many attempts have been made to prevent infinite loops.
7
+ It supports two types of retries:
8
+ - Error-based retries: when the model throws an error (e.g. timeouts, API errors, etc.)
9
+ - Result-based retries: when the model returns a successful response that needs retrying (e.g. content filtering, etc.)
8
10
 
9
11
  ### Installation
10
12
 
11
13
  This library only supports AI SDK v5.
12
14
 
13
15
  > [!WARNING]
14
- > `ai-retry` is in alpha stage and the API may change in future releases.
16
+ > `ai-retry` is in an early stage and the API may change in future releases.
15
17
 
16
18
  ```bash
17
- npm install ai-retry@alpha
19
+ npm install ai-retry
18
20
  ```
19
21
 
20
22
  ### Usage
@@ -34,13 +36,9 @@ import { contentFilterTriggered, requestTimeout } from 'ai-retry/retryables';
34
36
 
35
37
  // Create a retryable model
36
38
  const retryableModel = createRetryable({
37
- // Base model
38
- model: azure('gpt-4-mini'),
39
- // Retry strategies
39
+ model: azure('gpt-4-mini'), // Base model
40
40
  retries: [
41
- contentFilterTriggered(openai('gpt-4-mini')),
42
- requestTimeout(azure('gpt-4')),
43
- openai('gpt-4-mini'),
41
+ // Retry strategies and fallbacks...
44
42
  ],
45
43
  });
46
44
 
@@ -48,23 +46,11 @@ const retryableModel = createRetryable({
48
46
  const result = await generateText({
49
47
  model: retryableModel,
50
48
  prompt: 'Hello world!',
51
- abortSignal: AbortSignal.timeout(10_000),
52
49
  });
53
50
  ```
54
51
 
55
- #### Retryables
56
52
 
57
- A retryable is a function that receives the current failed attempt and determines whether to retry with a different model based on the error and any previous attempts.
58
- There are several built-in retryables:
59
-
60
- - `contentFilterTriggered`: Content filter was triggered based on the prompt or completion.
61
- - `responseSchemaMismatch`: Structured output validation failed.
62
- - `requestTimeout`: Request timeout occurred.
63
- - `requestNotRetryable`: Request failed with a non-retryable error.
64
-
65
- By default, each retryable will only attempt to retry once per model to avoid infinite loops. You can customize this behavior by returning a `maxAttempts` value from your retryable function.
66
-
67
- ##### Content Filter Triggered
53
+ #### Content Filter
68
54
 
69
55
  Automatically switch to a different model when content filtering blocks your request.
70
56
 
@@ -79,6 +65,7 @@ const retryableModel = createRetryable({
79
65
  });
80
66
  ```
81
67
 
68
+ <!--
82
69
  ##### Response Schema Mismatch
83
70
 
84
71
  Retry with different models when structured output validation fails:
@@ -105,13 +92,14 @@ const result = await generateObject({
105
92
  prompt: 'Generate a lasagna recipe.',
106
93
  });
107
94
  ```
95
+ -->
108
96
 
109
- ##### Request Timeout
97
+ #### Request Timeout
110
98
 
111
99
  Handle timeouts by switching to potentially faster models.
112
100
 
113
101
  > [!NOTE]
114
- > You need to set an `abortSignal` with a timeout on your request for this to work.
102
+ > You need to use an `abortSignal` with a timeout on your request.
115
103
 
116
104
  ```typescript
117
105
  import { requestTimeout } from 'ai-retry/retryables';
@@ -126,62 +114,70 @@ const retryableModel = createRetryable({
126
114
  const result = await generateText({
127
115
  model: retryableModel,
128
116
  prompt: 'Write a vegetarian lasagna recipe for 4 people.',
129
- abortSignal: AbortSignal.timeout(10_000),
117
+ abortSignal: AbortSignal.timeout(60_000),
130
118
  });
131
119
  ```
132
120
 
133
- ##### Request Not Retryable
121
+ #### Request Not Retryable
134
122
 
135
123
  Handle cases where the base model fails with a non-retryable error.
136
124
 
125
+ > [!NOTE]
126
+ > You can check if an error is retryable with the `isRetryable` property on an [`APICallError`](https://ai-sdk.dev/docs/reference/ai-sdk-errors/ai-api-call-error#ai_apicallerror).
127
+
128
+
137
129
  ```typescript
138
130
  import { requestNotRetryable } from 'ai-retry/retryables';
139
131
 
140
132
  const retryable = createRetryable({
141
133
  model: azure('gpt-4-mini'),
142
134
  retries: [
143
- requestNotRetryable(openai('gpt-4')), // Switch providers entirely
135
+ requestNotRetryable(openai('gpt-4')), // Switch provider if error is not retryable
144
136
  ],
145
137
  });
146
138
  ```
147
139
 
148
- ##### Custom Retryables
140
+ #### Fallbacks
149
141
 
150
- Create your own retryables for specific use cases:
142
+ If you always want to fallback to a different model on any error, you can simply provide a list of models.
151
143
 
152
144
  ```typescript
153
- import type { Retryable } from 'ai-retry';
154
-
155
- const customRetry: Retryable = (context) => {
156
- const { current, attempts, totalAttempts } = context;
157
-
158
- // Your custom logic here
159
- if (shouldRetryWithDifferentModel(current.error)) {
160
- return {
161
- model: myFallbackModel,
162
- maxAttempts: 3,
163
- };
164
- }
165
-
166
- return undefined; // Don't retry
167
- };
168
-
169
- const retryable = createRetryable({
170
- model: azure('gpt-4-mini'),
171
- retries: [customRetry],
145
+ const retryableModel = createRetryable({
146
+ model: azure('gpt-4'),
147
+ retries: [
148
+ openai('gpt-4'),
149
+ anthropic('claude-3-haiku-20240307')
150
+ ],
172
151
  });
173
152
  ```
174
153
 
175
- #### Default Fallback
154
+ #### Custom
176
155
 
177
- If you always want to fallback to a different model on any error, you can simply provide a list of models.
156
+ Create your own retryables for specific use cases:
178
157
 
179
158
  ```typescript
159
+ import { anthropic } from '@ai-sdk/anthropic';
160
+ import { openai } from '@ai-sdk/openai';
161
+ import { APICallError } from 'ai';
162
+ import { createRetryable, isErrorAttempt } from 'ai-retry';
163
+ import type { Retryable } from 'ai-retry';
164
+
165
+ const rateLimitRetry: Retryable = (context) => {
166
+ if (isErrorAttempt(context.current)) {
167
+ const { error } = context.current;
168
+
169
+ if (APICallError.isInstance(error) && error.statusCode === 429) {
170
+ return { model: anthropic('claude-3-haiku-20240307') };
171
+ }
172
+ }
173
+
174
+ return undefined;
175
+ };
176
+
180
177
  const retryableModel = createRetryable({
181
- model: azure('gpt-4'),
178
+ model: openai('gpt-4'),
182
179
  retries: [
183
- openai('gpt-4'),
184
- anthropic('claude-3-haiku-20240307')
180
+ rateLimitRetry
185
181
  ],
186
182
  });
187
183
  ```
@@ -236,6 +232,17 @@ const retryableModel = createRetryable({
236
232
  });
237
233
  ```
238
234
 
235
+ ### Retryables
236
+
237
+ 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.
238
+ There are several built-in retryables:
239
+
240
+ - [`contentFilterTriggered`](./src/retryables/content-filter-triggered.ts): Content filter was triggered based on the prompt or completion.
241
+ - [`requestTimeout`](./src/retryables/request-timeout.ts): Request timeout occurred.
242
+ - [`requestNotRetryable`](./src/retryables/request-not-retryable.ts): Request failed with a non-retryable error.
243
+
244
+ By default, each retryable will only attempt to retry once per model to avoid infinite loops. You can customize this behavior by returning a `maxAttempts` value from your retryable function.
245
+
239
246
  ### API Reference
240
247
 
241
248
  #### `createRetryable(options: CreateRetryableOptions): LanguageModelV2`
@@ -247,14 +254,14 @@ interface CreateRetryableOptions {
247
254
  model: LanguageModelV2;
248
255
  retries: Array<Retryable | LanguageModelV2>;
249
256
  onError?: (context: RetryContext) => void;
250
- onRetry?: (context: RetryContext) => void;
257
+ onRetry?: (context: RetryContext) => void;
251
258
  }
252
259
  ```
253
260
 
254
261
  #### `Retryable`
255
262
 
256
- A `Retryable` is a function that receives a `RetryContext` with the current error and model and all previous attempts.
257
- It should evaluate the error and decide whether to retry by returning a `RetryModel` or to skip by returning `undefined`.
263
+ A `Retryable` is a function that receives a `RetryContext` with the current error or result and model and all previous attempts.
264
+ It should evaluate the error/result and decide whether to retry by returning a `RetryModel` or to skip by returning `undefined`.
258
265
 
259
266
  ```ts
260
267
  type Retryable = (context: RetryContext) => RetryModel | Promise<RetryModel> | undefined;
@@ -286,13 +293,16 @@ interface RetryContext {
286
293
 
287
294
  #### `RetryAttempt`
288
295
 
289
- A `RetryAttempt` represents a single failed attempt with a specific model.
296
+ A `RetryAttempt` represents a single attempt with a specific model, which can be either an error or a successful result that triggered a retry.
290
297
 
291
298
  ```typescript
292
- interface RetryAttempt {
293
- error: unknown;
294
- model: LanguageModelV2;
295
- }
299
+ type RetryAttempt =
300
+ | { type: 'error'; error: unknown; model: LanguageModelV2 }
301
+ | { type: 'result'; result: LanguageModelV2Generate; model: LanguageModelV2 };
302
+
303
+ // Type guards for discriminating attempts
304
+ function isErrorAttempt(attempt: RetryAttempt): attempt is RetryErrorAttempt;
305
+ function isResultAttempt(attempt: RetryAttempt): attempt is RetryResultAttempt;
296
306
  ```
297
307
 
298
308
  ### License
@@ -0,0 +1,221 @@
1
+ import { getErrorMessage } from "@ai-sdk/provider-utils";
2
+ import { RetryError } from "ai";
3
+
4
+ //#region src/get-model-key.ts
5
+ /**
6
+ * Generate a unique key for a LanguageModelV2 instance.
7
+ */
8
+ const getModelKey = (model) => {
9
+ return `${model.provider}/${model.modelId}`;
10
+ };
11
+
12
+ //#endregion
13
+ //#region src/create-retryable-model.ts
14
+ /**
15
+ * Type guard to check if a retry attempt is an error attempt
16
+ */
17
+ function isErrorAttempt(attempt) {
18
+ return attempt.type === "error";
19
+ }
20
+ /**
21
+ * Type guard to check if a retry attempt is a result attempt
22
+ */
23
+ function isResultAttempt(attempt) {
24
+ return attempt.type === "result";
25
+ }
26
+ var RetryableModel = class {
27
+ specificationVersion = "v2";
28
+ baseModel;
29
+ currentModel;
30
+ options;
31
+ get modelId() {
32
+ return this.currentModel.modelId;
33
+ }
34
+ get provider() {
35
+ return this.currentModel.provider;
36
+ }
37
+ get supportedUrls() {
38
+ return this.currentModel.supportedUrls;
39
+ }
40
+ constructor(options) {
41
+ this.options = options;
42
+ this.baseModel = options.model;
43
+ this.currentModel = options.model;
44
+ }
45
+ /**
46
+ * Find the next model to retry with based on the retry context
47
+ */
48
+ async findNextModel(context) {
49
+ /**
50
+ * Filter retryables based on attempt type:
51
+ * - Result-based attempts: Only consider function retryables (skip plain models)
52
+ * - Error-based attempts: Consider all retryables (functions + plain models)
53
+ */
54
+ const applicableRetries = isResultAttempt(context.current) ? this.options.retries.filter((retry) => typeof retry === "function") : this.options.retries;
55
+ /**
56
+ * Iterate through the applicable retryables to find a model to retry with
57
+ */
58
+ for (const retry of applicableRetries) {
59
+ const retryModel = typeof retry === "function" ? await retry(context) : {
60
+ model: retry,
61
+ maxAttempts: 1
62
+ };
63
+ if (retryModel) {
64
+ /**
65
+ * The model key uniquely identifies a model instance (provider + modelId)
66
+ */
67
+ const retryModelKey = getModelKey(retryModel.model);
68
+ /**
69
+ * Find all attempts with the same model
70
+ */
71
+ const retryAttempts = context.attempts.filter((a) => getModelKey(a.model) === retryModelKey);
72
+ const maxAttempts = retryModel.maxAttempts ?? 1;
73
+ /**
74
+ * Check if the model can still be retried based on maxAttempts
75
+ */
76
+ if (retryAttempts.length < maxAttempts) return retryModel.model;
77
+ }
78
+ }
79
+ }
80
+ async doGenerate(options) {
81
+ /**
82
+ * Always start with the original model
83
+ */
84
+ this.currentModel = this.baseModel;
85
+ /**
86
+ * Track number of attempts
87
+ */
88
+ let totalAttempts = 0;
89
+ /**
90
+ * Track all attempts.
91
+ */
92
+ const attempts = [];
93
+ /**
94
+ * The previous attempt that triggered a retry, or undefined if this is the first attempt
95
+ */
96
+ let previousAttempt;
97
+ while (true) {
98
+ /**
99
+ * Call the onRetry handler if provided.
100
+ * Skip on the first attempt since no previous attempt exists yet.
101
+ */
102
+ if (previousAttempt) {
103
+ /**
104
+ * Context for the onRetry handler
105
+ */
106
+ const context = {
107
+ current: {
108
+ ...previousAttempt,
109
+ model: this.currentModel
110
+ },
111
+ attempts,
112
+ totalAttempts
113
+ };
114
+ /**
115
+ * Call the onRetry handler if provided
116
+ */
117
+ this.options.onRetry?.(context);
118
+ }
119
+ totalAttempts++;
120
+ try {
121
+ const result = await this.currentModel.doGenerate(options);
122
+ /**
123
+ * Check if the result should trigger a retry
124
+ */
125
+ const resultAttempt = {
126
+ type: "result",
127
+ result,
128
+ model: this.currentModel
129
+ };
130
+ /**
131
+ * Add the current attempt to the list before checking for retries
132
+ */
133
+ attempts.push(resultAttempt);
134
+ const resultContext = {
135
+ current: resultAttempt,
136
+ attempts,
137
+ totalAttempts
138
+ };
139
+ const nextModel = await this.findNextModel(resultContext);
140
+ if (nextModel) {
141
+ /**
142
+ * Set the model for the next attempt
143
+ */
144
+ this.currentModel = nextModel;
145
+ /**
146
+ * Set the previous attempt that triggered this retry
147
+ */
148
+ previousAttempt = resultAttempt;
149
+ /**
150
+ * Continue to the next iteration to retry
151
+ */
152
+ continue;
153
+ }
154
+ /**
155
+ * No retry needed, remove the attempt since it was successful and return the result
156
+ */
157
+ attempts.pop();
158
+ return result;
159
+ } catch (error) {
160
+ /**
161
+ * Current attempt with current error
162
+ */
163
+ const errorAttempt = {
164
+ type: "error",
165
+ error,
166
+ model: this.currentModel
167
+ };
168
+ /**
169
+ * Save the current attempt
170
+ */
171
+ attempts.push(errorAttempt);
172
+ /**
173
+ * Context for the retryables and onError handler
174
+ */
175
+ const context = {
176
+ current: errorAttempt,
177
+ attempts,
178
+ totalAttempts
179
+ };
180
+ /**
181
+ * Call the onError handler if provided
182
+ */
183
+ this.options.onError?.(context);
184
+ const nextModel = await this.findNextModel(context);
185
+ /**
186
+ * Handler didn't return any models to try next, rethrow the error.
187
+ * If we retried the request, wrap the error into a `RetryError` for better visibility.
188
+ */
189
+ if (!nextModel) {
190
+ if (totalAttempts > 1) {
191
+ const errorMessage = getErrorMessage(error);
192
+ const errors = attempts.flatMap((a) => isErrorAttempt(a) ? a.error : `Result with finishReason: ${a.result.finishReason}`);
193
+ throw new RetryError({
194
+ message: `Failed after ${totalAttempts} attempts. Last error: ${errorMessage}`,
195
+ reason: "maxRetriesExceeded",
196
+ errors
197
+ });
198
+ }
199
+ throw error;
200
+ }
201
+ /**
202
+ * Set the model for the next attempt
203
+ */
204
+ this.currentModel = nextModel;
205
+ /**
206
+ * Set the previous attempt that triggered this retry
207
+ */
208
+ previousAttempt = errorAttempt;
209
+ }
210
+ }
211
+ }
212
+ async doStream(options) {
213
+ throw new Error("Streaming not implemented");
214
+ }
215
+ };
216
+ function createRetryable(config) {
217
+ return new RetryableModel(config);
218
+ }
219
+
220
+ //#endregion
221
+ export { createRetryable, getModelKey, isErrorAttempt, isResultAttempt };
@@ -0,0 +1,59 @@
1
+ import { LanguageModelV2 } from "@ai-sdk/provider";
2
+
3
+ //#region src/types.d.ts
4
+ type LanguageModelV2Generate = Awaited<ReturnType<LanguageModelV2['doGenerate']>>;
5
+ //#endregion
6
+ //#region src/create-retryable-model.d.ts
7
+ /**
8
+ * The context provided to Retryables with the current attempt and all previous attempts.
9
+ */
10
+ interface RetryContext<CURRENT extends RetryAttempt = RetryAttempt> {
11
+ current: CURRENT;
12
+ attempts: Array<RetryAttempt>;
13
+ totalAttempts: number;
14
+ }
15
+ type RetryErrorAttempt = {
16
+ type: 'error';
17
+ error: unknown;
18
+ model: LanguageModelV2;
19
+ };
20
+ type RetryResultAttempt = {
21
+ type: 'result';
22
+ result: LanguageModelV2Generate;
23
+ model: LanguageModelV2;
24
+ };
25
+ /**
26
+ * A retry attempt with either an error or a result and the model used
27
+ */
28
+ type RetryAttempt = RetryErrorAttempt | RetryResultAttempt;
29
+ /**
30
+ * Type guard to check if a retry attempt is an error attempt
31
+ */
32
+ declare function isErrorAttempt(attempt: RetryAttempt): attempt is RetryErrorAttempt;
33
+ /**
34
+ * Type guard to check if a retry attempt is a result attempt
35
+ */
36
+ declare function isResultAttempt(attempt: RetryAttempt): attempt is RetryResultAttempt;
37
+ /**
38
+ * A model to retry with and the maximum number of attempts for that model.
39
+ */
40
+ type RetryModel = {
41
+ model: LanguageModelV2;
42
+ maxAttempts?: number;
43
+ };
44
+ /**
45
+ * A function that determines whether to retry with a different model based on the current attempt and all previous attempts.
46
+ */
47
+ type Retryable = (context: RetryContext) => RetryModel | Promise<RetryModel> | undefined;
48
+ /**
49
+ * Options for creating a retryable model.
50
+ */
51
+ interface CreateRetryableOptions {
52
+ model: LanguageModelV2;
53
+ retries: Array<Retryable | LanguageModelV2>;
54
+ onError?: (context: RetryContext<RetryErrorAttempt>) => void;
55
+ onRetry?: (context: RetryContext<RetryErrorAttempt | RetryResultAttempt>) => void;
56
+ }
57
+ declare function createRetryable(config: CreateRetryableOptions): LanguageModelV2;
58
+ //#endregion
59
+ export { CreateRetryableOptions, RetryAttempt, RetryContext, RetryModel, Retryable, createRetryable, isErrorAttempt, isResultAttempt };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { CreateRetryableOptions, RetryAttempt, RetryContext, RetryModel, Retryable, createRetryable } from "./create-retryable-model-DKKgxKLw.js";
1
+ import { CreateRetryableOptions, RetryAttempt, RetryContext, RetryModel, Retryable, createRetryable, isErrorAttempt, isResultAttempt } from "./create-retryable-model-DzDFqgQO.js";
2
2
  import { LanguageModelV2 } from "@ai-sdk/provider";
3
3
 
4
4
  //#region src/get-model-key.d.ts
@@ -7,4 +7,4 @@ import { LanguageModelV2 } from "@ai-sdk/provider";
7
7
  */
8
8
  declare const getModelKey: (model: LanguageModelV2) => string;
9
9
  //#endregion
10
- export { CreateRetryableOptions, RetryAttempt, RetryContext, RetryModel, Retryable, createRetryable, getModelKey };
10
+ export { CreateRetryableOptions, RetryAttempt, RetryContext, RetryModel, Retryable, createRetryable, getModelKey, isErrorAttempt, isResultAttempt };
package/dist/index.js CHANGED
@@ -1,161 +1,3 @@
1
- import { getErrorMessage } from "@ai-sdk/provider-utils";
2
- import { RetryError } from "ai";
1
+ import { createRetryable, getModelKey, isErrorAttempt, isResultAttempt } from "./create-retryable-model-CnrFowSg.js";
3
2
 
4
- //#region src/get-model-key.ts
5
- /**
6
- * Generate a unique key for a LanguageModelV2 instance.
7
- */
8
- const getModelKey = (model) => {
9
- return `${model.provider}/${model.modelId}`;
10
- };
11
-
12
- //#endregion
13
- //#region src/create-retryable-model.ts
14
- function defaultRetryModel(model) {
15
- return {
16
- model,
17
- maxAttempts: 1
18
- };
19
- }
20
- var RetryableModel = class {
21
- specificationVersion = "v2";
22
- baseModel;
23
- currentModel;
24
- options;
25
- get modelId() {
26
- return this.currentModel.modelId;
27
- }
28
- get provider() {
29
- return this.currentModel.provider;
30
- }
31
- get supportedUrls() {
32
- return this.currentModel.supportedUrls;
33
- }
34
- constructor(options) {
35
- this.options = options;
36
- this.baseModel = options.model;
37
- this.currentModel = options.model;
38
- }
39
- async doGenerate(options) {
40
- /**
41
- * Always start with the original model
42
- */
43
- this.currentModel = this.baseModel;
44
- /**
45
- * Track number of attempts
46
- */
47
- let totalAttempts = 0;
48
- /**
49
- * Track all attempts.
50
- */
51
- const attempts = [];
52
- /**
53
- * The error occured in the previous attempt or undefined if this is the first attempt
54
- */
55
- let currentError;
56
- while (true) {
57
- /**
58
- * Call the onRetry handler if provided.
59
- * Skip on the first attempt since no error occured yet.
60
- */
61
- if (currentError) {
62
- /**
63
- * Context for the onRetry handler
64
- */
65
- const context = {
66
- current: {
67
- error: currentError,
68
- model: this.currentModel
69
- },
70
- attempts,
71
- totalAttempts
72
- };
73
- /**
74
- * Call the onRetry handler if provided
75
- */
76
- this.options.onRetry?.(context);
77
- }
78
- try {
79
- totalAttempts++;
80
- return await this.currentModel.doGenerate(options);
81
- } catch (error) {
82
- /**
83
- * Save the error of the current attempt for the retry of the next iteration
84
- */
85
- currentError = error;
86
- /**
87
- * Current attempt with current error
88
- */
89
- const currentAttempt = {
90
- error: currentError,
91
- model: this.currentModel
92
- };
93
- /**
94
- * Save the current attempt
95
- */
96
- attempts.push(currentAttempt);
97
- /**
98
- * Context for the retryables and onError handler
99
- */
100
- const context = {
101
- current: currentAttempt,
102
- attempts,
103
- totalAttempts
104
- };
105
- /**
106
- * Call the onError handler if provided
107
- */
108
- this.options.onError?.(context);
109
- let nextModel;
110
- /**
111
- * Iterate through the retryables to find a model to retry with
112
- */
113
- for (const retry of this.options.retries) {
114
- const retryModel = typeof retry === "function" ? await retry(context) : defaultRetryModel(retry);
115
- if (retryModel) {
116
- /**
117
- * The model key uniquely identifies a model instance (provider + modelId)
118
- */
119
- const retryModelKey = getModelKey(retryModel.model);
120
- /**
121
- * Check if the model can still be retried based on maxAttempts
122
- */
123
- if (attempts.filter((a) => getModelKey(a.model) === retryModelKey).length < retryModel.maxAttempts) {
124
- nextModel = retryModel.model;
125
- break;
126
- }
127
- }
128
- }
129
- /**
130
- * Handler didn't return any models to try next, rethrow the error.
131
- * If we retried the request, wrap the error into a `RetryError` for better visibility.
132
- */
133
- if (!nextModel) {
134
- if (totalAttempts > 1) {
135
- const errorMessage = getErrorMessage(error);
136
- const errors = attempts.flatMap((a) => a.error);
137
- throw new RetryError({
138
- message: `Failed after ${totalAttempts} attempts. Last error: ${errorMessage}`,
139
- reason: "maxRetriesExceeded",
140
- errors
141
- });
142
- }
143
- throw error;
144
- }
145
- /**
146
- * Set the model for the next attempt
147
- */
148
- this.currentModel = nextModel;
149
- }
150
- }
151
- }
152
- async doStream(options) {
153
- throw new Error("Streaming not implemented");
154
- }
155
- };
156
- function createRetryable(config) {
157
- return new RetryableModel(config);
158
- }
159
-
160
- //#endregion
161
- export { createRetryable, getModelKey };
3
+ export { createRetryable, getModelKey, isErrorAttempt, isResultAttempt };
@@ -1,4 +1,4 @@
1
- import { RetryModel, Retryable } from "../create-retryable-model-DKKgxKLw.js";
1
+ import { RetryModel, Retryable } from "../create-retryable-model-DzDFqgQO.js";
2
2
  import { LanguageModelV2 } from "@ai-sdk/provider";
3
3
 
4
4
  //#region src/retryables/content-filter-triggered.d.ts
@@ -6,22 +6,22 @@ import { LanguageModelV2 } from "@ai-sdk/provider";
6
6
  /**
7
7
  * Fallback to a different model if the content filter was triggered.
8
8
  */
9
- declare function contentFilterTriggered(input: LanguageModelV2 | RetryModel): Retryable;
9
+ declare function contentFilterTriggered(model: LanguageModelV2, options?: Omit<RetryModel, 'model'>): Retryable;
10
10
  //#endregion
11
11
  //#region src/retryables/request-not-retryable.d.ts
12
12
  /**
13
13
  * Fallback to a different model if the error is non-retryable.
14
14
  */
15
- declare function requestNotRetryable(input: LanguageModelV2 | RetryModel): Retryable;
15
+ declare function requestNotRetryable(model: LanguageModelV2, options?: Omit<RetryModel, 'model'>): Retryable;
16
16
  //#endregion
17
17
  //#region src/retryables/request-timeout.d.ts
18
18
  /**
19
19
  * Fallback to a different model after a timeout/abort error.
20
20
  * Use in combination with the `abortSignal` option in `generateText`.
21
21
  */
22
- declare function requestTimeout(input: LanguageModelV2 | RetryModel): Retryable;
22
+ declare function requestTimeout(model: LanguageModelV2, options?: Omit<RetryModel, 'model'>): Retryable;
23
23
  //#endregion
24
24
  //#region src/retryables/response-schema-mismatch.d.ts
25
- declare function responseSchemaMismatch(input: LanguageModelV2 | RetryModel): Retryable;
25
+ declare function responseSchemaMismatch(model: LanguageModelV2, options?: Omit<RetryModel, 'model'>): Retryable;
26
26
  //#endregion
27
27
  export { contentFilterTriggered, requestNotRetryable, requestTimeout, responseSchemaMismatch };
@@ -1,6 +1,6 @@
1
+ import { isErrorAttempt, isResultAttempt } from "../create-retryable-model-CnrFowSg.js";
1
2
  import { isAbortError } from "@ai-sdk/provider-utils";
2
3
  import { APICallError, NoObjectGeneratedError, TypeValidationError } from "ai";
3
- import { APICallError as APICallError$1 } from "@ai-sdk/provider";
4
4
 
5
5
  //#region src/utils.ts
6
6
  const isObject = (value) => typeof value === "object" && value !== null;
@@ -11,18 +11,25 @@ const isString = (value) => typeof value === "string";
11
11
  /**
12
12
  * Fallback to a different model if the content filter was triggered.
13
13
  */
14
- function contentFilterTriggered(input) {
14
+ function contentFilterTriggered(model, options) {
15
15
  return (context) => {
16
- const { error } = context.current;
17
- const model = "model" in input ? input.model : input;
18
- if (APICallError.isInstance(error) && isObject(error.data) && isObject(error.data.error) && isString(error.data.error.code) && error.data.error.code === "content_filter") return {
19
- model,
20
- maxAttempts: 1
21
- };
22
- if (NoObjectGeneratedError.isInstance(error) && error.finishReason === "content-filter") return {
23
- model,
24
- maxAttempts: 1
25
- };
16
+ const { current } = context;
17
+ if (isErrorAttempt(current)) {
18
+ const { error } = current;
19
+ if (APICallError.isInstance(error) && isObject(error.data) && isObject(error.data.error) && isString(error.data.error.code) && error.data.error.code === "content_filter") return {
20
+ model,
21
+ maxAttempts: 1,
22
+ ...options
23
+ };
24
+ }
25
+ if (isResultAttempt(current)) {
26
+ const { result } = current;
27
+ if (result.finishReason === "content-filter") return {
28
+ model,
29
+ maxAttempts: 1,
30
+ ...options
31
+ };
32
+ }
26
33
  };
27
34
  }
28
35
 
@@ -31,14 +38,16 @@ function contentFilterTriggered(input) {
31
38
  /**
32
39
  * Fallback to a different model if the error is non-retryable.
33
40
  */
34
- function requestNotRetryable(input) {
41
+ function requestNotRetryable(model, options) {
35
42
  return (context) => {
36
- const { error } = context.current;
37
- const model = "model" in input ? input.model : input;
38
- if (APICallError$1.isInstance(error) && error.isRetryable === false) return {
39
- model,
40
- maxAttempts: 1
41
- };
43
+ const { current } = context;
44
+ if (isErrorAttempt(current)) {
45
+ if (APICallError.isInstance(current.error) && current.error.isRetryable === false) return {
46
+ model,
47
+ maxAttempts: 1,
48
+ ...options
49
+ };
50
+ }
42
51
  };
43
52
  }
44
53
 
@@ -48,30 +57,34 @@ function requestNotRetryable(input) {
48
57
  * Fallback to a different model after a timeout/abort error.
49
58
  * Use in combination with the `abortSignal` option in `generateText`.
50
59
  */
51
- function requestTimeout(input) {
60
+ function requestTimeout(model, options) {
52
61
  return (context) => {
53
- const { error } = context.current;
54
- const model = "model" in input ? input.model : input;
55
- /**
56
- * Fallback to the specified model after all retries are exhausted.
57
- */
58
- if (isAbortError(error)) return {
59
- model,
60
- maxAttempts: 1
61
- };
62
+ const { current } = context;
63
+ if (isErrorAttempt(current)) {
64
+ /**
65
+ * Fallback to the specified model after all retries are exhausted.
66
+ */
67
+ if (isAbortError(current.error)) return {
68
+ model,
69
+ maxAttempts: 1,
70
+ ...options
71
+ };
72
+ }
62
73
  };
63
74
  }
64
75
 
65
76
  //#endregion
66
77
  //#region src/retryables/response-schema-mismatch.ts
67
- function responseSchemaMismatch(input) {
78
+ function responseSchemaMismatch(model, options) {
68
79
  return (context) => {
69
- const { error } = context.current;
70
- const model = "model" in input ? input.model : input;
71
- if (NoObjectGeneratedError.isInstance(error) && error.finishReason === "stop" && TypeValidationError.isInstance(error.cause)) return {
72
- model,
73
- maxAttempts: 1
74
- };
80
+ const { current } = context;
81
+ if (isErrorAttempt(current)) {
82
+ if (NoObjectGeneratedError.isInstance(current.error) && current.error.finishReason === "stop" && TypeValidationError.isInstance(current.error.cause)) return {
83
+ model,
84
+ maxAttempts: 1,
85
+ ...options
86
+ };
87
+ }
75
88
  };
76
89
  }
77
90
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-retry",
3
- "version": "0.0.1-alpha.3",
3
+ "version": "0.0.1",
4
4
  "description": "AI SDK Retry",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -17,7 +17,12 @@
17
17
  "publishConfig": {
18
18
  "access": "public"
19
19
  },
20
- "keywords": [],
20
+ "keywords": [
21
+ "ai",
22
+ "ai-sdk",
23
+ "retry",
24
+ "fallback"
25
+ ],
21
26
  "author": "Chris Cook",
22
27
  "license": "MIT",
23
28
  "repository": {
@@ -1,42 +0,0 @@
1
- import { LanguageModelV2 } from "@ai-sdk/provider";
2
-
3
- //#region src/create-retryable-model.d.ts
4
-
5
- /**
6
- * The context provided to Retryables with the current attempt and all previous attempts.
7
- */
8
- interface RetryContext {
9
- current: RetryAttempt;
10
- attempts: Array<RetryAttempt>;
11
- totalAttempts: number;
12
- }
13
- /**
14
- * A retry attempt with the error and model used
15
- */
16
- interface RetryAttempt {
17
- error: unknown;
18
- model: LanguageModelV2;
19
- }
20
- /**
21
- * A model to retry with and the maximum number of attempts for that model.
22
- */
23
- type RetryModel = {
24
- model: LanguageModelV2;
25
- maxAttempts: number;
26
- };
27
- /**
28
- * A function that determines whether to retry with a different model based on the current attempt and all previous attempts.
29
- */
30
- type Retryable = (context: RetryContext) => RetryModel | Promise<RetryModel> | undefined;
31
- /**
32
- * Options for creating a retryable model.
33
- */
34
- interface CreateRetryableOptions {
35
- model: LanguageModelV2;
36
- retries: Array<Retryable | LanguageModelV2>;
37
- onError?: (context: RetryContext) => void;
38
- onRetry?: (context: RetryContext) => void;
39
- }
40
- declare function createRetryable(config: CreateRetryableOptions): LanguageModelV2;
41
- //#endregion
42
- export { CreateRetryableOptions, RetryAttempt, RetryContext, RetryModel, Retryable, createRetryable };