ai-retry 1.6.1 → 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 +100 -61
  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 -913
  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-CRKV-hdW.d.mts → types-DYMm5YMu.d.mts} +8 -4
  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:
647
682
 
648
- Compose conditions with the free functions or the methods on `Condition`:
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` |
691
+
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
711
+
712
+ Every condition exposes two terminal actions that turn it into a `Retryable`:
713
+
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.
668
716
 
669
- The two lowest-level builders. Reach for them when no helper covers your case:
717
+ #### Combinators
670
718
 
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) |
719
+ Compose conditions with `.and`, `.or`, `.not`:
675
720
 
676
721
  ```typescript
677
- import { APICallError } from 'ai';
678
- import { error } from 'ai-retry/retryables/experimental';
722
+ import {
723
+ error,
724
+ httpStatus,
725
+ } from 'ai-retry/experimental/language-model';
679
726
 
680
- error<MODEL, APICallError>(
681
- (e) => APICallError.isInstance(e) && e.statusCode === 418,
682
- ).switch({ model: fallback });
727
+ httpStatus(429).or(error.message('overloaded'));
728
+ httpStatus(503).and(error.message('temporary'));
729
+ error.isRetryable(true).not();
683
730
  ```
684
731
 
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
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]
@@ -1169,7 +1208,7 @@ interface SuccessAttempt {
1169
1208
  type: 'success';
1170
1209
  model: LanguageModelV3 | EmbeddingModelV3 | ImageModelV3;
1171
1210
  result:
1172
- | LanguageModelGenerate
1211
+ | LanguageModelResult
1173
1212
  | LanguageModelStream
1174
1213
  | EmbeddingModelEmbed
1175
1214
  | ImageModelGenerate;
@@ -1198,12 +1237,12 @@ type RetryAttempt =
1198
1237
  }
1199
1238
  | {
1200
1239
  type: 'result';
1201
- result: LanguageModelV3Generate;
1240
+ result: LanguageModelResult;
1202
1241
  model: LanguageModelV3;
1203
1242
  options: LanguageModelV3CallOptions;
1204
1243
  };
1205
1244
 
1206
- // 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.
1207
1246
 
1208
1247
  // Type guards for discriminating attempts
1209
1248
  function isErrorAttempt(attempt: RetryAttempt): attempt is RetryErrorAttempt;