@ai-sdk/gateway 4.0.94 → 4.0.95

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.
@@ -1472,12 +1472,16 @@ The following gateway provider options are available:
1472
1472
 
1473
1473
  When `sort` is active, the response's `providerMetadata.gateway.routing.sort` object contains the sort option used, the resulting execution order, per-provider metric values, and any providers that were deprioritized.
1474
1474
 
1475
- - **models** _string[]_
1475
+ - **models** _string[] | [GatewayModelFallback, ...string[]]_
1476
1476
 
1477
- Specifies fallback models to use when the primary model fails or is unavailable. The gateway will try the primary model first (specified in the `model` parameter), then try each model in this array in order until one succeeds.
1477
+ Specifies fallback models in attempt order. String entries keep their existing behavior: the gateway tries them when the primary model fails or is unavailable. Evaluation requests can also start the list with one conditional `{ model, when }` entry to retry a successful but uncertain evaluation with another model. Only the first entry can be conditional, and only one conditional entry is allowed. If the primary model fails, the gateway also advances to the conditional entry under its existing error fallback behavior. Conditional entries are rejected on non-evaluation requests. Type evaluation options with `GatewayEvaluationProviderOptions`, since `GatewayProviderOptions` only accepts string entries.
1478
1478
 
1479
1479
  Example: `models: ['openai/gpt-5.4-nano', 'google/gemini-3.8-flash']` will try the fallback models in order if the primary model fails.
1480
1480
 
1481
+ A direct condition uses `{ question, confidenceBelow }` for Choice and Score questions, or `{ question, probabilityBetween: [minimum, maximum] }` for Boolean questions. Boolean probability is P(true), not confidence in the selected Boolean outcome. Bounds are inclusive, finite, ordered, and within `[0, 1]`.
1482
+
1483
+ Combine conditions with `{ any: [...] }`, `{ all: [...] }`, or `{ atLeast: { count, conditions: [...] } }`. Each condition list holds 1 to 20 conditions. `atLeast.count` must be an integer from `1` through the number of conditions. Conditions nest at most 5 levels deep. Question IDs and the conditional `model` are 1 to 256 characters. The SDK checks these bounds before sending the request, and the gateway also checks that each question exists and has a kind that matches its condition. String error fallbacks may follow the conditional entry.
1484
+
1481
1485
  - **user** _string_
1482
1486
 
1483
1487
  Optional identifier for the end user on whose behalf the request is being made. This is used for spend tracking and attribution purposes, allowing you to track usage per end-user in your application.
