ai-retry 0.0.2 → 0.1.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
@@ -24,15 +24,12 @@ npm install ai-retry
24
24
  ### Usage
25
25
 
26
26
  Create a retryable model by providing a base model and a list of retryables or fallback models.
27
-
28
- > [!WARNING]
29
- > `ai-retry` currently only supports `generateText` and `generateObject` calls.
30
- > Streaming via `streamText` and `streamObject` is not supported yet.
27
+ When an error occurs, it will evaluate each retryable in order and use the first one that indicates a retry should be attempted with a different model.
31
28
 
32
29
  ```typescript
33
30
  import { azure } from '@ai-sdk/azure';
34
31
  import { openai } from '@ai-sdk/openai';
35
- import { generateText } from 'ai';
32
+ import { generateText, streamText } from 'ai';
36
33
  import { createRetryable } from 'ai-retry';
37
34
  import { contentFilterTriggered, requestTimeout } from 'ai-retry/retryables';
38
35
 
@@ -49,13 +46,25 @@ const result = await generateText({
49
46
  model: retryableModel,
50
47
  prompt: 'Hello world!',
51
48
  });
52
- ```
53
49
 
50
+ // Or with streaming
51
+ const result = streamText({
52
+ model: retryableModel,
53
+ prompt: 'Write a story about a robot...',
54
+ });
55
+
56
+ for await (const chunk of result.textStream) {
57
+ console.log(chunk.text);
58
+ }
59
+ ```
54
60
 
55
61
  #### Content Filter
56
62
 
57
63
  Automatically switch to a different model when content filtering blocks your request.
58
64
 
65
+ > [!WARNING]
66
+ > This retryable currently does not work with streaming requests, because the content filter is only indicated in the final response.
67
+
59
68
  ```typescript
60
69
  import { contentFilterTriggered } from 'ai-retry/retryables';
61
70
 
