ai-retry 0.1.0 → 0.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
@@ -1,8 +1,15 @@
1
- <a href="https://www.npmjs.com/package/ai-retry" alt="ai-retry"><img src="https://img.shields.io/npm/dt/ai-retry?label=ai-retry"></a> <a href="https://github.com/zirkelc/ai-retry/actions/workflows/ci.yml" alt="CI"><img src="https://img.shields.io/github/actions/workflow/status/zirkelc/ai-retry/ci.yml?branch=main"></a>
1
+ <div align='center'>
2
2
 
3
- # ai-retry: Retry and fallback mechanisms for AI SDK
3
+ # ai-retry
4
4
 
5
- Automatically handle API failures, content filtering and timeouts by switching between different AI models.
5
+ <p align="center">Retry and fallback mechanisms for AI SDK</p>
6
+ <p align="center">
7
+ <a href="https://www.npmjs.com/package/ai-retry" alt="ai-retry"><img src="https://img.shields.io/npm/dt/ai-retry?label=ai-retry"></a> <a href="https://github.com/zirkelc/ai-retry/actions/workflows/ci.yml" alt="CI"><img src="https://img.shields.io/github/actions/workflow/status/zirkelc/ai-retry/ci.yml?branch=main"></a>
8
+ </p>
9
+
10
+ </div>
11
+
12
+ Automatically handle API failures, content filtering, timeouts and other errors by switching between different AI models and providers.
6
13
 
7
14
  `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.
8
15
 
@@ -26,16 +33,17 @@ npm install ai-retry
26
33
  Create a retryable model by providing a base model and a list of retryables or fallback models.
27
34
  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.
28
35
 
36
+ > [!NOTE]
37
+ > `ai-retry` supports both language models and embedding models.
38
+
29
39
  ```typescript
30
- import { azure } from '@ai-sdk/azure';
31
40
  import { openai } from '@ai-sdk/openai';
32
41
  import { generateText, streamText } from 'ai';
33
42
  import { createRetryable } from 'ai-retry';
34
- import { contentFilterTriggered, requestTimeout } from 'ai-retry/retryables';
35
43
 
36
44
  // Create a retryable model
