ai-retry 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,14 +25,14 @@ npm install ai-retry
25
25
 
26
26
  Create a retryable model by providing a base model and a list of retryables or fallback models.
27
27
 
28
- > [!WARNING]
29
- > `ai-retry` currently only supports `generateText` and `generateObject` calls.
30
- > Streaming via `streamText` and `streamObject` is not supported yet.
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.
31
31
 
32
32
  ```typescript
33
33
  import { azure } from '@ai-sdk/azure';
34
34
  import { openai } from '@ai-sdk/openai';
35
- import { generateText } from 'ai';
35
+ import { generateText, streamText } from 'ai';
36
36
  import { createRetryable } from 'ai-retry';
37
37
  import { contentFilterTriggered, requestTimeout } from 'ai-retry/retryables';
38
38
 
@@ -49,13 +49,25 @@ const result = await generateText({
49
49
  model: retryableModel,
50
50
  prompt: 'Hello world!',
51
51
  });
52
- ```
53
52
 
53
+ // Or with streaming
54
+ const result = streamText({
55
+ model: retryableModel,
56
+ prompt: 'Write a story about a robot...',
57
+ });
58
+
59
+ for await (const chunk of result.textStream) {
60
+ console.log(chunk.text);
61
+ }
62
+ ```
54
63
 
55
64
  #### Content Filter
56
65
 
57
66
  Automatically switch to a different model when content filtering blocks your request.
58
67
 
68
+ > [!WARNING]
69
+ > This retryable currently does not work with streaming requests, because the content filter is only indicated in the final response.
70
+
59
71
  ```typescript
60
72
  import { contentFilterTriggered } from 'ai-retry/retryables';
61
73
 
