ai-retry 1.2.0 → 1.3.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
@@ -41,7 +41,7 @@ Create a retryable model by providing a base model and a list of retryables or f
41
41
  When an error occurs, it will evaluate each retryable in order and use the first one that indicates a retry should be attempted with a different model.
42
42
 
43
43
  > [!NOTE]
44
- > `ai-retry` supports both language models and embedding models.
44
+ > `ai-retry` supports language models, embedding models, and image models.
45
45
 
46
46
  ```typescript
47
47
  import { openai } from '@ai-sdk/openai';
@@ -101,6 +101,28 @@ const result = await embed({
101
101
  console.log(result.embedding);
102
102
  ```
103
103
 
104
+ This also works with image models:
105
+
106
+ ```typescript
107
+ import { openai } from '@ai-sdk/openai';
108
+ import { generateImage } from 'ai';
109
+ import { createRetryable } from 'ai-retry';
110
+
111
+ const retryableModel = createRetryable({
112
+ model: openai.image('dall-e-3'),
113
+ retries: [
114
+ // Retry strategies and fallbacks...
115
+ ],
116
+ });
117
+
118
+ const result = await generateImage({
119
+ model: retryableModel,
120
+ prompt: 'A sunset over mountains',
121
+ });
122
+
123
+ console.log(result.images);
124
+ ```
125
+
104
126
  #### Vercel AI Gateway
105
127
 
