ai-retry 0.0.3 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,10 +24,7 @@ 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
- > [!NOTE]
29
- > `ai-retry` currently supports `generateText`, `generateObject`, `streamText`, and `streamObject` calls.
30
- > Note that streaming retry has limitations: retries are only possible before content starts flowing or very early in the stream.
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';
@@ -134,7 +131,10 @@ const result = await generateText({
134
131
 
135
132
  #### Service Overloaded
136
133
 
137
- 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.
135
+
136
+ > [!NOTE]
137
+ > You can use this retryable to handle Anthropic's overloaded errors.
138
138
 
139
139
  ```typescript
140
140
  import { serviceOverloaded } from 'ai-retry/retryables';
@@ -253,14 +253,25 @@ const retryableModel = createRetryable({
253
253
  model: openai('gpt-4-mini'),
254
254
  retries: [/* your retryables */],
255
255
  onError: (context) => {
256
- 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
+ );
257
259
  },
258
260
  onRetry: (context) => {
259
- 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}...`);
260
262
  },
261
263
  });
262
264
  ```
263
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
+
264
275
  ### Retryables
265
276
 
266
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.
@@ -270,6 +281,7 @@ There are several built-in retryables:
270
281
  - [`requestTimeout`](./src/retryables/request-timeout.ts): Request timeout occurred.
271
282
  - [`requestNotRetryable`](./src/retryables/request-not-retryable.ts): Request failed with a non-retryable error.
272
283
  - [`serviceOverloaded`](./src/retryables/service-overloaded.ts): Response with status code 529 (service overloaded).
284
+ - Use this retryable to handle Anthropic's overloaded errors.
273
285
 
274
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.
275
287
 
@@ -317,7 +329,6 @@ The `RetryContext` object contains information about the current attempt and all
317
329
  interface RetryContext {
318
330
  current: RetryAttempt;
319
331
  attempts: Array<RetryAttempt>;
320
- totalAttempts: number;
321
332
  }
322
333
  ```
323
334
 