@@ -124,9 +136,6 @@ const result = await generateText({
124
136
 
125
137
  Handle service overload errors (HTTP code 529) by switching to a provider.
126
138
 
127
- > [!NOTE]
128
- > For Anthropic specifically, use `anthropicServiceOverloaded` instead as Anthropic sometimes returns HTTP 200 OK with an error payload rather than the standard HTTP 529.
129
-
130
139
  ```typescript
131
140
  import { serviceOverloaded } from 'ai-retry/retryables';
132
141
 
@@ -261,7 +270,6 @@ There are several built-in retryables:
261
270
  - [`requestTimeout`](./src/retryables/request-timeout.ts): Request timeout occurred.
262
271
  - [`requestNotRetryable`](./src/retryables/request-not-retryable.ts): Request failed with a non-retryable error.
263
272
  - [`serviceOverloaded`](./src/retryables/service-overloaded.ts): Response with status code 529 (service overloaded).
264
- - [`anthropicServiceOverloaded`](./src/retryables/anthropic-service-overloaded.ts): Anthropic-specific overloaded error handling for both HTTP 529 and 200 OK responses with overloaded error payloads.
265
273
 
266
274
  By default, each retryable will only attempt to retry once per model to avoid infinite loops. You can customize this behavior by returning a `maxAttempts` value from your retryable function.
267
275
 
@@ -9,6 +9,12 @@ const getModelKey = (model) => {
9
9
  return `${model.provider}/${model.modelId}`;
10
10
  };
11
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
+
12
18
  //#endregion
13
19
  //#region src/create-retryable-model.ts
14
20
  /**
@@ -77,19 +83,22 @@ var RetryableModel = class {
77
83
  }
78
84
  }
79
85
  }
80
- async doGenerate(options) {
86
+ /**
87
+ * Execute a function with retry logic for handling errors
88
+ */
89
+ async executeWithRetry(fn, retryState) {
81
90
  /**
82
91
  * Always start with the original model
83
92
  */
84
- this.currentModel = this.baseModel;
93
+ this.currentModel = retryState?.currentModel ?? this.baseModel;
85
94
  /**
86
95
  * Track number of attempts
87
96
  */
88
- let totalAttempts = 0;
97
+ let totalAttempts = retryState?.totalAttempts ?? 0;
89
98
  /**
90
99
  * Track all attempts.
91
100
  */
92
- const attempts = [];
101
+ const attempts = retryState?.attempts ?? [];
93
102
  /**
94
103
  * The previous attempt that triggered a retry, or undefined if this is the first attempt
95
104
  */
@@ -118,43 +127,48 @@ var RetryableModel = class {
118
127
  }
119
128
  totalAttempts++;
120
129
  try {
121
- const result = await this.currentModel.doGenerate(options);
130
+ const result = await fn();
122
131
  /**
123
- * Check if the result should trigger a retry
132
+ * Check if the result should trigger a retry (only for generate results, not streams)
124
133
  */
125
- const resultAttempt = {
126
- type: "result",
127
- result,
128
- model: this.currentModel
129
- };
130
- /**
131
- * Add the current attempt to the list before checking for retries
132
- */
133
- attempts.push(resultAttempt);
134
- const resultContext = {
135
- current: resultAttempt,
136
- attempts,
137
- totalAttempts
138
- };
139
- const nextModel = await this.findNextModel(resultContext);
140
- if (nextModel) {
134
+ if (isGenerateResult(result)) {
141
135
  /**
142
- * Set the model for the next attempt
136
+ * Check if the result should trigger a retry
143
137
  */
144
- this.currentModel = nextModel;
138
+ const resultAttempt = {
139
+ type: "result",
140
+ result,
141
+ model: this.currentModel
142
+ };
145
143
  /**
146
- * Set the previous attempt that triggered this retry
144
+ * Add the current attempt to the list before checking for retries
147
145
  */
148
- previousAttempt = resultAttempt;
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
+ }
149
167
  /**
150
- * Continue to the next iteration to retry
168
+ * No retry needed, remove the attempt since it was successful
151
169
  */
152
- continue;
170
+ attempts.pop();
153
171
  }
154
- /**
155
- * No retry needed, remove the attempt since it was successful and return the result
156
- */
157
- attempts.pop();
158
172
  return result;
159
173
  } catch (error) {
160
174
  /**
@@ -187,15 +201,7 @@ var RetryableModel = class {
187
201
  * If we retried the request, wrap the error into a `RetryError` for better visibility.
188
202
  */
189
203
  if (!nextModel) {
190
- if (totalAttempts > 1) {
191
- const errorMessage = getErrorMessage(error);
192
- const errors = attempts.flatMap((a) => isErrorAttempt(a) ? a.error : `Result with finishReason: ${a.result.finishReason}`);
193
- throw new RetryError({
194
- message: `Failed after ${totalAttempts} attempts. Last error: ${errorMessage}`,
195
- reason: "maxRetriesExceeded",
196
- errors
197
- });
198
- }
204
+ if (totalAttempts > 1) throw this.prepareRetryError(error, attempts);
199
205
  throw error;
200
206
  }
201
207
  /**
@@ -209,8 +215,20 @@ var RetryableModel = class {
209
215
  }
210
216
  }
211
217
  }
218
+ async doGenerate(options) {
219
+ return this.executeWithRetry(async () => await this.currentModel.doGenerate(options));
220
+ }
212
221
  async doStream(options) {
213
- throw new Error("Streaming not implemented");
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
+ }));
214
232
  }
215
233
  };
216
234
  function createRetryable(config) {
@@ -218,4 +236,4 @@ function createRetryable(config) {
218
236
  }
219
237
 
220
238
  //#endregion
221
- export { createRetryable, getModelKey, isErrorAttempt, isResultAttempt };
239
+ export { createRetryable, getModelKey, isErrorAttempt, isObject, isResultAttempt, isString };
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import { createRetryable, getModelKey, isErrorAttempt, isResultAttempt } from "./create-retryable-model-CnrFowSg.js";
1
+ import { createRetryable, getModelKey, isErrorAttempt, isResultAttempt } from "./create-retryable-model-C4nAHxnW.js";
2
2
 
3
3
  export { createRetryable, getModelKey, isErrorAttempt, isResultAttempt };
@@ -44,13 +44,10 @@ declare function requestNotRetryable(model: LanguageModelV2, options?: Omit<Retr
44
44
  */
45
45
  declare function requestTimeout(model: LanguageModelV2, options?: Omit<RetryModel, 'model'>): Retryable;
46
46
  //#endregion
47
- //#region src/retryables/response-schema-mismatch.d.ts
48
- declare function responseSchemaMismatch(model: LanguageModelV2, options?: Omit<RetryModel, 'model'>): Retryable;
49
- //#endregion
50
47
  //#region src/retryables/service-overloaded.d.ts
51
48
  /**
52
49
  * Fallback to a different model if the provider returns a HTTP 529 error.
53
50
  */
54
51
  declare function serviceOverloaded(model: LanguageModelV2, options?: Omit<RetryModel, 'model'>): Retryable;
55
52
  //#endregion
56
- export { AnthropicErrorResponse, anthropicServiceOverloaded, contentFilterTriggered, requestNotRetryable, requestTimeout, responseSchemaMismatch, serviceOverloaded };
53
+ export { AnthropicErrorResponse, anthropicServiceOverloaded, contentFilterTriggered, requestNotRetryable, requestTimeout, serviceOverloaded };
@@ -1,12 +1,7 @@
1
- import { isErrorAttempt, isResultAttempt } from "../create-retryable-model-CnrFowSg.js";
1
+ import { isErrorAttempt, isObject, isResultAttempt, isString } from "../create-retryable-model-C4nAHxnW.js";
2
2
  import { isAbortError } from "@ai-sdk/provider-utils";
3
- import { APICallError, NoObjectGeneratedError, TypeValidationError } from "ai";
3
+ import { APICallError } from "ai";
4
4
 
5
- //#region src/utils.ts
6
- const isObject = (value) => typeof value === "object" && value !== null;
7
- const isString = (value) => typeof value === "string";
8
-
9
- //#endregion
10
5
  //#region src/retryables/anthropic-service-overloaded.ts
11
6
  /**
12
7
  * Fallback if Anthropic returns an "overloaded" error with HTTP 200.
@@ -105,21 +100,6 @@ function requestTimeout(model, options) {
105
100
  };
106
101
  }
107
102
 
108
- //#endregion
109
- //#region src/retryables/response-schema-mismatch.ts
110
- function responseSchemaMismatch(model, options) {
111
- return (context) => {
112
- const { current } = context;
113
- if (isErrorAttempt(current)) {
114
- if (NoObjectGeneratedError.isInstance(current.error) && current.error.finishReason === "stop" && TypeValidationError.isInstance(current.error.cause)) return {
115
- model,
116
- maxAttempts: 1,
117
- ...options
118
- };
119
- }
120
- };
121
- }
122
-
123
103
  //#endregion
124
104
  //#region src/retryables/service-overloaded.ts
125
105
  /**
@@ -140,4 +120,4 @@ function serviceOverloaded(model, options) {
140
120
  }
141
121
 
142
122
  //#endregion
143
- export { anthropicServiceOverloaded, contentFilterTriggered, requestNotRetryable, requestTimeout, responseSchemaMismatch, serviceOverloaded };
123
+ export { anthropicServiceOverloaded, contentFilterTriggered, requestNotRetryable, requestTimeout, serviceOverloaded };
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "ai-retry",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "AI SDK Retry",
5
+ "packageManager": "pnpm@9.0.0",
5
6
  "main": "./dist/index.js",
6
7
  "module": "./dist/index.js",
7
8
  "types": "./dist/index.d.ts",
@@ -17,6 +18,14 @@
17
18
  "publishConfig": {
18
19
  "access": "public"
19
20
  },
21
+ "scripts": {
22
+ "prepublishOnly": "pnpm build",
23
+ "publish:alpha": "pnpm version prerelease --preid alpha && pnpm publish --tag alpha",
24
+ "build": "tsdown",
25
+ "test": "vitest",
26
+ "lint": "biome check . --write",
27
+ "prepare": "husky"
28
+ },
20
29
  "keywords": [
21
30
  "ai",
22
31
  "ai-sdk",
@@ -55,11 +64,5 @@
55
64
  "dependencies": {
56
65
  "@ai-sdk/provider": "^2.0.0",
57
66
  "@ai-sdk/provider-utils": "^3.0.9"
58
- },
59
- "scripts": {
60
- "publish:alpha": "pnpm version prerelease --preid alpha && pnpm publish --tag alpha",
61
- "build": "tsdown",
62
- "test": "vitest",
63
- "lint": "biome check . --write"
64
67
  }
65
- }
68
+ }