ai-retry 0.1.1 → 0.3.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
81
96
 
82
- Retry with different models when structured output validation fails:
97
+ Automatically switch to a different model when content filtering blocks your request.
98
+
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
 
@@ -242,6 +247,42 @@ try {
242
247
  }
243
248
  ```
244
249
 
250
+ #### Retry Delays
251
+
252
+ You can add delays before retrying to handle rate limiting or give services time to recover. The delay respects abort signals, so requests can still be cancelled during the delay period.
253
+
254
+ ```typescript
255
+ const retryableModel = createRetryable({
256
+ model: openai('gpt-4'),
257
+ retries: [
258
+ // Wait 1 second before retrying
259
+ () => ({
260
+ model: openai('gpt-4'),
261
+ delay: 1_000
262
+ }),
263
+ // Wait 2 seconds before trying a different provider
264
+ () => ({
265
+ model: anthropic('claude-3-haiku-20240307'),
266
+ delay: 2_000
267
+ }),
268
+ ],
269
+ });
270
+ ```
271
+
272
+ You can also use delays with built-in retryables:
273
+
274
+ ```typescript
275
+ import { serviceOverloaded } from 'ai-retry/retryables';
276
+
277
+ const retryableModel = createRetryable({
278
+ model: openai('gpt-4'),
279
+ retries: [
280
+ // Wait 5 seconds before retrying on service overload
281
+ serviceOverloaded(openai('gpt-4'), { maxAttempts: 3, delay: 5_000 }),
282
+ ],
283
+ });
284
+ ```
285
+
245
286
  #### Logging
246
287
 
247
288
  You can use the following callbacks to log retry attempts and errors:
@@ -287,16 +328,16 @@ By default, each retryable will only attempt to retry once per model to avoid in
287
328
 
288
329
  ### API Reference
289
330
 
290
- #### `createRetryable(options: CreateRetryableOptions): LanguageModelV2`
331
+ #### `createRetryable(options: RetryableModelOptions): LanguageModelV2 | EmbeddingModelV2`
291
332
 
292
- Creates a retryable language model.
333
+ Creates a retryable model that works with both language models and embedding models.
293
334
 
294
335
  ```ts
295
- interface CreateRetryableOptions {
296
- model: LanguageModelV2;
297
- retries: Array<Retryable | LanguageModelV2>;
298
- onError?: (context: RetryContext) => void;
299
- onRetry?: (context: RetryContext) => void;
336
+ interface RetryableModelOptions<MODEL extends LanguageModelV2 | EmbeddingModelV2> {
337
+ model: MODEL;
338
+ retries: Array<Retryable<MODEL> | MODEL>;
339
+ onError?: (context: RetryContext<MODEL>) => void;
340
+ onRetry?: (context: RetryContext<MODEL>) => void;
300
341
  }
301
342
  ```
302
343
 
@@ -306,21 +347,27 @@ A `Retryable` is a function that receives a `RetryContext` with the current erro
306
347
  It should evaluate the error/result and decide whether to retry by returning a `RetryModel` or to skip by returning `undefined`.
307
348
 
308
349
  ```ts
309
- type Retryable = (context: RetryContext) => RetryModel | Promise<RetryModel> | undefined;
350
+ type Retryable = (
351
+ context: RetryContext
352
+ ) => RetryModel | Promise<RetryModel> | undefined;
310
353
  ```
311
354
 
312
355
  #### `RetryModel`
313
356
 
314
- A `RetryModel` specifies the model to retry and an optional `maxAttempts` to limit how many times this model can be retried.
315
- By default, each retryable will only attempt to retry once per model. This can be customized by setting the `maxAttempts` property.
357
+ A `RetryModel` specifies the model to retry and optional settings like `maxAttempts` and `delay`.
316
358
 
317
359
  ```typescript
318
360
  interface RetryModel {
319
- model: LanguageModelV2;
320
- maxAttempts?: number;
361
+ model: LanguageModelV2 | EmbeddingModelV2;
362
+ maxAttempts?: number; // Maximum retry attempts per model (default: 1)
363
+ delay?: number; // Delay in milliseconds before retrying
321
364
  }
322
365
  ```
323
366
 
367
+ **Options:**
368
+ - `maxAttempts`: Maximum number of times this model can be retried. Default is 1.
369
+ - `delay`: Delay in milliseconds to wait before retrying. Useful for rate limiting or giving services time to recover. The delay respects abort signals from the request.
370
+
324
371
  #### `RetryContext`
325
372
 
326
373
  The `RetryContext` object contains information about the current attempt and all previous attempts.
@@ -337,10 +384,13 @@ interface RetryContext {
337
384
  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
385
 
339
386
  ```typescript
340
- type RetryAttempt =
341
- | { type: 'error'; error: unknown; model: LanguageModelV2 }
387
+ // For both language and embedding models
388
+ type RetryAttempt =
389
+ | { type: 'error'; error: unknown; model: LanguageModelV2 | EmbeddingModelV2 }
342
390
  | { type: 'result'; result: LanguageModelV2Generate; model: LanguageModelV2 };
343
391
 
392
+ // Note: Result-based retries only apply to language models, not embedding models
393
+
344
394
  // Type guards for discriminating attempts
345
395
  function isErrorAttempt(attempt: RetryAttempt): attempt is RetryErrorAttempt;
346
396
  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, EmbeddingModelV2CallOptions, EmbeddingModelV2Embed, LanguageModelV2, LanguageModelV2Generate, LanguageModelV2Stream, Retries, RetryAttempt, RetryContext, RetryErrorAttempt, RetryModel, RetryResultAttempt, Retryable, RetryableModelOptions } from "./types-BrJaHkFh.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 { EmbeddingModelV2, EmbeddingModelV2CallOptions, EmbeddingModelV2Embed, LanguageModelV2, LanguageModelV2Generate, LanguageModelV2Stream, Retries, RetryAttempt, RetryContext, RetryErrorAttempt, RetryModel, RetryResultAttempt, Retryable, RetryableModelOptions, createRetryable, getModelKey };