106
128
  You can use `ai-retry` with Vercel AI Gateway by providing the model as a string. Internally, the model will be resolved with the default `gateway` [provider instance](https://ai-sdk.dev/providers/ai-sdk-providers/ai-gateway#provider-instance) from AI SDK.
@@ -356,9 +378,9 @@ There are several built-in dynamic retryables available for common use cases:
356
378
  - [`requestNotRetryable`](./src/retryables/request-not-retryable.ts): Request failed with a non-retryable error.
357
379
  - [`retryAfterDelay`](./src/retryables/retry-after-delay.ts): Retry with delay and exponential backoff and respect `retry-after` headers.
358
380
  - [`serviceOverloaded`](./src/retryables/service-overloaded.ts): Response with status code 529 (service overloaded).
359
- - Use this retryable to handle Anthropic's overloaded errors.
360
381
  - [`serviceUnavailable`](./src/retryables/service-unavailable.ts): Response with status code 503 (service unavailable).
361
382
  - [`schemaMismatch`](./src/retryables/schema-mismatch.ts): Response JSON doesn't match the expected schema from structured output modes (`Output.object()`, `Output.array()`, `Output.choice()`).
383
+ - [`noImageGenerated`](./src/retryables/no-image-generated.ts): Image generation failed with `NoImageGeneratedError`.
362
384
 
363
385
  #### Content Filter
364
386
 
@@ -412,9 +434,6 @@ const result = await generateText({
412
434
 
413
435
  Handle service overload errors (status code 529) by switching to a provider.
414
436
 
415
- > [!NOTE]
416
- > You can use this retryable to handle Anthropic's overloaded errors.
417
-
418
437
  ```typescript
419
438
  import { serviceOverloaded } from 'ai-retry/retryables';
420
439
 
@@ -453,6 +472,29 @@ const retryableModel = createRetryable({
453
472
  });
454
473
  ```
455
474
 
475
+ #### No Image Generated
476
+
477
+ Handle image generation failures by switching to a different model.
478
+
479
+ ```typescript
480
+ import { openai } from '@ai-sdk/openai';
481
+ import { generateImage } from 'ai';
482
+ import { createRetryable } from 'ai-retry';
483
+ import { noImageGenerated } from 'ai-retry/retryables';
484
+
485
+ const retryableModel = createRetryable({
486
+ model: openai.image('dall-e-3'),
487
+ retries: [
488
+ noImageGenerated(openai.image('dall-e-2')), // Fallback to DALL-E 2
489
+ ],
490
+ });
491
+
492
+ const result = await generateImage({
493
+ model: retryableModel,
494
+ prompt: 'A sunset over mountains',
495
+ });
496
+ ```
497
+
456
498
  #### Request Not Retryable
457
499
 
458
500
  Handle cases where the base model fails with a non-retryable error.
@@ -736,6 +778,17 @@ The following options can be overridden:
736
778
  | [`headers`](https://ai-sdk.dev/docs/reference/ai-sdk-core/embed#headers) | Additional HTTP headers |
737
779
  | [`providerOptions`](https://ai-sdk.dev/docs/reference/ai-sdk-core/embed#provideroptions) | Provider-specific options |
738
780
 
781
+ ##### Image Model Options
782
+
783
+ | Option | Description |
784
+ |--------|-------------|
785
+ | [`n`](https://ai-sdk.dev/docs/reference/ai-sdk-core/generate-image#n) | Number of images to generate |
786
+ | [`size`](https://ai-sdk.dev/docs/reference/ai-sdk-core/generate-image#size) | Size of generated images |
787
+ | [`aspectRatio`](https://ai-sdk.dev/docs/reference/ai-sdk-core/generate-image#aspectratio) | Aspect ratio of generated images |
788
+ | [`seed`](https://ai-sdk.dev/docs/reference/ai-sdk-core/generate-image#seed) | Random seed for reproducibility |
789
+ | [`headers`](https://ai-sdk.dev/docs/reference/ai-sdk-core/generate-image#headers) | Additional HTTP headers |
790
+ | [`providerOptions`](https://ai-sdk.dev/docs/reference/ai-sdk-core/generate-image#provideroptions) | Provider-specific options |
791
+
739
792
  #### Logging
740
793
 
741
794
  You can use the following callbacks to log retry attempts and errors:
@@ -763,21 +816,21 @@ By default, every new request starts with the base model, even if a previous req
763
816
 
764
817
  | Value | Description |
765
818
  |-------|-------------|
766
- | `'after-request'` | Reset immediately after the next request (default) |
767
- | `` `after-${N}-requests` `` | Keep the retry model for the next **N** requests, then reset |
768
- | `` `after-${N}-seconds` `` | Keep the retry model for **N** seconds, then reset |
819
+ | `after-request` | Reset immediately after the next request (default) |
820
+ | `after-N-requests` | Keep the retry model for the next **N** requests, then reset |
821
+ | `after-N-seconds` | Keep the retry model for **N** seconds, then reset |
769
822
 
770
- **Reset after each request (default)**
823
+ ##### Reset after each request (default)
771
824
 
772
825
  ```typescript
773
826
  const retryableModel = createRetryable({
774
827
  model: openai('gpt-4o-mini'),
775
828
  retries: [anthropic('claude-sonnet-4-20250514')],
776
- reset: 'after-request', // default always start with the base model
829
+ reset: 'after-request', // default: always start with the base model
777
830
  });
778
831
  ```
779
832
 
780
- **Keep the retry model for N requests**
833
+ ##### Keep the retry model for N requests
781
834
 
782
835
  ```typescript
783
836
  const retryableModel = createRetryable({
@@ -787,7 +840,7 @@ const retryableModel = createRetryable({
787
840
  });
788
841
  ```
789
842
 
790
- **Keep the retry model for N seconds**
843
+ ##### Keep the retry model for N seconds
791
844
 
792
845
  ```typescript
793
846
  const retryableModel = createRetryable({
@@ -808,12 +861,12 @@ In the second case, errors during stream processing will not always be retried,
808
861
 
809
862
  ### API Reference
810
863
 
811
- #### `createRetryable(options: RetryableModelOptions): LanguageModelV2 | EmbeddingModelV2`
864
+ #### `createRetryable(options: RetryableModelOptions): LanguageModelV3 | EmbeddingModelV3 | ImageModelV3`
812
865
 
813
- Creates a retryable model that works with both language models and embedding models.
866
+ Creates a retryable model that works with language models, embedding models, and image models.
814
867
 
815
868
  ```ts
816
- interface RetryableModelOptions<MODEL extends LanguageModelV2 | EmbeddingModelV2> {
869
+ interface RetryableModelOptions<MODEL extends LanguageModelV3 | EmbeddingModelV3 | ImageModelV3> {
817
870
  model: MODEL;
818
871
  retries: Array<Retryable<MODEL> | MODEL>;
819
872
  disabled?: boolean | (() => boolean);
@@ -827,7 +880,7 @@ interface RetryableModelOptions<MODEL extends LanguageModelV2 | EmbeddingModelV2
827
880
  - `model`: The base model to use for the initial request.
828
881
  - `retries`: Array of retryables (functions, models, or retry objects) to attempt on failure.
829
882
  - `disabled`: Disable all retry logic. Can be a boolean or function returning boolean. Default: `false` (retries enabled).
830
- - `reset`: Controls when to reset back to the base model after a successful retry. See [Reset](#reset) for details. Default: `'after-request'`.
883
+ - `reset`: Controls when to reset back to the base model after a successful retry. Default: `after-request`.
831
884
  - `onError`: Callback invoked when an error occurs.
832
885
  - `onRetry`: Callback invoked before attempting a retry.
833
886
 
@@ -842,9 +895,9 @@ type Reset =
842
895
  | `after-${number}-seconds`;
843
896
  ```
844
897
 
845
- - `'after-request'` — reset immediately after the next request (default).
846
- - `` `after-${N}-requests` `` — keep the retry model for the next N requests, then reset.
847
- - `` `after-${N}-seconds` `` — keep the retry model for N seconds, then reset.
898
+ - `after-request` — reset immediately after the next request (default).
899
+ - `after-N-requests` — keep the retry model for the next N requests, then reset.
900
+ - `after-N-seconds` — keep the retry model for N seconds, then reset.
848
901
 
849
902
  #### `Retryable`
850
903
 
@@ -859,17 +912,17 @@ type Retryable = (
859
912
 
860
913
  #### `Retry`
861
914
 
862
- A `Retry` specifies the model to retry and optional settings. The available options depend on the model type (language model or embedding model).
915
+ A `Retry` specifies the model to retry and optional settings. The available options depend on the model type (language model, embedding model, or image model).
863
916
 
864
917
  ```typescript
865
918
  interface Retry {
866
- model: LanguageModelV2 | EmbeddingModelV2;
919
+ model: LanguageModelV3 | EmbeddingModelV3 | ImageModelV3;
867
920
  maxAttempts?: number; // Maximum retry attempts per model (default: 1)
868
921
  delay?: number; // Delay in milliseconds before retrying
869
922
  backoffFactor?: number; // Multiplier for exponential backoff
870
923
  timeout?: number; // Timeout in milliseconds for the retry attempt
871
924
  providerOptions?: ProviderOptions; // @deprecated - use options.providerOptions instead
872
- options?: LanguageModelV2CallOptions | EmbeddingModelV2CallOptions; // Call options to override for this retry
925
+ options?: LanguageModelV3CallOptions | EmbeddingModelV3CallOptions | ImageModelV3CallOptions; // Call options to override for this retry
873
926
  }
874
927
  ```
875
928
 
@@ -889,22 +942,22 @@ interface RetryContext {
889
942
  A `RetryAttempt` represents a single attempt with a specific model, which can be either an error or a successful result that triggered a retry. Each attempt includes the call options that were used for that specific attempt. For retry attempts, this will reflect any overridden options from the retry configuration.
890
943
 
891
944
  ```typescript
892
- // For both language and embedding models
945
+ // For language, embedding, and image models
893
946
  type RetryAttempt =
894
- | {
895
- type: 'error';
896
- error: unknown;
897
- model: LanguageModelV2 | EmbeddingModelV2;
898
- options: LanguageModelV2CallOptions | EmbeddingModelV2CallOptions;
947
+ | {
948
+ type: 'error';
949
+ error: unknown;
950
+ model: LanguageModelV3 | EmbeddingModelV3 | ImageModelV3;
951
+ options: LanguageModelV3CallOptions | EmbeddingModelV3CallOptions | ImageModelV3CallOptions;
899
952
  }
900
- | {
901
- type: 'result';
902
- result: LanguageModelV2Generate;
903
- model: LanguageModelV2;
904
- options: LanguageModelV2CallOptions;
953
+ | {
954
+ type: 'result';
955
+ result: LanguageModelV3Generate;
956
+ model: LanguageModelV3;
957
+ options: LanguageModelV3CallOptions;
905
958
  };
906
959
 
907
- // Note: Result-based retries only apply to language models, not embedding models
960
+ // Note: Result-based retries only apply to language models, not embedding or image models
908
961
 
909
962
  // Type guards for discriminating attempts
910
963
  function isErrorAttempt(attempt: RetryAttempt): attempt is RetryErrorAttempt;
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { C as Retryable, S as RetryResultAttempt, T as RetryableOptions, _ as Retries, a as GatewayLanguageModelId, b as RetryContext, c as LanguageModelGenerate, d as LanguageModelStreamPart, f as ProviderOptions, g as ResolvedModel, h as ResolvableModel, i as EmbeddingModelRetryCallOptions, l as LanguageModelRetryCallOptions, m as ResolvableLanguageModel, n as EmbeddingModelCallOptions, o as LanguageModel, p as Reset, r as EmbeddingModelEmbed, s as LanguageModelCallOptions, t as EmbeddingModel, u as LanguageModelStream, v as Retry, w as RetryableModelOptions, x as RetryErrorAttempt, y as RetryAttempt } from "./types-Dk5KMZMd.mjs";
1
+ import { C as RetryAttempt, D as Retryable, E as RetryResultAttempt, O as RetryableModelOptions, S as Retry, T as RetryErrorAttempt, _ as Reset, a as GatewayLanguageModelId, b as ResolvedModel, c as ImageModelGenerate, d as LanguageModelCallOptions, f as LanguageModelGenerate, g as ProviderOptions, h as LanguageModelStreamPart, i as EmbeddingModelRetryCallOptions, k as RetryableOptions, l as ImageModelRetryCallOptions, m as LanguageModelStream, n as EmbeddingModelCallOptions, o as ImageModel, p as LanguageModelRetryCallOptions, r as EmbeddingModelEmbed, s as ImageModelCallOptions, t as EmbeddingModel, u as LanguageModel, v as ResolvableLanguageModel, w as RetryContext, x as Retries, y as ResolvableModel } from "./types-wEZKtEcH.mjs";
2
2
  import * as _ai_sdk_provider0 from "@ai-sdk/provider";
3
3
 
4
4
  //#region src/create-retryable-model.d.ts
@@ -7,19 +7,21 @@ declare function createRetryable<MODEL extends LanguageModel>(options: Omit<Retr
7
7
  }): LanguageModel;
8
8
  declare function createRetryable<MODEL extends LanguageModel>(options: RetryableModelOptions<MODEL>): LanguageModel;
9
9
  declare function createRetryable<MODEL extends EmbeddingModel>(options: RetryableModelOptions<MODEL>): EmbeddingModel;
10
+ declare function createRetryable<MODEL extends ImageModel>(options: RetryableModelOptions<MODEL>): ImageModel;
10
11
  //#endregion
11
12
  //#region src/get-model-key.d.ts
12
13
  /**
13
- * Generate a unique key for a LanguageModel instance.
14
+ * Generate a unique key for a model instance.
14
15
  */
15
- declare const getModelKey: (model: LanguageModel | EmbeddingModel) => string;
16
+ declare const getModelKey: (model: LanguageModel | EmbeddingModel | ImageModel) => string;
16
17
  //#endregion
17
18
  //#region src/utils.d.ts
18
19
  declare const isObject: (value: unknown) => value is Record<string, unknown>;
19
20
  declare const isString: (value: unknown) => value is string;
20
- declare const isModel: (model: unknown) => model is LanguageModel | EmbeddingModel;
21
+ declare const isModel: (model: unknown) => model is LanguageModel | EmbeddingModel | ImageModel;
21
22
  declare const isLanguageModel: (model: unknown) => model is LanguageModel;
22
23
  declare const isEmbeddingModel: (model: unknown) => model is EmbeddingModel;
24
+ declare const isImageModel: (model: unknown) => model is ImageModel;
23
25
  declare const isStreamResult: (result: LanguageModelGenerate | LanguageModelStream) => result is LanguageModelStream;
24
26
  declare const isGenerateResult: (result: LanguageModelGenerate | LanguageModelStream) => result is LanguageModelGenerate;
25
27
  /**
@@ -73,4 +75,4 @@ declare const isAbortError: (error: unknown) => boolean;
73
75
  */
74
76
  declare const isTimeoutError: (error: unknown) => boolean;
75
77
  //#endregion
76
- export { EmbeddingModel, EmbeddingModelCallOptions, EmbeddingModelEmbed, EmbeddingModelRetryCallOptions, GatewayLanguageModelId, LanguageModel, LanguageModelCallOptions, LanguageModelGenerate, LanguageModelRetryCallOptions, LanguageModelStream, LanguageModelStreamPart, ProviderOptions, Reset, ResolvableLanguageModel, ResolvableModel, ResolvedModel, Retries, Retry, RetryAttempt, RetryContext, RetryErrorAttempt, RetryResultAttempt, Retryable, RetryableModelOptions, RetryableOptions, createRetryable, getModelKey, isAbortError, isEmbeddingModel, isErrorAttempt, isGenerateResult, isLanguageModel, isModel, isObject, isResultAttempt, isStreamContentPart, isStreamResult, isString, isTimeoutError };
78
+ export { EmbeddingModel, EmbeddingModelCallOptions, EmbeddingModelEmbed, EmbeddingModelRetryCallOptions, GatewayLanguageModelId, ImageModel, ImageModelCallOptions, ImageModelGenerate, ImageModelRetryCallOptions, LanguageModel, LanguageModelCallOptions, LanguageModelGenerate, LanguageModelRetryCallOptions, LanguageModelStream, LanguageModelStreamPart, ProviderOptions, Reset, ResolvableLanguageModel, ResolvableModel, ResolvedModel, Retries, Retry, RetryAttempt, RetryContext, RetryErrorAttempt, RetryResultAttempt, Retryable, RetryableModelOptions, RetryableOptions, createRetryable, getModelKey, isAbortError, isEmbeddingModel, isErrorAttempt, isGenerateResult, isImageModel, isLanguageModel, isModel, isObject, isResultAttempt, isStreamContentPart, isStreamResult, isString, isTimeoutError };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { a as isLanguageModel, c as isResultAttempt, d as isString, f as isTimeoutError, i as isGenerateResult, l as isStreamContentPart, n as isEmbeddingModel, o as isModel, r as isErrorAttempt, s as isObject, t as isAbortError, u as isStreamResult } from "./utils-fYGELi1C.mjs";
1
+ import { a as isImageModel, c as isObject, d as isStreamResult, f as isString, i as isGenerateResult, l as isResultAttempt, n as isEmbeddingModel, o as isLanguageModel, p as isTimeoutError, r as isErrorAttempt, s as isModel, t as isAbortError, u as isStreamContentPart } from "./utils-CfnsSGrw.mjs";
2
2
  import { RetryError, gateway } from "ai";
3
3
  import { delay } from "@ai-sdk/provider-utils";
4
4
  import { getErrorMessage } from "@ai-sdk/provider";
@@ -94,7 +94,7 @@ function calculateExponentialBackoff(baseDelay, backoffFactor = 1, attempts) {
94
94
  //#endregion
95
95
  //#region src/get-model-key.ts
96
96
  /**
97
- * Generate a unique key for a LanguageModel instance.
97
+ * Generate a unique key for a model instance.
98
98
  */
99
99
  const getModelKey = (model) => {
100
100
  return `${model.provider}/${model.modelId}`;
@@ -110,7 +110,7 @@ function countModelAttempts(model, attempts) {
110
110
  //#endregion
111
111
  //#region src/resolve-model.ts
112
112
  /**
113
- * Resolve a model string via the AI SDK Gateway to a modelinstance
113
+ * Resolve a model string via the AI SDK Gateway to a model instance
114
114
  */
115
115
  function resolveModel(model) {
116
116
  return isModel(model) ? model : gateway(model);
@@ -133,9 +133,20 @@ async function findRetryModel(retries, context) {
133
133
  */
134
134
  for (const retry of applicableRetries) {
135
135
  let retryModel;
136
- if (typeof retry === "function") retryModel = await retry(context);
137
- else if (isObject(retry) && "model" in retry) retryModel = retry;
138
- else retryModel = { model: retry };
136
+ if (typeof retry === `function`)
137
+ /**
138
+ * Function retryable - call it with context
139
+ * The function can be either Retryable<MODEL> or Retryable<ResolvableLanguageModel>
140
+ * At runtime, both work because the context is structurally compatible
141
+ * We use type assertion here because TypeScript can't prove the union type compatibility
142
+ */
143
+ retryModel = await retry(context);
144
+ else if (isObject(retry) && `model` in retry)
145
+ /** Static Retry object */
146
+ retryModel = retry;
147
+ else
148
+ /** Plain model */
149
+ retryModel = { model: retry };
139
150
  if (retryModel) {
140
151
  /**
141
152
  * The model can be string or an instance.
@@ -322,6 +333,152 @@ var RetryableEmbeddingModel = class extends BaseRetryableModel {
322
333
  }
323
334
  };
324
335
 
336
+ //#endregion
337
+ //#region src/retryable-image-model.ts
338
+ var RetryableImageModel = class extends BaseRetryableModel {
339
+ specificationVersion = "v3";
340
+ get modelId() {
341
+ return this.currentModel.modelId;
342
+ }
343
+ get provider() {
344
+ return this.currentModel.provider;
345
+ }
346
+ get maxImagesPerCall() {
347
+ return this.currentModel.maxImagesPerCall;
348
+ }
349
+ /**
350
+ * Get the retry call options overrides from a retry configuration.
351
+ */
352
+ getRetryCallOptions(callOptions, currentRetry) {
353
+ const retryOptions = currentRetry?.options ?? {};
354
+ return {
355
+ ...callOptions,
356
+ n: retryOptions.n ?? callOptions.n,
357
+ size: retryOptions.size ?? callOptions.size,
358
+ aspectRatio: retryOptions.aspectRatio ?? callOptions.aspectRatio,
359
+ seed: retryOptions.seed ?? callOptions.seed,
360
+ headers: retryOptions.headers ?? callOptions.headers,
361
+ providerOptions: retryOptions.providerOptions ?? currentRetry?.providerOptions ?? callOptions.providerOptions,
362
+ abortSignal: currentRetry?.timeout ? AbortSignal.timeout(currentRetry.timeout) : callOptions.abortSignal
363
+ };
364
+ }
365
+ /**
366
+ * Execute a function with retry logic for handling errors
367
+ */
368
+ async withRetry(input) {
369
+ /**
370
+ * Track all attempts.
371
+ */
372
+ const attempts = input.attempts ?? [];
373
+ /**
374
+ * Track current retry configuration.
375
+ */
376
+ let currentRetry;
377
+ while (true) {
378
+ /**
379
+ * The previous attempt that triggered a retry, or undefined if this is the first attempt
380
+ */
381
+ const previousAttempt = attempts.at(-1);
382
+ /**
383
+ * Call the onRetry handler if provided.
384
+ * Skip on the first attempt since no previous attempt exists yet.
385
+ */
386
+ if (previousAttempt) {
387
+ const context = {
388
+ current: {
389
+ ...previousAttempt,
390
+ model: this.currentModel
391
+ },
392
+ attempts: [...attempts]
393
+ };
394
+ this.options.onRetry?.(context);
395
+ }
396
+ /**
397
+ * Get the retry call options overrides for this attempt
398
+ */
399
+ const retryCallOptions = this.getRetryCallOptions(input.callOptions, currentRetry);
400
+ try {
401
+ return {
402
+ result: await input.fn(retryCallOptions),
403
+ attempts
404
+ };
405
+ } catch (error) {
406
+ /** Don't retry if user manually aborted the request. */
407
+ /** TimeoutError from AbortSignal.timeout() will still be handled by retry handlers. */
408
+ if (isAbortError(error)) throw error;
409
+ const { retryModel, attempt } = await this.handleError(error, attempts, retryCallOptions);
410
+ attempts.push(attempt);
411
+ if (retryModel.delay) {
412
+ /**
413
+ * Calculate exponential backoff delay based on the number of attempts for this specific model.
414
+ * The delay grows exponentially: baseDelay * backoffFactor^attempts
415
+ * Example: With delay=1000ms and backoffFactor=2:
416
+ * - Attempt 1: 1000ms
417
+ * - Attempt 2: 2000ms
418
+ * - Attempt 3: 4000ms
419
+ */
420
+ const modelAttemptsCount = countModelAttempts(retryModel.model, attempts);
421
+ await delay(calculateExponentialBackoff(retryModel.delay, retryModel.backoffFactor, modelAttemptsCount), { abortSignal: retryCallOptions.abortSignal });
422
+ }
423
+ this.currentModel = retryModel.model;
424
+ currentRetry = retryModel;
425
+ }
426
+ }
427
+ }
428
+ /**
429
+ * Handle an error and determine if a retry is needed
430
+ */
431
+ async handleError(error, attempts, callOptions) {
432
+ const errorAttempt = {
433
+ type: "error",
434
+ error,
435
+ model: this.currentModel,
436
+ options: callOptions
437
+ };
438
+ /**
439
+ * Save the current attempt
440
+ */
441
+ const updatedAttempts = [...attempts, errorAttempt];
442
+ const context = {
443
+ current: errorAttempt,
444
+ attempts: updatedAttempts
445
+ };
446
+ this.options.onError?.(context);
447
+ const retryModel = await findRetryModel(this.options.retries, context);
448
+ /**
449
+ * Handler didn't return any models to try next, rethrow the error.
450
+ * If we retried the request, wrap the error into a `RetryError` for better visibility.
451
+ */
452
+ if (!retryModel) {
453
+ if (updatedAttempts.length > 1) throw prepareRetryError(error, updatedAttempts);
454
+ throw error;
455
+ }
456
+ return {
457
+ retryModel,
458
+ attempt: errorAttempt
459
+ };
460
+ }
461
+ async doGenerate(callOptions) {
462
+ /**
463
+ * Resolve the starting model (base or sticky)
464
+ */
465
+ const startModel = this.resolveStartModel();
466
+ this.currentModel = startModel;
467
+ /**
468
+ * If retries are disabled, bypass retry machinery entirely
469
+ */
470
+ if (this.isDisabled()) return this.currentModel.doGenerate(callOptions);
471
+ const { result } = await this.withRetry({
472
+ fn: async (retryCallOptions) => {
473
+ return this.currentModel.doGenerate(retryCallOptions);
474
+ },
475
+ callOptions
476
+ });
477
+ this.updateStickyModel(startModel);
478
+ return result;
479
+ }
480
+ };
481
+
325
482
  //#endregion
326
483
  //#region src/retryable-language-model.ts
327
484
  var RetryableLanguageModel = class extends BaseRetryableModel {
@@ -630,6 +787,10 @@ function createRetryable(options) {
630
787
  ...options,
631
788
  model
632
789
  });
790
+ if (isImageModel(model)) return new RetryableImageModel({
791
+ ...options,
792
+ model
793
+ });
633
794
  return new RetryableLanguageModel({
634
795
  ...options,
635
796
  model
@@ -637,4 +798,4 @@ function createRetryable(options) {
637
798
  }
638
799
 
639
800
  //#endregion
640
- export { createRetryable, getModelKey, isAbortError, isEmbeddingModel, isErrorAttempt, isGenerateResult, isLanguageModel, isModel, isObject, isResultAttempt, isStreamContentPart, isStreamResult, isString, isTimeoutError };
801
+ export { createRetryable, getModelKey, isAbortError, isEmbeddingModel, isErrorAttempt, isGenerateResult, isImageModel, isLanguageModel, isModel, isObject, isResultAttempt, isStreamContentPart, isStreamResult, isString, isTimeoutError };
@@ -1,4 +1,4 @@
1
- import { C as Retryable, T as RetryableOptions, m as ResolvableLanguageModel, t as EmbeddingModel } from "../types-Dk5KMZMd.mjs";
1
+ import { D as Retryable, k as RetryableOptions, o as ImageModel, t as EmbeddingModel, v as ResolvableLanguageModel } from "../types-wEZKtEcH.mjs";
2
2
 
3
3
  //#region src/retryables/content-filter-triggered.d.ts
4
4
  /**
@@ -6,11 +6,17 @@ import { C as Retryable, T as RetryableOptions, m as ResolvableLanguageModel, t
6
6
  */
7
7
  declare function contentFilterTriggered<MODEL extends ResolvableLanguageModel>(model: MODEL, options?: RetryableOptions<MODEL>): Retryable<MODEL>;
8
8
  //#endregion
9
+ //#region src/retryables/no-image-generated.d.ts
10
+ /**
11
+ * Fallback to a different model if image generation fails with NoImageGeneratedError.
12
+ */
13
+ declare function noImageGenerated<MODEL extends ImageModel>(model: MODEL, options?: RetryableOptions<MODEL>): Retryable<MODEL>;
14
+ //#endregion
9
15
  //#region src/retryables/request-not-retryable.d.ts
10
16
  /**
11
17
  * Fallback to a different model if the error is non-retryable.
12
18
  */
13
- declare function requestNotRetryable<MODEL extends ResolvableLanguageModel | EmbeddingModel>(model: MODEL, options?: RetryableOptions<MODEL>): Retryable<MODEL>;
19
+ declare function requestNotRetryable<MODEL extends ResolvableLanguageModel | EmbeddingModel | ImageModel>(model: MODEL, options?: RetryableOptions<MODEL>): Retryable<MODEL>;
14
20
  //#endregion
15
21
  //#region src/retryables/request-timeout.d.ts
16
22
  /**
@@ -18,7 +24,7 @@ declare function requestNotRetryable<MODEL extends ResolvableLanguageModel | Emb
18
24
  * Use in combination with the `abortSignal` option.
19
25
  * If no timeout is specified, a default of 60 seconds is used.
20
26
  */
21
- declare function requestTimeout<MODEL extends ResolvableLanguageModel | EmbeddingModel>(model: MODEL, options?: RetryableOptions<MODEL>): Retryable<MODEL>;
27
+ declare function requestTimeout<MODEL extends ResolvableLanguageModel | EmbeddingModel | ImageModel>(model: MODEL, options?: RetryableOptions<MODEL>): Retryable<MODEL>;
22
28
  //#endregion
23
29
  //#region src/retryables/retry-after-delay.d.ts
24
30
  /**
@@ -26,7 +32,7 @@ declare function requestTimeout<MODEL extends ResolvableLanguageModel | Embeddin
26
32
  * Uses the `Retry-After` or `Retry-After-Ms` headers if present.
27
33
  * Otherwise uses the specified `delay` and `backoffFactor` if provided.
28
34
  */
29
- declare function retryAfterDelay<MODEL extends ResolvableLanguageModel | EmbeddingModel>(options: RetryableOptions<MODEL>): Retryable<MODEL>;
35
+ declare function retryAfterDelay<MODEL extends ResolvableLanguageModel | EmbeddingModel | ImageModel>(options: RetryableOptions<MODEL>): Retryable<MODEL>;
30
36
  //#endregion
31
37
  //#region src/retryables/schema-mismatch.d.ts
32
38
  /**
@@ -58,13 +64,13 @@ declare function schemaMismatch<MODEL extends ResolvableLanguageModel>(model: MO
58
64
  * - Response with `type: "overloaded_error"`
59
65
  * - Response with a `message` containing "overloaded"
60
66
  */
61
- declare function serviceOverloaded<MODEL extends ResolvableLanguageModel | EmbeddingModel>(model: MODEL, options?: RetryableOptions<MODEL>): Retryable<MODEL>;
67
+ declare function serviceOverloaded<MODEL extends ResolvableLanguageModel | EmbeddingModel | ImageModel>(model: MODEL, options?: RetryableOptions<MODEL>): Retryable<MODEL>;
62
68
  //#endregion
63
69
  //#region src/retryables/service-unavailable.d.ts
64
70
  /**
65
71
  * Fallback to a different model if the provider returns a service unavailable error.
66
72
  * This retryable handles HTTP status code 503 (Service Unavailable).
67
73
  */
68
- declare function serviceUnavailable<MODEL extends ResolvableLanguageModel | EmbeddingModel>(model: MODEL, options?: RetryableOptions<MODEL>): Retryable<MODEL>;
74
+ declare function serviceUnavailable<MODEL extends ResolvableLanguageModel | EmbeddingModel | ImageModel>(model: MODEL, options?: RetryableOptions<MODEL>): Retryable<MODEL>;
69
75
  //#endregion
70
- export { contentFilterTriggered, requestNotRetryable, requestTimeout, retryAfterDelay, schemaMismatch, serviceOverloaded, serviceUnavailable };
76
+ export { contentFilterTriggered, noImageGenerated, requestNotRetryable, requestTimeout, retryAfterDelay, schemaMismatch, serviceOverloaded, serviceUnavailable };
@@ -1,5 +1,5 @@
1
- import { c as isResultAttempt, d as isString, f as isTimeoutError, r as isErrorAttempt, s as isObject } from "../utils-fYGELi1C.mjs";
2
- import { APICallError } from "ai";
1
+ import { c as isObject, f as isString, l as isResultAttempt, p as isTimeoutError, r as isErrorAttempt } from "../utils-CfnsSGrw.mjs";
2
+ import { APICallError, NoImageGeneratedError } from "ai";
3
3
  import { safeParseJSON } from "@ai-sdk/provider-utils";
4
4
  import { fromJSONSchema } from "zod";
5
5
 
@@ -29,6 +29,25 @@ function contentFilterTriggered(model, options) {
29
29
  };
30
30
  }
31
31
 
32
+ //#endregion
33
+ //#region src/retryables/no-image-generated.ts
34
+ /**
35
+ * Fallback to a different model if image generation fails with NoImageGeneratedError.
36
+ */
37
+ function noImageGenerated(model, options) {
38
+ return (context) => {
39
+ const { current } = context;
40
+ if (isErrorAttempt(current)) {
41
+ const { error } = current;
42
+ if (NoImageGeneratedError.isInstance(error)) return {
43
+ model,
44
+ maxAttempts: 1,
45
+ ...options
46
+ };
47
+ }
48
+ };
49
+ }
50
+
32
51
  //#endregion
33
52
  //#region src/retryables/request-not-retryable.ts
34
53
  /**
@@ -219,4 +238,4 @@ function serviceUnavailable(model, options) {
219
238
  }
220
239
 
221
240
  //#endregion
222
- export { contentFilterTriggered, requestNotRetryable, requestTimeout, retryAfterDelay, schemaMismatch, serviceOverloaded, serviceUnavailable };
241
+ export { contentFilterTriggered, noImageGenerated, requestNotRetryable, requestTimeout, retryAfterDelay, schemaMismatch, serviceOverloaded, serviceUnavailable };
@@ -1,17 +1,19 @@
1
1
  import { gateway } from "ai";
2
- import { EmbeddingModelV3, LanguageModelV3, LanguageModelV3CallOptions, LanguageModelV3StreamPart, SharedV3ProviderOptions } from "@ai-sdk/provider";
2
+ import { EmbeddingModelV3, ImageModelV3, ImageModelV3CallOptions, LanguageModelV3, LanguageModelV3CallOptions, LanguageModelV3StreamPart, SharedV3ProviderOptions } from "@ai-sdk/provider";
3
3
 
4
4
  //#region src/types.d.ts
5
5
  type Literals<T> = T extends string ? string extends T ? never : T : never;
6
6
  type LanguageModel = LanguageModelV3;
7
7
  type EmbeddingModel = EmbeddingModelV3;
8
+ type ImageModel = ImageModelV3;
8
9
  type LanguageModelCallOptions = LanguageModelV3CallOptions;
9
10
  type LanguageModelStreamPart = LanguageModelV3StreamPart;
11
+ type ImageModelCallOptions = ImageModelV3CallOptions;
10
12
  type ProviderOptions = SharedV3ProviderOptions;
11
13
  type GatewayLanguageModelId = Parameters<(typeof gateway)['languageModel']>[0];
12
14
  type ResolvableLanguageModel = LanguageModel | Literals<GatewayLanguageModelId>;
13
- type ResolvableModel<MODEL extends LanguageModel | EmbeddingModel> = MODEL extends LanguageModel ? ResolvableLanguageModel : EmbeddingModel;
14
- type ResolvedModel<MODEL extends ResolvableLanguageModel | EmbeddingModel> = MODEL extends ResolvableLanguageModel ? LanguageModel : EmbeddingModel;
15
+ type ResolvableModel<MODEL extends LanguageModel | EmbeddingModel | ImageModel> = MODEL extends LanguageModel ? ResolvableLanguageModel : MODEL extends EmbeddingModel ? EmbeddingModel : ImageModel;
16
+ type ResolvedModel<MODEL extends ResolvableLanguageModel | EmbeddingModel | ImageModel> = MODEL extends ResolvableLanguageModel ? LanguageModel : MODEL extends EmbeddingModel ? EmbeddingModel : ImageModel;
15
17
  /**
16
18
  * Call options that can be overridden during retry for language models.
17
19
  */
@@ -20,10 +22,14 @@ type LanguageModelRetryCallOptions = Partial<Pick<LanguageModelCallOptions, 'pro
20
22
  * Call options that can be overridden during retry for embedding models.
21
23
  */
22
24
  type EmbeddingModelRetryCallOptions = Partial<Pick<EmbeddingModelCallOptions, 'values' | 'headers' | 'providerOptions'>>;
25
+ /**
26
+ * Call options that can be overridden during retry for image models.
27
+ */
28
+ type ImageModelRetryCallOptions = Partial<Pick<ImageModelCallOptions, 'n' | 'size' | 'aspectRatio' | 'seed' | 'headers' | 'providerOptions'>>;
23
29
  /**
24
30
  * A retry attempt with an error
25
31
  */
26
- type RetryErrorAttempt<MODEL extends LanguageModel | EmbeddingModel> = {
32
+ type RetryErrorAttempt<MODEL extends LanguageModel | EmbeddingModel | ImageModel> = {
27
33
  type: 'error';
28
34
  error: unknown;
29
35
  result?: undefined;
@@ -31,7 +37,7 @@ type RetryErrorAttempt<MODEL extends LanguageModel | EmbeddingModel> = {
31
37
  /**
32
38
  * The call options used for this attempt.
33
39
  */
34
- options: MODEL extends LanguageModel ? LanguageModelCallOptions : EmbeddingModelCallOptions;
40
+ options: MODEL extends LanguageModel ? LanguageModelCallOptions : MODEL extends EmbeddingModel ? EmbeddingModelCallOptions : ImageModelCallOptions;
35
41
  };
36
42
  /**
37
43
  * A retry attempt with a successful result
@@ -49,11 +55,11 @@ type RetryResultAttempt = {
49
55
  /**
50
56
  * A retry attempt with either an error or a result and the model used
51
57
  */
52
- type RetryAttempt<MODEL extends LanguageModel | EmbeddingModel> = RetryErrorAttempt<MODEL> | RetryResultAttempt;
58
+ type RetryAttempt<MODEL extends LanguageModel | EmbeddingModel | ImageModel> = MODEL extends LanguageModel ? RetryErrorAttempt<MODEL> | RetryResultAttempt : RetryErrorAttempt<MODEL>;
53
59
  /**
54
60
  * The context provided to Retryables with the current attempt and all previous attempts.
55
61
  */
56
- type RetryContext<MODEL extends ResolvableLanguageModel | EmbeddingModel> = {
62
+ type RetryContext<MODEL extends ResolvableLanguageModel | EmbeddingModel | ImageModel> = {
57
63
  /**
58
64
  * Current attempt that caused the retry
59
65
  */
@@ -66,7 +72,7 @@ type RetryContext<MODEL extends ResolvableLanguageModel | EmbeddingModel> = {
66
72
  /**
67
73
  * Options for creating a retryable model.
68
74
  */
69
- interface RetryableModelOptions<MODEL extends LanguageModel | EmbeddingModel> {
75
+ interface RetryableModelOptions<MODEL extends LanguageModel | EmbeddingModel | ImageModel> {
70
76
  model: MODEL;
71
77
  retries: Retries<MODEL>;
72
78
  disabled?: boolean | (() => boolean);
@@ -90,7 +96,7 @@ interface RetryableModelOptions<MODEL extends LanguageModel | EmbeddingModel> {
90
96
  * This flexible approach allows retryable functions to return the exact model type
91
97
  * they received without type assertions, while still supporting string-based gateway models.
92
98
  */
93
- type Retry<MODEL extends ResolvableLanguageModel | EmbeddingModel> = {
99
+ type Retry<MODEL extends ResolvableLanguageModel | EmbeddingModel | ImageModel> = {
94
100
  model: MODEL;
95
101
  /**
96
102
  * Maximum number of attempts for this model.
@@ -112,7 +118,7 @@ type Retry<MODEL extends ResolvableLanguageModel | EmbeddingModel> = {
112
118
  /**
113
119
  * Call options to override for this retry.
114
120
  */
115
- options?: MODEL extends LanguageModel ? Partial<LanguageModelRetryCallOptions> : Partial<EmbeddingModelRetryCallOptions>;
121
+ options?: MODEL extends LanguageModel ? Partial<LanguageModelRetryCallOptions> : MODEL extends EmbeddingModel ? Partial<EmbeddingModelRetryCallOptions> : Partial<ImageModelRetryCallOptions>;
116
122
  /**
117
123
  * @deprecated Use `options.providerOptions` instead.
118
124
  * Provider options to override for this retry.
@@ -124,9 +130,9 @@ type Retry<MODEL extends ResolvableLanguageModel | EmbeddingModel> = {
124
130
  /**
125
131
  * A function that determines whether to retry with a different model based on the current attempt and all previous attempts.
126
132
  */
127
- type Retryable<MODEL extends ResolvableLanguageModel | EmbeddingModel> = (context: RetryContext<MODEL>) => Retry<MODEL> | Promise<Retry<MODEL> | undefined> | undefined;
128
- type Retries<MODEL extends LanguageModel | EmbeddingModel> = Array<Retryable<ResolvableModel<MODEL>> | Retry<ResolvableModel<MODEL>> | ResolvableModel<MODEL>>;
129
- type RetryableOptions<MODEL extends ResolvableLanguageModel | EmbeddingModel> = Partial<Omit<Retry<MODEL>, 'model'>>;
133
+ type Retryable<MODEL extends ResolvableLanguageModel | EmbeddingModel | ImageModel> = (context: RetryContext<MODEL>) => Retry<MODEL> | Promise<Retry<MODEL> | undefined> | undefined;
134
+ type Retries<MODEL extends LanguageModel | EmbeddingModel | ImageModel> = Array<Retryable<ResolvableModel<MODEL>> | Retry<ResolvableModel<MODEL>> | ResolvableModel<MODEL>>;
135
+ type RetryableOptions<MODEL extends ResolvableLanguageModel | EmbeddingModel | ImageModel> = Partial<Omit<Retry<MODEL>, 'model'>>;
130
136
  /**
131
137
  * Controls when to reset the sticky model back to the base model.
132
138
  *
@@ -139,5 +145,6 @@ type LanguageModelGenerate = Awaited<ReturnType<LanguageModel['doGenerate']>>;
139
145
  type LanguageModelStream = Awaited<ReturnType<LanguageModel['doStream']>>;
140
146
  type EmbeddingModelCallOptions = Parameters<EmbeddingModel['doEmbed']>[0];
141
147
  type EmbeddingModelEmbed = Awaited<ReturnType<EmbeddingModel['doEmbed']>>;
148
+ type ImageModelGenerate = Awaited<ReturnType<ImageModel['doGenerate']>>;
142
149
  //#endregion
143
- export { Retryable as C, RetryResultAttempt as S, RetryableOptions as T, Retries as _, GatewayLanguageModelId as a, RetryContext as b, LanguageModelGenerate as c, LanguageModelStreamPart as d, ProviderOptions as f, ResolvedModel as g, ResolvableModel as h, EmbeddingModelRetryCallOptions as i, LanguageModelRetryCallOptions as l, ResolvableLanguageModel as m, EmbeddingModelCallOptions as n, LanguageModel as o, Reset as p, EmbeddingModelEmbed as r, LanguageModelCallOptions as s, EmbeddingModel as t, LanguageModelStream as u, Retry as v, RetryableModelOptions as w, RetryErrorAttempt as x, RetryAttempt as y };
150
+ export { RetryAttempt as C, Retryable as D, RetryResultAttempt as E, RetryableModelOptions as O, Retry as S, RetryErrorAttempt as T, Reset as _, GatewayLanguageModelId as a, ResolvedModel as b, ImageModelGenerate as c, LanguageModelCallOptions as d, LanguageModelGenerate as f, ProviderOptions as g, LanguageModelStreamPart as h, EmbeddingModelRetryCallOptions as i, RetryableOptions as k, ImageModelRetryCallOptions as l, LanguageModelStream as m, EmbeddingModelCallOptions as n, ImageModel as o, LanguageModelRetryCallOptions as p, EmbeddingModelEmbed as r, ImageModelCallOptions as s, EmbeddingModel as t, LanguageModel as u, ResolvableLanguageModel as v, RetryContext as w, Retries as x, ResolvableModel as y };
@@ -1,9 +1,10 @@
1
1
  //#region src/utils.ts
2
2
  const isObject = (value) => typeof value === "object" && value !== null;
3
3
  const isString = (value) => typeof value === "string";
4
- const isModel = (model) => isLanguageModel(model) || isEmbeddingModel(model);
5
- const isLanguageModel = (model) => isObject(model) && "provider" in model && "modelId" in model && "specificationVersion" in model && "doGenerate" in model && model.specificationVersion === "v3";
4
+ const isModel = (model) => isLanguageModel(model) || isEmbeddingModel(model) || isImageModel(model);
5
+ const isLanguageModel = (model) => isObject(model) && "provider" in model && "modelId" in model && "specificationVersion" in model && "doGenerate" in model && "doStream" in model && model.specificationVersion === "v3";
6
6
  const isEmbeddingModel = (model) => isObject(model) && "provider" in model && "modelId" in model && "specificationVersion" in model && "doEmbed" in model && model.specificationVersion === "v3";
7
+ const isImageModel = (model) => isObject(model) && "provider" in model && "modelId" in model && "specificationVersion" in model && "doGenerate" in model && model.specificationVersion === "v3" && !("doStream" in model) && !("doEmbed" in model);
7
8
  const isStreamResult = (result) => "stream" in result;
8
9
  const isGenerateResult = (result) => "content" in result;
9
10
  /**
@@ -38,4 +39,4 @@ const isAbortError = (error) => error instanceof Error && error.name === "AbortE
38
39
  const isTimeoutError = (error) => error instanceof Error && error.name === "TimeoutError";
39
40
 
40
41
  //#endregion
41
- export { isLanguageModel as a, isResultAttempt as c, isString as d, isTimeoutError as f, isGenerateResult as i, isStreamContentPart as l, isEmbeddingModel as n, isModel as o, isErrorAttempt as r, isObject as s, isAbortError as t, isStreamResult as u };
42
+ export { isImageModel as a, isObject as c, isStreamResult as d, isString as f, isGenerateResult as i, isResultAttempt as l, isEmbeddingModel as n, isLanguageModel as o, isTimeoutError as p, isErrorAttempt as r, isModel as s, isAbortError as t, isStreamContentPart as u };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ai-retry",
3
- "version": "1.2.0",
4
- "description": "AI SDK Retry",
3
+ "version": "1.3.0",
4
+ "description": "Retry and fallback mechanisms for AI SDK",
5
5
  "types": "./dist/index.d.mts",
6
6
  "type": "module",
7
7
  "files": [
@@ -34,6 +34,7 @@
34
34
  "@ai-sdk/anthropic": "3.0.23",
35
35
  "@ai-sdk/azure": "3.0.19",
36
36
  "@ai-sdk/gateway": "3.0.23",
37
+ "@ai-sdk/google": "^3.0.33",
37
38
  "@ai-sdk/groq": "3.0.15",
38
39
  "@ai-sdk/openai": "3.0.19",
39
40
  "@ai-sdk/test-server": "1.0.3",