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 +19 -8
- package/dist/{create-retryable-model-DzDFqgQO.d.ts → create-retryable-model-DtspEawi.d.ts} +15 -0
- package/dist/create-retryable-model-YqmeNfbq.js +308 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/retryables/index.d.ts +8 -2
- package/dist/retryables/index.js +20 -10
- package/package.json +1 -1
- package/dist/create-retryable-model-C4nAHxnW.js +0 -239
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 (
|
|
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.
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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
|
package/dist/retryables/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isErrorAttempt, isObject, isResultAttempt, isString } from "../create-retryable-model-
|
|
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 (
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
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,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 };
|