ai-retry 1.6.0 → 1.7.0
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 +105 -64
- package/dist/create-retryable-model-CgYBIeV6.mjs +1009 -0
- package/dist/error-CPbAtI-h.d.mts +95 -0
- package/dist/error-_63RHJTp.mjs +247 -0
- package/dist/experimental/embedding-model/index.d.mts +8 -0
- package/dist/experimental/embedding-model/index.mjs +19 -0
- package/dist/experimental/embedding-model/retryables/index.d.mts +20 -0
- package/dist/experimental/embedding-model/retryables/index.mjs +7 -0
- package/dist/experimental/image-model/index.d.mts +8 -0
- package/dist/experimental/image-model/index.mjs +19 -0
- package/dist/experimental/image-model/retryables/index.d.mts +4 -0
- package/dist/experimental/image-model/retryables/index.mjs +4 -0
- package/dist/experimental/language-model/index.d.mts +11 -0
- package/dist/experimental/language-model/index.mjs +19 -0
- package/dist/experimental/language-model/retryables/index.d.mts +4 -0
- package/dist/experimental/language-model/retryables/index.mjs +4 -0
- package/dist/{utils-CfnsSGrw.mjs → guards-D8UJtxDK.mjs} +2 -8
- package/dist/index-DOM9pSF9.d.mts +60 -0
- package/dist/index-Dvxg4bnp.d.mts +30 -0
- package/dist/index.d.mts +4 -55
- package/dist/index.mjs +3 -886
- package/dist/{parse-retry-headers-DIPVbwW5.mjs → parse-retry-headers-CRxgluhe.mjs} +1 -1
- package/dist/retryables/index.d.mts +1 -1
- package/dist/retryables/index.mjs +2 -2
- package/dist/retryables-D0wMy6Qt.mjs +25 -0
- package/dist/retryables-nm5-elvB.mjs +76 -0
- package/dist/{types-pGdkwtOE.d.mts → types-DYMm5YMu.d.mts} +9 -5
- package/package.json +7 -2
- package/dist/retryables/experimental/index.d.mts +0 -248
- package/dist/retryables/experimental/index.mjs +0 -310
package/README.md
CHANGED
|
@@ -249,7 +249,7 @@ const retryableModel = createRetryable({
|
|
|
249
249
|
});
|
|
250
250
|
```
|
|
251
251
|
|
|
252
|
-
Result-based retryables
|
|
252
|
+
Result-based retryables apply to language models for both generate (`generateText`, `generateObject`) and streaming (`streamText`, `streamObject`) calls. For streams, the retry decision happens when the upstream `finish` part arrives and only fires if no content has been emitted yet, so behavior like `finishReason: 'content-filter'` on an otherwise empty response can still trigger a fallback. Once any content chunk has been forwarded, the stream is committed and result-based retries are skipped.
|
|
253
253
|
|
|
254
254
|
#### Fallbacks
|
|
255
255
|
|
|
@@ -389,8 +389,8 @@ There are several built-in dynamic retryables available for common use cases:
|
|
|
389
389
|
|
|
390
390
|
Automatically switch to a different model when content filtering blocks your request.
|
|
391
391
|
|
|
392
|
-
> [!
|
|
393
|
-
>
|
|
392
|
+
> [!NOTE]
|
|
393
|
+
> For streaming requests this retryable can only fire if the content filter trips before any content has been emitted. Once a text chunk flows through, the stream is committed and the fallback is skipped.
|
|
394
394
|
|
|
395
395
|
```typescript
|
|
396
396
|
import { contentFilterTriggered } from 'ai-retry/retryables';
|
|
@@ -587,24 +587,40 @@ console.log(result.object); // { name: "Alice", age: 30 }
|
|
|
587
587
|
### Experimental: Composable Conditions
|
|
588
588
|
|
|
589
589
|
> [!WARNING]
|
|
590
|
-
> This API is experimental and may change. It is not exported from the package root; opt in via the deep
|
|
590
|
+
> This API is experimental and may change. It is not exported from the package root; opt in via one of the per-model deep imports:
|
|
591
|
+
>
|
|
592
|
+
> ```ts
|
|
593
|
+
> import { ... } from 'ai-retry/experimental/language-model';
|
|
594
|
+
> import { ... } from 'ai-retry/experimental/image-model';
|
|
595
|
+
> import { ... } from 'ai-retry/experimental/embedding-model';
|
|
596
|
+
> ```
|
|
597
|
+
>
|
|
598
|
+
> Each entry point also re-exports `createRetryable` already typed for that model family, so you can either import everything from one path:
|
|
591
599
|
>
|
|
592
600
|
> ```ts
|
|
593
|
-
> import {
|
|
601
|
+
> import { createRetryable, error, httpStatus } from 'ai-retry/experimental/language-model';
|
|
602
|
+
> ```
|
|
603
|
+
>
|
|
604
|
+
> or pull retryables from the dedicated `/retryables` subpath:
|
|
605
|
+
>
|
|
606
|
+
> ```ts
|
|
607
|
+
> import { error, httpStatus } from 'ai-retry/experimental/language-model/retryables';
|
|
608
|
+
> // or
|
|
609
|
+
> import * as retryables from 'ai-retry/experimental/language-model/retryables';
|
|
594
610
|
> ```
|
|
595
611
|
|
|
596
|
-
A `condition().action()` API for retryables. Conditions are built from small primitives (`error(fn)`, `result(fn)`), composed with
|
|
612
|
+
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
613
|
|
|
598
614
|
```typescript
|
|
599
615
|
import { anthropic } from '@ai-sdk/anthropic';
|
|
600
616
|
import { openai } from '@ai-sdk/openai';
|
|
601
617
|
import { generateText } from 'ai';
|
|
602
|
-
import { createRetryable } from 'ai-retry';
|
|
603
618
|
import {
|
|
619
|
+
createRetryable,
|
|
604
620
|
error,
|
|
605
621
|
finishReason,
|
|
606
622
|
httpStatus,
|
|
607
|
-
} from 'ai-retry/
|
|
623
|
+
} from 'ai-retry/experimental/language-model';
|
|
608
624
|
|
|
609
625
|
const retryableModel = createRetryable({
|
|
610
626
|
model: openai('gpt-4'),
|
|
@@ -623,86 +639,109 @@ const retryableModel = createRetryable({
|
|
|
623
639
|
});
|
|
624
640
|
```
|
|
625
641
|
|
|
626
|
-
####
|
|
642
|
+
#### Picking an entry point
|
|
627
643
|
|
|
628
|
-
|
|
644
|
+
Pick the entry point that matches the model you pass to `createRetryable`. Each module exposes the helpers that make sense for that model family already typed for it, so you don't need to add type annotations yourself.
|
|
629
645
|
|
|
630
|
-
|
|
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` |
|
|
646
|
+
#### Low-level conditions
|
|
638
647
|
|
|
639
|
-
|
|
648
|
+
The primitive builders `error(...)` and `result(...)` take a predicate and turn it into a condition; their namespaces bundle the most common field matchers on top.
|
|
640
649
|
|
|
641
|
-
|
|
650
|
+
| Helper | Matches when | Available in |
|
|
651
|
+
| --------------------------------- | ------------------------------------------------------------------------------------- | ------------------------- |
|
|
652
|
+
| `error(predicate)` | The current attempt failed and `predicate(err, ctx)` returns true | all three entry points |
|
|
653
|
+
| `error.isRetryable(flag)` | `APICallError.isRetryable === flag` (default `true`) | all three entry points |
|
|
654
|
+
| `error.statusCode(...patterns)` | Numbers match exactly; regex matches the stringified code (e.g. `/^5\d\d$/` for 5xx) | all three entry points |
|
|
655
|
+
| `error.message(...patterns)` | Substring (case-insensitive) or regex match against the error message | all three entry points |
|
|
656
|
+
| `result(predicate)` | The current attempt succeeded and `predicate(res, ctx)` returns true | `language-model` only |
|
|
657
|
+
| `result.finishReason(...reasons)` | The result's `finishReason.unified` matches one of the given values | `language-model` only |
|
|
642
658
|
|
|
643
|
-
|
|
644
|
-
|
|
659
|
+
```typescript
|
|
660
|
+
import { APICallError } from 'ai';
|
|
661
|
+
import { error } from 'ai-retry/experimental/language-model';
|
|
645
662
|
|
|
646
|
-
|
|
663
|
+
error((e) => APICallError.isInstance(e) && e.statusCode === 418).switch({
|
|
664
|
+
model: fallback,
|
|
665
|
+
});
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
#### High-level conditions
|
|
669
|
+
|
|
670
|
+
Convenience matchers built on top of the low-level ones for the common cases. Each returns a condition that you finalize with `.switch(...)` or `.retry(...)`.
|
|
671
|
+
|
|
672
|
+
| Helper | language-model | image-model | embedding-model |
|
|
673
|
+
| -------------------------- | :------------: | :---------: | :-------------: |
|
|
674
|
+
| `httpStatus(...patterns)` | ✓ | ✓ | ✓ |
|
|
675
|
+
| `timeout()` | ✓ | ✓ | ✓ |
|
|
676
|
+
| `aborted()` | ✓ | ✓ | ✓ |
|
|
677
|
+
| `finishReason(...reasons)` | ✓ | — | — |
|
|
678
|
+
| `schemaInvalid()` | ✓ | — | — |
|
|
679
|
+
| `noImage()` | — | ✓ | — |
|
|
680
|
+
|
|
681
|
+
What each one matches:
|
|
682
|
+
|
|
683
|
+
| Helper | Matches when |
|
|
684
|
+
| -------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
685
|
+
| `httpStatus(...patterns)` | Numbers match the status code; strings match the message (substring); regex matches either |
|
|
686
|
+
| `timeout()` | `Error.name === 'TimeoutError'` (`AbortSignal.timeout()` fired) |
|
|
687
|
+
| `aborted()` | `Error.name === 'AbortError'` (manual `controller.abort()`) |
|
|
688
|
+
| `finishReason(...reasons)` | The result's `finishReason.unified` matches one of the given values |
|
|
689
|
+
| `schemaInvalid()` | The result text fails JSON-schema validation against the call's `responseFormat` |
|
|
690
|
+
| `noImage()` | The image model threw `NoImageGeneratedError` |
|
|
647
691
|
|
|
648
|
-
|
|
692
|
+
Each high-level helper is a thin wrapper around the low-level ones. For example, `timeout()` is roughly:
|
|
649
693
|
|
|
650
694
|
```typescript
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
} from 'ai-retry/retryables/experimental';
|
|
695
|
+
function timeout() {
|
|
696
|
+
return error(
|
|
697
|
+
(err) => err instanceof Error && err.name === 'TimeoutError',
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
```
|
|
658
701
|
|
|
659
|
-
|
|
660
|
-
and(httpStatus(503), error.message('temporary'));
|
|
661
|
-
not(error.isRetryable(true));
|
|
702
|
+
and `finishReason(...)` just delegates to `result.finishReason(...)`:
|
|
662
703
|
|
|
663
|
-
|
|
664
|
-
|
|
704
|
+
```typescript
|
|
705
|
+
function finishReason(...reasons: Array<string>) {
|
|
706
|
+
return result.finishReason(...reasons);
|
|
707
|
+
}
|
|
665
708
|
```
|
|
666
709
|
|
|
667
|
-
####
|
|
710
|
+
#### Actions
|
|
668
711
|
|
|
669
|
-
|
|
712
|
+
Every condition exposes two terminal actions that turn it into a `Retryable`:
|
|
670
713
|
|
|
671
|
-
|
|
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) |
|
|
714
|
+
- **`.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. `maxAttempts` defaults to `1`.
|
|
715
|
+
- **`.retry({ delay?, backoffFactor?, maxAttempts?, ... })`** retries the current model when the condition matches. Honors `Retry-After` and `Retry-After-Ms` response headers when present, capped at 60 seconds. `maxAttempts` defaults to `2` (one original attempt + one retry); values below `2` throw, since the retry budget is consumed by the original failure.
|
|
675
716
|
|
|
676
|
-
|
|
677
|
-
import { APICallError } from 'ai';
|
|
678
|
-
import { error } from 'ai-retry/retryables/experimental';
|
|
717
|
+
#### Combinators
|
|
679
718
|
|
|
680
|
-
|
|
681
|
-
(e) => APICallError.isInstance(e) && e.statusCode === 418,
|
|
682
|
-
).switch({ model: fallback });
|
|
683
|
-
```
|
|
719
|
+
Compose conditions with `.and`, `.or`, `.not`:
|
|
684
720
|
|
|
685
|
-
|
|
721
|
+
```typescript
|
|
722
|
+
import {
|
|
723
|
+
error,
|
|
724
|
+
httpStatus,
|
|
725
|
+
} from 'ai-retry/experimental/language-model';
|
|
686
726
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
| `error.message(...patterns)` | Substring (case-insensitive) or regex match against the error message |
|
|
727
|
+
httpStatus(429).or(error.message('overloaded'));
|
|
728
|
+
httpStatus(503).and(error.message('temporary'));
|
|
729
|
+
error.isRetryable(true).not();
|
|
730
|
+
```
|
|
692
731
|
|
|
693
732
|
#### Mapping from Built-in retryables
|
|
694
733
|
|
|
695
|
-
Each stable retryable has an equivalent in the new shape:
|
|
734
|
+
Each stable retryable has an equivalent in the new shape (imports from `ai-retry/experimental/language-model` unless noted):
|
|
696
735
|
|
|
697
736
|
| Built-in | Composable form |
|
|
698
737
|
| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
699
|
-
| `contentFilterTriggered(m)` | `
|
|
738
|
+
| `contentFilterTriggered(m)` | `error(/* check e.data.error.code === 'content_filter' */).or(finishReason('content-filter')).switch({ model: m })` |
|
|
700
739
|
| `requestTimeout(m)` | `timeout().switch({ model: m, timeout: 60_000 })` |
|
|
701
740
|
| `requestNotRetryable(m)` | `error.isRetryable(false).switch({ model: m })` |
|
|
702
741
|
| `schemaMismatch(m)` | `schemaInvalid().switch({ model: m })` |
|
|
703
742
|
| `serviceOverloaded(m)` | `httpStatus(529, 'overloaded').switch({ model: m })` |
|
|
704
743
|
| `serviceUnavailable(m)` | `error.statusCode(503).switch({ model: m })` |
|
|
705
|
-
| `noImageGenerated(m)` | `noImage().switch({ model: m })`
|
|
744
|
+
| `noImageGenerated(m)` | `noImage().switch({ model: m })` (from `image-model`) |
|
|
706
745
|
| `retryAfterDelay({ delay, backoffFactor })` | `error.isRetryable(true).retry({ delay, backoffFactor })` |
|
|
707
746
|
|
|
708
747
|
> [!NOTE]
|
|
@@ -782,7 +821,9 @@ const retryableModel = createRetryable({
|
|
|
782
821
|
|
|
783
822
|
#### Timeouts
|
|
784
823
|
|
|
785
|
-
When a retry specifies a `timeout` value, a fresh `AbortSignal.timeout()` is created for that retry attempt,
|
|
824
|
+
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.
|
|
825
|
+
|
|
826
|
+
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.
|
|
786
827
|
|
|
787
828
|
```typescript
|
|
788
829
|
const retryableModel = createRetryable({
|
|
@@ -925,7 +966,7 @@ The following options can be overridden:
|
|
|
925
966
|
|
|
926
967
|
#### Dynamic Call Options
|
|
927
968
|
|
|
928
|
-
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
|
|
969
|
+
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).
|
|
929
970
|
|
|
930
971
|
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:
|
|
931
972
|
|
|
@@ -1090,7 +1131,7 @@ interface RetryableModelOptions<
|
|
|
1090
1131
|
- `disabled`: Disable all retry logic. Can be a boolean or function returning boolean. Default: `false` (retries enabled).
|
|
1091
1132
|
- `reset`: Controls when to reset back to the base model after a successful retry. Default: `after-request`.
|
|
1092
1133
|
- `onError`: Callback invoked when an error occurs.
|
|
1093
|
-
- `onRetry`: Callback invoked before attempting a retry. May optionally return an `OnRetryOverrides` object (or a `Promise` of one) to override `options.*`
|
|
1134
|
+
- `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).
|
|
1094
1135
|
- `onSuccess`: Callback invoked after a successful request. Receives the model that handled the request and all previous attempts.
|
|
1095
1136
|
|
|
1096
1137
|
#### `Reset`
|
|
@@ -1167,7 +1208,7 @@ interface SuccessAttempt {
|
|
|
1167
1208
|
type: 'success';
|
|
1168
1209
|
model: LanguageModelV3 | EmbeddingModelV3 | ImageModelV3;
|
|
1169
1210
|
result:
|
|
1170
|
-
|
|
|
1211
|
+
| LanguageModelResult
|
|
1171
1212
|
| LanguageModelStream
|
|
1172
1213
|
| EmbeddingModelEmbed
|
|
1173
1214
|
| ImageModelGenerate;
|
|
@@ -1196,12 +1237,12 @@ type RetryAttempt =
|
|
|
1196
1237
|
}
|
|
1197
1238
|
| {
|
|
1198
1239
|
type: 'result';
|
|
1199
|
-
result:
|
|
1240
|
+
result: LanguageModelResult;
|
|
1200
1241
|
model: LanguageModelV3;
|
|
1201
1242
|
options: LanguageModelV3CallOptions;
|
|
1202
1243
|
};
|
|
1203
1244
|
|
|
1204
|
-
// Note: Result-based retries only apply to language models
|
|
1245
|
+
// Note: Result-based retries only apply to language models (both generate and stream paths). They do not apply to embedding or image models. For streaming, retries are only possible before any content has been emitted; once a text-delta flows through, the stream is committed.
|
|
1205
1246
|
|
|
1206
1247
|
// Type guards for discriminating attempts
|
|
1207
1248
|
function isErrorAttempt(attempt: RetryAttempt): attempt is RetryErrorAttempt;
|