ai-retry 0.0.1-alpha.2 → 0.0.1-alpha.3

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,24 +1,29 @@
1
- # ai-retry
2
-
3
- 🚧 **WORK IN PROGRESS - DO NOT USE IN PRODUCTION**
1
+ ### ai-retry
4
2
 
5
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.
6
4
 
7
- ## How It Works
5
+ #### How?
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.
8
8
 
9
- `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 tracks which models have been tried and how many attempts have been made to prevent infinite loops.
9
+ ### Installation
10
10
 
11
- ## Installation
11
+ This library only supports AI SDK v5.
12
+
13
+ > [!WARNING]
14
+ > `ai-retry` is in alpha stage and the API may change in future releases.
12
15
 
13
16
  ```bash
14
- npm install ai-retry
15
- # or
16
- pnpm add ai-retry
17
- # or
18
- yarn add ai-retry
17
+ npm install ai-retry@alpha
19
18
  ```
20
19
 
21
- ## Usage
20
+ ### Usage
21
+
22
+ Create a retryable model by providing a base model and a list of retryables or fallback models.
23
+
24
+ > [!WARNING]
25
+ > `ai-retry` currently only supports `generateText` and `generateObject` calls.
26
+ > Streaming via `streamText` and `streamObject` is not supported yet.
22
27
 
23
28
  ```typescript
24
29
  import { azure } from '@ai-sdk/azure';
@@ -47,35 +52,21 @@ const result = await generateText({
47
52
  });
48
53
  ```
49
54
 
50
- ### Default Fallback
51
-
52
- If you always want to fallback to a different model on any error, you can simply provide a list of models:
53
-
54
- ```typescript
55
- const retryableModel = createRetryable({
56
- model: azure('gpt-4'),
57
- retries: [
58
- openai('gpt-4'),
59
- anthropic('claude-3-haiku-20240307')
60
- ],
61
- });
62
- ```
63
-
64
- ## Retryables
55
+ #### Retryables
65
56
 
66
- A retryable is a function that receives an error and determines whether to retry with a different model based on the error and context of previous attempts.
67
- `ai-retry` includes several built-in retryables:
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:
68
59
 
69
- - `contentFilterTriggered`: Automatically switch to a different model when content filtering blocks your request.
70
- - `responseSchemaMismatch`: Retry with different models when structured output validation fails.
71
- - `requestTimeout`: Handle timeouts by switching to potentially faster models.
72
- - `requestNotRetryable`: Handle cases where the primary model cannot process the request.
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.
73
64
 
74
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.
75
66
 
76
- ### Content Filter Triggered
67
+ ##### Content Filter Triggered
77
68
 
78
- Automatically switch to a different model when content filtering blocks your request:
69
+ Automatically switch to a different model when content filtering blocks your request.
79
70
 
80
71
  ```typescript
81
72
  import { contentFilterTriggered } from 'ai-retry/retryables';
@@ -88,7 +79,7 @@ const retryableModel = createRetryable({
88
79
  });
89
80
  ```
90
81
 
91
- ### Response Schema Mismatch
82
+ ##### Response Schema Mismatch
92
83
 
93
84
  Retry with different models when structured output validation fails:
94
85
 
@@ -98,7 +89,7 @@ import { responseSchemaMismatch } from 'ai-retry/retryables';
98
89
  const retryableModel = createRetryable({
99
90
  model: azure('gpt-4-mini'),
100
91
  retries: [
101
- responseSchemaMismatch(azure('gpt-4')), // Try full model for better structure output
92
+ responseSchemaMismatch(azure('gpt-4')), // Try full model for better structured output
102
93
  ],
103
94
  });
104
95
 
@@ -115,9 +106,12 @@ const result = await generateObject({
115
106
  });
116
107
  ```
117
108
 
118
- ### Request Timeout
109
+ ##### Request Timeout
119
110
 
120
- Handle timeouts by switching to potentially faster models:
111
+ Handle timeouts by switching to potentially faster models.
112
+
113
+ > [!NOTE]
114
+ > You need to set an `abortSignal` with a timeout on your request for this to work.
121
115
 
122
116
  ```typescript
123
117
  import { requestTimeout } from 'ai-retry/retryables';
@@ -136,9 +130,9 @@ const result = await generateText({
136
130
  });
137
131
  ```
138
132
 
139
- ### Request Not Retryable
133
+ ##### Request Not Retryable
140
134
 
141
- Handle cases where the base model fails with a non-retryable error (e.g., unsupported features):
135
+ Handle cases where the base model fails with a non-retryable error.
142
136
 
143
137
  ```typescript
144
138
  import { requestNotRetryable } from 'ai-retry/retryables';
@@ -151,7 +145,7 @@ const retryable = createRetryable({
151
145
  });
152
146
  ```