@@ -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;
@@ -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({
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 };
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-C4nAHxnW.js";
1
+ import { createRetryable, getModelKey, isErrorAttempt, isResultAttempt } from "./create-retryable-model-YqmeNfbq.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
@@ -46,7 +48,11 @@ declare function requestTimeout(model: LanguageModelV2, options?: Omit<RetryMode
46
48
  //#endregion
47
49
  //#region src/retryables/service-overloaded.d.ts
48
50
  /**
49
- * 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"
50
56
  */
51
57
  declare function serviceOverloaded(model: LanguageModelV2, options?: Omit<RetryModel, 'model'>): Retryable;
52
58
  //#endregion
@@ -1,4 +1,4 @@
1
- import { isErrorAttempt, isObject, isResultAttempt, isString } from "../create-retryable-model-C4nAHxnW.js";
1
+ import { isErrorAttempt, isObject, isResultAttempt, isString } from "../create-retryable-model-YqmeNfbq.js";
2
2
  import { isAbortError } from "@ai-sdk/provider-utils";
3
3
  import { APICallError } from "ai";
4
4
 
@@ -10,6 +10,8 @@ import { APICallError } from "ai";
10
10
  * HTTP 200 OK
11
11
  * {"type":"error","error":{"type":"overloaded_error","message":"Overloaded"}}
12
12
  * ```
13
+ *
14
+ * @deprecated Use `serviceOverloaded` instead
13
15
  */
14
16
  function anthropicServiceOverloaded(model, options) {
15
17
  return (context) => {
@@ -21,14 +23,11 @@ function anthropicServiceOverloaded(model, options) {
21
23
  maxAttempts: 1,
22
24
  ...options
23
25
  };
24
- if (APICallError.isInstance(error) && error.statusCode === 200) try {
25
- const responseBody = JSON.parse(error.responseBody ?? "");
26
- if (responseBody.error && isObject(responseBody.error) && isString(responseBody.error.type) && responseBody.error.type === "overloaded_error") return {
27
- model,
28
- maxAttempts: 1,
29
- ...options
30
- };
31
- } catch {}
26
+ if (isObject(error) && isString(error.type) && error.type === "overloaded_error") return {
27
+ model,
28
+ maxAttempts: 1,
29
+ ...options
30
+ };
32
31
  }
33
32
  };
34
33
  }
@@ -103,7 +102,11 @@ function requestTimeout(model, options) {
103
102
  //#endregion
104
103
  //#region src/retryables/service-overloaded.ts
105
104
  /**
106
- * 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"
107
110
  */
108
111
  function serviceOverloaded(model, options) {
109
112
  return (context) => {
@@ -115,6 +118,13 @@ function serviceOverloaded(model, options) {
115
118
  maxAttempts: 1,
116
119
  ...options
117
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
+ }
118
128
  }
119
129
  };
120
130
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-retry",
3
- "version": "0.0.3",
3
+ "version": "0.1.1",
4
4
  "description": "AI SDK Retry",
5
5
  "packageManager": "pnpm@9.0.0",
6
6
  "main": "./dist/index.js",
@@ -1,239 +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
- //#endregion
19
- //#region src/create-retryable-model.ts
20
- /**
21
- * Type guard to check if a retry attempt is an error attempt
22
- */
23
- function isErrorAttempt(attempt) {
24
- return attempt.type === "error";
25
- }
26
- /**
27
- * Type guard to check if a retry attempt is a result attempt
28
- */
29
- function isResultAttempt(attempt) {
30
- return attempt.type === "result";
31
- }
32
- var RetryableModel = class {
33
- specificationVersion = "v2";
34
- baseModel;
35
- currentModel;
36
- options;
37
- get modelId() {
38
- return this.currentModel.modelId;
39
- }
40
- get provider() {
41
- return this.currentModel.provider;
42
- }
43
- get supportedUrls() {
44
- return this.currentModel.supportedUrls;
45
- }
46
- constructor(options) {
47
- this.options = options;
48
- this.baseModel = options.model;
49
- this.currentModel = options.model;
50
- }
51
- /**
52
- * Find the next model to retry with based on the retry context
53
- */
54
- async findNextModel(context) {
55
- /**
56
- * Filter retryables based on attempt type:
57
- * - Result-based attempts: Only consider function retryables (skip plain models)
58
- * - Error-based attempts: Consider all retryables (functions + plain models)
59
- */
60
- const applicableRetries = isResultAttempt(context.current) ? this.options.retries.filter((retry) => typeof retry === "function") : this.options.retries;
61
- /**
62
- * Iterate through the applicable retryables to find a model to retry with
63
- */
64
- for (const retry of applicableRetries) {
65
- const retryModel = typeof retry === "function" ? await retry(context) : {
66
- model: retry,
67
- maxAttempts: 1
68
- };
69
- if (retryModel) {
70
- /**
71
- * The model key uniquely identifies a model instance (provider + modelId)
72
- */
73
- const retryModelKey = getModelKey(retryModel.model);
74
- /**
75
- * Find all attempts with the same model
76
- */
77
- const retryAttempts = context.attempts.filter((a) => getModelKey(a.model) === retryModelKey);
78
- const maxAttempts = retryModel.maxAttempts ?? 1;
79
- /**
80
- * Check if the model can still be retried based on maxAttempts
81
- */
82
- if (retryAttempts.length < maxAttempts) return retryModel.model;
83
- }
84
- }
85
- }
86
- /**
87
- * Execute a function with retry logic for handling errors
88
- */
89
- async executeWithRetry(fn, retryState) {
90
- /**
91
- * Always start with the original model
92
- */
93
- this.currentModel = retryState?.currentModel ?? this.baseModel;
94
- /**
95
- * Track number of attempts
96
- */
97
- let totalAttempts = retryState?.totalAttempts ?? 0;
98
- /**
99
- * Track all attempts.
100
- */
101
- const attempts = retryState?.attempts ?? [];
102
- /**
103
- * The previous attempt that triggered a retry, or undefined if this is the first attempt
104
- */
105
- let previousAttempt;
106
- while (true) {
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
- /**
113
- * Context for the onRetry handler
114
- */
115
- const context = {
116
- current: {
117
- ...previousAttempt,
118
- model: this.currentModel
119
- },
120
- attempts,
121
- totalAttempts
122
- };
123
- /**
124
- * Call the onRetry handler if provided
125
- */
126
- this.options.onRetry?.(context);
127
- }
128
- totalAttempts++;
129
- try {
130
- const result = await fn();
131
- /**
132
- * Check if the result should trigger a retry (only for generate results, not streams)
133
- */
134
- if (isGenerateResult(result)) {
135
- /**
136
- * Check if the result should trigger a retry
137
- */
138
- const resultAttempt = {
139
- type: "result",
140
- result,
141
- model: this.currentModel
142
- };
143
- /**
144
- * Add the current attempt to the list before checking for retries
145
- */
146
- attempts.push(resultAttempt);
147
- const resultContext = {
148
- current: resultAttempt,
149
- attempts,
150
- totalAttempts
151
- };
152
- const nextModel = await this.findNextModel(resultContext);
153
- if (nextModel) {
154
- /**
155
- * Set the model for the next attempt
156
- */
157
- this.currentModel = nextModel;
158
- /**
159
- * Set the previous attempt that triggered this retry
160
- */
161
- previousAttempt = resultAttempt;
162
- /**
163
- * Continue to the next iteration to retry
164
- */
165
- continue;
166
- }
167
- /**
168
- * No retry needed, remove the attempt since it was successful
169
- */
170
- attempts.pop();
171
- }
172
- return result;
173
- } catch (error) {
174
- /**
175
- * Current attempt with current error
176
- */
177
- const errorAttempt = {
178
- type: "error",
179
- error,
180
- model: this.currentModel
181
- };
182
- /**
183
- * Save the current attempt
184
- */
185
- attempts.push(errorAttempt);
186
- /**
187
- * Context for the retryables and onError handler
188
- */
189
- const context = {
190
- current: errorAttempt,
191
- attempts,
192
- totalAttempts
193
- };
194
- /**
195
- * Call the onError handler if provided
196
- */
197
- this.options.onError?.(context);
198
- const nextModel = await this.findNextModel(context);
199
- /**
200
- * Handler didn't return any models to try next, rethrow the error.
201
- * If we retried the request, wrap the error into a `RetryError` for better visibility.
202
- */
203
- if (!nextModel) {
204
- if (totalAttempts > 1) throw this.prepareRetryError(error, attempts);
205
- throw error;
206
- }
207
- /**
208
- * Set the model for the next attempt
209
- */
210
- this.currentModel = nextModel;
211
- /**
212
- * Set the previous attempt that triggered this retry
213
- */
214
- previousAttempt = errorAttempt;
215
- }
216
- }
217
- }
218
- async doGenerate(options) {
219
- return this.executeWithRetry(async () => await this.currentModel.doGenerate(options));
220
- }
221
- async doStream(options) {
222
- return this.executeWithRetry(async () => await this.currentModel.doStream(options));
223
- }
224
- prepareRetryError(error, attempts) {
225
- const errorMessage = getErrorMessage(error);
226
- const errors = attempts.flatMap((a) => isErrorAttempt(a) ? a.error : `Result with finishReason: ${a.result.finishReason}`);
227
- return new RetryError(new RetryError({
228
- message: `Failed after ${attempts.length} attempts. Last error: ${errorMessage}`,
229
- reason: "maxRetriesExceeded",
230
- errors
231
- }));
232
- }
233
- };
234
- function createRetryable(config) {
235
- return new RetryableModel(config);
236
- }
237
-
238
- //#endregion
239
- export { createRetryable, getModelKey, isErrorAttempt, isObject, isResultAttempt, isString };