@ai-sdk/gateway 4.0.94 → 4.0.96
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/CHANGELOG.md +13 -0
- package/dist/index.d.ts +45 -4
- package/dist/index.js +140 -47
- package/dist/index.js.map +1 -1
- package/docs/00-ai-gateway.mdx +63 -2
- package/package.json +1 -1
- package/src/gateway-evaluation-model.ts +16 -2
- package/src/gateway-provider-options.ts +184 -3
- package/src/index.ts +2 -0
package/docs/00-ai-gateway.mdx
CHANGED
|
@@ -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
|
|
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.
|
|
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 are 1 to 256 characters, and the conditional `model` must not be empty. 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 { GatewayProviderOptions } 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 GatewayProviderOptions,
|
|
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
|
@@ -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
|
-
...(
|
|
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,5 +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
|
|
2
|
-
export
|
|
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
|
+
|
|
10
|
+
export const gatewayEvaluationProviderOptionsSchema = lazySchema(() =>
|
|
11
|
+
zodSchema(
|
|
12
|
+
z
|
|
13
|
+
.object({
|
|
14
|
+
models: gatewayModelFallbacksSchema.optional(),
|
|
15
|
+
})
|
|
16
|
+
.catchall(z.unknown()),
|
|
17
|
+
),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A condition on the primary model's answers. `QUESTION_ID` narrows
|
|
22
|
+
* `question` to your question IDs. Groups nest at most five levels deep,
|
|
23
|
+
* which the SDK checks at runtime.
|
|
24
|
+
*/
|
|
25
|
+
export type EvaluationFallbackCondition<QUESTION_ID extends string = string> =
|
|
26
|
+
| ExclusiveCondition<{ question: QUESTION_ID; confidenceBelow: number }>
|
|
27
|
+
| ExclusiveCondition<{
|
|
28
|
+
question: QUESTION_ID;
|
|
29
|
+
probabilityBetween: [number, number];
|
|
30
|
+
}>
|
|
31
|
+
| ExclusiveCondition<{ any: EvaluationFallbackConditionList<QUESTION_ID> }>
|
|
32
|
+
| ExclusiveCondition<{ all: EvaluationFallbackConditionList<QUESTION_ID> }>
|
|
33
|
+
| ExclusiveCondition<{
|
|
34
|
+
atLeast: {
|
|
35
|
+
count: number;
|
|
36
|
+
conditions: EvaluationFallbackConditionList<QUESTION_ID>;
|
|
37
|
+
};
|
|
38
|
+
}>;
|
|
39
|
+
|
|
40
|
+
export type GatewayModelFallback<QUESTION_ID extends string = string> =
|
|
41
|
+
| string
|
|
42
|
+
| {
|
|
43
|
+
model: string;
|
|
44
|
+
when: EvaluationFallbackCondition<QUESTION_ID>;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type GatewayProviderOptions<QUESTION_ID extends string = string> = {
|
|
3
48
|
/**
|
|
4
49
|
* Service-owned options may be added by the Gateway without requiring an SDK
|
|
5
50
|
* release. The Gateway service validates and applies the runtime schema.
|
|
@@ -43,8 +88,14 @@ export type GatewayProviderOptions = {
|
|
|
43
88
|
*/
|
|
44
89
|
idempotencyKey?: string;
|
|
45
90
|
|
|
46
|
-
/**
|
|
47
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Fallback models to try in order. On evaluation requests, the first entry
|
|
93
|
+
* can be a conditional `{ model, when }` fallback that reruns the evaluation
|
|
94
|
+
* when `when` matches the primary answers. Other request types reject a
|
|
95
|
+
* conditional entry. Pass your question IDs as `QUESTION_ID` to check the
|
|
96
|
+
* `question` names in `when`.
|
|
97
|
+
*/
|
|
98
|
+
models?: GatewayModelFallbackList<QUESTION_ID>;
|
|
48
99
|
|
|
49
100
|
/** Array of provider slugs that are the only ones allowed to be used. */
|
|
50
101
|
only?: string[];
|
|
@@ -75,3 +126,133 @@ export type GatewayProviderOptions = {
|
|
|
75
126
|
/** Filter to providers with zero data retention agreements. */
|
|
76
127
|
zeroDataRetention?: boolean;
|
|
77
128
|
};
|
|
129
|
+
|
|
130
|
+
type EvaluationFallbackConditionList<QUESTION_ID extends string> = [
|
|
131
|
+
EvaluationFallbackCondition<QUESTION_ID>,
|
|
132
|
+
...EvaluationFallbackCondition<QUESTION_ID>[],
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
type ConditionKey =
|
|
136
|
+
| 'question'
|
|
137
|
+
| 'confidenceBelow'
|
|
138
|
+
| 'probabilityBetween'
|
|
139
|
+
| 'any'
|
|
140
|
+
| 'all'
|
|
141
|
+
| 'atLeast';
|
|
142
|
+
|
|
143
|
+
// Rules out the other shapes' keys, so a condition can't mix two shapes.
|
|
144
|
+
type ExclusiveCondition<CONDITION> = CONDITION & {
|
|
145
|
+
[KEY in Exclude<ConditionKey, keyof CONDITION>]?: never;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
type ConditionalGatewayModelFallback<QUESTION_ID extends string> = Exclude<
|
|
149
|
+
GatewayModelFallback<QUESTION_ID>,
|
|
150
|
+
string
|
|
151
|
+
>;
|
|
152
|
+
|
|
153
|
+
type GatewayModelFallbackList<QUESTION_ID extends string> =
|
|
154
|
+
| string[]
|
|
155
|
+
| [ConditionalGatewayModelFallback<QUESTION_ID>, ...string[]];
|
|
156
|
+
|
|
157
|
+
const probabilitySchema = z.number().finite().min(0).max(1);
|
|
158
|
+
const questionSchema = z
|
|
159
|
+
.string()
|
|
160
|
+
.min(1)
|
|
161
|
+
.max(EVALUATION_FALLBACK_MAX_QUESTION_LENGTH);
|
|
162
|
+
const directConditionSchema = z.union([
|
|
163
|
+
z
|
|
164
|
+
.object({
|
|
165
|
+
question: questionSchema,
|
|
166
|
+
confidenceBelow: probabilitySchema,
|
|
167
|
+
})
|
|
168
|
+
.strict(),
|
|
169
|
+
z
|
|
170
|
+
.object({
|
|
171
|
+
question: questionSchema,
|
|
172
|
+
probabilityBetween: z
|
|
173
|
+
.array(probabilitySchema)
|
|
174
|
+
.length(2)
|
|
175
|
+
.refine(([minimum, maximum]) => minimum <= maximum, {
|
|
176
|
+
message:
|
|
177
|
+
'probabilityBetween minimum must be less than or equal to maximum',
|
|
178
|
+
}),
|
|
179
|
+
})
|
|
180
|
+
.strict(),
|
|
181
|
+
]) as ZodType<EvaluationFallbackCondition>;
|
|
182
|
+
|
|
183
|
+
const groupBeyondMaxDepthSchema = z
|
|
184
|
+
.union([
|
|
185
|
+
z.object({ any: z.unknown() }),
|
|
186
|
+
z.object({ all: z.unknown() }),
|
|
187
|
+
z.object({ atLeast: z.unknown() }),
|
|
188
|
+
])
|
|
189
|
+
.superRefine((_, context) => {
|
|
190
|
+
context.addIssue({
|
|
191
|
+
code: 'custom',
|
|
192
|
+
message: `conditions can be nested at most ${EVALUATION_FALLBACK_MAX_CONDITION_DEPTH} levels deep`,
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const conditionalModelFallbackSchema = z
|
|
197
|
+
.object({
|
|
198
|
+
model: z.string().min(1),
|
|
199
|
+
when: conditionSchema(1),
|
|
200
|
+
})
|
|
201
|
+
.strict();
|
|
202
|
+
|
|
203
|
+
const gatewayModelFallbacksSchema = z
|
|
204
|
+
.array(z.union([z.string(), conditionalModelFallbackSchema]))
|
|
205
|
+
.superRefine((entries, context) => {
|
|
206
|
+
const conditionalIndexes = entries.flatMap((entry, index) =>
|
|
207
|
+
typeof entry === 'string' ? [] : [index],
|
|
208
|
+
);
|
|
209
|
+
if (conditionalIndexes.length > 1) {
|
|
210
|
+
context.addIssue({
|
|
211
|
+
code: 'custom',
|
|
212
|
+
message: 'models supports at most one conditional evaluation fallback',
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
if (conditionalIndexes[0] !== undefined && conditionalIndexes[0] !== 0) {
|
|
216
|
+
context.addIssue({
|
|
217
|
+
code: 'custom',
|
|
218
|
+
message:
|
|
219
|
+
'a conditional evaluation fallback must be the first models entry',
|
|
220
|
+
path: [conditionalIndexes[0]],
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
function conditionSchema(depth: number): ZodType<EvaluationFallbackCondition> {
|
|
226
|
+
if (depth === EVALUATION_FALLBACK_MAX_CONDITION_DEPTH) {
|
|
227
|
+
return z.union([
|
|
228
|
+
directConditionSchema,
|
|
229
|
+
groupBeyondMaxDepthSchema,
|
|
230
|
+
]) as ZodType<EvaluationFallbackCondition>;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const childConditionSchema = conditionSchema(depth + 1);
|
|
234
|
+
const conditionListSchema = z
|
|
235
|
+
.array(childConditionSchema)
|
|
236
|
+
.min(1)
|
|
237
|
+
.max(EVALUATION_FALLBACK_MAX_CONDITIONS_PER_LIST);
|
|
238
|
+
|
|
239
|
+
return z.union([
|
|
240
|
+
directConditionSchema,
|
|
241
|
+
z.object({ any: conditionListSchema }).strict(),
|
|
242
|
+
z.object({ all: conditionListSchema }).strict(),
|
|
243
|
+
z
|
|
244
|
+
.object({
|
|
245
|
+
atLeast: z
|
|
246
|
+
.object({
|
|
247
|
+
count: z.number().int().min(1),
|
|
248
|
+
conditions: conditionListSchema,
|
|
249
|
+
})
|
|
250
|
+
.strict()
|
|
251
|
+
.refine(({ count, conditions }) => count <= conditions.length, {
|
|
252
|
+
message: 'atLeast count cannot exceed the number of conditions',
|
|
253
|
+
path: ['count'],
|
|
254
|
+
}),
|
|
255
|
+
})
|
|
256
|
+
.strict(),
|
|
257
|
+
]) as ZodType<EvaluationFallbackCondition>;
|
|
258
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -47,6 +47,8 @@ export type {
|
|
|
47
47
|
GatewayProviderMetadata,
|
|
48
48
|
} from './gateway-provider-metadata';
|
|
49
49
|
export type {
|
|
50
|
+
EvaluationFallbackCondition,
|
|
51
|
+
GatewayModelFallback,
|
|
50
52
|
GatewayProviderOptions,
|
|
51
53
|
/** @deprecated Use `GatewayProviderOptions` instead. */
|
|
52
54
|
GatewayProviderOptions as GatewayLanguageModelOptions,
|