37
45
  const retryableModel = createRetryable({
38
- model: azure('gpt-4-mini'), // Base model
46
+ model: openai('gpt-4-mini'), // Base model
39
47
  retries: [
40
48
  // Retry strategies and fallbacks...
41
49
  ],
@@ -47,6 +55,8 @@ const result = await generateText({
47
55
  prompt: 'Hello world!',
48
56
  });
49
57
 
58
+ console.log(result.text);
59
+
50
60
  // Or with streaming
51
61
  const result = streamText({
52
62
  model: retryableModel,
@@ -58,52 +68,47 @@ for await (const chunk of result.textStream) {
58
68
  }
59
69
  ```
60
70
 
61
- #### Content Filter
62
-
63
- Automatically switch to a different model when content filtering blocks your request.
64
-
65
- > [!WARNING]
66
- > This retryable currently does not work with streaming requests, because the content filter is only indicated in the final response.
71
+ This also works with embedding models:
67
72
 
68
73
  ```typescript
69
- import { contentFilterTriggered } from 'ai-retry/retryables';
74
+ import { openai } from '@ai-sdk/openai';
75
+ import { embed } from 'ai';
76
+ import { createRetryable } from 'ai-retry';
70
77
 
78
+ // Create a retryable model
71
79
  const retryableModel = createRetryable({
72
- model: azure('gpt-4-mini'),
80
+ model: openai.textEmbedding('text-embedding-3-large'), // Base model
73
81
  retries: [
74
- contentFilterTriggered(openai('gpt-4-mini')), // Try OpenAI if Azure filters
82
+ // Retry strategies and fallbacks...
75
83
  ],
76
84
  });
85
+
86
+ // Use like any other AI SDK model
87
+ const result = await embed({
88
+ model: retryableModel,
89
+ value: 'Hello world!',
90
+ });
91
+
92
+ console.log(result.embedding);
77
93
  ```
78
94
 
79
- <!--
80
- ##### Response Schema Mismatch
95
+ #### Content Filter
96
+
97
+ Automatically switch to a different model when content filtering blocks your request.
81
98
 
82
- Retry with different models when structured output validation fails:
99
+ > [!WARNING]
100
+ > This retryable currently does not work with streaming requests, because the content filter is only indicated in the final response.
83
101
 
84
102
  ```typescript
85
- import { responseSchemaMismatch } from 'ai-retry/retryables';
103
+ import { contentFilterTriggered } from 'ai-retry/retryables';
86
104
 
87
105
  const retryableModel = createRetryable({
88
106
  model: azure('gpt-4-mini'),
89
107
  retries: [
90
- responseSchemaMismatch(azure('gpt-4')), // Try full model for better structured output
108
+ contentFilterTriggered(openai('gpt-4-mini')), // Try OpenAI if Azure filters
91
109
  ],
92
110
  });
93
-
94
- const result = await generateObject({
95
- model: retryableModel,
96
- schema: z.object({
97
- recipe: z.object({
98
- name: z.string(),
99
- ingredients: z.array(z.object({ name: z.string(), amount: z.string() })),
100
- steps: z.array(z.string()),
101
- }),
102
- }),
103
- prompt: 'Generate a lasagna recipe.',
104
- });
105
111
  ```
106
- -->
107
112
 
108
113
  #### Request Timeout
109
114
 
@@ -287,16 +292,16 @@ By default, each retryable will only attempt to retry once per model to avoid in
287
292
 
288
293
  ### API Reference
289
294
 
290
- #### `createRetryable(options: CreateRetryableOptions): LanguageModelV2`
295
+ #### `createRetryable(options: RetryableModelOptions): LanguageModelV2 | EmbeddingModelV2`
291
296
 
292
- Creates a retryable language model.
297
+ Creates a retryable model that works with both language models and embedding models.
293
298
 
294
299
  ```ts
295
- interface CreateRetryableOptions {
296
- model: LanguageModelV2;
297
- retries: Array<Retryable | LanguageModelV2>;
298
- onError?: (context: RetryContext) => void;
299
- onRetry?: (context: RetryContext) => void;
300
+ interface RetryableModelOptions<MODEL extends LanguageModelV2 | EmbeddingModelV2> {
301
+ model: MODEL;
302
+ retries: Array<Retryable<MODEL> | MODEL>;
303
+ onError?: (context: RetryContext<MODEL>) => void;
304
+ onRetry?: (context: RetryContext<MODEL>) => void;
300
305
  }
301
306
  ```
302
307
 
@@ -306,7 +311,9 @@ A `Retryable` is a function that receives a `RetryContext` with the current erro
306
311
  It should evaluate the error/result and decide whether to retry by returning a `RetryModel` or to skip by returning `undefined`.
307
312
 
308
313
  ```ts
309
- type Retryable = (context: RetryContext) => RetryModel | Promise<RetryModel> | undefined;
314
+ type Retryable = (
315
+ context: RetryContext
316
+ ) => RetryModel | Promise<RetryModel> | undefined;
310
317
  ```
311
318
 
312
319
  #### `RetryModel`
@@ -316,7 +323,7 @@ By default, each retryable will only attempt to retry once per model. This can b
316
323
 
317
324
  ```typescript
318
325
  interface RetryModel {
319
- model: LanguageModelV2;
326
+ model: LanguageModelV2 | EmbeddingModelV2;
320
327
  maxAttempts?: number;
321
328
  }
322
329
  ```
@@ -337,10 +344,13 @@ interface RetryContext {
337
344
  A `RetryAttempt` represents a single attempt with a specific model, which can be either an error or a successful result that triggered a retry.
338
345
 
339
346
  ```typescript
340
- type RetryAttempt =
341
- | { type: 'error'; error: unknown; model: LanguageModelV2 }
347
+ // For both language and embedding models
348
+ type RetryAttempt =
349
+ | { type: 'error'; error: unknown; model: LanguageModelV2 | EmbeddingModelV2 }
342
350
  | { type: 'result'; result: LanguageModelV2Generate; model: LanguageModelV2 };
343
351
 
352
+ // Note: Result-based retries only apply to language models, not embedding models
353
+
344
354
  // Type guards for discriminating attempts
345
355
  function isErrorAttempt(attempt: RetryAttempt): attempt is RetryErrorAttempt;
346
356
  function isResultAttempt(attempt: RetryAttempt): attempt is RetryResultAttempt;
package/dist/index.d.ts CHANGED
@@ -1,10 +1,13 @@
1
- import { CreateRetryableOptions, RetryAttempt, RetryContext, RetryModel, Retryable, createRetryable, isErrorAttempt, isResultAttempt } from "./create-retryable-model-DtspEawi.js";
2
- import { LanguageModelV2 } from "@ai-sdk/provider";
1
+ import { EmbeddingModelV2, LanguageModelV2, RetryableModelOptions } from "./types-CHhEGL5x.js";
3
2
 
3
+ //#region src/create-retryable-model.d.ts
4
+ declare function createRetryable<MODEL extends LanguageModelV2>(options: RetryableModelOptions<MODEL>): LanguageModelV2;
5
+ declare function createRetryable<MODEL extends EmbeddingModelV2>(options: RetryableModelOptions<MODEL>): EmbeddingModelV2;
6
+ //#endregion
4
7
  //#region src/get-model-key.d.ts
5
8
  /**
6
9
  * Generate a unique key for a LanguageModelV2 instance.
7
10
  */
8
- declare const getModelKey: (model: LanguageModelV2) => string;
11
+ declare const getModelKey: (model: LanguageModelV2 | EmbeddingModelV2) => string;
9
12
  //#endregion
10
- export { CreateRetryableOptions, RetryAttempt, RetryContext, RetryModel, Retryable, createRetryable, getModelKey, isErrorAttempt, isResultAttempt };
13
+ export { createRetryable, getModelKey };
package/dist/index.js CHANGED
@@ -1,3 +1,404 @@
1
- import { createRetryable, getModelKey, isErrorAttempt, isResultAttempt } from "./create-retryable-model-C3mm1hSN.js";
1
+ import { isErrorAttempt, isGenerateResult, isResultAttempt, isStreamContentPart } from "./utils-lRsC105f.js";
2
+ import "@ai-sdk/provider-utils";
3
+ import { RetryError } from "ai";
4
+ import { getErrorMessage } from "@ai-sdk/provider";
2
5
 
3
- export { createRetryable, getModelKey, isErrorAttempt, isResultAttempt };
6
+ //#region src/get-model-key.ts
7
+ /**
8
+ * Generate a unique key for a LanguageModelV2 instance.
9
+ */
10
+ const getModelKey = (model) => {
11
+ return `${model.provider}/${model.modelId}`;
12
+ };
13
+
14
+ //#endregion
15
+ //#region src/find-retry-model.ts
16
+ /**
17
+ * Find the next model to retry with based on the retry context
18
+ */
19
+ async function findRetryModel(retries, context) {
20
+ /**
21
+ * Filter retryables based on attempt type:
22
+ * - Result-based attempts: Only consider function retryables (skip plain models)
23
+ * - Error-based attempts: Consider all retryables (functions + plain models)
24
+ */
25
+ const applicableRetries = isResultAttempt(context.current) ? retries.filter((retry) => typeof retry === "function") : retries;
26
+ /**
27
+ * Iterate through the applicable retryables to find a model to retry with
28
+ */
29
+ for (const retry of applicableRetries) {
30
+ const retryModel = typeof retry === "function" ? await retry(context) : {
31
+ model: retry,
32
+ maxAttempts: 1
33
+ };
34
+ if (retryModel) {
35
+ /**
36
+ * The model key uniquely identifies a model instance (provider + modelId)
37
+ */
38
+ const retryModelKey = getModelKey(retryModel.model);
39
+ /**
40
+ * Find all attempts with the same model
41
+ */
42
+ const retryAttempts = context.attempts.filter((a) => getModelKey(a.model) === retryModelKey);
43
+ const maxAttempts = retryModel.maxAttempts ?? 1;
44
+ /**
45
+ * Check if the model can still be retried based on maxAttempts
46
+ */
47
+ if (retryAttempts.length < maxAttempts) return retryModel.model;
48
+ }
49
+ }
50
+ }
51
+
52
+ //#endregion
53
+ //#region src/prepare-retry-error.ts
54
+ /**
55
+ * Prepare a RetryError that includes all errors from previous attempts.
56
+ */
57
+ function prepareRetryError(error, attempts) {
58
+ const errorMessage = getErrorMessage(error);
59
+ const errors = attempts.flatMap((a) => isErrorAttempt(a) ? a.error : `Result with finishReason: ${a.result.finishReason}`);
60
+ return new RetryError({
61
+ message: `Failed after ${attempts.length} attempts. Last error: ${errorMessage}`,
62
+ reason: "maxRetriesExceeded",
63
+ errors
64
+ });
65
+ }
66
+
67
+ //#endregion
68
+ //#region src/retryable-embedding-model.ts
69
+ var RetryableEmbeddingModel = class {
70
+ specificationVersion = "v2";
71
+ baseModel;
72
+ currentModel;
73
+ options;
74
+ get modelId() {
75
+ return this.currentModel.modelId;
76
+ }
77
+ get provider() {
78
+ return this.currentModel.provider;
79
+ }
80
+ get maxEmbeddingsPerCall() {
81
+ return this.currentModel.maxEmbeddingsPerCall;
82
+ }
83
+ get supportsParallelCalls() {
84
+ return this.currentModel.supportsParallelCalls;
85
+ }
86
+ constructor(options) {
87
+ this.options = options;
88
+ this.baseModel = options.model;
89
+ this.currentModel = options.model;
90
+ }
91
+ /**
92
+ * Execute a function with retry logic for handling errors
93
+ */
94
+ async withRetry(input) {
95
+ /**
96
+ * Track all attempts.
97
+ */
98
+ const attempts = input.attempts ?? [];
99
+ while (true) {
100
+ /**
101
+ * The previous attempt that triggered a retry, or undefined if this is the first attempt
102
+ */
103
+ const previousAttempt = attempts.at(-1);
104
+ /**
105
+ * Call the onRetry handler if provided.
106
+ * Skip on the first attempt since no previous attempt exists yet.
107
+ */
108
+ if (previousAttempt) {
109
+ const currentAttempt = {
110
+ ...previousAttempt,
111
+ model: this.currentModel
112
+ };
113
+ /**
114
+ * Create a shallow copy of the attempts for testing purposes
115
+ */
116
+ const updatedAttempts = [...attempts];
117
+ const context = {
118
+ current: currentAttempt,
119
+ attempts: updatedAttempts
120
+ };
121
+ this.options.onRetry?.(context);
122
+ }
123
+ try {
124
+ return {
125
+ result: await input.fn(),
126
+ attempts
127
+ };
128
+ } catch (error) {
129
+ const { nextModel, attempt } = await this.handleError(error, attempts);
130
+ attempts.push(attempt);
131
+ this.currentModel = nextModel;
132
+ }
133
+ }
134
+ }
135
+ /**
136
+ * Handle an error and determine if a retry is needed
137
+ */
138
+ async handleError(error, attempts) {
139
+ const errorAttempt = {
140
+ type: "error",
141
+ error,
142
+ model: this.currentModel
143
+ };
144
+ /**
145
+ * Save the current attempt
146
+ */
147
+ const updatedAttempts = [...attempts, errorAttempt];
148
+ const context = {
149
+ current: errorAttempt,
150
+ attempts: updatedAttempts
151
+ };
152
+ this.options.onError?.(context);
153
+ const nextModel = await findRetryModel(this.options.retries, context);
154
+ /**
155
+ * Handler didn't return any models to try next, rethrow the error.
156
+ * If we retried the request, wrap the error into a `RetryError` for better visibility.
157
+ */
158
+ if (!nextModel) {
159
+ if (updatedAttempts.length > 1) throw prepareRetryError(error, updatedAttempts);
160
+ throw error;
161
+ }
162
+ return {
163
+ nextModel,
164
+ attempt: errorAttempt
165
+ };
166
+ }
167
+ async doEmbed(options) {
168
+ /**
169
+ * Always start with the original model
170
+ */
171
+ this.currentModel = this.baseModel;
172
+ const { result } = await this.withRetry({ fn: async () => await this.currentModel.doEmbed(options) });
173
+ return result;
174
+ }
175
+ };
176
+
177
+ //#endregion
178
+ //#region src/retryable-language-model.ts
179
+ var RetryableLanguageModel = class {
180
+ specificationVersion = "v2";
181
+ baseModel;
182
+ currentModel;
183
+ options;
184
+ get modelId() {
185
+ return this.currentModel.modelId;
186
+ }
187
+ get provider() {
188
+ return this.currentModel.provider;
189
+ }
190
+ get supportedUrls() {
191
+ return this.currentModel.supportedUrls;
192
+ }
193
+ constructor(options) {
194
+ this.options = options;
195
+ this.baseModel = options.model;
196
+ this.currentModel = options.model;
197
+ }
198
+ /**
199
+ * Execute a function with retry logic for handling errors
200
+ */
201
+ async withRetry(input) {
202
+ /**
203
+ * Track all attempts.
204
+ */
205
+ const attempts = input.attempts ?? [];
206
+ while (true) {
207
+ /**
208
+ * The previous attempt that triggered a retry, or undefined if this is the first attempt
209
+ */
210
+ const previousAttempt = attempts.at(-1);
211
+ /**
212
+ * Call the onRetry handler if provided.
213
+ * Skip on the first attempt since no previous attempt exists yet.
214
+ */
215
+ if (previousAttempt) {
216
+ const currentAttempt = {
217
+ ...previousAttempt,
218
+ model: this.currentModel
219
+ };
220
+ /**
221
+ * Create a shallow copy of the attempts for testing purposes
222
+ */
223
+ const updatedAttempts = [...attempts];
224
+ const context = {
225
+ current: currentAttempt,
226
+ attempts: updatedAttempts
227
+ };
228
+ this.options.onRetry?.(context);
229
+ }
230
+ try {
231
+ /**
232
+ * Call the function that may need to be retried
233
+ */
234
+ const result = await input.fn();
235
+ /**
236
+ * Check if the result should trigger a retry (only for generate results, not streams)
237
+ */
238
+ if (isGenerateResult(result)) {
239
+ const { nextModel, attempt } = await this.handleResult(result, attempts);
240
+ attempts.push(attempt);
241
+ if (nextModel) {
242
+ this.currentModel = nextModel;
243
+ /**
244
+ * Continue to the next iteration to retry
245
+ */
246
+ continue;
247
+ }
248
+ }
249
+ return {
250
+ result,
251
+ attempts
252
+ };
253
+ } catch (error) {
254
+ const { nextModel, attempt } = await this.handleError(error, attempts);
255
+ attempts.push(attempt);
256
+ this.currentModel = nextModel;
257
+ }
258
+ }
259
+ }
260
+ /**
261
+ * Handle a successful result and determine if a retry is needed
262
+ */
263
+ async handleResult(result, attempts) {
264
+ const resultAttempt = {
265
+ type: "result",
266
+ result,
267
+ model: this.currentModel
268
+ };
269
+ /**
270
+ * Save the current attempt
271
+ */
272
+ const updatedAttempts = [...attempts, resultAttempt];
273
+ const context = {
274
+ current: resultAttempt,
275
+ attempts: updatedAttempts
276
+ };
277
+ return {
278
+ nextModel: await findRetryModel(this.options.retries, context),
279
+ attempt: resultAttempt
280
+ };
281
+ }
282
+ /**
283
+ * Handle an error and determine if a retry is needed
284
+ */
285
+ async handleError(error, attempts) {
286
+ const errorAttempt = {
287
+ type: "error",
288
+ error,
289
+ model: this.currentModel
290
+ };
291
+ /**
292
+ * Save the current attempt
293
+ */
294
+ const updatedAttempts = [...attempts, errorAttempt];
295
+ const context = {
296
+ current: errorAttempt,
297
+ attempts: updatedAttempts
298
+ };
299
+ this.options.onError?.(context);
300
+ const nextModel = await findRetryModel(this.options.retries, context);
301
+ /**
302
+ * Handler didn't return any models to try next, rethrow the error.
303
+ * If we retried the request, wrap the error into a `RetryError` for better visibility.
304
+ */
305
+ if (!nextModel) {
306
+ if (updatedAttempts.length > 1) throw prepareRetryError(error, updatedAttempts);
307
+ throw error;
308
+ }
309
+ return {
310
+ nextModel,
311
+ attempt: errorAttempt
312
+ };
313
+ }
314
+ async doGenerate(options) {
315
+ /**
316
+ * Always start with the original model
317
+ */
318
+ this.currentModel = this.baseModel;
319
+ const { result } = await this.withRetry({ fn: async () => await this.currentModel.doGenerate(options) });
320
+ return result;
321
+ }
322
+ async doStream(options) {
323
+ /**
324
+ * Always start with the original model
325
+ */
326
+ this.currentModel = this.baseModel;
327
+ /**
328
+ * Perform the initial call to doStream with retry logic to handle errors before any data is streamed.
329
+ */
330
+ let { result, attempts } = await this.withRetry({ fn: async () => await this.currentModel.doStream(options) });
331
+ /**
332
+ * Wrap the original stream to handle retries if an error occurs during streaming.
333
+ */
334
+ const retryableStream = new ReadableStream({ start: async (controller) => {
335
+ let reader;
336
+ let isStreaming = false;
337
+ while (true) try {
338
+ reader = result.stream.getReader();
339
+ while (true) {
340
+ const { done, value } = await reader.read();
341
+ if (done) break;
342
+ /**
343
+ * If the stream part is an error and no data has been streamed yet, we can retry
344
+ * Throw the error to trigger the retry logic in withRetry
345
+ */
346
+ if (value.type === "error") {
347
+ if (!isStreaming) throw value.error;
348
+ }
349
+ /**
350
+ * Mark that streaming has started once we receive actual content
351
+ */
352
+ if (isStreamContentPart(value)) isStreaming = true;
353
+ /**
354
+ * Enqueue the chunk to the consumer of the stream
355
+ */
356
+ controller.enqueue(value);
357
+ }
358
+ controller.close();
359
+ break;
360
+ } catch (error) {
361
+ /**
362
+ * Check if the error from the stream can be retried.
363
+ * Otherwise it will rethrow the error.
364
+ */
365
+ const { nextModel, attempt } = await this.handleError(error, attempts);
366
+ this.currentModel = nextModel;
367
+ /**
368
+ * Save the attempt
369
+ */
370
+ attempts.push(attempt);
371
+ /**
372
+ * Retry the request by calling doStream again.
373
+ * This will create a new stream.
374
+ */
375
+ const retriedResult = await this.withRetry({
376
+ fn: async () => await this.currentModel.doStream(options),
377
+ attempts
378
+ });
379
+ /**
380
+ * Cancel the previous reader and stream if we are retrying
381
+ */
382
+ await reader?.cancel();
383
+ result = retriedResult.result;
384
+ attempts = retriedResult.attempts;
385
+ } finally {
386
+ reader?.releaseLock();
387
+ }
388
+ } });
389
+ return {
390
+ ...result,
391
+ stream: retryableStream
392
+ };
393
+ }
394
+ };
395
+
396
+ //#endregion
397
+ //#region src/create-retryable-model.ts
398
+ function createRetryable(options) {
399
+ if ("doEmbed" in options.model) return new RetryableEmbeddingModel(options);
400
+ return new RetryableLanguageModel(options);
401
+ }
402
+
403
+ //#endregion
404
+ export { createRetryable, getModelKey };
@@ -1,50 +1,25 @@
1
- import { RetryModel, Retryable } from "../create-retryable-model-DtspEawi.js";
2
- import { LanguageModelV2 } from "@ai-sdk/provider";
1
+ import { EmbeddingModelV2, LanguageModelV2, RetryModel, Retryable } from "../types-CHhEGL5x.js";
3
2
 
4
- //#region src/retryables/anthropic-service-overloaded.d.ts
5
-
6
- /**
7
- * Type for Anthropic error responses.
8
- *
9
- * @see https://docs.claude.com/en/api/errors#error-shapes
10
- */
11
- type AnthropicErrorResponse = {
12
- type: 'error';
13
- error: {
14
- type: string;
15
- message: string;
16
- };
17
- };
18
- /**
19
- * Fallback if Anthropic returns an "overloaded" error with HTTP 200.
20
- *
21
- * ```
22
- * HTTP 200 OK
23
- * {"type":"error","error":{"type":"overloaded_error","message":"Overloaded"}}
24
- * ```
25
- *
26
- * @deprecated Use `serviceOverloaded` instead
27
- */
28
- declare function anthropicServiceOverloaded(model: LanguageModelV2, options?: Omit<RetryModel, 'model'>): Retryable;
29
- //#endregion
30
3
  //#region src/retryables/content-filter-triggered.d.ts
4
+
31
5
  /**
32
6
  * Fallback to a different model if the content filter was triggered.
33
7
  */
34
- declare function contentFilterTriggered(model: LanguageModelV2, options?: Omit<RetryModel, 'model'>): Retryable;
8
+ declare function contentFilterTriggered<MODEL extends LanguageModelV2>(model: MODEL, options?: Omit<RetryModel<MODEL>, 'model'>): Retryable<MODEL>;
35
9
  //#endregion
36
10
  //#region src/retryables/request-not-retryable.d.ts
37
11
  /**
38
12
  * Fallback to a different model if the error is non-retryable.
39
13
  */
40
- declare function requestNotRetryable(model: LanguageModelV2, options?: Omit<RetryModel, 'model'>): Retryable;
14
+ declare function requestNotRetryable<MODEL extends LanguageModelV2 | EmbeddingModelV2>(model: MODEL, options?: Omit<RetryModel<MODEL>, 'model'>): Retryable<MODEL>;
41
15
  //#endregion
42
16
  //#region src/retryables/request-timeout.d.ts
43
17
  /**
44
18
  * Fallback to a different model after a timeout/abort error.
45
- * Use in combination with the `abortSignal` option in `generateText`.
19
+ * Use in combination with the `abortSignal` option.
20
+ * Works with both `LanguageModelV2` and `EmbeddingModelV2`.
46
21
  */
47
- declare function requestTimeout(model: LanguageModelV2, options?: Omit<RetryModel, 'model'>): Retryable;
22
+ declare function requestTimeout<MODEL extends LanguageModelV2 | EmbeddingModelV2>(model: MODEL, options?: Omit<RetryModel<MODEL>, 'model'>): Retryable<MODEL>;
48
23
  //#endregion
49
24
  //#region src/retryables/service-overloaded.d.ts
50
25
  /**
@@ -54,6 +29,6 @@ declare function requestTimeout(model: LanguageModelV2, options?: Omit<RetryMode
54
29
  * - Response with `type: "overloaded_error"`
55
30
  * - Response with a `message` containing "overloaded"
56
31
  */
57
- declare function serviceOverloaded(model: LanguageModelV2, options?: Omit<RetryModel, 'model'>): Retryable;
32
+ declare function serviceOverloaded<MODEL extends LanguageModelV2 | EmbeddingModelV2>(model: MODEL, options?: Omit<RetryModel<MODEL>, 'model'>): Retryable<MODEL>;
58
33
  //#endregion
59
- export { AnthropicErrorResponse, anthropicServiceOverloaded, contentFilterTriggered, requestNotRetryable, requestTimeout, serviceOverloaded };
34
+ export { contentFilterTriggered, requestNotRetryable, requestTimeout, serviceOverloaded };
@@ -1,38 +1,7 @@
1
- import { isErrorAttempt, isObject, isResultAttempt, isString } from "../create-retryable-model-C3mm1hSN.js";
1
+ import { isErrorAttempt, isObject, isResultAttempt, isString } from "../utils-lRsC105f.js";
2
2
  import { isAbortError } from "@ai-sdk/provider-utils";
3
3
  import { APICallError } from "ai";
4
4
 
5
- //#region src/retryables/anthropic-service-overloaded.ts
6
- /**
7
- * Fallback if Anthropic returns an "overloaded" error with HTTP 200.
8
- *
9
- * ```
10
- * HTTP 200 OK
11
- * {"type":"error","error":{"type":"overloaded_error","message":"Overloaded"}}
12
- * ```
13
- *
14
- * @deprecated Use `serviceOverloaded` instead
15
- */
16
- function anthropicServiceOverloaded(model, options) {
17
- return (context) => {
18
- const { current } = context;
19
- if (isErrorAttempt(current)) {
20
- const { error } = current;
21
- if (APICallError.isInstance(error) && error.statusCode === 529) return {
22
- model,
23
- maxAttempts: 1,
24
- ...options
25
- };
26
- if (isObject(error) && isString(error.type) && error.type === "overloaded_error") return {
27
- model,
28
- maxAttempts: 1,
29
- ...options
30
- };
31
- }
32
- };
33
- }
34
-
35
- //#endregion
36
5
  //#region src/retryables/content-filter-triggered.ts
37
6
  /**
38
7
  * Fallback to a different model if the content filter was triggered.
@@ -81,7 +50,8 @@ function requestNotRetryable(model, options) {
81
50
  //#region src/retryables/request-timeout.ts
82
51
  /**
83
52
  * Fallback to a different model after a timeout/abort error.
84
- * Use in combination with the `abortSignal` option in `generateText`.
53
+ * Use in combination with the `abortSignal` option.
54
+ * Works with both `LanguageModelV2` and `EmbeddingModelV2`.
85
55
  */
86
56
  function requestTimeout(model, options) {
87
57
  return (context) => {
@@ -130,4 +100,4 @@ function serviceOverloaded(model, options) {
130
100
  }
131
101
 
132
102
  //#endregion
133
- export { anthropicServiceOverloaded, contentFilterTriggered, requestNotRetryable, requestTimeout, serviceOverloaded };
103
+ export { contentFilterTriggered, requestNotRetryable, requestTimeout, serviceOverloaded };
@@ -0,0 +1,63 @@
1
+ import { EmbeddingModelV2, LanguageModelV2 } from "@ai-sdk/provider";
2
+
3
+ //#region src/types.d.ts
4
+ type EmbeddingModelV2$1<VALUE = any> = EmbeddingModelV2<VALUE>;
5
+ /**
6
+ * Options for creating a retryable model.
7
+ */
8
+ interface RetryableModelOptions<MODEL extends LanguageModelV2 | EmbeddingModelV2$1> {
9
+ model: MODEL;
10
+ retries: Retries<MODEL>;
11
+ onError?: (context: RetryContext<MODEL>) => void;
12
+ onRetry?: (context: RetryContext<MODEL>) => void;
13
+ }
14
+ /**
15
+ * The context provided to Retryables with the current attempt and all previous attempts.
16
+ */
17
+ type RetryContext<MODEL extends LanguageModelV2 | EmbeddingModelV2$1> = {
18
+ /**
19
+ * Current attempt that caused the retry
20
+ */
21
+ current: RetryAttempt<MODEL>;
22
+ /**
23
+ * All attempts made so far, including the current one
24
+ */
25
+ attempts: Array<RetryAttempt<MODEL>>;
26
+ };
27
+ /**
28
+ * A retry attempt with an error
29
+ */
30
+ type RetryErrorAttempt<MODEL extends LanguageModelV2 | EmbeddingModelV2$1> = {
31
+ type: 'error';
32
+ error: unknown;
33
+ result?: undefined;
34
+ model: MODEL;
35
+ };
36
+ /**
37
+ * A retry attempt with a successful result
38
+ */
39
+ type RetryResultAttempt = {
40
+ type: 'result';
41
+ result: LanguageModelV2Generate;
42
+ error?: undefined;
43
+ model: LanguageModelV2;
44
+ };
45
+ /**
46
+ * A retry attempt with either an error or a result and the model used
47
+ */
48
+ type RetryAttempt<MODEL extends LanguageModelV2 | EmbeddingModelV2$1> = RetryErrorAttempt<MODEL> | RetryResultAttempt;
49
+ /**
50
+ * A model to retry with and the maximum number of attempts for that model.
51
+ */
52
+ type RetryModel<MODEL extends LanguageModelV2 | EmbeddingModelV2$1> = {
53
+ model: MODEL;
54
+ maxAttempts?: number;
55
+ };
56
+ /**
57
+ * A function that determines whether to retry with a different model based on the current attempt and all previous attempts.
58
+ */
59
+ type Retryable<MODEL extends LanguageModelV2 | EmbeddingModelV2$1> = (context: RetryContext<MODEL>) => RetryModel<MODEL> | Promise<RetryModel<MODEL>> | undefined;
60
+ type Retries<MODEL extends LanguageModelV2 | EmbeddingModelV2$1> = Array<Retryable<MODEL> | MODEL>;
61
+ type LanguageModelV2Generate = Awaited<ReturnType<LanguageModelV2['doGenerate']>>;
62
+ //#endregion
63
+ export { EmbeddingModelV2$1 as EmbeddingModelV2, type LanguageModelV2, RetryModel, Retryable, RetryableModelOptions };
@@ -0,0 +1,27 @@
1
+ //#region src/utils.ts
2
+ const isObject = (value) => typeof value === "object" && value !== null;
3
+ const isString = (value) => typeof value === "string";
4
+ const isGenerateResult = (result) => "content" in result;
5
+ /**
6
+ * Type guard to check if a retry attempt is an error attempt
7
+ */
8
+ function isErrorAttempt(attempt) {
9
+ return attempt.type === "error";
10
+ }
11
+ /**
12
+ * Type guard to check if a retry attempt is a result attempt
13
+ */
14
+ function isResultAttempt(attempt) {
15
+ return attempt.type === "result";
16
+ }
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
+ export { isErrorAttempt, isGenerateResult, isObject, isResultAttempt, isStreamContentPart, isString };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-retry",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "AI SDK Retry",
5
5
  "packageManager": "pnpm@9.0.0",
6
6
  "main": "./dist/index.js",
@@ -1,308 +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/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 };
@@ -1,74 +0,0 @@
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
- /**
12
- * Current attempt that caused the retry
13
- */
14
- current: CURRENT;
15
- /**
16
- * All attempts made so far, including the current one
17
- */
18
- attempts: Array<RetryAttempt>;
19
- /**
20
- * @deprecated Use `attempts.length` instead
21
- */
22
- totalAttempts: number;
23
- }
24
- /**
25
- * A retry attempt with an error
26
- */
27
- type RetryErrorAttempt = {
28
- type: 'error';
29
- error: unknown;
30
- model: LanguageModelV2;
31
- };
32
- /**
33
- * A retry attempt with a successful result
34
- */
35
- type RetryResultAttempt = {
36
- type: 'result';
37
- result: LanguageModelV2Generate;
38
- model: LanguageModelV2;
39
- };
40
- /**
41
- * A retry attempt with either an error or a result and the model used
42
- */
43
- type RetryAttempt = RetryErrorAttempt | RetryResultAttempt;
44
- /**
45
- * Type guard to check if a retry attempt is an error attempt
46
- */
47
- declare function isErrorAttempt(attempt: RetryAttempt): attempt is RetryErrorAttempt;
48
- /**
49
- * Type guard to check if a retry attempt is a result attempt
50
- */
51
- declare function isResultAttempt(attempt: RetryAttempt): attempt is RetryResultAttempt;
52
- /**
53
- * A model to retry with and the maximum number of attempts for that model.
54
- */
55
- type RetryModel = {
56
- model: LanguageModelV2;
57
- maxAttempts?: number;
58
- };
59
- /**
60
- * A function that determines whether to retry with a different model based on the current attempt and all previous attempts.
61
- */
62
- type Retryable = (context: RetryContext) => RetryModel | Promise<RetryModel> | undefined;
63
- /**
64
- * Options for creating a retryable model.
65
- */
66
- interface CreateRetryableOptions {
67
- model: LanguageModelV2;
68
- retries: Array<Retryable | LanguageModelV2>;
69
- onError?: (context: RetryContext<RetryErrorAttempt>) => void;
70
- onRetry?: (context: RetryContext<RetryErrorAttempt | RetryResultAttempt>) => void;
71
- }
72
- declare function createRetryable(config: CreateRetryableOptions): LanguageModelV2;
73
- //#endregion
74
- export { CreateRetryableOptions, RetryAttempt, RetryContext, RetryModel, Retryable, createRetryable, isErrorAttempt, isResultAttempt };