@@ -1608,6 +1612,63 @@ const { text } = await generateText({
1608
1612
  // 4. Return the result from the first model that succeeds
1609
1613
  ```
1610
1614
 
1615
+ #### Conditional Evaluation Fallbacks Example
1616
+
1617
+ Conditional model entries are available only for evaluation requests. Keep the
1618
+ configuration under `providerOptions.gateway.models`. `experimental_evaluate`
1619
+ does not have a top-level fallback option.
1620
+
1621
+ ```ts
1622
+ import type { GatewayEvaluationProviderOptions } from '@ai-sdk/gateway';
1623
+ import { experimental_evaluate } from 'ai';
1624
+
1625
+ const questions = {
1626
+ department: {
1627
+ type: 'choice',
1628
+ instructions: 'Which team should handle this?',
1629
+ criteria: { billing: 'Charges and refunds', support: 'Other requests' },
1630
+ },
1631
+ requestsRefund: {
1632
+ type: 'boolean',
1633
+ instructions: 'Is the customer requesting money back?',
1634
+ },
1635
+ } as const;
1636
+
1637
+ const result = await experimental_evaluate({
1638
+ model: 'typesafe-ai/jev',
1639
+ state: 'I was charged twice. Please refund the duplicate.',
1640
+ questions,
1641
+ providerOptions: {
1642
+ gateway: {
1643
+ models: [
1644
+ {
1645
+ model: 'openai/gpt-5.6-sol',
1646
+ when: {
1647
+ any: [
1648
+ { question: 'department', confidenceBelow: 0.6 },
1649
+ {
1650
+ question: 'requestsRefund',
1651
+ probabilityBetween: [0.4, 0.6],
1652
+ },
1653
+ ],
1654
+ },
1655
+ },
1656
+ ],
1657
+ } satisfies GatewayEvaluationProviderOptions<keyof typeof questions>,
1658
+ },
1659
+ });
1660
+
1661
+ // The model that produced the answers, the fallback when the condition matched.
1662
+ console.log(result.response.modelId);
1663
+ console.log(JSON.stringify(result.providerMetadata?.gateway?.routing, null, 2));
1664
+ ```
1665
+
1666
+ When the condition matches, `result.response.modelId` names the fallback model,
1667
+ and `providerMetadata.gateway.routing.modelAttempts` lists the primary and
1668
+ fallback attempts. The successful attempt of each stage carries its own
1669
+ `generationId`, `usage`, and cost fields, and the first fallback attempt lists
1670
+ the questions that triggered it in `triggeredBy`.
1671
+
1611
1672
  #### Zero Data Retention Example
1612
1673
 
1613
1674
  Set `zeroDataRetention` to true to route requests to providers with zero data retention agreements with Vercel for AI Gateway. BYOK credentials are skipped by default, since your provider agreements differ from Vercel's. When this filter is on, AI Gateway routes only to providers Vercel has ZDR agreements with for the model. If you have BYOK keys marked as ZDR, those keys are tried first, then AI Gateway falls back to its system credentials. You are responsible for the accuracy of that marking. Applies to both account-wide and request-level ZDR. The request fails if no ZDR-eligible credentials are available. When `zeroDataRetention` is `false` or not specified, there is no enforcement of restricting routing. Request-level ZDR is only available for Vercel Pro and Enterprise plans.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ai-sdk/gateway",
3
3
  "private": false,
4
- "version": "4.0.94",
4
+ "version": "4.0.95",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
7
7
  "sideEffects": false,
@@ -8,6 +8,7 @@ import {
8
8
  createJsonResponseHandler,
9
9
  getErrorMessage,
10
10
  lazySchema,
11
+ parseProviderOptions,
11
12
  postJsonToApi,
12
13
  resolve,
13
14
  zodSchema,
@@ -17,6 +18,7 @@ import { z } from './zod';
17
18
  import { asGatewayError } from './errors';
18
19
  import { parseAuthMethod } from './errors/parse-auth-method';
19
20
  import type { GatewayConfig } from './gateway-config';
21
+ import { gatewayEvaluationProviderOptionsSchema } from './gateway-provider-options';
20
22
 
21
23
  export class GatewayEvaluationModel implements EvaluationModelV4 {
22
24
  readonly specificationVersion = 'v4';
@@ -44,6 +46,15 @@ export class GatewayEvaluationModel implements EvaluationModelV4 {
44
46
  }: Parameters<EvaluationModelV4['doEvaluate']>[0]): Promise<
45
47
  Awaited<ReturnType<EvaluationModelV4['doEvaluate']>>
46
48
  > {
49
+ const gatewayOptions = await parseProviderOptions({
50
+ provider: 'gateway',
51
+ providerOptions,
52
+ schema: gatewayEvaluationProviderOptionsSchema,
53
+ });
54
+ const validatedProviderOptions =
55
+ gatewayOptions == null
56
+ ? providerOptions
57
+ : { ...providerOptions, gateway: gatewayOptions };
47
58
  const resolvedHeaders = this.config.headers
48
59
  ? await resolve(this.config.headers)
49
60
  : undefined;
@@ -63,7 +74,9 @@ export class GatewayEvaluationModel implements EvaluationModelV4 {
63
74
  body: {
64
75
  state,
65
76
  questions,
66
- ...(providerOptions ? { providerOptions } : {}),
77
+ ...(validatedProviderOptions
78
+ ? { providerOptions: validatedProviderOptions }
79
+ : {}),
67
80
  },
68
81
  successfulResponseHandler: createJsonResponseHandler(
69
82
  gatewayEvaluationResponseSchema,
@@ -84,7 +97,7 @@ export class GatewayEvaluationModel implements EvaluationModelV4 {
84
97
  providerMetadata:
85
98
  responseBody.providerMetadata as unknown as SharedV4ProviderMetadata,
86
99
  response: {
87
- modelId: this.modelId,
100
+ modelId: responseBody.model ?? this.modelId,
88
101
  headers: responseHeaders,
89
102
  body: rawValue,
90
103
  },
@@ -152,6 +165,7 @@ const gatewayEvaluationResponseSchema = lazySchema(() =>
152
165
  zodSchema(
153
166
  z.object({
154
167
  answers: z.record(z.string(), gatewayEvaluationAnswerSchema),
168
+ model: z.string().optional(),
155
169
  rounding: z
156
170
  .object({
157
171
  probabilityDecimals: z.number().optional(),
@@ -1,4 +1,50 @@
1
+ import { lazySchema, zodSchema } from '@ai-sdk/provider-utils';
2
+ import type { ZodType } from 'zod/v4';
3
+ import { z } from './zod';
4
+
1
5
  // https://vercel.com/docs/ai-gateway/provider-options
6
+ export const EVALUATION_FALLBACK_MAX_CONDITION_DEPTH = 5;
7
+ export const EVALUATION_FALLBACK_MAX_CONDITIONS_PER_LIST = 20;
8
+ export const EVALUATION_FALLBACK_MAX_QUESTION_LENGTH = 256;
9
+ export const EVALUATION_FALLBACK_MAX_MODEL_LENGTH = 256;
10
+
11
+ export const gatewayEvaluationProviderOptionsSchema = lazySchema(() =>
12
+ zodSchema(
13
+ z
14
+ .object({
15
+ models: gatewayModelFallbacksSchema.optional(),
16
+ })
17
+ .catchall(z.unknown()),
18
+ ),
19
+ );
20
+
21
+ /**
22
+ * A condition on the primary model's answers. `QUESTION_ID` narrows
23
+ * `question` to your question IDs. Groups nest at most five levels deep,
24
+ * which the SDK checks at runtime.
25
+ */
26
+ export type EvaluationFallbackCondition<QUESTION_ID extends string = string> =
27
+ | ExclusiveCondition<{ question: QUESTION_ID; confidenceBelow: number }>
28
+ | ExclusiveCondition<{
29
+ question: QUESTION_ID;
30
+ probabilityBetween: [number, number];
31
+ }>
32
+ | ExclusiveCondition<{ any: EvaluationFallbackConditionList<QUESTION_ID> }>
33
+ | ExclusiveCondition<{ all: EvaluationFallbackConditionList<QUESTION_ID> }>
34
+ | ExclusiveCondition<{
35
+ atLeast: {
36
+ count: number;
37
+ conditions: EvaluationFallbackConditionList<QUESTION_ID>;
38
+ };
39
+ }>;
40
+
41
+ export type GatewayModelFallback<QUESTION_ID extends string = string> =
42
+ | string
43
+ | {
44
+ model: string;
45
+ when: EvaluationFallbackCondition<QUESTION_ID>;
46
+ };
47
+
2
48
  export type GatewayProviderOptions = {
3
49
  /**
4
50
  * Service-owned options may be added by the Gateway without requiring an SDK
@@ -43,7 +89,11 @@ export type GatewayProviderOptions = {
43
89
  */
44
90
  idempotencyKey?: string;
45
91
 
46
- /** Array of model slugs specifying fallback models to use in order. */
92
+ /**
93
+ * Array of model slugs specifying fallback models to use in order.
94
+ * Conditional entries are only valid on evaluation requests, see
95
+ * `GatewayEvaluationProviderOptions`.
96
+ */
47
97
  models?: string[];
48
98
 
49
99
  /** Array of provider slugs that are the only ones allowed to be used. */
@@ -75,3 +125,153 @@ export type GatewayProviderOptions = {
75
125
  /** Filter to providers with zero data retention agreements. */
76
126
  zeroDataRetention?: boolean;
77
127
  };
128
+
129
+ /**
130
+ * Gateway provider options for evaluation requests. Same as
131
+ * `GatewayProviderOptions`, except `models` may start with one conditional
132
+ * `{ model, when }` entry followed by string error fallbacks.
133
+ *
134
+ * The SDK validates `models` strictly before sending the request, so new
135
+ * condition shapes need an SDK release. Other keys pass through unchanged.
136
+ */
137
+ export type GatewayEvaluationProviderOptions<
138
+ QUESTION_ID extends string = string,
139
+ > = GatewayProviderOptionsWithoutModels & {
140
+ models?: GatewayModelFallbackList<QUESTION_ID>;
141
+ };
142
+
143
+ type EvaluationFallbackConditionList<QUESTION_ID extends string> = [
144
+ EvaluationFallbackCondition<QUESTION_ID>,
145
+ ...EvaluationFallbackCondition<QUESTION_ID>[],
146
+ ];
147
+
148
+ type ConditionKey =
149
+ | 'question'
150
+ | 'confidenceBelow'
151
+ | 'probabilityBetween'
152
+ | 'any'
153
+ | 'all'
154
+ | 'atLeast';
155
+
156
+ // Rules out the other shapes' keys, so a condition can't mix two shapes.
157
+ type ExclusiveCondition<CONDITION> = CONDITION & {
158
+ [KEY in Exclude<ConditionKey, keyof CONDITION>]?: never;
159
+ };
160
+
161
+ type GatewayProviderOptionsWithoutModels = {
162
+ [KEY in keyof GatewayProviderOptions as KEY extends 'models'
163
+ ? never
164
+ : KEY]: GatewayProviderOptions[KEY];
165
+ };
166
+
167
+ type ConditionalGatewayModelFallback<QUESTION_ID extends string> = Exclude<
168
+ GatewayModelFallback<QUESTION_ID>,
169
+ string
170
+ >;
171
+
172
+ type GatewayModelFallbackList<QUESTION_ID extends string> =
173
+ | string[]
174
+ | [ConditionalGatewayModelFallback<QUESTION_ID>, ...string[]];
175
+
176
+ const probabilitySchema = z.number().finite().min(0).max(1);
177
+ const questionSchema = z
178
+ .string()
179
+ .min(1)
180
+ .max(EVALUATION_FALLBACK_MAX_QUESTION_LENGTH);
181
+ const directConditionSchema = z.union([
182
+ z
183
+ .object({
184
+ question: questionSchema,
185
+ confidenceBelow: probabilitySchema,
186
+ })
187
+ .strict(),
188
+ z
189
+ .object({
190
+ question: questionSchema,
191
+ probabilityBetween: z
192
+ .array(probabilitySchema)
193
+ .length(2)
194
+ .refine(([minimum, maximum]) => minimum <= maximum, {
195
+ message:
196
+ 'probabilityBetween minimum must be less than or equal to maximum',
197
+ }),
198
+ })
199
+ .strict(),
200
+ ]) as ZodType<EvaluationFallbackCondition>;
201
+
202
+ const groupBeyondMaxDepthSchema = z
203
+ .union([
204
+ z.object({ any: z.unknown() }),
205
+ z.object({ all: z.unknown() }),
206
+ z.object({ atLeast: z.unknown() }),
207
+ ])
208
+ .superRefine((_, context) => {
209
+ context.addIssue({
210
+ code: 'custom',
211
+ message: `conditions can be nested at most ${EVALUATION_FALLBACK_MAX_CONDITION_DEPTH} levels deep`,
212
+ });
213
+ });
214
+
215
+ const conditionalModelFallbackSchema = z
216
+ .object({
217
+ model: z.string().min(1).max(EVALUATION_FALLBACK_MAX_MODEL_LENGTH),
218
+ when: conditionSchema(1),
219
+ })
220
+ .strict();
221
+
222
+ const gatewayModelFallbacksSchema = z
223
+ .array(z.union([z.string(), conditionalModelFallbackSchema]))
224
+ .superRefine((entries, context) => {
225
+ const conditionalIndexes = entries.flatMap((entry, index) =>
226
+ typeof entry === 'string' ? [] : [index],
227
+ );
228
+ if (conditionalIndexes.length > 1) {
229
+ context.addIssue({
230
+ code: 'custom',
231
+ message: 'models supports at most one conditional evaluation fallback',
232
+ });
233
+ }
234
+ if (conditionalIndexes[0] !== undefined && conditionalIndexes[0] !== 0) {
235
+ context.addIssue({
236
+ code: 'custom',
237
+ message:
238
+ 'a conditional evaluation fallback must be the first models entry',
239
+ path: [conditionalIndexes[0]],
240
+ });
241
+ }
242
+ });
243
+
244
+ function conditionSchema(depth: number): ZodType<EvaluationFallbackCondition> {
245
+ if (depth === EVALUATION_FALLBACK_MAX_CONDITION_DEPTH) {
246
+ return z.union([
247
+ directConditionSchema,
248
+ groupBeyondMaxDepthSchema,
249
+ ]) as ZodType<EvaluationFallbackCondition>;
250
+ }
251
+
252
+ const childConditionSchema = conditionSchema(depth + 1);
253
+ const conditionListSchema = z
254
+ .array(childConditionSchema)
255
+ .min(1)
256
+ .max(EVALUATION_FALLBACK_MAX_CONDITIONS_PER_LIST);
257
+
258
+ return z.union([
259
+ directConditionSchema,
260
+ z.object({ any: conditionListSchema }).strict(),
261
+ z.object({ all: conditionListSchema }).strict(),
262
+ z
263
+ .object({
264
+ atLeast: z
265
+ .object({
266
+ count: z.number().int().min(1),
267
+ conditions: conditionListSchema,
268
+ })
269
+ .strict()
270
+ .refine(({ count, conditions }) => count <= conditions.length, {
271
+ message: 'atLeast count cannot exceed the number of conditions',
272
+ path: ['count'],
273
+ }),
274
+ })
275
+ .strict(),
276
+ ]) as ZodType<EvaluationFallbackCondition>;
277
+ }
package/src/index.ts CHANGED
@@ -47,6 +47,9 @@ export type {
47
47
  GatewayProviderMetadata,
48
48
  } from './gateway-provider-metadata';
49
49
  export type {
50
+ EvaluationFallbackCondition,
51
+ GatewayEvaluationProviderOptions,
52
+ GatewayModelFallback,
50
53
  GatewayProviderOptions,
51
54
  /** @deprecated Use `GatewayProviderOptions` instead. */
52
55
  GatewayProviderOptions as GatewayLanguageModelOptions,