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.
Files changed (30) hide show
  1. package/README.md +105 -64
  2. package/dist/create-retryable-model-CgYBIeV6.mjs +1009 -0
  3. package/dist/error-CPbAtI-h.d.mts +95 -0
  4. package/dist/error-_63RHJTp.mjs +247 -0
  5. package/dist/experimental/embedding-model/index.d.mts +8 -0
  6. package/dist/experimental/embedding-model/index.mjs +19 -0
  7. package/dist/experimental/embedding-model/retryables/index.d.mts +20 -0
  8. package/dist/experimental/embedding-model/retryables/index.mjs +7 -0
  9. package/dist/experimental/image-model/index.d.mts +8 -0
  10. package/dist/experimental/image-model/index.mjs +19 -0
  11. package/dist/experimental/image-model/retryables/index.d.mts +4 -0
  12. package/dist/experimental/image-model/retryables/index.mjs +4 -0
  13. package/dist/experimental/language-model/index.d.mts +11 -0
  14. package/dist/experimental/language-model/index.mjs +19 -0
  15. package/dist/experimental/language-model/retryables/index.d.mts +4 -0
  16. package/dist/experimental/language-model/retryables/index.mjs +4 -0
  17. package/dist/{utils-CfnsSGrw.mjs → guards-D8UJtxDK.mjs} +2 -8
  18. package/dist/index-DOM9pSF9.d.mts +60 -0
  19. package/dist/index-Dvxg4bnp.d.mts +30 -0
  20. package/dist/index.d.mts +4 -55
  21. package/dist/index.mjs +3 -886
  22. package/dist/{parse-retry-headers-DIPVbwW5.mjs → parse-retry-headers-CRxgluhe.mjs} +1 -1
  23. package/dist/retryables/index.d.mts +1 -1
  24. package/dist/retryables/index.mjs +2 -2
  25. package/dist/retryables-D0wMy6Qt.mjs +25 -0
  26. package/dist/retryables-nm5-elvB.mjs +76 -0
  27. package/dist/{types-pGdkwtOE.d.mts → types-DYMm5YMu.d.mts} +9 -5
  28. package/package.json +7 -2
  29. package/dist/retryables/experimental/index.d.mts +0 -248
  30. 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 are only available for generate calls like `generateText` and `generateObject`. They are not available for streaming calls like `streamText` and `streamObject`.
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
- > [!WARNING]
393
- > This retryable currently does not work with streaming requests, because the content filter is only indicated in the final response.
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 import:
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 { ... } from 'ai-retry/retryables/experimental';
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 `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.
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/retryables/experimental';
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
- #### High-level helpers
642
+ #### Picking an entry point
627
643
 
628
- These cover the common cases. Each returns a `Condition` that you finalize with `.switch(...)` or `.retry(...)`.
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
- | 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` |
646
+ #### Low-level conditions
638
647
 
639
- #### Actions
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
- Every `Condition` exposes two terminal actions that turn it into a `Retryable`:
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
- - **`.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.
659
+ ```typescript
660
+ import { APICallError } from 'ai';
661
+ import { error } from 'ai-retry/experimental/language-model';
645
662
 
646
- #### Combinators
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
- Compose conditions with the free functions or the methods on `Condition`:
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
- import {
652
- and,
653
- error,
654
- httpStatus,
655
- not,
656
- or,
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
- or(httpStatus(429), error.message('overloaded'));
660
- and(httpStatus(503), error.message('temporary'));
661
- not(error.isRetryable(true));
702
+ and `finishReason(...)` just delegates to `result.finishReason(...)`:
662
703
 
663
- // Method form
664
- httpStatus(429).or(error.message('overloaded'));
704
+ ```typescript
705
+ function finishReason(...reasons: Array<string>) {
706
+ return result.finishReason(...reasons);
707
+ }
665
708
  ```
666
709
 
667
- #### Primitives
710
+ #### Actions
668
711
 
669
- The two lowest-level builders. Reach for them when no helper covers your case:
712
+ Every condition exposes two terminal actions that turn it into a `Retryable`:
670
713
 
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) |
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
- ```typescript
677
- import { APICallError } from 'ai';
678
- import { error } from 'ai-retry/retryables/experimental';
717
+ #### Combinators
679
718
 
680
- error<MODEL, APICallError>(
681
- (e) => APICallError.isInstance(e) && e.statusCode === 418,
682
- ).switch({ model: fallback });
683
- ```
719
+ Compose conditions with `.and`, `.or`, `.not`:
684
720
 
685
- A few common error fields have ready-made matchers on the `error` namespace:
721
+ ```typescript
722
+ import {
723
+ error,
724
+ httpStatus,
725
+ } from 'ai-retry/experimental/language-model';
686
726
 
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 |
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)` | `or(error(/* check e.data.error.code === 'content_filter' */), finishReason('content-filter')).switch({ model: 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, replacing any existing abort signal. This is essential when retrying after timeout errors, as the original abort signal would already be in an aborted state.
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 plus the request `timeout`. The callback may also be `async` if computing the override needs to do work (e.g. fetching a fresh credential).
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.*` and `timeout` for the upcoming attempt only. See [Dynamic Call Options via `onRetry`](#dynamic-call-options-via-onretry).
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
- | LanguageModelGenerate
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: LanguageModelV3Generate;
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, not embedding or image 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;