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
|
-
> [!
|
|
29
|
-
> `ai-retry` currently
|
|
30
|
-
>
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
136
|
+
* Check if the result should trigger a retry
|
|
143
137
|
*/
|
|
144
|
-
|
|
138
|
+
const resultAttempt = {
|
|
139
|
+
type: "result",
|
|
140
|
+
result,
|
|
141
|
+
model: this.currentModel
|
|
142
|
+
};
|
|
145
143
|
/**
|
|
146
|
-
*
|
|
144
|
+
* Add the current attempt to the list before checking for retries
|
|
147
145
|
*/
|
|
148
|
-
|
|
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
|
-
*
|
|
168
|
+
* No retry needed, remove the attempt since it was successful
|
|
151
169
|
*/
|
|
152
|
-
|
|
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
|
-
|
|
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-
|
|
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,
|
|
53
|
+
export { AnthropicErrorResponse, anthropicServiceOverloaded, contentFilterTriggered, requestNotRetryable, requestTimeout, serviceOverloaded };
|
package/dist/retryables/index.js
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
import { isErrorAttempt, isResultAttempt } from "../create-retryable-model-
|
|
1
|
+
import { isErrorAttempt, isObject, isResultAttempt, isString } from "../create-retryable-model-C4nAHxnW.js";
|
|
2
2
|
import { isAbortError } from "@ai-sdk/provider-utils";
|
|
3
|
-
import { APICallError
|
|
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,
|
|
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.
|
|
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
|
+
}
|