@@ -122,10 +131,10 @@ const result = await generateText({
122
131
 
123
132
  #### Service Overloaded
124
133
 
125
- Handle service overload errors (HTTP code 529) by switching to a provider.
134
+ Handle service overload errors (status code 529) by switching to a provider.
126
135
 
127
136
  > [!NOTE]
128
- > For Anthropic specifically, use `anthropicServiceOverloaded` instead as Anthropic sometimes returns HTTP 200 OK with an error payload rather than the standard HTTP 529.
137
+ > You can use this retryable to handle Anthropic's overloaded errors.
129
138
 
130
139
  ```typescript
131
140
  import { serviceOverloaded } from 'ai-retry/retryables';
@@ -244,14 +253,25 @@ const retryableModel = createRetryable({
244
253
  model: openai('gpt-4-mini'),
245
254
  retries: [/* your retryables */],
246
255
  onError: (context) => {
247
- console.log(`Attempt ${context.totalAttempts} with ${context.current.model.provider}/${context.current.model.modelId} failed:`, context.current.error);
256
+ console.error(`Attempt ${context.attempts.length} with ${context.current.model.provider}/${context.current.model.modelId} failed:`,
257
+ context.current.error
258
+ );
248
259
  },
249
260
  onRetry: (context) => {
250
- console.log(`Retrying with model ${context.current.model.provider}/${context.current.model.modelId}...`);
261
+ console.log(`Retrying attempt ${context.attempts.length + 1} with model ${context.current.model.provider}/${context.current.model.modelId}...`);
251
262
  },
252
263
  });
253
264
  ```
254
265
 
266
+ ### Streaming
267
+
268
+ Errors during streaming requests can occur in two ways:
269
+
270
+ 1. When the stream is initially created (e.g. network error, API error, etc.) by calling `streamText`.
271
+ 2. While the stream is being processed (e.g. timeout, API error, etc.) by reading from the returned `result.textStream` async iterable.
272
+
273
+ In the second case, errors during stream processing will not always be retried, because the stream might have already emitted some actual content and the consumer might have processed it. Retrying will be stopped as soon as the first content chunk (e.g. types of `text-delta`, `tool-call`, etc.) is emitted. The type of chunks considered as content are the same as the ones that are passed to [onChunk()](https://github.com/vercel/ai/blob/1fe4bd4144bff927f5319d9d206e782a73979ccb/packages/ai/src/generate-text/stream-text.ts#L684-L697).
274
+
255
275
  ### Retryables
256
276
 
257
277
  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.
@@ -261,7 +281,7 @@ There are several built-in retryables:
261
281
  - [`requestTimeout`](./src/retryables/request-timeout.ts): Request timeout occurred.
262
282
  - [`requestNotRetryable`](./src/retryables/request-not-retryable.ts): Request failed with a non-retryable error.
263
283
  - [`serviceOverloaded`](./src/retryables/service-overloaded.ts): Response with status code 529 (service overloaded).
264
- - [`anthropicServiceOverloaded`](./src/retryables/anthropic-service-overloaded.ts): Anthropic-specific overloaded error handling for both HTTP 529 and 200 OK responses with overloaded error payloads.
284
+ - Use this retryable to handle Anthropic's overloaded errors.
265
285
 
266
286
  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.
267
287
 
@@ -309,7 +329,6 @@ The `RetryContext` object contains information about the current attempt and all
309
329
  interface RetryContext {
310
330
  current: RetryAttempt;
311
331
  attempts: Array<RetryAttempt>;
312
- totalAttempts: number;
313
332
  }
314
333
  ```
315
334
 
@@ -0,0 +1,308 @@
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/utils.ts
14
+ const isObject = (value) => typeof value === "object" && value !== null;
15
+ const isString = (value) => typeof value === "string";
16
+ const isGenerateResult = (result) => "content" in result;
17
+ /**
18
+ * Check if a stream part is a content part (e.g., text delta, reasoning delta, source, tool call, tool result).
19
+ * These types are also emitted by `onChunk` callbacks.
20
+ * @see https://github.com/vercel/ai/blob/1fe4bd4144bff927f5319d9d206e782a73979ccb/packages/ai/src/generate-text/stream-text.ts#L686-L697
21
+ */
22
+ const isStreamContentPart = (part) => {
23
+ return part.type === "text-delta" || part.type === "reasoning-delta" || part.type === "source" || part.type === "tool-call" || part.type === "tool-result" || part.type === "tool-input-start" || part.type === "tool-input-delta" || part.type === "raw";
24
+ };
25
+
26
+ //#endregion
27
+ //#region src/create-retryable-model.ts
28
+ /**
29
+ * Type guard to check if a retry attempt is an error attempt
30
+ */
31
+ function isErrorAttempt(attempt) {
32
+ return attempt.type === "error";
33
+ }
34
+ /**
35
+ * Type guard to check if a retry attempt is a result attempt
36
+ */
37
+ function isResultAttempt(attempt) {
38
+ return attempt.type === "result";
39
+ }
40
+ var RetryableModel = class {
41
+ specificationVersion = "v2";
42
+ baseModel;
43
+ currentModel;
44
+ options;
45
+ get modelId() {
46
+ return this.currentModel.modelId;
47
+ }
48
+ get provider() {
49
+ return this.currentModel.provider;
50
+ }
51
+ get supportedUrls() {
52
+ return this.currentModel.supportedUrls;
53
+ }
54
+ constructor(options) {
55
+ this.options = options;
56
+ this.baseModel = options.model;
57
+ this.currentModel = options.model;
58
+ }
59
+ /**
60
+ * Find the next model to retry with based on the retry context
61
+ */
62
+ async findNextModel(context) {
63
+ /**
64
+ * Filter retryables based on attempt type:
65
+ * - Result-based attempts: Only consider function retryables (skip plain models)
66
+ * - Error-based attempts: Consider all retryables (functions + plain models)
67
+ */
68
+ const applicableRetries = isResultAttempt(context.current) ? this.options.retries.filter((retry) => typeof retry === "function") : this.options.retries;
69
+ /**
70
+ * Iterate through the applicable retryables to find a model to retry with
71
+ */
72
+ for (const retry of applicableRetries) {
73
+ const retryModel = typeof retry === "function" ? await retry(context) : {
74
+ model: retry,
75
+ maxAttempts: 1
76
+ };
77
+ if (retryModel) {
78
+ /**
79
+ * The model key uniquely identifies a model instance (provider + modelId)
80
+ */
81
+ const retryModelKey = getModelKey(retryModel.model);
82
+ /**
83
+ * Find all attempts with the same model
84
+ */
85
+ const retryAttempts = context.attempts.filter((a) => getModelKey(a.model) === retryModelKey);
86
+ const maxAttempts = retryModel.maxAttempts ?? 1;
87
+ /**
88
+ * Check if the model can still be retried based on maxAttempts
89
+ */
90
+ if (retryAttempts.length < maxAttempts) return retryModel.model;
91
+ }
92
+ }
93
+ }
94
+ /**
95
+ * Execute a function with retry logic for handling errors
96
+ */
97
+ async withRetry(input) {
98
+ /**
99
+ * Track all attempts.
100
+ */
101
+ const attempts = input.attempts ?? [];
102
+ while (true) {
103
+ /**
104
+ * The previous attempt that triggered a retry, or undefined if this is the first attempt
105
+ */
106
+ const previousAttempt = attempts.at(-1);
107
+ /**
108
+ * Call the onRetry handler if provided.
109
+ * Skip on the first attempt since no previous attempt exists yet.
110
+ */
111
+ if (previousAttempt) {
112
+ const currentAttempt = {
113
+ ...previousAttempt,
114
+ model: this.currentModel
115
+ };
116
+ /**
117
+ * Create a shallow copy of the attempts for testing purposes
118
+ */
119
+ const updatedAttempts = [...attempts];
120
+ const context = {
121
+ current: currentAttempt,
122
+ attempts: updatedAttempts,
123
+ totalAttempts: updatedAttempts.length
124
+ };
125
+ this.options.onRetry?.(context);
126
+ }
127
+ try {
128
+ /**
129
+ * Call the function that may need to be retried
130
+ */
131
+ const result = await input.fn();
132
+ /**
133
+ * Check if the result should trigger a retry (only for generate results, not streams)
134
+ */
135
+ if (isGenerateResult(result)) {
136
+ const { nextModel, attempt } = await this.handleResult(result, attempts);
137
+ attempts.push(attempt);
138
+ if (nextModel) {
139
+ this.currentModel = nextModel;
140
+ /**
141
+ * Continue to the next iteration to retry
142
+ */
143
+ continue;
144
+ }
145
+ }
146
+ return {
147
+ result,
148
+ attempts
149
+ };
150
+ } catch (error) {
151
+ const { nextModel, attempt } = await this.handleError(error, attempts);
152
+ attempts.push(attempt);
153
+ this.currentModel = nextModel;
154
+ }
155
+ }
156
+ }
157
+ /**
158
+ * Handle a successful result and determine if a retry is needed
159
+ */
160
+ async handleResult(result, attempts) {
161
+ const resultAttempt = {
162
+ type: "result",
163
+ result,
164
+ model: this.currentModel
165
+ };
166
+ /**
167
+ * Save the current attempt
168
+ */
169
+ const updatedAttempts = [...attempts, resultAttempt];
170
+ const resultContext = {
171
+ current: resultAttempt,
172
+ attempts: updatedAttempts,
173
+ totalAttempts: updatedAttempts.length
174
+ };
175
+ return {
176
+ nextModel: await this.findNextModel(resultContext),
177
+ attempt: resultAttempt
178
+ };
179
+ }
180
+ /**
181
+ * Handle an error and determine if a retry is needed
182
+ */
183
+ async handleError(error, attempts) {
184
+ const errorAttempt = {
185
+ type: "error",
186
+ error,
187
+ model: this.currentModel
188
+ };
189
+ /**
190
+ * Save the current attempt
191
+ */
192
+ const updatedAttempts = [...attempts, errorAttempt];
193
+ const context = {
194
+ current: errorAttempt,
195
+ attempts: updatedAttempts,
196
+ totalAttempts: updatedAttempts.length
197
+ };
198
+ this.options.onError?.(context);
199
+ const nextModel = await this.findNextModel(context);
200
+ /**
201
+ * Handler didn't return any models to try next, rethrow the error.
202
+ * If we retried the request, wrap the error into a `RetryError` for better visibility.
203
+ */
204
+ if (!nextModel) {
205
+ if (updatedAttempts.length > 1) throw this.prepareRetryError(error, updatedAttempts);
206
+ throw error;
207
+ }
208
+ return {
209
+ nextModel,
210
+ attempt: errorAttempt
211
+ };
212
+ }
213
+ async doGenerate(options) {
214
+ /**
215
+ * Always start with the original model
216
+ */
217
+ this.currentModel = this.baseModel;
218
+ const { result } = await this.withRetry({ fn: async () => await this.currentModel.doGenerate(options) });
219
+ return result;
220
+ }
221
+ async doStream(options) {
222
+ /**
223
+ * Always start with the original model
224
+ */
225
+ this.currentModel = this.baseModel;
226
+ /**
227
+ * Perform the initial call to doStream with retry logic to handle errors before any data is streamed.
228
+ */
229
+ let { result, attempts } = await this.withRetry({ fn: async () => await this.currentModel.doStream(options) });
230
+ /**
231
+ * Wrap the original stream to handle retries if an error occurs during streaming.
232
+ */
233
+ const retryableStream = new ReadableStream({ start: async (controller) => {
234
+ let reader;
235
+ let isStreaming = false;
236
+ while (true) try {
237
+ reader = result.stream.getReader();
238
+ while (true) {
239
+ const { done, value } = await reader.read();
240
+ if (done) break;
241
+ /**
242
+ * If the stream part is an error and no data has been streamed yet, we can retry
243
+ * Throw the error to trigger the retry logic in withRetry
244
+ */
245
+ if (value.type === "error") {
246
+ if (!isStreaming) throw value.error;
247
+ }
248
+ /**
249
+ * Mark that streaming has started once we receive actual content
250
+ */
251
+ if (isStreamContentPart(value)) isStreaming = true;
252
+ /**
253
+ * Enqueue the chunk to the consumer of the stream
254
+ */
255
+ controller.enqueue(value);
256
+ }
257
+ controller.close();
258
+ break;
259
+ } catch (error) {
260
+ /**
261
+ * Check if the error from the stream can be retried.
262
+ * Otherwise it will rethrow the error.
263
+ */
264
+ const { nextModel, attempt } = await this.handleError(error, attempts);
265
+ this.currentModel = nextModel;
266
+ /**
267
+ * Save the attempt
268
+ */
269
+ attempts.push(attempt);
270
+ /**
271
+ * Retry the request by calling doStream again.
272
+ * This will create a new stream.
273
+ */
274
+ const retriedResult = await this.withRetry({
275
+ fn: async () => await this.currentModel.doStream(options),
276
+ attempts
277
+ });
278
+ /**
279
+ * Cancel the previous reader and stream if we are retrying
280
+ */
281
+ await reader?.cancel();
282
+ result = retriedResult.result;
283
+ attempts = retriedResult.attempts;
284
+ } finally {
285
+ reader?.releaseLock();
286
+ }
287
+ } });
288
+ return {
289
+ ...result,
290
+ stream: retryableStream
291
+ };
292
+ }
293
+ prepareRetryError(error, attempts) {
294
+ const errorMessage = getErrorMessage(error);
295
+ const errors = attempts.flatMap((a) => isErrorAttempt(a) ? a.error : `Result with finishReason: ${a.result.finishReason}`);
296
+ return new RetryError(new RetryError({
297
+ message: `Failed after ${attempts.length} attempts. Last error: ${errorMessage}`,
298
+ reason: "maxRetriesExceeded",
299
+ errors
300
+ }));
301
+ }
302
+ };
303
+ function createRetryable(config) {
304
+ return new RetryableModel(config);
305
+ }
306
+
307
+ //#endregion
308
+ export { createRetryable, getModelKey, isErrorAttempt, isObject, isResultAttempt, isString };
@@ -8,15 +8,30 @@ type LanguageModelV2Generate = Awaited<ReturnType<LanguageModelV2['doGenerate']>
8
8
  * The context provided to Retryables with the current attempt and all previous attempts.
9
9
  */
10
10
  interface RetryContext<CURRENT extends RetryAttempt = RetryAttempt> {
11
+ /**
12
+ * Current attempt that caused the retry
13
+ */
11
14
  current: CURRENT;
15
+ /**
16
+ * All attempts made so far, including the current one
17
+ */
12
18
  attempts: Array<RetryAttempt>;
19
+ /**
20
+ * @deprecated Use `attempts.length` instead
21
+ */
13
22
  totalAttempts: number;
14
23
  }
24
+ /**
25
+ * A retry attempt with an error
26
+ */
15
27
  type RetryErrorAttempt = {
16
28
  type: 'error';
17
29
  error: unknown;
18
30
  model: LanguageModelV2;
19
31
  };
32
+ /**
33
+ * A retry attempt with a successful result
34
+ */
20
35
  type RetryResultAttempt = {
21
36
  type: 'result';
22
37
  result: LanguageModelV2Generate;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { CreateRetryableOptions, RetryAttempt, RetryContext, RetryModel, Retryable, createRetryable, isErrorAttempt, isResultAttempt } from "./create-retryable-model-DzDFqgQO.js";
1
+ import { CreateRetryableOptions, RetryAttempt, RetryContext, RetryModel, Retryable, createRetryable, isErrorAttempt, isResultAttempt } from "./create-retryable-model-DtspEawi.js";
2
2
  import { LanguageModelV2 } from "@ai-sdk/provider";
3
3
 
4
4
  //#region src/get-model-key.d.ts
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import { createRetryable, getModelKey, isErrorAttempt, isResultAttempt } from "./create-retryable-model-CnrFowSg.js";
1
+ import { createRetryable, getModelKey, isErrorAttempt, isResultAttempt } from "./create-retryable-model-C3mm1hSN.js";
2
2
 
3
3
  export { createRetryable, getModelKey, isErrorAttempt, isResultAttempt };
@@ -1,4 +1,4 @@
1
- import { RetryModel, Retryable } from "../create-retryable-model-DzDFqgQO.js";
1
+ import { RetryModel, Retryable } from "../create-retryable-model-DtspEawi.js";
2
2
  import { LanguageModelV2 } from "@ai-sdk/provider";
3
3
 
4
4
  //#region src/retryables/anthropic-service-overloaded.d.ts
@@ -22,6 +22,8 @@ type AnthropicErrorResponse = {
22
22
  * HTTP 200 OK
23
23
  * {"type":"error","error":{"type":"overloaded_error","message":"Overloaded"}}
24
24
  * ```
25
+ *
26
+ * @deprecated Use `serviceOverloaded` instead
25
27
  */
26
28
  declare function anthropicServiceOverloaded(model: LanguageModelV2, options?: Omit<RetryModel, 'model'>): Retryable;
27
29
  //#endregion
@@ -44,13 +46,14 @@ declare function requestNotRetryable(model: LanguageModelV2, options?: Omit<Retr
44
46
  */
45
47
  declare function requestTimeout(model: LanguageModelV2, options?: Omit<RetryModel, 'model'>): Retryable;
46
48
  //#endregion
47
- //#region src/retryables/response-schema-mismatch.d.ts
48
- declare function responseSchemaMismatch(model: LanguageModelV2, options?: Omit<RetryModel, 'model'>): Retryable;
49
- //#endregion
50
49
  //#region src/retryables/service-overloaded.d.ts
51
50
  /**
52
- * Fallback to a different model if the provider returns a HTTP 529 error.
51
+ * Fallback to a different model if the provider returns an overloaded error.
52
+ * This retryable handles the following cases:
53
+ * - Response with status code 529
54
+ * - Response with `type: "overloaded_error"`
55
+ * - Response with a `message` containing "overloaded"
53
56
  */
54
57
  declare function serviceOverloaded(model: LanguageModelV2, options?: Omit<RetryModel, 'model'>): Retryable;
55
58
  //#endregion
56
- export { AnthropicErrorResponse, anthropicServiceOverloaded, contentFilterTriggered, requestNotRetryable, requestTimeout, responseSchemaMismatch, serviceOverloaded };
59
+ export { AnthropicErrorResponse, anthropicServiceOverloaded, contentFilterTriggered, requestNotRetryable, requestTimeout, serviceOverloaded };
@@ -1,12 +1,7 @@
1
- import { isErrorAttempt, isResultAttempt } from "../create-retryable-model-CnrFowSg.js";
1
+ import { isErrorAttempt, isObject, isResultAttempt, isString } from "../create-retryable-model-C3mm1hSN.js";
2
2
  import { isAbortError } from "@ai-sdk/provider-utils";
3
- import { APICallError, NoObjectGeneratedError, TypeValidationError } from "ai";
3
+ import { APICallError } from "ai";
4
4
 
5
- //#region src/utils.ts
6
- const isObject = (value) => typeof value === "object" && value !== null;
7
- const isString = (value) => typeof value === "string";
8
-
9
- //#endregion
10
5
  //#region src/retryables/anthropic-service-overloaded.ts
11
6
  /**
12
7
  * Fallback if Anthropic returns an "overloaded" error with HTTP 200.
@@ -15,6 +10,8 @@ const isString = (value) => typeof value === "string";
15
10
  * HTTP 200 OK
16
11
  * {"type":"error","error":{"type":"overloaded_error","message":"Overloaded"}}
17
12
  * ```
13
+ *
14
+ * @deprecated Use `serviceOverloaded` instead
18
15
  */
19
16
  function anthropicServiceOverloaded(model, options) {
20
17
  return (context) => {
@@ -26,14 +23,11 @@ function anthropicServiceOverloaded(model, options) {
26
23
  maxAttempts: 1,
27
24
  ...options
28
25
  };
29
- if (APICallError.isInstance(error) && error.statusCode === 200) try {
30
- const responseBody = JSON.parse(error.responseBody ?? "");
31
- if (responseBody.error && isObject(responseBody.error) && isString(responseBody.error.type) && responseBody.error.type === "overloaded_error") return {
32
- model,
33
- maxAttempts: 1,
34
- ...options
35
- };
36
- } catch {}
26
+ if (isObject(error) && isString(error.type) && error.type === "overloaded_error") return {
27
+ model,
28
+ maxAttempts: 1,
29
+ ...options
30
+ };
37
31
  }
38
32
  };
39
33
  }
@@ -105,25 +99,14 @@ function requestTimeout(model, options) {
105
99
  };
106
100
  }
107
101
 
108
- //#endregion
109
- //#region src/retryables/response-schema-mismatch.ts
110
- function responseSchemaMismatch(model, options) {
111
- return (context) => {
112
- const { current } = context;
113
- if (isErrorAttempt(current)) {
114
- if (NoObjectGeneratedError.isInstance(current.error) && current.error.finishReason === "stop" && TypeValidationError.isInstance(current.error.cause)) return {
115
- model,
116
- maxAttempts: 1,
117
- ...options
118
- };
119
- }
120
- };
121
- }
122
-
123
102
  //#endregion
124
103
  //#region src/retryables/service-overloaded.ts
125
104
  /**
126
- * Fallback to a different model if the provider returns a HTTP 529 error.
105
+ * Fallback to a different model if the provider returns an overloaded error.
106
+ * This retryable handles the following cases:
107
+ * - Response with status code 529
108
+ * - Response with `type: "overloaded_error"`
109
+ * - Response with a `message` containing "overloaded"
127
110
  */
128
111
  function serviceOverloaded(model, options) {
129
112
  return (context) => {
@@ -135,9 +118,16 @@ function serviceOverloaded(model, options) {
135
118
  maxAttempts: 1,
136
119
  ...options
137
120
  };
121
+ if (isObject(error)) {
122
+ if (isString(error.type) && error.type === "overloaded_error" || isString(error.message) && error.message.toLowerCase().includes("overloaded")) return {
123
+ model,
124
+ maxAttempts: 1,
125
+ ...options
126
+ };
127
+ }
138
128
  }
139
129
  };
140
130
  }
141
131
 
142
132
  //#endregion
143
- export { anthropicServiceOverloaded, contentFilterTriggered, requestNotRetryable, requestTimeout, responseSchemaMismatch, serviceOverloaded };
133
+ export { anthropicServiceOverloaded, contentFilterTriggered, requestNotRetryable, requestTimeout, serviceOverloaded };
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "ai-retry",
3
- "version": "0.0.2",
3
+ "version": "0.1.0",
4
4
  "description": "AI SDK Retry",
5
+ "packageManager": "pnpm@9.0.0",
5
6
  "main": "./dist/index.js",
6
7
  "module": "./dist/index.js",
7
8
  "types": "./dist/index.d.ts",
@@ -17,6 +18,14 @@
17
18
  "publishConfig": {
18
19
  "access": "public"
19
20
  },
21
+ "scripts": {
22
+ "prepublishOnly": "pnpm build",
23
+ "publish:alpha": "pnpm version prerelease --preid alpha && pnpm publish --tag alpha",
24
+ "build": "tsdown",
25
+ "test": "vitest",
26
+ "lint": "biome check . --write",
27
+ "prepare": "husky"
28
+ },
20
29
  "keywords": [
21
30
  "ai",
22
31
  "ai-sdk",
@@ -55,11 +64,5 @@
55
64
  "dependencies": {
56
65
  "@ai-sdk/provider": "^2.0.0",
57
66
  "@ai-sdk/provider-utils": "^3.0.9"
58
- },
59
- "scripts": {
60
- "publish:alpha": "pnpm version prerelease --preid alpha && pnpm publish --tag alpha",
61
- "build": "tsdown",
62
- "test": "vitest",
63
- "lint": "biome check . --write"
64
67
  }
65
- }
68
+ }
@@ -1,221 +0,0 @@
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 };