ai-retry 1.5.0 → 1.6.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 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
@@ -815,10 +945,7 @@ const retryableModel = createRetryable({
815
945
  // Strip provider-scoped metadata from the prompt before retrying on a different provider
816
946
  return {
817
947
  options: {
818
- prompt: sanitizePromptForProvider(
819
- previous.options.prompt,
820
- current.model.provider,
821
- ),
948
+ prompt: stripProviderMetadata(current.options.prompt),
822
949
  },
823
950
  };
824
951
  }
@@ -826,6 +953,8 @@ const retryableModel = createRetryable({
826
953
  });
827
954
  ```
828
955
 
956
+ 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`.
957
+
829
958
  `onRetry` may also be `async`, which is useful if computing the override needs to do work (e.g. fetching a fresh credential):
830
959
 
831
960
  ```typescript
@@ -844,7 +973,7 @@ const retryableModel = createRetryable({
844
973
  **Precedence** for the upcoming retry attempt (highest to lowest):
845
974
 
846
975
  1. The value returned from `onRetry`
847
- 2. `Retry.options` (declared on the retryable)
976
+ 2. The `options` returned from the retryable
848
977
  3. The original call options from the request
849
978
 
850
979
  #### Logging
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-wrgO_vOH.mjs";
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-pGdkwtOE.mjs";
2
2
  import * as _ai_sdk_provider0 from "@ai-sdk/provider";
3
3
 
4
4
  //#region src/create-retryable-model.d.ts
@@ -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-pGdkwtOE.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-wrgO_vOH.mjs";
1
+ import { N as RetryableOptions, b as ResolvableLanguageModel, j as Retryable, n as EmbeddingModel, s as ImageModel } from "../types-pGdkwtOE.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-retry",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
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": {