ai-retry 1.5.0 → 1.6.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 +139 -8
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +42 -15
- package/dist/parse-retry-headers-DIPVbwW5.mjs +26 -0
- package/dist/retryables/experimental/index.d.mts +248 -0
- package/dist/retryables/experimental/index.mjs +310 -0
- package/dist/retryables/index.d.mts +1 -1
- package/dist/retryables/index.mjs +1 -20
- package/dist/{types-wrgO_vOH.d.mts → types-CRKV-hdW.d.mts} +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -288,6 +288,9 @@ If you need more control over when to retry and which model to use, you can crea
|
|
|
288
288
|
> [!NOTE]
|
|
289
289
|
> You can return additional options like `maxAttempts`, `delay`, etc. along with the model.
|
|
290
290
|
|
|
291
|
+
> [!TIP]
|
|
292
|
+
> If you'd like the same flexibility with a typed, composable condition system, see [Experimental: Composable Conditions](#experimental-composable-conditions).
|
|
293
|
+
|
|
291
294
|
```typescript
|
|
292
295
|
import { anthropic } from '@ai-sdk/anthropic';
|
|
293
296
|
import { openai } from '@ai-sdk/openai';
|
|
@@ -370,6 +373,9 @@ There are several built-in dynamic retryables available for common use cases:
|
|
|
370
373
|
> [!TIP]
|
|
371
374
|
> You are missing a retryable for your use case? [Open an issue](https://github.com/zirkelc/ai-retry/issues/new) and let's discuss it!
|
|
372
375
|
|
|
376
|
+
> [!NOTE]
|
|
377
|
+
> Looking for a composable alternative? See [Experimental: Composable Conditions](#experimental-composable-conditions) for a `condition().action()` API that builds on small primitives.
|
|
378
|
+
|
|
373
379
|
- [`contentFilterTriggered`](./src/retryables/content-filter-triggered.ts): Content filter was triggered based on the prompt or completion.
|
|
374
380
|
- [`requestTimeout`](./src/retryables/request-timeout.ts): Request timeout occurred.
|
|
375
381
|
- [`requestNotRetryable`](./src/retryables/request-not-retryable.ts): Request failed with a non-retryable error.
|
|
@@ -578,6 +584,130 @@ const result = await generateText({
|
|
|
578
584
|
console.log(result.object); // { name: "Alice", age: 30 }
|
|
579
585
|
```
|
|
580
586
|
|
|
587
|
+
### Experimental: Composable Conditions
|
|
588
|
+
|
|
589
|
+
> [!WARNING]
|
|
590
|
+
> This API is experimental and may change. It is not exported from the package root; opt in via the deep import:
|
|
591
|
+
>
|
|
592
|
+
> ```ts
|
|
593
|
+
> import { ... } from 'ai-retry/retryables/experimental';
|
|
594
|
+
> ```
|
|
595
|
+
|
|
596
|
+
A `condition().action()` API for retryables. Conditions are built from small primitives (`error(fn)`, `result(fn)`), composed with `and` / `or` / `not`, and turned into a `Retryable` by one of two terminal actions: `.switch({ model })` or `.retry({ delay })`. The result drops into the same `retries: [...]` array as the stable helpers, so you can mix the two styles freely.
|
|
597
|
+
|
|
598
|
+
```typescript
|
|
599
|
+
import { anthropic } from '@ai-sdk/anthropic';
|
|
600
|
+
import { openai } from '@ai-sdk/openai';
|
|
601
|
+
import { generateText } from 'ai';
|
|
602
|
+
import { createRetryable } from 'ai-retry';
|
|
603
|
+
import {
|
|
604
|
+
error,
|
|
605
|
+
finishReason,
|
|
606
|
+
httpStatus,
|
|
607
|
+
} from 'ai-retry/retryables/experimental';
|
|
608
|
+
|
|
609
|
+
const retryableModel = createRetryable({
|
|
610
|
+
model: openai('gpt-4'),
|
|
611
|
+
retries: [
|
|
612
|
+
// Switch on 529 or any "overloaded" message
|
|
613
|
+
httpStatus(529, 'overloaded').switch({
|
|
614
|
+
model: anthropic('claude-3-haiku-20240307'),
|
|
615
|
+
}),
|
|
616
|
+
|
|
617
|
+
// Switch when the response was content-filtered
|
|
618
|
+
finishReason('content-filter').switch({ model: openai('gpt-4o') }),
|
|
619
|
+
|
|
620
|
+
// Retry the same model with exponential backoff on retryable errors
|
|
621
|
+
error.isRetryable(true).retry({ delay: 1_000, backoffFactor: 2 }),
|
|
622
|
+
],
|
|
623
|
+
});
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
#### High-level helpers
|
|
627
|
+
|
|
628
|
+
These cover the common cases. Each returns a `Condition` that you finalize with `.switch(...)` or `.retry(...)`.
|
|
629
|
+
|
|
630
|
+
| Helper | Matches when |
|
|
631
|
+
| ------------------------------ | -------------------------------------------------------------------------------------------------- |
|
|
632
|
+
| `httpStatus(...patterns)` | Numbers match the status code; strings match the message (substring); regex matches either |
|
|
633
|
+
| `timeout()` | `Error.name === 'TimeoutError'` (`AbortSignal.timeout()` fired) |
|
|
634
|
+
| `aborted()` | `Error.name === 'AbortError'` (manual `controller.abort()`) |
|
|
635
|
+
| `noImage()` | The image model threw `NoImageGeneratedError` |
|
|
636
|
+
| `finishReason(...reasons)` | The result's `finishReason.unified` matches one of the given values |
|
|
637
|
+
| `schemaInvalid()` | The result text fails JSON-schema validation against the call's `responseFormat` |
|
|
638
|
+
|
|
639
|
+
#### Actions
|
|
640
|
+
|
|
641
|
+
Every `Condition` exposes two terminal actions that turn it into a `Retryable`:
|
|
642
|
+
|
|
643
|
+
- **`.switch({ model, ...options })`** falls back to a different model when the condition matches. Optional fields (`maxAttempts`, `delay`, `backoffFactor`, `timeout`, `options`) are the same as on a normal `Retry` object.
|
|
644
|
+
- **`.retry({ delay?, backoffFactor?, ... })`** retries the current model when the condition matches. Honors `Retry-After` and `Retry-After-Ms` response headers when present, capped at 60 seconds.
|
|
645
|
+
|
|
646
|
+
#### Combinators
|
|
647
|
+
|
|
648
|
+
Compose conditions with the free functions or the methods on `Condition`:
|
|
649
|
+
|
|
650
|
+
```typescript
|
|
651
|
+
import {
|
|
652
|
+
and,
|
|
653
|
+
error,
|
|
654
|
+
httpStatus,
|
|
655
|
+
not,
|
|
656
|
+
or,
|
|
657
|
+
} from 'ai-retry/retryables/experimental';
|
|
658
|
+
|
|
659
|
+
or(httpStatus(429), error.message('overloaded'));
|
|
660
|
+
and(httpStatus(503), error.message('temporary'));
|
|
661
|
+
not(error.isRetryable(true));
|
|
662
|
+
|
|
663
|
+
// Method form
|
|
664
|
+
httpStatus(429).or(error.message('overloaded'));
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
#### Primitives
|
|
668
|
+
|
|
669
|
+
The two lowest-level builders. Reach for them when no helper covers your case:
|
|
670
|
+
|
|
671
|
+
| Primitive | Matches when |
|
|
672
|
+
| ------------------ | ----------------------------------------------------------------------------- |
|
|
673
|
+
| `error(predicate)` | The current attempt failed and `predicate(err, ctx)` returns true |
|
|
674
|
+
| `result(predicate)`| The current attempt succeeded and `predicate(res, ctx)` returns true (language models only) |
|
|
675
|
+
|
|
676
|
+
```typescript
|
|
677
|
+
import { APICallError } from 'ai';
|
|
678
|
+
import { error } from 'ai-retry/retryables/experimental';
|
|
679
|
+
|
|
680
|
+
error<MODEL, APICallError>(
|
|
681
|
+
(e) => APICallError.isInstance(e) && e.statusCode === 418,
|
|
682
|
+
).switch({ model: fallback });
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
A few common error fields have ready-made matchers on the `error` namespace:
|
|
686
|
+
|
|
687
|
+
| Helper | Matches when |
|
|
688
|
+
| ------------------------------- | ------------------------------------------------------------------------------------- |
|
|
689
|
+
| `error.isRetryable(flag)` | `APICallError.isRetryable === flag` (default `true`) |
|
|
690
|
+
| `error.statusCode(...patterns)` | Numbers match exactly; regex matches the stringified code (e.g. `/^5\d\d$/` for 5xx) |
|
|
691
|
+
| `error.message(...patterns)` | Substring (case-insensitive) or regex match against the error message |
|
|
692
|
+
|
|
693
|
+
#### Mapping from Built-in retryables
|
|
694
|
+
|
|
695
|
+
Each stable retryable has an equivalent in the new shape:
|
|
696
|
+
|
|
697
|
+
| Built-in | Composable form |
|
|
698
|
+
| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
699
|
+
| `contentFilterTriggered(m)` | `or(error(/* check e.data.error.code === 'content_filter' */), finishReason('content-filter')).switch({ model: m })` |
|
|
700
|
+
| `requestTimeout(m)` | `timeout().switch({ model: m, timeout: 60_000 })` |
|
|
701
|
+
| `requestNotRetryable(m)` | `error.isRetryable(false).switch({ model: m })` |
|
|
702
|
+
| `schemaMismatch(m)` | `schemaInvalid().switch({ model: m })` |
|
|
703
|
+
| `serviceOverloaded(m)` | `httpStatus(529, 'overloaded').switch({ model: m })` |
|
|
704
|
+
| `serviceUnavailable(m)` | `error.statusCode(503).switch({ model: m })` |
|
|
705
|
+
| `noImageGenerated(m)` | `noImage().switch({ model: m })` |
|
|
706
|
+
| `retryAfterDelay({ delay, backoffFactor })` | `error.isRetryable(true).retry({ delay, backoffFactor })` |
|
|
707
|
+
|
|
708
|
+
> [!NOTE]
|
|
709
|
+
> `error.isRetryable(true)` matches whatever the AI SDK's `APICallError` marks retryable. By default that's status codes 408, 409, 429, and any 5xx, plus network errors and provider-specific overrides (e.g. Anthropic flips it on `error.type === 'overloaded_error'`). It picks up more cases than a manual status-code list.
|
|
710
|
+
|
|
581
711
|
### Options
|
|
582
712
|
|
|
583
713
|
#### Disabling Retries
|
|
@@ -652,7 +782,9 @@ const retryableModel = createRetryable({
|
|
|
652
782
|
|
|
653
783
|
#### Timeouts
|
|
654
784
|
|
|
655
|
-
When a retry specifies a `timeout` value, a fresh `AbortSignal.timeout()` is created for that retry attempt,
|
|
785
|
+
When a retry specifies a `timeout` value, a fresh `AbortSignal.timeout()` is created for that retry attempt. If the original `abortSignal` is still alive, the fresh deadline is composed with it via `AbortSignal.any()` so user cancellation still works mid-retry. If the original signal is already aborted (for example it carried a request-level deadline that already fired), it is dropped so the retry runs against the fresh deadline alone.
|
|
786
|
+
|
|
787
|
+
If the original `abortSignal` is already aborted at the time of retry and the chosen retry does **not** supply a `timeout`, ai-retry rethrows the original error rather than firing a misleading retry against the dead signal. `onError` still fires for observability, but `onRetry` is skipped. Setting `retry.timeout` is the explicit opt-in for retrying past an aborted signal.
|
|
656
788
|
|
|
657
789
|
```typescript
|
|
658
790
|
const retryableModel = createRetryable({
|
|
@@ -795,7 +927,7 @@ The following options can be overridden:
|
|
|
795
927
|
|
|
796
928
|
#### Dynamic Call Options
|
|
797
929
|
|
|
798
|
-
You can also override call options dynamically from inside the `onRetry` callback, instead of declaring them statically on the retry object. This is useful when the override depends on something only known at runtime, like the prompt that just failed, the model that's about to be tried next, or the error that triggered the retry. The overrides apply to the upcoming retry attempt only, and can change the same fields as the static `options` on a retry
|
|
930
|
+
You can also override call options dynamically from inside the `onRetry` callback, instead of declaring them statically on the retry object. This is useful when the override depends on something only known at runtime, like the prompt that just failed, the model that's about to be tried next, or the error that triggered the retry. The overrides apply to the upcoming retry attempt only, and can change the same fields as the static `options` on a retry. The callback may also be `async` if computing the override needs to do work (e.g. fetching a fresh credential).
|
|
799
931
|
|
|
800
932
|
A common use case is sanitizing provider-scoped metadata when falling back to a different provider, for example stripping `providerOptions.azure.itemId` references from the previous prompt before retrying on OpenAI:
|
|
801
933
|
|
|
@@ -815,10 +947,7 @@ const retryableModel = createRetryable({
|
|
|
815
947
|
// Strip provider-scoped metadata from the prompt before retrying on a different provider
|
|
816
948
|
return {
|
|
817
949
|
options: {
|
|
818
|
-
prompt:
|
|
819
|
-
previous.options.prompt,
|
|
820
|
-
current.model.provider,
|
|
821
|
-
),
|
|
950
|
+
prompt: stripProviderMetadata(current.options.prompt),
|
|
822
951
|
},
|
|
823
952
|
};
|
|
824
953
|
}
|
|
@@ -826,6 +955,8 @@ const retryableModel = createRetryable({
|
|
|
826
955
|
});
|
|
827
956
|
```
|
|
828
957
|
|
|
958
|
+
Inside the `onRetry` callback, `context.current.model` is the model that's about to be tried next, while `context.current.options` and `context.current.error` describe the failed attempt that triggered the retry. The previous model is available at `context.attempts.at(-1).model`.
|
|
959
|
+
|
|
829
960
|
`onRetry` may also be `async`, which is useful if computing the override needs to do work (e.g. fetching a fresh credential):
|
|
830
961
|
|
|
831
962
|
```typescript
|
|
@@ -844,7 +975,7 @@ const retryableModel = createRetryable({
|
|
|
844
975
|
**Precedence** for the upcoming retry attempt (highest to lowest):
|
|
845
976
|
|
|
846
977
|
1. The value returned from `onRetry`
|
|
847
|
-
2. `
|
|
978
|
+
2. The `options` returned from the retryable
|
|
848
979
|
3. The original call options from the request
|
|
849
980
|
|
|
850
981
|
#### Logging
|
|
@@ -961,7 +1092,7 @@ interface RetryableModelOptions<
|
|
|
961
1092
|
- `disabled`: Disable all retry logic. Can be a boolean or function returning boolean. Default: `false` (retries enabled).
|
|
962
1093
|
- `reset`: Controls when to reset back to the base model after a successful retry. Default: `after-request`.
|
|
963
1094
|
- `onError`: Callback invoked when an error occurs.
|
|
964
|
-
- `onRetry`: Callback invoked before attempting a retry. May optionally return an `OnRetryOverrides` object (or a `Promise` of one) to override `options.*`
|
|
1095
|
+
- `onRetry`: Callback invoked before attempting a retry. May optionally return an `OnRetryOverrides` object (or a `Promise` of one) to override `options.*` for the upcoming attempt only. See [Dynamic Call Options via `onRetry`](#dynamic-call-options-via-onretry).
|
|
965
1096
|
- `onSuccess`: Callback invoked after a successful request. Receives the model that handled the request and all previous attempts.
|
|
966
1097
|
|
|
967
1098
|
#### `Reset`
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as RetryResultAttempt, C as Result, D as RetryCallOptions, E as RetryAttempt, F as SuccessContext, M as RetryableModelOptions, N as RetryableOptions, O as RetryContext, P as SuccessAttempt, S as ResolvedModel, T as Retry, _ as OnRetryOverrides, a as EmbeddingModelRetryCallOptions, b as ResolvableLanguageModel, c as ImageModelCallOptions, d as LanguageModel, f as LanguageModelCallOptions, g as LanguageModelStreamPart, h as LanguageModelStream, i as EmbeddingModelEmbed, j as Retryable, k as RetryErrorAttempt, l as ImageModelGenerate, m as LanguageModelRetryCallOptions, n as EmbeddingModel, o as GatewayLanguageModelId, p as LanguageModelGenerate, r as EmbeddingModelCallOptions, s as ImageModel, t as CallOptions, u as ImageModelRetryCallOptions, v as ProviderOptions, w as Retries, x as ResolvableModel, y as Reset } from "./types-
|
|
1
|
+
import { A as RetryResultAttempt, C as Result, D as RetryCallOptions, E as RetryAttempt, F as SuccessContext, M as RetryableModelOptions, N as RetryableOptions, O as RetryContext, P as SuccessAttempt, S as ResolvedModel, T as Retry, _ as OnRetryOverrides, a as EmbeddingModelRetryCallOptions, b as ResolvableLanguageModel, c as ImageModelCallOptions, d as LanguageModel, f as LanguageModelCallOptions, g as LanguageModelStreamPart, h as LanguageModelStream, i as EmbeddingModelEmbed, j as Retryable, k as RetryErrorAttempt, l as ImageModelGenerate, m as LanguageModelRetryCallOptions, n as EmbeddingModel, o as GatewayLanguageModelId, p as LanguageModelGenerate, r as EmbeddingModelCallOptions, s as ImageModel, t as CallOptions, u as ImageModelRetryCallOptions, v as ProviderOptions, w as Retries, x as ResolvableModel, y as Reset } from "./types-CRKV-hdW.mjs";
|
|
2
2
|
import * as _ai_sdk_provider0 from "@ai-sdk/provider";
|
|
3
3
|
|
|
4
4
|
//#region src/create-retryable-model.d.ts
|
package/dist/index.mjs
CHANGED
|
@@ -197,14 +197,18 @@ function resolveProviderOptions(base, currentRetry, onRetryOverrides) {
|
|
|
197
197
|
/**
|
|
198
198
|
* Resolve `abortSignal` for the upcoming attempt.
|
|
199
199
|
*
|
|
200
|
-
* If
|
|
201
|
-
*
|
|
202
|
-
*
|
|
200
|
+
* If `currentRetry.timeout` is set, a fresh `AbortSignal.timeout(...)` is
|
|
201
|
+
* created. When the base signal is still alive, the fresh deadline is
|
|
202
|
+
* composed with it via `AbortSignal.any` so the user can still cancel
|
|
203
|
+
* mid-retry. When the base is already aborted, it is dropped so the retry
|
|
204
|
+
* runs against the fresh deadline alone. Without a retry timeout, the base
|
|
205
|
+
* is preserved unchanged.
|
|
203
206
|
*/
|
|
204
|
-
function resolveAbortSignal(base, currentRetry
|
|
205
|
-
if (
|
|
206
|
-
|
|
207
|
-
return base;
|
|
207
|
+
function resolveAbortSignal(base, currentRetry) {
|
|
208
|
+
if (currentRetry?.timeout === void 0) return base;
|
|
209
|
+
const fresh = AbortSignal.timeout(currentRetry.timeout);
|
|
210
|
+
if (base !== void 0 && !base.aborted) return AbortSignal.any([base, fresh]);
|
|
211
|
+
return fresh;
|
|
208
212
|
}
|
|
209
213
|
/**
|
|
210
214
|
* Merge call options for the upcoming language model retry attempt.
|
|
@@ -231,7 +235,7 @@ function mergeLanguageModelCallOptions(input) {
|
|
|
231
235
|
seed: overrideOptions.seed ?? retryOptions.seed ?? callOptions.seed,
|
|
232
236
|
headers: overrideOptions.headers ?? retryOptions.headers ?? callOptions.headers,
|
|
233
237
|
providerOptions: resolveProviderOptions(callOptions.providerOptions, currentRetry, onRetryOverrides),
|
|
234
|
-
abortSignal: resolveAbortSignal(callOptions.abortSignal, currentRetry
|
|
238
|
+
abortSignal: resolveAbortSignal(callOptions.abortSignal, currentRetry)
|
|
235
239
|
};
|
|
236
240
|
}
|
|
237
241
|
/**
|
|
@@ -246,7 +250,7 @@ function mergeEmbeddingModelCallOptions(input) {
|
|
|
246
250
|
values: overrideOptions.values ?? retryOptions.values ?? callOptions.values,
|
|
247
251
|
headers: overrideOptions.headers ?? retryOptions.headers ?? callOptions.headers,
|
|
248
252
|
providerOptions: resolveProviderOptions(callOptions.providerOptions, currentRetry, onRetryOverrides),
|
|
249
|
-
abortSignal: resolveAbortSignal(callOptions.abortSignal, currentRetry
|
|
253
|
+
abortSignal: resolveAbortSignal(callOptions.abortSignal, currentRetry)
|
|
250
254
|
};
|
|
251
255
|
}
|
|
252
256
|
/**
|
|
@@ -264,7 +268,7 @@ function mergeImageModelCallOptions(input) {
|
|
|
264
268
|
seed: overrideOptions.seed ?? retryOptions.seed ?? callOptions.seed,
|
|
265
269
|
headers: overrideOptions.headers ?? retryOptions.headers ?? callOptions.headers,
|
|
266
270
|
providerOptions: resolveProviderOptions(callOptions.providerOptions, currentRetry, onRetryOverrides),
|
|
267
|
-
abortSignal: resolveAbortSignal(callOptions.abortSignal, currentRetry
|
|
271
|
+
abortSignal: resolveAbortSignal(callOptions.abortSignal, currentRetry)
|
|
268
272
|
};
|
|
269
273
|
}
|
|
270
274
|
|
|
@@ -331,9 +335,15 @@ var RetryableEmbeddingModel = class extends BaseRetryableModel {
|
|
|
331
335
|
callOptions: retryCallOptions
|
|
332
336
|
};
|
|
333
337
|
} catch (error) {
|
|
334
|
-
if (isAbortError(error)) throw error;
|
|
335
338
|
const { retryModel, attempt } = await this.handleError(error, attempts, retryCallOptions);
|
|
336
339
|
attempts.push(attempt);
|
|
340
|
+
/**
|
|
341
|
+
* If the inbound abort signal is already aborted and the chosen
|
|
342
|
+
* retry does not supply a fresh deadline, the retry would die
|
|
343
|
+
* instantly with the same abort. Rethrow rather than fire a
|
|
344
|
+
* misleading retry against a dead signal.
|
|
345
|
+
*/
|
|
346
|
+
if (input.callOptions.abortSignal?.aborted && retryModel.timeout === void 0) throw error;
|
|
337
347
|
if (retryModel.delay) {
|
|
338
348
|
/**
|
|
339
349
|
* Calculate exponential backoff delay based on the number of attempts for this specific model.
|
|
@@ -474,11 +484,15 @@ var RetryableImageModel = class extends BaseRetryableModel {
|
|
|
474
484
|
callOptions: retryCallOptions
|
|
475
485
|
};
|
|
476
486
|
} catch (error) {
|
|
477
|
-
/** Don't retry if user manually aborted the request. */
|
|
478
|
-
/** TimeoutError from AbortSignal.timeout() will still be handled by retry handlers. */
|
|
479
|
-
if (isAbortError(error)) throw error;
|
|
480
487
|
const { retryModel, attempt } = await this.handleError(error, attempts, retryCallOptions);
|
|
481
488
|
attempts.push(attempt);
|
|
489
|
+
/**
|
|
490
|
+
* If the inbound abort signal is already aborted and the chosen
|
|
491
|
+
* retry does not supply a fresh deadline, the retry would die
|
|
492
|
+
* instantly with the same abort. Rethrow rather than fire a
|
|
493
|
+
* misleading retry against a dead signal.
|
|
494
|
+
*/
|
|
495
|
+
if (input.callOptions.abortSignal?.aborted && retryModel.timeout === void 0) throw error;
|
|
482
496
|
if (retryModel.delay) {
|
|
483
497
|
/**
|
|
484
498
|
* Calculate exponential backoff delay based on the number of attempts for this specific model.
|
|
@@ -650,9 +664,15 @@ var RetryableLanguageModel = class extends BaseRetryableModel {
|
|
|
650
664
|
callOptions: retryCallOptions
|
|
651
665
|
};
|
|
652
666
|
} catch (error) {
|
|
653
|
-
if (isAbortError(error)) throw error;
|
|
654
667
|
const { retryModel, attempt } = await this.handleError(error, attempts, retryCallOptions);
|
|
655
668
|
attempts.push(attempt);
|
|
669
|
+
/**
|
|
670
|
+
* If the inbound abort signal is already aborted and the chosen
|
|
671
|
+
* retry does not supply a fresh deadline, the retry would die
|
|
672
|
+
* instantly with the same abort. Rethrow rather than fire a
|
|
673
|
+
* misleading retry against a dead signal.
|
|
674
|
+
*/
|
|
675
|
+
if (input.callOptions.abortSignal?.aborted && retryModel.timeout === void 0) throw error;
|
|
656
676
|
if (retryModel.delay) {
|
|
657
677
|
/**
|
|
658
678
|
* Calculate exponential backoff delay based on the number of attempts for this specific model.
|
|
@@ -825,6 +845,13 @@ var RetryableLanguageModel = class extends BaseRetryableModel {
|
|
|
825
845
|
* Save the attempt
|
|
826
846
|
*/
|
|
827
847
|
attempts.push(attempt);
|
|
848
|
+
/**
|
|
849
|
+
* If the inbound abort signal is already aborted and the chosen
|
|
850
|
+
* retry does not supply a fresh deadline, the retry would die
|
|
851
|
+
* instantly with the same abort. Rethrow rather than fire a
|
|
852
|
+
* misleading retry against a dead signal.
|
|
853
|
+
*/
|
|
854
|
+
if (callOptions.abortSignal?.aborted && retryModel.timeout === void 0) throw error;
|
|
828
855
|
if (retryModel.delay) {
|
|
829
856
|
/**
|
|
830
857
|
* Calculate exponential backoff delay based on the number of attempts for this specific model.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//#region src/parse-retry-headers.ts
|
|
2
|
+
/**
|
|
3
|
+
* Upper bound for `Retry-After` / `Retry-After-Ms` honored by retry actions.
|
|
4
|
+
* Servers can technically request very long delays; cap to keep retries
|
|
5
|
+
* responsive.
|
|
6
|
+
*/
|
|
7
|
+
const MAX_RETRY_AFTER_MS = 6e4;
|
|
8
|
+
function parseRetryHeaders(headers) {
|
|
9
|
+
if (!headers) return null;
|
|
10
|
+
const retryAfterMs = headers["retry-after-ms"];
|
|
11
|
+
if (retryAfterMs) {
|
|
12
|
+
const delayMs = Number.parseFloat(retryAfterMs);
|
|
13
|
+
if (!Number.isNaN(delayMs) && delayMs >= 0) return delayMs;
|
|
14
|
+
}
|
|
15
|
+
const retryAfter = headers["retry-after"];
|
|
16
|
+
if (retryAfter) {
|
|
17
|
+
const seconds = Number.parseFloat(retryAfter);
|
|
18
|
+
if (!Number.isNaN(seconds)) return seconds * 1e3;
|
|
19
|
+
const date = Date.parse(retryAfter);
|
|
20
|
+
if (!Number.isNaN(date)) return Math.max(0, date - Date.now());
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
//#endregion
|
|
26
|
+
export { parseRetryHeaders as n, MAX_RETRY_AFTER_MS as t };
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { O as RetryContext, T as Retry, b as ResolvableLanguageModel, j as Retryable, n as EmbeddingModel, p as LanguageModelGenerate, s as ImageModel } from "../../types-CRKV-hdW.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/retryables/experimental/condition.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Any model the retryable system supports.
|
|
6
|
+
*/
|
|
7
|
+
type AnyModel = ResolvableLanguageModel | EmbeddingModel | ImageModel;
|
|
8
|
+
/**
|
|
9
|
+
* Predicate over a `RetryContext`. May be sync or async.
|
|
10
|
+
*/
|
|
11
|
+
type Predicate<MODEL extends AnyModel> = (ctx: RetryContext<MODEL>) => boolean | Promise<boolean>;
|
|
12
|
+
/**
|
|
13
|
+
* Argument shape for `Condition.switch`. The target `model` is required;
|
|
14
|
+
* all other `Retry` fields are optional.
|
|
15
|
+
*/
|
|
16
|
+
type SwitchTarget<MODEL extends AnyModel> = {
|
|
17
|
+
model: MODEL;
|
|
18
|
+
} & Omit<Retry<MODEL>, 'model'>;
|
|
19
|
+
/**
|
|
20
|
+
* Argument shape for `Condition.retry`. Same as `Retry` without `model`,
|
|
21
|
+
* since retry reuses the current model.
|
|
22
|
+
*/
|
|
23
|
+
type RetryOptions<MODEL extends AnyModel> = Omit<Retry<MODEL>, 'model'>;
|
|
24
|
+
/**
|
|
25
|
+
* A predicate over a `RetryContext` paired with two terminal actions
|
|
26
|
+
* (`switch`, `retry`) that turn it into a `Retryable<MODEL>`. Compose
|
|
27
|
+
* conditions with `and`, `or`, `not`.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* const cond = httpStatus(429, 503);
|
|
31
|
+
* cond.switch({ model: fallback });
|
|
32
|
+
* cond.retry({ delay: 1000 });
|
|
33
|
+
*/
|
|
34
|
+
declare class Condition<MODEL extends AnyModel> {
|
|
35
|
+
private readonly predicate;
|
|
36
|
+
constructor(predicate: Predicate<MODEL>);
|
|
37
|
+
/**
|
|
38
|
+
* Run the predicate against a context and resolve to a boolean.
|
|
39
|
+
*/
|
|
40
|
+
evaluate(ctx: RetryContext<MODEL>): Promise<boolean>;
|
|
41
|
+
/**
|
|
42
|
+
* Switch to a different model when the condition matches.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* httpStatus(529).switch({ model: fallback })
|
|
46
|
+
*/
|
|
47
|
+
switch(target: SwitchTarget<MODEL>): Retryable<MODEL>;
|
|
48
|
+
/**
|
|
49
|
+
* Retry the same model when the condition matches. Honors
|
|
50
|
+
* `Retry-After` and `Retry-After-Ms` response headers when present,
|
|
51
|
+
* capped at 60 seconds, overriding any provided `delay`.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* error.isRetryable(true).retry({ delay: 1000, backoffFactor: 2 })
|
|
55
|
+
*/
|
|
56
|
+
retry(options?: RetryOptions<MODEL>): Retryable<MODEL>;
|
|
57
|
+
/**
|
|
58
|
+
* Combine with another condition; matches when both match.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* httpStatus(429).and(error.message('overloaded'))
|
|
62
|
+
*/
|
|
63
|
+
and(other: Condition<MODEL>): Condition<MODEL>;
|
|
64
|
+
/**
|
|
65
|
+
* Combine with another condition; matches when either matches.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* httpStatus(429).or(error.message('overloaded'))
|
|
69
|
+
*/
|
|
70
|
+
or(other: Condition<MODEL>): Condition<MODEL>;
|
|
71
|
+
/**
|
|
72
|
+
* Invert the condition.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* error.isRetryable(true).not()
|
|
76
|
+
*/
|
|
77
|
+
not(): Condition<MODEL>;
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
80
|
+
//#region src/retryables/experimental/aborted.d.ts
|
|
81
|
+
/**
|
|
82
|
+
* Match a manual abort: an `Error` with `name === 'AbortError'`, which
|
|
83
|
+
* `controller.abort()` produces. Distinct from `timeout()`, which
|
|
84
|
+
* matches `AbortSignal.timeout()` firing.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* aborted().switch({ model: fallback })
|
|
88
|
+
*/
|
|
89
|
+
declare function aborted<MODEL extends AnyModel = AnyModel>(): Condition<MODEL>;
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/retryables/experimental/and.d.ts
|
|
92
|
+
/**
|
|
93
|
+
* Match only when all of the given conditions match. Evaluates left to
|
|
94
|
+
* right and stops on the first miss.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* and(httpStatus(429), error.message('overloaded'))
|
|
98
|
+
*/
|
|
99
|
+
declare function and<MODEL extends AnyModel>(...conditions: Array<Condition<MODEL>>): Condition<MODEL>;
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/retryables/experimental/error.d.ts
|
|
102
|
+
/**
|
|
103
|
+
* Build a condition from a predicate over the current error. The
|
|
104
|
+
* predicate runs only when the current attempt failed with an error;
|
|
105
|
+
* result attempts return false.
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* error<MODEL, APICallError>(
|
|
109
|
+
* (e) => APICallError.isInstance(e) && e.statusCode === 418,
|
|
110
|
+
* )
|
|
111
|
+
*/
|
|
112
|
+
declare function error<MODEL extends AnyModel = AnyModel, E = unknown>(predicate: (err: E, ctx: RetryContext<MODEL>) => boolean | Promise<boolean>): Condition<MODEL>;
|
|
113
|
+
/**
|
|
114
|
+
* Higher-level matchers for common error fields. Drop down to `error(fn)`
|
|
115
|
+
* for anything not covered.
|
|
116
|
+
*
|
|
117
|
+
* `Error.name` is intentionally not exposed here because the property
|
|
118
|
+
* name would clash with `Function.prototype.name`. Use `timeout()` or
|
|
119
|
+
* `aborted()` for the common cases, or `error(fn)` for custom names.
|
|
120
|
+
*/
|
|
121
|
+
declare namespace error {
|
|
122
|
+
/**
|
|
123
|
+
* Match when the error explicitly carries `isRetryable === flag`.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* error.isRetryable(true).retry({ delay: 1000 })
|
|
127
|
+
* error.isRetryable(false).switch({ model: fallback })
|
|
128
|
+
*/
|
|
129
|
+
function isRetryable<MODEL extends AnyModel = AnyModel>(flag?: boolean): Condition<MODEL>;
|
|
130
|
+
/**
|
|
131
|
+
* Match by HTTP status code. Numbers match exactly; regular expressions
|
|
132
|
+
* match against the stringified code, useful for range checks.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* error.statusCode(429, 503)
|
|
136
|
+
* error.statusCode(/^5\d\d$/)
|
|
137
|
+
*/
|
|
138
|
+
function statusCode<MODEL extends AnyModel = AnyModel>(...patterns: Array<number | RegExp>): Condition<MODEL>;
|
|
139
|
+
/**
|
|
140
|
+
* Match the error message against substrings or regular expressions.
|
|
141
|
+
* Substring matching is case-insensitive: both the pattern and the
|
|
142
|
+
* message are lowercased before matching. Regular expressions match
|
|
143
|
+
* as written; use the `i` flag for case-insensitive regex matching.
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* error.message('overloaded')
|
|
147
|
+
* error.message(/rate.?limit/i)
|
|
148
|
+
* error.message('overloaded', /rate.?limit/i)
|
|
149
|
+
*/
|
|
150
|
+
function message<MODEL extends AnyModel = AnyModel>(...patterns: Array<string | RegExp>): Condition<MODEL>;
|
|
151
|
+
}
|
|
152
|
+
//#endregion
|
|
153
|
+
//#region src/retryables/experimental/finish-reason.d.ts
|
|
154
|
+
/**
|
|
155
|
+
* Match the result's finish reason against one of the given values.
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* finishReason('content-filter')
|
|
159
|
+
* finishReason('content-filter', 'length')
|
|
160
|
+
*/
|
|
161
|
+
declare function finishReason<MODEL extends ResolvableLanguageModel = ResolvableLanguageModel>(...reasons: Array<string>): Condition<MODEL>;
|
|
162
|
+
//#endregion
|
|
163
|
+
//#region src/retryables/experimental/http-status.d.ts
|
|
164
|
+
/**
|
|
165
|
+
* A pattern accepted by `httpStatus`. Numbers match the response status
|
|
166
|
+
* code; strings match the error message as a substring; regular
|
|
167
|
+
* expressions match against both the stringified status code and the
|
|
168
|
+
* error message.
|
|
169
|
+
*/
|
|
170
|
+
type StatusPattern = number | string | RegExp;
|
|
171
|
+
/**
|
|
172
|
+
* Match an `APICallError` by status code, message substring, or regular
|
|
173
|
+
* expression. Numbers match the status code; strings match the message;
|
|
174
|
+
* regular expressions match either the stringified status code or the
|
|
175
|
+
* message. Mix any combination in a single call; matches when any
|
|
176
|
+
* pattern matches.
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* httpStatus(529)
|
|
180
|
+
* httpStatus(529, 'overloaded')
|
|
181
|
+
* httpStatus(/^5\d\d$/)
|
|
182
|
+
* httpStatus(529, 'overloaded', /rate.?limit/i)
|
|
183
|
+
*/
|
|
184
|
+
declare function httpStatus<MODEL extends AnyModel = AnyModel>(...patterns: Array<StatusPattern>): Condition<MODEL>;
|
|
185
|
+
//#endregion
|
|
186
|
+
//#region src/retryables/experimental/no-image.d.ts
|
|
187
|
+
/**
|
|
188
|
+
* Match when image generation produced no images
|
|
189
|
+
* (`NoImageGeneratedError`).
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* noImage().switch({ model: fallback })
|
|
193
|
+
*/
|
|
194
|
+
declare function noImage<MODEL extends ImageModel = ImageModel>(): Condition<MODEL>;
|
|
195
|
+
//#endregion
|
|
196
|
+
//#region src/retryables/experimental/not.d.ts
|
|
197
|
+
/**
|
|
198
|
+
* Invert a condition.
|
|
199
|
+
*
|
|
200
|
+
* @example
|
|
201
|
+
* not(error.isRetryable(true))
|
|
202
|
+
*/
|
|
203
|
+
declare function not<MODEL extends AnyModel>(condition: Condition<MODEL>): Condition<MODEL>;
|
|
204
|
+
//#endregion
|
|
205
|
+
//#region src/retryables/experimental/or.d.ts
|
|
206
|
+
/**
|
|
207
|
+
* Match when any of the given conditions match. Evaluates left to right
|
|
208
|
+
* and stops on the first match.
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* or(httpStatus(429), error.message('overloaded'))
|
|
212
|
+
*/
|
|
213
|
+
declare function or<MODEL extends AnyModel>(...conditions: Array<Condition<MODEL>>): Condition<MODEL>;
|
|
214
|
+
//#endregion
|
|
215
|
+
//#region src/retryables/experimental/result.d.ts
|
|
216
|
+
/**
|
|
217
|
+
* Build a condition from a predicate over the current generate result.
|
|
218
|
+
* Available for language models only. The predicate runs only when the
|
|
219
|
+
* current attempt succeeded; error attempts return false.
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* result<MODEL>((res) => res.finishReason.unified === 'length')
|
|
223
|
+
*/
|
|
224
|
+
declare function result<MODEL extends ResolvableLanguageModel = ResolvableLanguageModel>(predicate: (res: LanguageModelGenerate, ctx: RetryContext<MODEL>) => boolean | Promise<boolean>): Condition<MODEL>;
|
|
225
|
+
//#endregion
|
|
226
|
+
//#region src/retryables/experimental/schema-invalid.d.ts
|
|
227
|
+
/**
|
|
228
|
+
* Match when the result text fails JSON schema validation. The schema is
|
|
229
|
+
* read from the call's `responseFormat`, which `Output.object()` sets
|
|
230
|
+
* automatically. No-op when no schema is configured.
|
|
231
|
+
*
|
|
232
|
+
* @example
|
|
233
|
+
* schemaInvalid().switch({ model: fallback })
|
|
234
|
+
*/
|
|
235
|
+
declare function schemaInvalid<MODEL extends ResolvableLanguageModel = ResolvableLanguageModel>(): Condition<MODEL>;
|
|
236
|
+
//#endregion
|
|
237
|
+
//#region src/retryables/experimental/timeout.d.ts
|
|
238
|
+
/**
|
|
239
|
+
* Match a timeout error: an `Error` with `name === 'TimeoutError'`,
|
|
240
|
+
* which `AbortSignal.timeout()` produces when the timeout fires.
|
|
241
|
+
* Distinct from `aborted()`, which matches manual aborts.
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* timeout().switch({ model: fallback, timeout: 60_000 })
|
|
245
|
+
*/
|
|
246
|
+
declare function timeout<MODEL extends AnyModel = AnyModel>(): Condition<MODEL>;
|
|
247
|
+
//#endregion
|
|
248
|
+
export { AnyModel, Condition, Predicate, RetryOptions, StatusPattern, SwitchTarget, aborted, and, error, finishReason, httpStatus, noImage, not, or, result, schemaInvalid, timeout };
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { l as isResultAttempt, r as isErrorAttempt } from "../../utils-CfnsSGrw.mjs";
|
|
2
|
+
import { n as parseRetryHeaders, t as MAX_RETRY_AFTER_MS } from "../../parse-retry-headers-DIPVbwW5.mjs";
|
|
3
|
+
import { APICallError, NoImageGeneratedError } from "ai";
|
|
4
|
+
import { safeParseJSON } from "@ai-sdk/provider-utils";
|
|
5
|
+
import { fromJSONSchema } from "zod";
|
|
6
|
+
|
|
7
|
+
//#region src/retryables/experimental/condition.ts
|
|
8
|
+
/**
|
|
9
|
+
* A predicate over a `RetryContext` paired with two terminal actions
|
|
10
|
+
* (`switch`, `retry`) that turn it into a `Retryable<MODEL>`. Compose
|
|
11
|
+
* conditions with `and`, `or`, `not`.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* const cond = httpStatus(429, 503);
|
|
15
|
+
* cond.switch({ model: fallback });
|
|
16
|
+
* cond.retry({ delay: 1000 });
|
|
17
|
+
*/
|
|
18
|
+
var Condition = class Condition {
|
|
19
|
+
constructor(predicate) {
|
|
20
|
+
this.predicate = predicate;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Run the predicate against a context and resolve to a boolean.
|
|
24
|
+
*/
|
|
25
|
+
async evaluate(ctx) {
|
|
26
|
+
return this.predicate(ctx);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Switch to a different model when the condition matches.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* httpStatus(529).switch({ model: fallback })
|
|
33
|
+
*/
|
|
34
|
+
switch(target) {
|
|
35
|
+
return async (ctx) => {
|
|
36
|
+
if (!await this.evaluate(ctx)) return void 0;
|
|
37
|
+
return {
|
|
38
|
+
maxAttempts: 1,
|
|
39
|
+
...target
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Retry the same model when the condition matches. Honors
|
|
45
|
+
* `Retry-After` and `Retry-After-Ms` response headers when present,
|
|
46
|
+
* capped at 60 seconds, overriding any provided `delay`.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* error.isRetryable(true).retry({ delay: 1000, backoffFactor: 2 })
|
|
50
|
+
*/
|
|
51
|
+
retry(options) {
|
|
52
|
+
return async (ctx) => {
|
|
53
|
+
if (!await this.evaluate(ctx)) return void 0;
|
|
54
|
+
const model = ctx.current.model;
|
|
55
|
+
if (isErrorAttempt(ctx.current)) {
|
|
56
|
+
const { error: err } = ctx.current;
|
|
57
|
+
if (APICallError.isInstance(err)) {
|
|
58
|
+
const headerDelay = parseRetryHeaders(err.responseHeaders);
|
|
59
|
+
if (headerDelay !== null) return {
|
|
60
|
+
model,
|
|
61
|
+
...options,
|
|
62
|
+
delay: Math.min(headerDelay, MAX_RETRY_AFTER_MS),
|
|
63
|
+
backoffFactor: 1
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
model,
|
|
69
|
+
...options
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Combine with another condition; matches when both match.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* httpStatus(429).and(error.message('overloaded'))
|
|
78
|
+
*/
|
|
79
|
+
and(other) {
|
|
80
|
+
return new Condition(async (ctx) => await this.evaluate(ctx) && await other.evaluate(ctx));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Combine with another condition; matches when either matches.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* httpStatus(429).or(error.message('overloaded'))
|
|
87
|
+
*/
|
|
88
|
+
or(other) {
|
|
89
|
+
return new Condition(async (ctx) => await this.evaluate(ctx) || await other.evaluate(ctx));
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Invert the condition.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* error.isRetryable(true).not()
|
|
96
|
+
*/
|
|
97
|
+
not() {
|
|
98
|
+
return new Condition(async (ctx) => !await this.evaluate(ctx));
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region src/retryables/experimental/error.ts
|
|
104
|
+
/**
|
|
105
|
+
* Build a condition from a predicate over the current error. The
|
|
106
|
+
* predicate runs only when the current attempt failed with an error;
|
|
107
|
+
* result attempts return false.
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* error<MODEL, APICallError>(
|
|
111
|
+
* (e) => APICallError.isInstance(e) && e.statusCode === 418,
|
|
112
|
+
* )
|
|
113
|
+
*/
|
|
114
|
+
function error(predicate) {
|
|
115
|
+
return new Condition(async (ctx) => {
|
|
116
|
+
if (!isErrorAttempt(ctx.current)) return false;
|
|
117
|
+
return predicate(ctx.current.error, ctx);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
(function(_error) {
|
|
121
|
+
function isRetryable(flag = true) {
|
|
122
|
+
return error((e) => APICallError.isInstance(e) && e.isRetryable === flag);
|
|
123
|
+
}
|
|
124
|
+
_error.isRetryable = isRetryable;
|
|
125
|
+
function statusCode(...patterns) {
|
|
126
|
+
return error((e) => {
|
|
127
|
+
if (!APICallError.isInstance(e)) return false;
|
|
128
|
+
const code = e.statusCode;
|
|
129
|
+
if (code === void 0) return false;
|
|
130
|
+
return patterns.some((p) => typeof p === "number" ? p === code : p.test(String(code)));
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
_error.statusCode = statusCode;
|
|
134
|
+
function message(...patterns) {
|
|
135
|
+
return error((e) => {
|
|
136
|
+
if (!(e instanceof Error)) return false;
|
|
137
|
+
const lower = e.message.toLowerCase();
|
|
138
|
+
return patterns.some((p) => typeof p === "string" ? lower.includes(p.toLowerCase()) : p.test(e.message));
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
_error.message = message;
|
|
142
|
+
})(error || (error = {}));
|
|
143
|
+
|
|
144
|
+
//#endregion
|
|
145
|
+
//#region src/retryables/experimental/aborted.ts
|
|
146
|
+
/**
|
|
147
|
+
* Match a manual abort: an `Error` with `name === 'AbortError'`, which
|
|
148
|
+
* `controller.abort()` produces. Distinct from `timeout()`, which
|
|
149
|
+
* matches `AbortSignal.timeout()` firing.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* aborted().switch({ model: fallback })
|
|
153
|
+
*/
|
|
154
|
+
function aborted() {
|
|
155
|
+
return error((err) => err instanceof Error && err.name === "AbortError");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
//#endregion
|
|
159
|
+
//#region src/retryables/experimental/and.ts
|
|
160
|
+
/**
|
|
161
|
+
* Match only when all of the given conditions match. Evaluates left to
|
|
162
|
+
* right and stops on the first miss.
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* and(httpStatus(429), error.message('overloaded'))
|
|
166
|
+
*/
|
|
167
|
+
function and(...conditions) {
|
|
168
|
+
return new Condition(async (ctx) => {
|
|
169
|
+
for (const c of conditions) if (!await c.evaluate(ctx)) return false;
|
|
170
|
+
return true;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
//#endregion
|
|
175
|
+
//#region src/retryables/experimental/result.ts
|
|
176
|
+
/**
|
|
177
|
+
* Build a condition from a predicate over the current generate result.
|
|
178
|
+
* Available for language models only. The predicate runs only when the
|
|
179
|
+
* current attempt succeeded; error attempts return false.
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* result<MODEL>((res) => res.finishReason.unified === 'length')
|
|
183
|
+
*/
|
|
184
|
+
function result(predicate) {
|
|
185
|
+
return new Condition(async (ctx) => {
|
|
186
|
+
if (!isResultAttempt(ctx.current)) return false;
|
|
187
|
+
return predicate(ctx.current.result, ctx);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
//#endregion
|
|
192
|
+
//#region src/retryables/experimental/finish-reason.ts
|
|
193
|
+
/**
|
|
194
|
+
* Match the result's finish reason against one of the given values.
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* finishReason('content-filter')
|
|
198
|
+
* finishReason('content-filter', 'length')
|
|
199
|
+
*/
|
|
200
|
+
function finishReason(...reasons) {
|
|
201
|
+
return result((res) => reasons.includes(res.finishReason.unified));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
//#endregion
|
|
205
|
+
//#region src/retryables/experimental/or.ts
|
|
206
|
+
/**
|
|
207
|
+
* Match when any of the given conditions match. Evaluates left to right
|
|
208
|
+
* and stops on the first match.
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* or(httpStatus(429), error.message('overloaded'))
|
|
212
|
+
*/
|
|
213
|
+
function or(...conditions) {
|
|
214
|
+
return new Condition(async (ctx) => {
|
|
215
|
+
for (const c of conditions) if (await c.evaluate(ctx)) return true;
|
|
216
|
+
return false;
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
//#endregion
|
|
221
|
+
//#region src/retryables/experimental/http-status.ts
|
|
222
|
+
/**
|
|
223
|
+
* Match an `APICallError` by status code, message substring, or regular
|
|
224
|
+
* expression. Numbers match the status code; strings match the message;
|
|
225
|
+
* regular expressions match either the stringified status code or the
|
|
226
|
+
* message. Mix any combination in a single call; matches when any
|
|
227
|
+
* pattern matches.
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* httpStatus(529)
|
|
231
|
+
* httpStatus(529, 'overloaded')
|
|
232
|
+
* httpStatus(/^5\d\d$/)
|
|
233
|
+
* httpStatus(529, 'overloaded', /rate.?limit/i)
|
|
234
|
+
*/
|
|
235
|
+
function httpStatus(...patterns) {
|
|
236
|
+
const numbers = patterns.filter((p) => typeof p === "number");
|
|
237
|
+
const strings = patterns.filter((p) => typeof p === "string");
|
|
238
|
+
const regexes = patterns.filter((p) => p instanceof RegExp);
|
|
239
|
+
const conditions = [];
|
|
240
|
+
if (numbers.length || regexes.length) conditions.push(error.statusCode(...numbers, ...regexes));
|
|
241
|
+
if (strings.length || regexes.length) conditions.push(error.message(...strings, ...regexes));
|
|
242
|
+
return or(...conditions);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
//#endregion
|
|
246
|
+
//#region src/retryables/experimental/no-image.ts
|
|
247
|
+
/**
|
|
248
|
+
* Match when image generation produced no images
|
|
249
|
+
* (`NoImageGeneratedError`).
|
|
250
|
+
*
|
|
251
|
+
* @example
|
|
252
|
+
* noImage().switch({ model: fallback })
|
|
253
|
+
*/
|
|
254
|
+
function noImage() {
|
|
255
|
+
return error((err) => NoImageGeneratedError.isInstance(err));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
//#endregion
|
|
259
|
+
//#region src/retryables/experimental/not.ts
|
|
260
|
+
/**
|
|
261
|
+
* Invert a condition.
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
* not(error.isRetryable(true))
|
|
265
|
+
*/
|
|
266
|
+
function not(condition) {
|
|
267
|
+
return condition.not();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
//#endregion
|
|
271
|
+
//#region src/retryables/experimental/schema-invalid.ts
|
|
272
|
+
/**
|
|
273
|
+
* Match when the result text fails JSON schema validation. The schema is
|
|
274
|
+
* read from the call's `responseFormat`, which `Output.object()` sets
|
|
275
|
+
* automatically. No-op when no schema is configured.
|
|
276
|
+
*
|
|
277
|
+
* @example
|
|
278
|
+
* schemaInvalid().switch({ model: fallback })
|
|
279
|
+
*/
|
|
280
|
+
function schemaInvalid() {
|
|
281
|
+
return result(async (res, ctx) => {
|
|
282
|
+
if (!isResultAttempt(ctx.current)) return false;
|
|
283
|
+
const callOptions = ctx.current.options;
|
|
284
|
+
const text = res.content.filter((part) => part.type === "text").map((part) => part.text).join("");
|
|
285
|
+
if (!text) return false;
|
|
286
|
+
const responseFormat = callOptions.responseFormat;
|
|
287
|
+
if (responseFormat?.type !== "json" || !responseFormat.schema) return false;
|
|
288
|
+
return !(await safeParseJSON({
|
|
289
|
+
text,
|
|
290
|
+
schema: fromJSONSchema(responseFormat.schema)
|
|
291
|
+
})).success;
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
//#endregion
|
|
296
|
+
//#region src/retryables/experimental/timeout.ts
|
|
297
|
+
/**
|
|
298
|
+
* Match a timeout error: an `Error` with `name === 'TimeoutError'`,
|
|
299
|
+
* which `AbortSignal.timeout()` produces when the timeout fires.
|
|
300
|
+
* Distinct from `aborted()`, which matches manual aborts.
|
|
301
|
+
*
|
|
302
|
+
* @example
|
|
303
|
+
* timeout().switch({ model: fallback, timeout: 60_000 })
|
|
304
|
+
*/
|
|
305
|
+
function timeout() {
|
|
306
|
+
return error((err) => err instanceof Error && err.name === "TimeoutError");
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
//#endregion
|
|
310
|
+
export { Condition, aborted, and, error, finishReason, httpStatus, noImage, not, or, result, schemaInvalid, timeout };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { N as RetryableOptions, b as ResolvableLanguageModel, j as Retryable, n as EmbeddingModel, s as ImageModel } from "../types-
|
|
1
|
+
import { N as RetryableOptions, b as ResolvableLanguageModel, j as Retryable, n as EmbeddingModel, s as ImageModel } from "../types-CRKV-hdW.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/retryables/content-filter-triggered.d.ts
|
|
4
4
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { c as isObject, f as isString, l as isResultAttempt, p as isTimeoutError, r as isErrorAttempt } from "../utils-CfnsSGrw.mjs";
|
|
2
|
+
import { n as parseRetryHeaders, t as MAX_RETRY_AFTER_MS } from "../parse-retry-headers-DIPVbwW5.mjs";
|
|
2
3
|
import { APICallError, NoImageGeneratedError } from "ai";
|
|
3
4
|
import { safeParseJSON } from "@ai-sdk/provider-utils";
|
|
4
5
|
import { fromJSONSchema } from "zod";
|
|
@@ -91,28 +92,8 @@ function requestTimeout(model, options) {
|
|
|
91
92
|
};
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
//#endregion
|
|
95
|
-
//#region src/parse-retry-headers.ts
|
|
96
|
-
function parseRetryHeaders(headers) {
|
|
97
|
-
if (!headers) return null;
|
|
98
|
-
const retryAfterMs = headers["retry-after-ms"];
|
|
99
|
-
if (retryAfterMs) {
|
|
100
|
-
const delayMs = Number.parseFloat(retryAfterMs);
|
|
101
|
-
if (!Number.isNaN(delayMs) && delayMs >= 0) return delayMs;
|
|
102
|
-
}
|
|
103
|
-
const retryAfter = headers["retry-after"];
|
|
104
|
-
if (retryAfter) {
|
|
105
|
-
const seconds = Number.parseFloat(retryAfter);
|
|
106
|
-
if (!Number.isNaN(seconds)) return seconds * 1e3;
|
|
107
|
-
const date = Date.parse(retryAfter);
|
|
108
|
-
if (!Number.isNaN(date)) return Math.max(0, date - Date.now());
|
|
109
|
-
}
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
95
|
//#endregion
|
|
114
96
|
//#region src/retryables/retry-after-delay.ts
|
|
115
|
-
const MAX_RETRY_AFTER_MS = 6e4;
|
|
116
97
|
/**
|
|
117
98
|
* Retry the current failed attempt with the same model, if the error is retryable.
|
|
118
99
|
* Uses the `Retry-After` or `Retry-After-Ms` headers if present.
|
|
@@ -34,7 +34,7 @@ type RetryCallOptions<MODEL extends LanguageModel | EmbeddingModel | ImageModel>
|
|
|
34
34
|
/**
|
|
35
35
|
* Override returned by `onRetry` to influence the upcoming retry attempt.
|
|
36
36
|
*/
|
|
37
|
-
type OnRetryOverrides<MODEL extends LanguageModel | EmbeddingModel | ImageModel> = Pick<Retry<MODEL>, 'options'
|
|
37
|
+
type OnRetryOverrides<MODEL extends LanguageModel | EmbeddingModel | ImageModel> = Pick<Retry<MODEL>, 'options'>;
|
|
38
38
|
/**
|
|
39
39
|
* Maps a model type to its call options type.
|
|
40
40
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-retry",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.1",
|
|
4
4
|
"description": "Retry and fallback mechanisms for AI SDK",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"exports": {
|
|
23
23
|
".": "./dist/index.mjs",
|
|
24
24
|
"./retryables": "./dist/retryables/index.mjs",
|
|
25
|
+
"./retryables/experimental": "./dist/retryables/experimental/index.mjs",
|
|
25
26
|
"./package.json": "./package.json"
|
|
26
27
|
},
|
|
27
28
|
"publishConfig": {
|