153
147
 
154
- ### Custom Retryables
148
+ ##### Custom Retryables
155
149
 
156
150
  Create your own retryables for specific use cases:
157
151
 
@@ -159,10 +153,10 @@ Create your own retryables for specific use cases:
159
153
  import type { Retryable } from 'ai-retry';
160
154
 
161
155
  const customRetry: Retryable = (context) => {
162
- const { error, triedModels, totalAttempts } = context;
156
+ const { current, attempts, totalAttempts } = context;
163
157
 
164
158
  // Your custom logic here
165
- if (shouldRetryWithDifferentModel(error)) {
159
+ if (shouldRetryWithDifferentModel(current.error)) {
166
160
  return {
167
161
  model: myFallbackModel,
168
162
  maxAttempts: 3,
@@ -178,24 +172,73 @@ const retryable = createRetryable({
178
172
  });
179
173
  ```
180
174
 
181
- ### Error Monitoring
175
+ #### Default Fallback
182
176
 
183
- Track retry attempts and errors:
177
+ If you always want to fallback to a different model on any error, you can simply provide a list of models.
184
178
 
185
179
  ```typescript
186
- const retryable = createRetryable({
187
- model: primaryModel,
180
+ const retryableModel = createRetryable({
181
+ model: azure('gpt-4'),
182
+ retries: [
183
+ openai('gpt-4'),
184
+ anthropic('claude-3-haiku-20240307')
185
+ ],
186
+ });
187
+ ```
188
+
189
+ #### All Retries Failed
190
+
191
+ If all retry attempts failed, a `RetryError` is thrown containing all individual errors.
192
+ If no retry was attempted (e.g. because all retryables returned `undefined`), the original error is thrown directly.
193
+
194
+ ```typescript
195
+ import { RetryError } from 'ai';
196
+
197
+ const retryableModel = createRetryable({
198
+ model: azure('gpt-4'),
199
+ retries: [
200
+ openai('gpt-4'),
201
+ anthropic('claude-3-haiku-20240307')
202
+ ],
203
+ });
204
+
205
+ try {
206
+ const result = await generateText({
207
+ model: retryableModel,
208
+ prompt: 'Hello world!',
209
+ });
210
+ } catch (error) {
211
+ // RetryError is an official AI SDK error
212
+ if (error instanceof RetryError) {
213
+ console.error('All retry attempts failed:', error.errors);
214
+ } else {
215
+ console.error('Request failed:', error);
216
+ }
217
+ }
218
+ ```
219
+
220
+ #### Logging
221
+
222
+ You can use the following callbacks to log retry attempts and errors:
223
+ - `onError` is invoked if an error occurs.
224
+ - `onRetry` is invoked before attempting a retry.
225
+
226
+ ```typescript
227
+ const retryableModel = createRetryable({
228
+ model: openai('gpt-4-mini'),
188
229
  retries: [/* your retryables */],
189
230
  onError: (context) => {
190
- console.log(`Attempt ${context.totalAttempts} failed:`, context.error);
191
- console.log(`Tried models:`, Array.from(context.triedModels.keys()));
231
+ console.log(`Attempt ${context.totalAttempts} with ${context.current.model.provider}/${context.current.model.modelId} failed:`, context.current.error);
232
+ },
233
+ onRetry: (context) => {
234
+ console.log(`Retrying with model ${context.current.model.provider}/${context.current.model.modelId}...`);
192
235
  },
193
236
  });
194
237
  ```
195
238
 
196
- ## API Reference
239
+ ### API Reference
197
240
 
198
- ### `createRetryable(options: CreateRetryableOptions): LanguageModelV2`
241
+ #### `createRetryable(options: CreateRetryableOptions): LanguageModelV2`
199
242
 
200
243
  Creates a retryable language model.
201
244
 
@@ -204,21 +247,23 @@ interface CreateRetryableOptions {
204
247
  model: LanguageModelV2;
205
248
  retries: Array<Retryable | LanguageModelV2>;
206
249
  onError?: (context: RetryContext) => void;
250
+ onRetry?: (context: RetryContext) => void;
207
251
  }
208
252
  ```
209
253
 
210
- ### `Retryable`
254
+ #### `Retryable`
211
255
 
212
- A `Retryable` is a function that receives a `RetryContext` with the current error and model and all previously tried models.
213
- It should evaluate the error and decide whether to retry by returning a new model or to skip by returning `undefined`.
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`.
214
258
 
215
259
  ```ts
216
260
  type Retryable = (context: RetryContext) => RetryModel | Promise<RetryModel> | undefined;
217
261
  ```
218
262
 
219
- ### `RetryModel`
263
+ #### `RetryModel`
220
264
 
221
- A `RetryModel` specifies the model to retry with and an optional `maxAttempts` to limit how many times this model can be retried.
265
+ A `RetryModel` specifies the model to retry and an optional `maxAttempts` to limit how many times this model can be retried.
266
+ By default, each retryable will only attempt to retry once per model. This can be customized by setting the `maxAttempts` property.
222
267
 
223
268
  ```typescript
224
269
  interface RetryModel {
@@ -227,38 +272,29 @@ interface RetryModel {
227
272
  }
228
273
  ```
229
274
 
230
- ### `RetryContext`
275
+ #### `RetryContext`
231
276
 
232
- The `RetryContext` object contains information about the current error and previously tried models.
277
+ The `RetryContext` object contains information about the current attempt and all previous attempts.
233
278
 
234
279
  ```typescript
235
280
  interface RetryContext {
236
- error: unknown;
237
- baseModel: LanguageModelV2;
238
- currentModel: LanguageModelV2;
239
- triedModels: Map<string, RetryState>;
240
- totalAttempts: number;
281
+ current: RetryAttempt;
282
+ attempts: Array<RetryAttempt>;
283
+ totalAttempts: number;
241
284
  }
242
285
  ```
243
286
 
244
- ### `RetryState`
287
+ #### `RetryAttempt`
245
288
 
246
- The `RetryState` tracks the state of each model that has been tried, including the number of attempts and any errors encountered. The `modelKey` is a unique identifier for the model instance to keep track of models without relying on object reference equality.
289
+ A `RetryAttempt` represents a single failed attempt with a specific model.
247
290
 
248
291
  ```typescript
249
- interface RetryState {
250
- modelKey: string;
292
+ interface RetryAttempt {
293
+ error: unknown;
251
294
  model: LanguageModelV2;
252
- attempts: number;
253
- errors: Array<unknown>;
254
295
  }
255
296
  ```
256
297
 
257
- ## Requirements
258
-
259
- - AI SDK v2.0+
260
- - Node.js 16+
261
-
262
- ## License
298
+ ### License
263
299
 
264
300
  MIT
@@ -0,0 +1,42 @@
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 };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { CreateRetryableOptions, RetryContext, RetryModel, RetryState, Retryable, createRetryable } from "./create-retryable-model-Ddjfs7Y2.js";
1
+ import { CreateRetryableOptions, RetryAttempt, RetryContext, RetryModel, Retryable, createRetryable } from "./create-retryable-model-DKKgxKLw.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, RetryContext, RetryModel, RetryState, Retryable, createRetryable, getModelKey };
10
+ export { CreateRetryableOptions, RetryAttempt, RetryContext, RetryModel, Retryable, createRetryable, getModelKey };
package/dist/index.js CHANGED
@@ -46,34 +46,60 @@ var RetryableModel = class {
46
46
  */
47
47
  let totalAttempts = 0;
48
48
  /**
49
- * Track models that have already been tried to avoid infinite loops
49
+ * Track all attempts.
50
50
  */
51
- const triedModels = /* @__PURE__ */ new Map();
51
+ const attempts = [];
52
+ /**
53
+ * The error occured in the previous attempt or undefined if this is the first attempt
54
+ */
55
+ let currentError;
52
56
  while (true) {
53
- totalAttempts++;
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
+ }
54
78
  try {
79
+ totalAttempts++;
55
80
  return await this.currentModel.doGenerate(options);
56
81
  } catch (error) {
57
- const currentModelKey = getModelKey(this.currentModel);
58
- const prevState = triedModels.get(currentModelKey);
59
82
  /**
60
- * Save failed attempt with the current model
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
61
88
  */
62
- const newState = {
63
- modelKey: currentModelKey,
64
- model: this.currentModel,
65
- attempts: (prevState?.attempts ?? 0) + 1,
66
- errors: [...prevState?.errors ?? [], error]
89
+ const currentAttempt = {
90
+ error: currentError,
91
+ model: this.currentModel
67
92
  };
68
- triedModels.set(currentModelKey, newState);
69
93
  /**
70
- * Prepare context for the retry handlers
94
+ * Save the current attempt
95
+ */
96
+ attempts.push(currentAttempt);
97
+ /**
98
+ * Context for the retryables and onError handler
71
99
  */
72
100
  const context = {
73
- error,
74
- baseModel: this.baseModel,
75
- currentModel: this.currentModel,
76
- triedModels,
101
+ current: currentAttempt,
102
+ attempts,
77
103
  totalAttempts
78
104
  };
79
105
  /**
@@ -87,12 +113,14 @@ var RetryableModel = class {
87
113
  for (const retry of this.options.retries) {
88
114
  const retryModel = typeof retry === "function" ? await retry(context) : defaultRetryModel(retry);
89
115
  if (retryModel) {
116
+ /**
117
+ * The model key uniquely identifies a model instance (provider + modelId)
118
+ */
90
119
  const retryModelKey = getModelKey(retryModel.model);
91
- const retryState = triedModels.get(retryModelKey);
92
120
  /**
93
121
  * Check if the model can still be retried based on maxAttempts
94
122
  */
95
- if (!retryState || retryState.attempts < retryModel.maxAttempts) {
123
+ if (attempts.filter((a) => getModelKey(a.model) === retryModelKey).length < retryModel.maxAttempts) {
96
124
  nextModel = retryModel.model;
97
125
  break;
98
126
  }
@@ -105,11 +133,11 @@ var RetryableModel = class {
105
133
  if (!nextModel) {
106
134
  if (totalAttempts > 1) {
107
135
  const errorMessage = getErrorMessage(error);
108
- const newErrors = Array.from(triedModels.values()).flatMap((state) => state.errors);
136
+ const errors = attempts.flatMap((a) => a.error);
109
137
  throw new RetryError({
110
138
  message: `Failed after ${totalAttempts} attempts. Last error: ${errorMessage}`,
111
139
  reason: "maxRetriesExceeded",
112
- errors: newErrors
140
+ errors
113
141
  });
114
142
  }
115
143
  throw error;
@@ -1,4 +1,4 @@
1
- import { RetryModel, Retryable } from "../create-retryable-model-Ddjfs7Y2.js";
1
+ import { RetryModel, Retryable } from "../create-retryable-model-DKKgxKLw.js";
2
2
  import { LanguageModelV2 } from "@ai-sdk/provider";
3
3
 
4
4
  //#region src/retryables/content-filter-triggered.d.ts
@@ -13,7 +13,7 @@ const isString = (value) => typeof value === "string";
13
13
  */
14
14
  function contentFilterTriggered(input) {
15
15
  return (context) => {
16
- const { error } = context;
16
+ const { error } = context.current;
17
17
  const model = "model" in input ? input.model : input;
18
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
19
  model,
@@ -33,7 +33,7 @@ function contentFilterTriggered(input) {
33
33
  */
34
34
  function requestNotRetryable(input) {
35
35
  return (context) => {
36
- const { error } = context;
36
+ const { error } = context.current;
37
37
  const model = "model" in input ? input.model : input;
38
38
  if (APICallError$1.isInstance(error) && error.isRetryable === false) return {
39
39
  model,
@@ -50,7 +50,7 @@ function requestNotRetryable(input) {
50
50
  */
51
51
  function requestTimeout(input) {
52
52
  return (context) => {
53
- const { error } = context;
53
+ const { error } = context.current;
54
54
  const model = "model" in input ? input.model : input;
55
55
  /**
56
56
  * Fallback to the specified model after all retries are exhausted.
@@ -66,7 +66,7 @@ function requestTimeout(input) {
66
66
  //#region src/retryables/response-schema-mismatch.ts
67
67
  function responseSchemaMismatch(input) {
68
68
  return (context) => {
69
- const { error } = context;
69
+ const { error } = context.current;
70
70
  const model = "model" in input ? input.model : input;
71
71
  if (NoObjectGeneratedError.isInstance(error) && error.finishReason === "stop" && TypeValidationError.isInstance(error.cause)) return {
72
72
  model,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-retry",
3
- "version": "0.0.1-alpha.2",
3
+ "version": "0.0.1-alpha.3",
4
4
  "description": "AI SDK Retry",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -1,29 +0,0 @@
1
- import { LanguageModelV2 } from "@ai-sdk/provider";
2
-
3
- //#region src/create-retryable-model.d.ts
4
- interface RetryContext {
5
- error: unknown;
6
- baseModel: LanguageModelV2;
7
- currentModel: LanguageModelV2;
8
- triedModels: Map<string, RetryState>;
9
- totalAttempts: number;
10
- }
11
- type RetryModel = {
12
- model: LanguageModelV2;
13
- maxAttempts: number;
14
- };
15
- type Retryable = (context: RetryContext) => RetryModel | Promise<RetryModel> | undefined;
16
- type RetryState = {
17
- modelKey: string;
18
- model: LanguageModelV2;
19
- attempts: number;
20
- errors: Array<unknown>;
21
- };
22
- interface CreateRetryableOptions {
23
- model: LanguageModelV2;
24
- retries: Array<Retryable | LanguageModelV2>;
25
- onError?: (context: RetryContext) => void;
26
- }
27
- declare function createRetryable(config: CreateRetryableOptions): LanguageModelV2;
28
- //#endregion
29
- export { CreateRetryableOptions, RetryContext, RetryModel, RetryState, Retryable, createRetryable };