package/dist/index.js CHANGED
@@ -1,3 +1,418 @@
1
- import { createRetryable, getModelKey, isErrorAttempt, isResultAttempt } from "./create-retryable-model-YqmeNfbq.js";
1
+ import { isErrorAttempt, isGenerateResult, isResultAttempt, isStreamContentPart } from "./utils-lRsC105f.js";
2
+ import { delay } from "@ai-sdk/provider-utils";
3
+ import { getErrorMessage } from "@ai-sdk/provider";
4
+ import { RetryError } from "ai";
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;
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 { retryModel, attempt } = await this.handleError(error, attempts);
130
+ attempts.push(attempt);
131
+ if (retryModel.delay) await delay(retryModel.delay, { abortSignal: input.abortSignal });
132
+ this.currentModel = retryModel.model;
133
+ }
134
+ }
135
+ }
136
+ /**
137
+ * Handle an error and determine if a retry is needed
138
+ */
139
+ async handleError(error, attempts) {
140
+ const errorAttempt = {
141
+ type: "error",
142
+ error,
143
+ model: this.currentModel
144
+ };
145
+ /**
146
+ * Save the current attempt
147
+ */
148
+ const updatedAttempts = [...attempts, errorAttempt];
149
+ const context = {
150
+ current: errorAttempt,
151
+ attempts: updatedAttempts
152
+ };
153
+ this.options.onError?.(context);
154
+ const retryModel = await findRetryModel(this.options.retries, context);
155
+ /**
156
+ * Handler didn't return any models to try next, rethrow the error.
157
+ * If we retried the request, wrap the error into a `RetryError` for better visibility.
158
+ */
159
+ if (!retryModel) {
160
+ if (updatedAttempts.length > 1) throw prepareRetryError(error, updatedAttempts);
161
+ throw error;
162
+ }
163
+ return {
164
+ retryModel,
165
+ attempt: errorAttempt
166
+ };
167
+ }
168
+ async doEmbed(options) {
169
+ /**
170
+ * Always start with the original model
171
+ */
172
+ this.currentModel = this.baseModel;
173
+ const { result } = await this.withRetry({
174
+ fn: async () => await this.currentModel.doEmbed(options),
175
+ abortSignal: options.abortSignal
176
+ });
177
+ return result;
178
+ }
179
+ };
180
+
181
+ //#endregion
182
+ //#region src/retryable-language-model.ts
183
+ var RetryableLanguageModel = class {
184
+ specificationVersion = "v2";
185
+ baseModel;
186
+ currentModel;
187
+ options;
188
+ get modelId() {
189
+ return this.currentModel.modelId;
190
+ }
191
+ get provider() {
192
+ return this.currentModel.provider;
193
+ }
194
+ get supportedUrls() {
195
+ return this.currentModel.supportedUrls;
196
+ }
197
+ constructor(options) {
198
+ this.options = options;
199
+ this.baseModel = options.model;
200
+ this.currentModel = options.model;
201
+ }
202
+ /**
203
+ * Execute a function with retry logic for handling errors
204
+ */
205
+ async withRetry(input) {
206
+ /**
207
+ * Track all attempts.
208
+ */
209
+ const attempts = input.attempts ?? [];
210
+ while (true) {
211
+ /**
212
+ * The previous attempt that triggered a retry, or undefined if this is the first attempt
213
+ */
214
+ const previousAttempt = attempts.at(-1);
215
+ /**
216
+ * Call the onRetry handler if provided.
217
+ * Skip on the first attempt since no previous attempt exists yet.
218
+ */
219
+ if (previousAttempt) {
220
+ const currentAttempt = {
221
+ ...previousAttempt,
222
+ model: this.currentModel
223
+ };
224
+ /**
225
+ * Create a shallow copy of the attempts for testing purposes
226
+ */
227
+ const updatedAttempts = [...attempts];
228
+ const context = {
229
+ current: currentAttempt,
230
+ attempts: updatedAttempts
231
+ };
232
+ this.options.onRetry?.(context);
233
+ }
234
+ try {
235
+ /**
236
+ * Call the function that may need to be retried
237
+ */
238
+ const result = await input.fn();
239
+ /**
240
+ * Check if the result should trigger a retry (only for generate results, not streams)
241
+ */
242
+ if (isGenerateResult(result)) {
243
+ const { retryModel, attempt } = await this.handleResult(result, attempts);
244
+ attempts.push(attempt);
245
+ if (retryModel) {
246
+ if (retryModel.delay) await delay(retryModel.delay, { abortSignal: input.abortSignal });
247
+ this.currentModel = retryModel.model;
248
+ /**
249
+ * Continue to the next iteration to retry
250
+ */
251
+ continue;
252
+ }
253
+ }
254
+ return {
255
+ result,
256
+ attempts
257
+ };
258
+ } catch (error) {
259
+ const { retryModel, attempt } = await this.handleError(error, attempts);
260
+ attempts.push(attempt);
261
+ if (retryModel.delay) await delay(retryModel.delay, { abortSignal: input.abortSignal });
262
+ this.currentModel = retryModel.model;
263
+ }
264
+ }
265
+ }
266
+ /**
267
+ * Handle a successful result and determine if a retry is needed
268
+ */
269
+ async handleResult(result, attempts) {
270
+ const resultAttempt = {
271
+ type: "result",
272
+ result,
273
+ model: this.currentModel
274
+ };
275
+ /**
276
+ * Save the current attempt
277
+ */
278
+ const updatedAttempts = [...attempts, resultAttempt];
279
+ const context = {
280
+ current: resultAttempt,
281
+ attempts: updatedAttempts
282
+ };
283
+ return {
284
+ retryModel: await findRetryModel(this.options.retries, context),
285
+ attempt: resultAttempt
286
+ };
287
+ }
288
+ /**
289
+ * Handle an error and determine if a retry is needed
290
+ */
291
+ async handleError(error, attempts) {
292
+ const errorAttempt = {
293
+ type: "error",
294
+ error,
295
+ model: this.currentModel
296
+ };
297
+ /**
298
+ * Save the current attempt
299
+ */
300
+ const updatedAttempts = [...attempts, errorAttempt];
301
+ const context = {
302
+ current: errorAttempt,
303
+ attempts: updatedAttempts
304
+ };
305
+ this.options.onError?.(context);
306
+ const retryModel = await findRetryModel(this.options.retries, context);
307
+ /**
308
+ * Handler didn't return any models to try next, rethrow the error.
309
+ * If we retried the request, wrap the error into a `RetryError` for better visibility.
310
+ */
311
+ if (!retryModel) {
312
+ if (updatedAttempts.length > 1) throw prepareRetryError(error, updatedAttempts);
313
+ throw error;
314
+ }
315
+ return {
316
+ retryModel,
317
+ attempt: errorAttempt
318
+ };
319
+ }
320
+ async doGenerate(options) {
321
+ /**
322
+ * Always start with the original model
323
+ */
324
+ this.currentModel = this.baseModel;
325
+ const { result } = await this.withRetry({
326
+ fn: async () => await this.currentModel.doGenerate(options),
327
+ abortSignal: options.abortSignal
328
+ });
329
+ return result;
330
+ }
331
+ async doStream(options) {
332
+ /**
333
+ * Always start with the original model
334
+ */
335
+ this.currentModel = this.baseModel;
336
+ /**
337
+ * Perform the initial call to doStream with retry logic to handle errors before any data is streamed.
338
+ */
339
+ let { result, attempts } = await this.withRetry({
340
+ fn: async () => await this.currentModel.doStream(options),
341
+ abortSignal: options.abortSignal
342
+ });
343
+ /**
344
+ * Wrap the original stream to handle retries if an error occurs during streaming.
345
+ */
346
+ const retryableStream = new ReadableStream({ start: async (controller) => {
347
+ let reader;
348
+ let isStreaming = false;
349
+ while (true) try {
350
+ reader = result.stream.getReader();
351
+ while (true) {
352
+ const { done, value } = await reader.read();
353
+ if (done) break;
354
+ /**
355
+ * If the stream part is an error and no data has been streamed yet, we can retry
356
+ * Throw the error to trigger the retry logic in withRetry
357
+ */
358
+ if (value.type === "error") {
359
+ if (!isStreaming) throw value.error;
360
+ }
361
+ /**
362
+ * Mark that streaming has started once we receive actual content
363
+ */
364
+ if (isStreamContentPart(value)) isStreaming = true;
365
+ /**
366
+ * Enqueue the chunk to the consumer of the stream
367
+ */
368
+ controller.enqueue(value);
369
+ }
370
+ controller.close();
371
+ break;
372
+ } catch (error) {
373
+ /**
374
+ * Check if the error from the stream can be retried.
375
+ * Otherwise it will rethrow the error.
376
+ */
377
+ const { retryModel, attempt } = await this.handleError(error, attempts);
378
+ /**
379
+ * Save the attempt
380
+ */
381
+ attempts.push(attempt);
382
+ if (retryModel.delay) await delay(retryModel.delay, { abortSignal: options.abortSignal });
383
+ this.currentModel = retryModel.model;
384
+ /**
385
+ * Retry the request by calling doStream again.
386
+ * This will create a new stream.
387
+ */
388
+ const retriedResult = await this.withRetry({
389
+ fn: async () => await this.currentModel.doStream(options),
390
+ attempts,
391
+ abortSignal: options.abortSignal
392
+ });
393
+ /**
394
+ * Cancel the previous reader and stream if we are retrying
395
+ */
396
+ await reader?.cancel();
397
+ result = retriedResult.result;
398
+ attempts = retriedResult.attempts;
399
+ } finally {
400
+ reader?.releaseLock();
401
+ }
402
+ } });
403
+ return {
404
+ ...result,
405
+ stream: retryableStream
406
+ };
407
+ }
408
+ };
409
+
410
+ //#endregion
411
+ //#region src/create-retryable-model.ts
412
+ function createRetryable(options) {
413
+ if ("doEmbed" in options.model) return new RetryableEmbeddingModel(options);
414
+ return new RetryableLanguageModel(options);
415
+ }
416
+
417
+ //#endregion
418
+ 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-BrJaHkFh.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-YqmeNfbq.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,67 @@
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
+ delay?: number;
56
+ };
57
+ /**
58
+ * A function that determines whether to retry with a different model based on the current attempt and all previous attempts.
59
+ */
60
+ type Retryable<MODEL extends LanguageModelV2 | EmbeddingModelV2$1> = (context: RetryContext<MODEL>) => RetryModel<MODEL> | Promise<RetryModel<MODEL>> | undefined;
61
+ type Retries<MODEL extends LanguageModelV2 | EmbeddingModelV2$1> = Array<Retryable<MODEL> | MODEL>;
62
+ type LanguageModelV2Generate = Awaited<ReturnType<LanguageModelV2['doGenerate']>>;
63
+ type LanguageModelV2Stream = Awaited<ReturnType<LanguageModelV2['doStream']>>;
64
+ type EmbeddingModelV2CallOptions<VALUE> = Parameters<EmbeddingModelV2$1<VALUE>['doEmbed']>[0];
65
+ type EmbeddingModelV2Embed<VALUE> = Awaited<ReturnType<EmbeddingModelV2$1<VALUE>['doEmbed']>>;
66
+ //#endregion
67
+ export { EmbeddingModelV2$1 as EmbeddingModelV2, EmbeddingModelV2CallOptions, EmbeddingModelV2Embed, type LanguageModelV2, LanguageModelV2Generate, LanguageModelV2Stream, Retries, RetryAttempt, RetryContext, RetryErrorAttempt, RetryModel, RetryResultAttempt, 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.1",
3
+ "version": "0.3.0",
4
4
  "description": "AI SDK Retry",
5
5
  "packageManager": "pnpm@9.0.0",
6
6
  "main": "./dist/index.js",
@@ -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 };
@@ -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({
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 };