ai-retry 0.0.1-alpha.3 → 0.0.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 +74 -64
- package/dist/create-retryable-model-CnrFowSg.js +221 -0
- package/dist/create-retryable-model-DzDFqgQO.d.ts +59 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -160
- package/dist/retryables/index.d.ts +5 -5
- package/dist/retryables/index.js +49 -36
- package/package.json +7 -2
- package/dist/create-retryable-model-DKKgxKLw.d.ts +0 -42
package/README.md
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
|
-
|
|
1
|
+
# ai-retry: Retry and fallback mechanisms for AI SDK
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Automatically handle API failures, content filtering and timeouts by switching between different AI models.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
`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.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
It supports two types of retries:
|
|
8
|
+
- Error-based retries: when the model throws an error (e.g. timeouts, API errors, etc.)
|
|
9
|
+
- Result-based retries: when the model returns a successful response that needs retrying (e.g. content filtering, etc.)
|
|
8
10
|
|
|
9
11
|
### Installation
|
|
10
12
|
|
|
11
13
|
This library only supports AI SDK v5.
|
|
12
14
|
|
|
13
15
|
> [!WARNING]
|
|
14
|
-
> `ai-retry` is in
|
|
16
|
+
> `ai-retry` is in an early stage and the API may change in future releases.
|
|
15
17
|
|
|
16
18
|
```bash
|
|
17
|
-
npm install ai-retry
|
|
19
|
+
npm install ai-retry
|
|
18
20
|
```
|
|
19
21
|
|
|
20
22
|
### Usage
|
|
@@ -34,13 +36,9 @@ import { contentFilterTriggered, requestTimeout } from 'ai-retry/retryables';
|
|
|
34
36
|
|
|
35
37
|
// Create a retryable model
|
|
36
38
|
const retryableModel = createRetryable({
|
|
37
|
-
// Base model
|
|
38
|
-
model: azure('gpt-4-mini'),
|
|
39
|
-
// Retry strategies
|
|
39
|
+
model: azure('gpt-4-mini'), // Base model
|
|
40
40
|
retries: [
|
|
41
|
-
|
|
42
|
-
requestTimeout(azure('gpt-4')),
|
|
43
|
-
openai('gpt-4-mini'),
|
|
41
|
+
// Retry strategies and fallbacks...
|
|
44
42
|
],
|
|
45
43
|
});
|
|
46
44
|
|
|
@@ -48,23 +46,11 @@ const retryableModel = createRetryable({
|
|
|
48
46
|
const result = await generateText({
|
|
49
47
|
model: retryableModel,
|
|
50
48
|
prompt: 'Hello world!',
|
|
51
|
-
abortSignal: AbortSignal.timeout(10_000),
|
|
52
49
|
});
|
|
53
50
|
```
|
|
54
51
|
|
|
55
|
-
#### Retryables
|
|
56
52
|
|
|
57
|
-
|
|
58
|
-
There are several built-in retryables:
|
|
59
|
-
|
|
60
|
-
- `contentFilterTriggered`: Content filter was triggered based on the prompt or completion.
|
|
61
|
-
- `responseSchemaMismatch`: Structured output validation failed.
|
|
62
|
-
- `requestTimeout`: Request timeout occurred.
|
|
63
|
-
- `requestNotRetryable`: Request failed with a non-retryable error.
|
|
64
|
-
|
|
65
|
-
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.
|
|
66
|
-
|
|
67
|
-
##### Content Filter Triggered
|
|
53
|
+
#### Content Filter
|
|
68
54
|
|
|
69
55
|
Automatically switch to a different model when content filtering blocks your request.
|
|
70
56
|
|
|
@@ -79,6 +65,7 @@ const retryableModel = createRetryable({
|
|
|
79
65
|
});
|
|
80
66
|
```
|
|
81
67
|
|
|
68
|
+
<!--
|
|
82
69
|
##### Response Schema Mismatch
|
|
83
70
|
|
|
84
71
|
Retry with different models when structured output validation fails:
|
|
@@ -105,13 +92,14 @@ const result = await generateObject({
|
|
|
105
92
|
prompt: 'Generate a lasagna recipe.',
|
|
106
93
|
});
|
|
107
94
|
```
|
|
95
|
+
-->
|
|
108
96
|
|
|
109
|
-
|
|
97
|
+
#### Request Timeout
|
|
110
98
|
|
|
111
99
|
Handle timeouts by switching to potentially faster models.
|
|
112
100
|
|
|
113
101
|
> [!NOTE]
|
|
114
|
-
> You need to
|
|
102
|
+
> You need to use an `abortSignal` with a timeout on your request.
|
|
115
103
|
|
|
116
104
|
```typescript
|
|
117
105
|
import { requestTimeout } from 'ai-retry/retryables';
|
|
@@ -126,62 +114,70 @@ const retryableModel = createRetryable({
|
|
|
126
114
|
const result = await generateText({
|
|
127
115
|
model: retryableModel,
|
|
128
116
|
prompt: 'Write a vegetarian lasagna recipe for 4 people.',
|
|
129
|
-
abortSignal: AbortSignal.timeout(
|
|
117
|
+
abortSignal: AbortSignal.timeout(60_000),
|
|
130
118
|
});
|
|
131
119
|
```
|
|
132
120
|
|
|
133
|
-
|
|
121
|
+
#### Request Not Retryable
|
|
134
122
|
|
|
135
123
|
Handle cases where the base model fails with a non-retryable error.
|
|
136
124
|
|
|
125
|
+
> [!NOTE]
|
|
126
|
+
> You can check if an error is retryable with the `isRetryable` property on an [`APICallError`](https://ai-sdk.dev/docs/reference/ai-sdk-errors/ai-api-call-error#ai_apicallerror).
|
|
127
|
+
|
|
128
|
+
|
|
137
129
|
```typescript
|
|
138
130
|
import { requestNotRetryable } from 'ai-retry/retryables';
|
|
139
131
|
|
|
140
132
|
const retryable = createRetryable({
|
|
141
133
|
model: azure('gpt-4-mini'),
|
|
142
134
|
retries: [
|
|
143
|
-
requestNotRetryable(openai('gpt-4')), // Switch
|
|
135
|
+
requestNotRetryable(openai('gpt-4')), // Switch provider if error is not retryable
|
|
144
136
|
],
|
|
145
137
|
});
|
|
146
138
|
```
|
|
147
139
|
|
|
148
|
-
|
|
140
|
+
#### Fallbacks
|
|
149
141
|
|
|
150
|
-
|
|
142
|
+
If you always want to fallback to a different model on any error, you can simply provide a list of models.
|
|
151
143
|
|
|
152
144
|
```typescript
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (shouldRetryWithDifferentModel(current.error)) {
|
|
160
|
-
return {
|
|
161
|
-
model: myFallbackModel,
|
|
162
|
-
maxAttempts: 3,
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return undefined; // Don't retry
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
const retryable = createRetryable({
|
|
170
|
-
model: azure('gpt-4-mini'),
|
|
171
|
-
retries: [customRetry],
|
|
145
|
+
const retryableModel = createRetryable({
|
|
146
|
+
model: azure('gpt-4'),
|
|
147
|
+
retries: [
|
|
148
|
+
openai('gpt-4'),
|
|
149
|
+
anthropic('claude-3-haiku-20240307')
|
|
150
|
+
],
|
|
172
151
|
});
|
|
173
152
|
```
|
|
174
153
|
|
|
175
|
-
####
|
|
154
|
+
#### Custom
|
|
176
155
|
|
|
177
|
-
|
|
156
|
+
Create your own retryables for specific use cases:
|
|
178
157
|
|
|
179
158
|
```typescript
|
|
159
|
+
import { anthropic } from '@ai-sdk/anthropic';
|
|
160
|
+
import { openai } from '@ai-sdk/openai';
|
|
161
|
+
import { APICallError } from 'ai';
|
|
162
|
+
import { createRetryable, isErrorAttempt } from 'ai-retry';
|
|
163
|
+
import type { Retryable } from 'ai-retry';
|
|
164
|
+
|
|
165
|
+
const rateLimitRetry: Retryable = (context) => {
|
|
166
|
+
if (isErrorAttempt(context.current)) {
|
|
167
|
+
const { error } = context.current;
|
|
168
|
+
|
|
169
|
+
if (APICallError.isInstance(error) && error.statusCode === 429) {
|
|
170
|
+
return { model: anthropic('claude-3-haiku-20240307') };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return undefined;
|
|
175
|
+
};
|
|
176
|
+
|
|
180
177
|
const retryableModel = createRetryable({
|
|
181
|
-
model:
|
|
178
|
+
model: openai('gpt-4'),
|
|
182
179
|
retries: [
|
|
183
|
-
|
|
184
|
-
anthropic('claude-3-haiku-20240307')
|
|
180
|
+
rateLimitRetry
|
|
185
181
|
],
|
|
186
182
|
});
|
|
187
183
|
```
|
|
@@ -236,6 +232,17 @@ const retryableModel = createRetryable({
|
|
|
236
232
|
});
|
|
237
233
|
```
|
|
238
234
|
|
|
235
|
+
### Retryables
|
|
236
|
+
|
|
237
|
+
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.
|
|
238
|
+
There are several built-in retryables:
|
|
239
|
+
|
|
240
|
+
- [`contentFilterTriggered`](./src/retryables/content-filter-triggered.ts): Content filter was triggered based on the prompt or completion.
|
|
241
|
+
- [`requestTimeout`](./src/retryables/request-timeout.ts): Request timeout occurred.
|
|
242
|
+
- [`requestNotRetryable`](./src/retryables/request-not-retryable.ts): Request failed with a non-retryable error.
|
|
243
|
+
|
|
244
|
+
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.
|
|
245
|
+
|
|
239
246
|
### API Reference
|
|
240
247
|
|
|
241
248
|
#### `createRetryable(options: CreateRetryableOptions): LanguageModelV2`
|
|
@@ -247,14 +254,14 @@ interface CreateRetryableOptions {
|
|
|
247
254
|
model: LanguageModelV2;
|
|
248
255
|
retries: Array<Retryable | LanguageModelV2>;
|
|
249
256
|
onError?: (context: RetryContext) => void;
|
|
250
|
-
onRetry?: (context: RetryContext) => void;
|
|
257
|
+
onRetry?: (context: RetryContext) => void;
|
|
251
258
|
}
|
|
252
259
|
```
|
|
253
260
|
|
|
254
261
|
#### `Retryable`
|
|
255
262
|
|
|
256
|
-
A `Retryable` is a function that receives a `RetryContext` with the current error and model and all previous attempts.
|
|
257
|
-
It should evaluate the error and decide whether to retry by returning a `RetryModel` or to skip by returning `undefined`.
|
|
263
|
+
A `Retryable` is a function that receives a `RetryContext` with the current error or result and model and all previous attempts.
|
|
264
|
+
It should evaluate the error/result and decide whether to retry by returning a `RetryModel` or to skip by returning `undefined`.
|
|
258
265
|
|
|
259
266
|
```ts
|
|
260
267
|
type Retryable = (context: RetryContext) => RetryModel | Promise<RetryModel> | undefined;
|
|
@@ -286,13 +293,16 @@ interface RetryContext {
|
|
|
286
293
|
|
|
287
294
|
#### `RetryAttempt`
|
|
288
295
|
|
|
289
|
-
A `RetryAttempt` represents a single
|
|
296
|
+
A `RetryAttempt` represents a single attempt with a specific model, which can be either an error or a successful result that triggered a retry.
|
|
290
297
|
|
|
291
298
|
```typescript
|
|
292
|
-
|
|
293
|
-
error: unknown;
|
|
294
|
-
model: LanguageModelV2;
|
|
295
|
-
|
|
299
|
+
type RetryAttempt =
|
|
300
|
+
| { type: 'error'; error: unknown; model: LanguageModelV2 }
|
|
301
|
+
| { type: 'result'; result: LanguageModelV2Generate; model: LanguageModelV2 };
|
|
302
|
+
|
|
303
|
+
// Type guards for discriminating attempts
|
|
304
|
+
function isErrorAttempt(attempt: RetryAttempt): attempt is RetryErrorAttempt;
|
|
305
|
+
function isResultAttempt(attempt: RetryAttempt): attempt is RetryResultAttempt;
|
|
296
306
|
```
|
|
297
307
|
|
|
298
308
|
### License
|
|
@@ -0,0 +1,221 @@
|
|
|
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/create-retryable-model.ts
|
|
14
|
+
/**
|
|
15
|
+
* Type guard to check if a retry attempt is an error attempt
|
|
16
|
+
*/
|
|
17
|
+
function isErrorAttempt(attempt) {
|
|
18
|
+
return attempt.type === "error";
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Type guard to check if a retry attempt is a result attempt
|
|
22
|
+
*/
|
|
23
|
+
function isResultAttempt(attempt) {
|
|
24
|
+
return attempt.type === "result";
|
|
25
|
+
}
|
|
26
|
+
var RetryableModel = class {
|
|
27
|
+
specificationVersion = "v2";
|
|
28
|
+
baseModel;
|
|
29
|
+
currentModel;
|
|
30
|
+
options;
|
|
31
|
+
get modelId() {
|
|
32
|
+
return this.currentModel.modelId;
|
|
33
|
+
}
|
|
34
|
+
get provider() {
|
|
35
|
+
return this.currentModel.provider;
|
|
36
|
+
}
|
|
37
|
+
get supportedUrls() {
|
|
38
|
+
return this.currentModel.supportedUrls;
|
|
39
|
+
}
|
|
40
|
+
constructor(options) {
|
|
41
|
+
this.options = options;
|
|
42
|
+
this.baseModel = options.model;
|
|
43
|
+
this.currentModel = options.model;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Find the next model to retry with based on the retry context
|
|
47
|
+
*/
|
|
48
|
+
async findNextModel(context) {
|
|
49
|
+
/**
|
|
50
|
+
* Filter retryables based on attempt type:
|
|
51
|
+
* - Result-based attempts: Only consider function retryables (skip plain models)
|
|
52
|
+
* - Error-based attempts: Consider all retryables (functions + plain models)
|
|
53
|
+
*/
|
|
54
|
+
const applicableRetries = isResultAttempt(context.current) ? this.options.retries.filter((retry) => typeof retry === "function") : this.options.retries;
|
|
55
|
+
/**
|
|
56
|
+
* Iterate through the applicable retryables to find a model to retry with
|
|
57
|
+
*/
|
|
58
|
+
for (const retry of applicableRetries) {
|
|
59
|
+
const retryModel = typeof retry === "function" ? await retry(context) : {
|
|
60
|
+
model: retry,
|
|
61
|
+
maxAttempts: 1
|
|
62
|
+
};
|
|
63
|
+
if (retryModel) {
|
|
64
|
+
/**
|
|
65
|
+
* The model key uniquely identifies a model instance (provider + modelId)
|
|
66
|
+
*/
|
|
67
|
+
const retryModelKey = getModelKey(retryModel.model);
|
|
68
|
+
/**
|
|
69
|
+
* Find all attempts with the same model
|
|
70
|
+
*/
|
|
71
|
+
const retryAttempts = context.attempts.filter((a) => getModelKey(a.model) === retryModelKey);
|
|
72
|
+
const maxAttempts = retryModel.maxAttempts ?? 1;
|
|
73
|
+
/**
|
|
74
|
+
* Check if the model can still be retried based on maxAttempts
|
|
75
|
+
*/
|
|
76
|
+
if (retryAttempts.length < maxAttempts) return retryModel.model;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async doGenerate(options) {
|
|
81
|
+
/**
|
|
82
|
+
* Always start with the original model
|
|
83
|
+
*/
|
|
84
|
+
this.currentModel = this.baseModel;
|
|
85
|
+
/**
|
|
86
|
+
* Track number of attempts
|
|
87
|
+
*/
|
|
88
|
+
let totalAttempts = 0;
|
|
89
|
+
/**
|
|
90
|
+
* Track all attempts.
|
|
91
|
+
*/
|
|
92
|
+
const attempts = [];
|
|
93
|
+
/**
|
|
94
|
+
* The previous attempt that triggered a retry, or undefined if this is the first attempt
|
|
95
|
+
*/
|
|
96
|
+
let previousAttempt;
|
|
97
|
+
while (true) {
|
|
98
|
+
/**
|
|
99
|
+
* Call the onRetry handler if provided.
|
|
100
|
+
* Skip on the first attempt since no previous attempt exists yet.
|
|
101
|
+
*/
|
|
102
|
+
if (previousAttempt) {
|
|
103
|
+
/**
|
|
104
|
+
* Context for the onRetry handler
|
|
105
|
+
*/
|
|
106
|
+
const context = {
|
|
107
|
+
current: {
|
|
108
|
+
...previousAttempt,
|
|
109
|
+
model: this.currentModel
|
|
110
|
+
},
|
|
111
|
+
attempts,
|
|
112
|
+
totalAttempts
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* Call the onRetry handler if provided
|
|
116
|
+
*/
|
|
117
|
+
this.options.onRetry?.(context);
|
|
118
|
+
}
|
|
119
|
+
totalAttempts++;
|
|
120
|
+
try {
|
|
121
|
+
const result = await this.currentModel.doGenerate(options);
|
|
122
|
+
/**
|
|
123
|
+
* Check if the result should trigger a retry
|
|
124
|
+
*/
|
|
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) {
|
|
141
|
+
/**
|
|
142
|
+
* Set the model for the next attempt
|
|
143
|
+
*/
|
|
144
|
+
this.currentModel = nextModel;
|
|
145
|
+
/**
|
|
146
|
+
* Set the previous attempt that triggered this retry
|
|
147
|
+
*/
|
|
148
|
+
previousAttempt = resultAttempt;
|
|
149
|
+
/**
|
|
150
|
+
* Continue to the next iteration to retry
|
|
151
|
+
*/
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* No retry needed, remove the attempt since it was successful and return the result
|
|
156
|
+
*/
|
|
157
|
+
attempts.pop();
|
|
158
|
+
return result;
|
|
159
|
+
} catch (error) {
|
|
160
|
+
/**
|
|
161
|
+
* Current attempt with current error
|
|
162
|
+
*/
|
|
163
|
+
const errorAttempt = {
|
|
164
|
+
type: "error",
|
|
165
|
+
error,
|
|
166
|
+
model: this.currentModel
|
|
167
|
+
};
|
|
168
|
+
/**
|
|
169
|
+
* Save the current attempt
|
|
170
|
+
*/
|
|
171
|
+
attempts.push(errorAttempt);
|
|
172
|
+
/**
|
|
173
|
+
* Context for the retryables and onError handler
|
|
174
|
+
*/
|
|
175
|
+
const context = {
|
|
176
|
+
current: errorAttempt,
|
|
177
|
+
attempts,
|
|
178
|
+
totalAttempts
|
|
179
|
+
};
|
|
180
|
+
/**
|
|
181
|
+
* Call the onError handler if provided
|
|
182
|
+
*/
|
|
183
|
+
this.options.onError?.(context);
|
|
184
|
+
const nextModel = await this.findNextModel(context);
|
|
185
|
+
/**
|
|
186
|
+
* Handler didn't return any models to try next, rethrow the error.
|
|
187
|
+
* If we retried the request, wrap the error into a `RetryError` for better visibility.
|
|
188
|
+
*/
|
|
189
|
+
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
|
+
}
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Set the model for the next attempt
|
|
203
|
+
*/
|
|
204
|
+
this.currentModel = nextModel;
|
|
205
|
+
/**
|
|
206
|
+
* Set the previous attempt that triggered this retry
|
|
207
|
+
*/
|
|
208
|
+
previousAttempt = errorAttempt;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async doStream(options) {
|
|
213
|
+
throw new Error("Streaming not implemented");
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
function createRetryable(config) {
|
|
217
|
+
return new RetryableModel(config);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
//#endregion
|
|
221
|
+
export { createRetryable, getModelKey, isErrorAttempt, isResultAttempt };
|
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
current: CURRENT;
|
|
12
|
+
attempts: Array<RetryAttempt>;
|
|
13
|
+
totalAttempts: number;
|
|
14
|
+
}
|
|
15
|
+
type RetryErrorAttempt = {
|
|
16
|
+
type: 'error';
|
|
17
|
+
error: unknown;
|
|
18
|
+
model: LanguageModelV2;
|
|
19
|
+
};
|
|
20
|
+
type RetryResultAttempt = {
|
|
21
|
+
type: 'result';
|
|
22
|
+
result: LanguageModelV2Generate;
|
|
23
|
+
model: LanguageModelV2;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* A retry attempt with either an error or a result and the model used
|
|
27
|
+
*/
|
|
28
|
+
type RetryAttempt = RetryErrorAttempt | RetryResultAttempt;
|
|
29
|
+
/**
|
|
30
|
+
* Type guard to check if a retry attempt is an error attempt
|
|
31
|
+
*/
|
|
32
|
+
declare function isErrorAttempt(attempt: RetryAttempt): attempt is RetryErrorAttempt;
|
|
33
|
+
/**
|
|
34
|
+
* Type guard to check if a retry attempt is a result attempt
|
|
35
|
+
*/
|
|
36
|
+
declare function isResultAttempt(attempt: RetryAttempt): attempt is RetryResultAttempt;
|
|
37
|
+
/**
|
|
38
|
+
* A model to retry with and the maximum number of attempts for that model.
|
|
39
|
+
*/
|
|
40
|
+
type RetryModel = {
|
|
41
|
+
model: LanguageModelV2;
|
|
42
|
+
maxAttempts?: number;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* A function that determines whether to retry with a different model based on the current attempt and all previous attempts.
|
|
46
|
+
*/
|
|
47
|
+
type Retryable = (context: RetryContext) => RetryModel | Promise<RetryModel> | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* Options for creating a retryable model.
|
|
50
|
+
*/
|
|
51
|
+
interface CreateRetryableOptions {
|
|
52
|
+
model: LanguageModelV2;
|
|
53
|
+
retries: Array<Retryable | LanguageModelV2>;
|
|
54
|
+
onError?: (context: RetryContext<RetryErrorAttempt>) => void;
|
|
55
|
+
onRetry?: (context: RetryContext<RetryErrorAttempt | RetryResultAttempt>) => void;
|
|
56
|
+
}
|
|
57
|
+
declare function createRetryable(config: CreateRetryableOptions): LanguageModelV2;
|
|
58
|
+
//#endregion
|
|
59
|
+
export { CreateRetryableOptions, RetryAttempt, RetryContext, RetryModel, Retryable, createRetryable, isErrorAttempt, isResultAttempt };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CreateRetryableOptions, RetryAttempt, RetryContext, RetryModel, Retryable, createRetryable } from "./create-retryable-model-
|
|
1
|
+
import { CreateRetryableOptions, RetryAttempt, RetryContext, RetryModel, Retryable, createRetryable, isErrorAttempt, isResultAttempt } from "./create-retryable-model-DzDFqgQO.js";
|
|
2
2
|
import { LanguageModelV2 } from "@ai-sdk/provider";
|
|
3
3
|
|
|
4
4
|
//#region src/get-model-key.d.ts
|
|
@@ -7,4 +7,4 @@ import { LanguageModelV2 } from "@ai-sdk/provider";
|
|
|
7
7
|
*/
|
|
8
8
|
declare const getModelKey: (model: LanguageModelV2) => string;
|
|
9
9
|
//#endregion
|
|
10
|
-
export { CreateRetryableOptions, RetryAttempt, RetryContext, RetryModel, Retryable, createRetryable, getModelKey };
|
|
10
|
+
export { CreateRetryableOptions, RetryAttempt, RetryContext, RetryModel, Retryable, createRetryable, getModelKey, isErrorAttempt, isResultAttempt };
|
package/dist/index.js
CHANGED
|
@@ -1,161 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { RetryError } from "ai";
|
|
1
|
+
import { createRetryable, getModelKey, isErrorAttempt, isResultAttempt } from "./create-retryable-model-CnrFowSg.js";
|
|
3
2
|
|
|
4
|
-
|
|
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/create-retryable-model.ts
|
|
14
|
-
function defaultRetryModel(model) {
|
|
15
|
-
return {
|
|
16
|
-
model,
|
|
17
|
-
maxAttempts: 1
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
var RetryableModel = class {
|
|
21
|
-
specificationVersion = "v2";
|
|
22
|
-
baseModel;
|
|
23
|
-
currentModel;
|
|
24
|
-
options;
|
|
25
|
-
get modelId() {
|
|
26
|
-
return this.currentModel.modelId;
|
|
27
|
-
}
|
|
28
|
-
get provider() {
|
|
29
|
-
return this.currentModel.provider;
|
|
30
|
-
}
|
|
31
|
-
get supportedUrls() {
|
|
32
|
-
return this.currentModel.supportedUrls;
|
|
33
|
-
}
|
|
34
|
-
constructor(options) {
|
|
35
|
-
this.options = options;
|
|
36
|
-
this.baseModel = options.model;
|
|
37
|
-
this.currentModel = options.model;
|
|
38
|
-
}
|
|
39
|
-
async doGenerate(options) {
|
|
40
|
-
/**
|
|
41
|
-
* Always start with the original model
|
|
42
|
-
*/
|
|
43
|
-
this.currentModel = this.baseModel;
|
|
44
|
-
/**
|
|
45
|
-
* Track number of attempts
|
|
46
|
-
*/
|
|
47
|
-
let totalAttempts = 0;
|
|
48
|
-
/**
|
|
49
|
-
* Track all attempts.
|
|
50
|
-
*/
|
|
51
|
-
const attempts = [];
|
|
52
|
-
/**
|
|
53
|
-
* The error occured in the previous attempt or undefined if this is the first attempt
|
|
54
|
-
*/
|
|
55
|
-
let currentError;
|
|
56
|
-
while (true) {
|
|
57
|
-
/**
|
|
58
|
-
* Call the onRetry handler if provided.
|
|
59
|
-
* Skip on the first attempt since no error occured yet.
|
|
60
|
-
*/
|
|
61
|
-
if (currentError) {
|
|
62
|
-
/**
|
|
63
|
-
* Context for the onRetry handler
|
|
64
|
-
*/
|
|
65
|
-
const context = {
|
|
66
|
-
current: {
|
|
67
|
-
error: currentError,
|
|
68
|
-
model: this.currentModel
|
|
69
|
-
},
|
|
70
|
-
attempts,
|
|
71
|
-
totalAttempts
|
|
72
|
-
};
|
|
73
|
-
/**
|
|
74
|
-
* Call the onRetry handler if provided
|
|
75
|
-
*/
|
|
76
|
-
this.options.onRetry?.(context);
|
|
77
|
-
}
|
|
78
|
-
try {
|
|
79
|
-
totalAttempts++;
|
|
80
|
-
return await this.currentModel.doGenerate(options);
|
|
81
|
-
} catch (error) {
|
|
82
|
-
/**
|
|
83
|
-
* Save the error of the current attempt for the retry of the next iteration
|
|
84
|
-
*/
|
|
85
|
-
currentError = error;
|
|
86
|
-
/**
|
|
87
|
-
* Current attempt with current error
|
|
88
|
-
*/
|
|
89
|
-
const currentAttempt = {
|
|
90
|
-
error: currentError,
|
|
91
|
-
model: this.currentModel
|
|
92
|
-
};
|
|
93
|
-
/**
|
|
94
|
-
* Save the current attempt
|
|
95
|
-
*/
|
|
96
|
-
attempts.push(currentAttempt);
|
|
97
|
-
/**
|
|
98
|
-
* Context for the retryables and onError handler
|
|
99
|
-
*/
|
|
100
|
-
const context = {
|
|
101
|
-
current: currentAttempt,
|
|
102
|
-
attempts,
|
|
103
|
-
totalAttempts
|
|
104
|
-
};
|
|
105
|
-
/**
|
|
106
|
-
* Call the onError handler if provided
|
|
107
|
-
*/
|
|
108
|
-
this.options.onError?.(context);
|
|
109
|
-
let nextModel;
|
|
110
|
-
/**
|
|
111
|
-
* Iterate through the retryables to find a model to retry with
|
|
112
|
-
*/
|
|
113
|
-
for (const retry of this.options.retries) {
|
|
114
|
-
const retryModel = typeof retry === "function" ? await retry(context) : defaultRetryModel(retry);
|
|
115
|
-
if (retryModel) {
|
|
116
|
-
/**
|
|
117
|
-
* The model key uniquely identifies a model instance (provider + modelId)
|
|
118
|
-
*/
|
|
119
|
-
const retryModelKey = getModelKey(retryModel.model);
|
|
120
|
-
/**
|
|
121
|
-
* Check if the model can still be retried based on maxAttempts
|
|
122
|
-
*/
|
|
123
|
-
if (attempts.filter((a) => getModelKey(a.model) === retryModelKey).length < retryModel.maxAttempts) {
|
|
124
|
-
nextModel = retryModel.model;
|
|
125
|
-
break;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
/**
|
|
130
|
-
* Handler didn't return any models to try next, rethrow the error.
|
|
131
|
-
* If we retried the request, wrap the error into a `RetryError` for better visibility.
|
|
132
|
-
*/
|
|
133
|
-
if (!nextModel) {
|
|
134
|
-
if (totalAttempts > 1) {
|
|
135
|
-
const errorMessage = getErrorMessage(error);
|
|
136
|
-
const errors = attempts.flatMap((a) => a.error);
|
|
137
|
-
throw new RetryError({
|
|
138
|
-
message: `Failed after ${totalAttempts} attempts. Last error: ${errorMessage}`,
|
|
139
|
-
reason: "maxRetriesExceeded",
|
|
140
|
-
errors
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
throw error;
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* Set the model for the next attempt
|
|
147
|
-
*/
|
|
148
|
-
this.currentModel = nextModel;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
async doStream(options) {
|
|
153
|
-
throw new Error("Streaming not implemented");
|
|
154
|
-
}
|
|
155
|
-
};
|
|
156
|
-
function createRetryable(config) {
|
|
157
|
-
return new RetryableModel(config);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
//#endregion
|
|
161
|
-
export { createRetryable, getModelKey };
|
|
3
|
+
export { createRetryable, getModelKey, isErrorAttempt, isResultAttempt };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RetryModel, Retryable } from "../create-retryable-model-
|
|
1
|
+
import { RetryModel, Retryable } from "../create-retryable-model-DzDFqgQO.js";
|
|
2
2
|
import { LanguageModelV2 } from "@ai-sdk/provider";
|
|
3
3
|
|
|
4
4
|
//#region src/retryables/content-filter-triggered.d.ts
|
|
@@ -6,22 +6,22 @@ import { LanguageModelV2 } from "@ai-sdk/provider";
|
|
|
6
6
|
/**
|
|
7
7
|
* Fallback to a different model if the content filter was triggered.
|
|
8
8
|
*/
|
|
9
|
-
declare function contentFilterTriggered(
|
|
9
|
+
declare function contentFilterTriggered(model: LanguageModelV2, options?: Omit<RetryModel, 'model'>): Retryable;
|
|
10
10
|
//#endregion
|
|
11
11
|
//#region src/retryables/request-not-retryable.d.ts
|
|
12
12
|
/**
|
|
13
13
|
* Fallback to a different model if the error is non-retryable.
|
|
14
14
|
*/
|
|
15
|
-
declare function requestNotRetryable(
|
|
15
|
+
declare function requestNotRetryable(model: LanguageModelV2, options?: Omit<RetryModel, 'model'>): Retryable;
|
|
16
16
|
//#endregion
|
|
17
17
|
//#region src/retryables/request-timeout.d.ts
|
|
18
18
|
/**
|
|
19
19
|
* Fallback to a different model after a timeout/abort error.
|
|
20
20
|
* Use in combination with the `abortSignal` option in `generateText`.
|
|
21
21
|
*/
|
|
22
|
-
declare function requestTimeout(
|
|
22
|
+
declare function requestTimeout(model: LanguageModelV2, options?: Omit<RetryModel, 'model'>): Retryable;
|
|
23
23
|
//#endregion
|
|
24
24
|
//#region src/retryables/response-schema-mismatch.d.ts
|
|
25
|
-
declare function responseSchemaMismatch(
|
|
25
|
+
declare function responseSchemaMismatch(model: LanguageModelV2, options?: Omit<RetryModel, 'model'>): Retryable;
|
|
26
26
|
//#endregion
|
|
27
27
|
export { contentFilterTriggered, requestNotRetryable, requestTimeout, responseSchemaMismatch };
|
package/dist/retryables/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { isErrorAttempt, isResultAttempt } from "../create-retryable-model-CnrFowSg.js";
|
|
1
2
|
import { isAbortError } from "@ai-sdk/provider-utils";
|
|
2
3
|
import { APICallError, NoObjectGeneratedError, TypeValidationError } from "ai";
|
|
3
|
-
import { APICallError as APICallError$1 } from "@ai-sdk/provider";
|
|
4
4
|
|
|
5
5
|
//#region src/utils.ts
|
|
6
6
|
const isObject = (value) => typeof value === "object" && value !== null;
|
|
@@ -11,18 +11,25 @@ const isString = (value) => typeof value === "string";
|
|
|
11
11
|
/**
|
|
12
12
|
* Fallback to a different model if the content filter was triggered.
|
|
13
13
|
*/
|
|
14
|
-
function contentFilterTriggered(
|
|
14
|
+
function contentFilterTriggered(model, options) {
|
|
15
15
|
return (context) => {
|
|
16
|
-
const {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
const { current } = context;
|
|
17
|
+
if (isErrorAttempt(current)) {
|
|
18
|
+
const { error } = current;
|
|
19
|
+
if (APICallError.isInstance(error) && isObject(error.data) && isObject(error.data.error) && isString(error.data.error.code) && error.data.error.code === "content_filter") return {
|
|
20
|
+
model,
|
|
21
|
+
maxAttempts: 1,
|
|
22
|
+
...options
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
if (isResultAttempt(current)) {
|
|
26
|
+
const { result } = current;
|
|
27
|
+
if (result.finishReason === "content-filter") return {
|
|
28
|
+
model,
|
|
29
|
+
maxAttempts: 1,
|
|
30
|
+
...options
|
|
31
|
+
};
|
|
32
|
+
}
|
|
26
33
|
};
|
|
27
34
|
}
|
|
28
35
|
|
|
@@ -31,14 +38,16 @@ function contentFilterTriggered(input) {
|
|
|
31
38
|
/**
|
|
32
39
|
* Fallback to a different model if the error is non-retryable.
|
|
33
40
|
*/
|
|
34
|
-
function requestNotRetryable(
|
|
41
|
+
function requestNotRetryable(model, options) {
|
|
35
42
|
return (context) => {
|
|
36
|
-
const {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
const { current } = context;
|
|
44
|
+
if (isErrorAttempt(current)) {
|
|
45
|
+
if (APICallError.isInstance(current.error) && current.error.isRetryable === false) return {
|
|
46
|
+
model,
|
|
47
|
+
maxAttempts: 1,
|
|
48
|
+
...options
|
|
49
|
+
};
|
|
50
|
+
}
|
|
42
51
|
};
|
|
43
52
|
}
|
|
44
53
|
|
|
@@ -48,30 +57,34 @@ function requestNotRetryable(input) {
|
|
|
48
57
|
* Fallback to a different model after a timeout/abort error.
|
|
49
58
|
* Use in combination with the `abortSignal` option in `generateText`.
|
|
50
59
|
*/
|
|
51
|
-
function requestTimeout(
|
|
60
|
+
function requestTimeout(model, options) {
|
|
52
61
|
return (context) => {
|
|
53
|
-
const {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
const { current } = context;
|
|
63
|
+
if (isErrorAttempt(current)) {
|
|
64
|
+
/**
|
|
65
|
+
* Fallback to the specified model after all retries are exhausted.
|
|
66
|
+
*/
|
|
67
|
+
if (isAbortError(current.error)) return {
|
|
68
|
+
model,
|
|
69
|
+
maxAttempts: 1,
|
|
70
|
+
...options
|
|
71
|
+
};
|
|
72
|
+
}
|
|
62
73
|
};
|
|
63
74
|
}
|
|
64
75
|
|
|
65
76
|
//#endregion
|
|
66
77
|
//#region src/retryables/response-schema-mismatch.ts
|
|
67
|
-
function responseSchemaMismatch(
|
|
78
|
+
function responseSchemaMismatch(model, options) {
|
|
68
79
|
return (context) => {
|
|
69
|
-
const {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
80
|
+
const { current } = context;
|
|
81
|
+
if (isErrorAttempt(current)) {
|
|
82
|
+
if (NoObjectGeneratedError.isInstance(current.error) && current.error.finishReason === "stop" && TypeValidationError.isInstance(current.error.cause)) return {
|
|
83
|
+
model,
|
|
84
|
+
maxAttempts: 1,
|
|
85
|
+
...options
|
|
86
|
+
};
|
|
87
|
+
}
|
|
75
88
|
};
|
|
76
89
|
}
|
|
77
90
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-retry",
|
|
3
|
-
"version": "0.0.1
|
|
3
|
+
"version": "0.0.1",
|
|
4
4
|
"description": "AI SDK Retry",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -17,7 +17,12 @@
|
|
|
17
17
|
"publishConfig": {
|
|
18
18
|
"access": "public"
|
|
19
19
|
},
|
|
20
|
-
"keywords": [
|
|
20
|
+
"keywords": [
|
|
21
|
+
"ai",
|
|
22
|
+
"ai-sdk",
|
|
23
|
+
"retry",
|
|
24
|
+
"fallback"
|
|
25
|
+
],
|
|
21
26
|
"author": "Chris Cook",
|
|
22
27
|
"license": "MIT",
|
|
23
28
|
"repository": {
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { LanguageModelV2 } from "@ai-sdk/provider";
|
|
2
|
-
|
|
3
|
-
//#region src/create-retryable-model.d.ts
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* The context provided to Retryables with the current attempt and all previous attempts.
|
|
7
|
-
*/
|
|
8
|
-
interface RetryContext {
|
|
9
|
-
current: RetryAttempt;
|
|
10
|
-
attempts: Array<RetryAttempt>;
|
|
11
|
-
totalAttempts: number;
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* A retry attempt with the error and model used
|
|
15
|
-
*/
|
|
16
|
-
interface RetryAttempt {
|
|
17
|
-
error: unknown;
|
|
18
|
-
model: LanguageModelV2;
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* A model to retry with and the maximum number of attempts for that model.
|
|
22
|
-
*/
|
|
23
|
-
type RetryModel = {
|
|
24
|
-
model: LanguageModelV2;
|
|
25
|
-
maxAttempts: number;
|
|
26
|
-
};
|
|
27
|
-
/**
|
|
28
|
-
* A function that determines whether to retry with a different model based on the current attempt and all previous attempts.
|
|
29
|
-
*/
|
|
30
|
-
type Retryable = (context: RetryContext) => RetryModel | Promise<RetryModel> | undefined;
|
|
31
|
-
/**
|
|
32
|
-
* Options for creating a retryable model.
|
|
33
|
-
*/
|
|
34
|
-
interface CreateRetryableOptions {
|
|
35
|
-
model: LanguageModelV2;
|
|
36
|
-
retries: Array<Retryable | LanguageModelV2>;
|
|
37
|
-
onError?: (context: RetryContext) => void;
|
|
38
|
-
onRetry?: (context: RetryContext) => void;
|
|
39
|
-
}
|
|
40
|
-
declare function createRetryable(config: CreateRetryableOptions): LanguageModelV2;
|
|
41
|
-
//#endregion
|
|
42
|
-
export { CreateRetryableOptions, RetryAttempt, RetryContext, RetryModel, Retryable, createRetryable };
|