zuplo 6.73.16 → 6.73.18
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/docs/articles/opentelemetry.mdx +70 -0
- package/docs/articles/performance-testing.mdx +48 -0
- package/docs/articles/securing-the-gateway-with-client-mtls.mdx +8 -0
- package/docs/cli/dev.mdx +11 -0
- package/docs/policies/_index.md +2 -1
- package/docs/policies/ai-gateway-fallback-model-v2-inbound/doc.md +129 -0
- package/docs/policies/ai-gateway-fallback-model-v2-inbound/intro.md +4 -0
- package/docs/policies/ai-gateway-fallback-model-v2-inbound/schema.json +114 -0
- package/docs/policies/ai-gateway-model-filtering-v2-inbound/doc.md +148 -0
- package/docs/policies/ai-gateway-model-filtering-v2-inbound/intro.md +5 -0
- package/docs/policies/ai-gateway-model-filtering-v2-inbound/schema.json +148 -0
- package/package.json +4 -4
- package/docs/policies/ai-gateway-model-routing-v2-inbound/doc.md +0 -165
- package/docs/policies/ai-gateway-model-routing-v2-inbound/intro.md +0 -18
- package/docs/policies/ai-gateway-model-routing-v2-inbound/schema.json +0 -263
|
@@ -227,6 +227,76 @@ export function runtimeInit(runtime: RuntimeExtensions) {
|
|
|
227
227
|
}
|
|
228
228
|
```
|
|
229
229
|
|
|
230
|
+
## Performance and Sampling
|
|
231
|
+
|
|
232
|
+
Tracing isn't free. The plugin instruments every request: it opens spans for the
|
|
233
|
+
request, each policy, the handler, and any `fetch` subrequests, and holds them
|
|
234
|
+
in memory until the request finishes. Spans are exported after the response is
|
|
235
|
+
sent, so tracing adds little to any single response time, but the CPU and memory
|
|
236
|
+
it consumes per request reduce the headroom available for the next one. On
|
|
237
|
+
high-throughput APIs, that shows up as lower peak throughput and higher tail
|
|
238
|
+
latency.
|
|
239
|
+
|
|
240
|
+
For environments that need the most performance — high-RPS production traffic,
|
|
241
|
+
latency-sensitive service-to-service calls, or a
|
|
242
|
+
[load test](./performance-testing.mdx) — sample tracing down or turn it off.
|
|
243
|
+
|
|
244
|
+
### Sample a Percentage of Traces
|
|
245
|
+
|
|
246
|
+
Set `sampling.headSampler.ratio` to export a fraction of traces. The ratio
|
|
247
|
+
applies to every destination, including Zuplo's built-in storage:
|
|
248
|
+
|
|
249
|
+
```ts title="zuplo.runtime.ts"
|
|
250
|
+
import { OpenTelemetryPlugin } from "@zuplo/otel";
|
|
251
|
+
import { RuntimeExtensions } from "@zuplo/runtime";
|
|
252
|
+
|
|
253
|
+
export function runtimeInit(runtime: RuntimeExtensions) {
|
|
254
|
+
runtime.addPlugin(
|
|
255
|
+
new OpenTelemetryPlugin({
|
|
256
|
+
sampling: {
|
|
257
|
+
headSampler: {
|
|
258
|
+
ratio: 0.05, // Export 5% of traces
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
}),
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
By default, traces whose root span ends in an error are exported whatever the
|
|
267
|
+
ratio, so errors stay debuggable on a heavily sampled API. Supplying your own
|
|
268
|
+
`sampling.tailSampler` replaces that behavior.
|
|
269
|
+
|
|
270
|
+
Sampling cuts the volume of trace data exported and stored, which lowers export
|
|
271
|
+
overhead and backend cost. The per-request instrumentation still runs on every
|
|
272
|
+
request, so sampling reduces the cost of tracing rather than removing it.
|
|
273
|
+
|
|
274
|
+
### Disable Tracing
|
|
275
|
+
|
|
276
|
+
To remove the overhead entirely, don't register the plugin. Gate it on an
|
|
277
|
+
[environment variable](./environment-variables.mdx) so you can control tracing
|
|
278
|
+
per environment without changing code:
|
|
279
|
+
|
|
280
|
+
```ts title="zuplo.runtime.ts"
|
|
281
|
+
import { OpenTelemetryPlugin } from "@zuplo/otel";
|
|
282
|
+
import { RuntimeExtensions, environment } from "@zuplo/runtime";
|
|
283
|
+
|
|
284
|
+
export function runtimeInit(runtime: RuntimeExtensions) {
|
|
285
|
+
if (environment.TRACING_ENABLED === "true") {
|
|
286
|
+
runtime.addPlugin(new OpenTelemetryPlugin());
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Set `TRACING_ENABLED` to `true` on the environments you want traced and leave it
|
|
292
|
+
unset everywhere else. Tracing is off unless you opt in, so an environment built
|
|
293
|
+
for maximum throughput — or for a load test — carries no tracing overhead until
|
|
294
|
+
you ask for it.
|
|
295
|
+
|
|
296
|
+
Where the plugin doesn't run, traces stop appearing in the **Observability** tab
|
|
297
|
+
for that environment. Request analytics and logs come from a separate pipeline,
|
|
298
|
+
so they aren't affected.
|
|
299
|
+
|
|
230
300
|
## Logging
|
|
231
301
|
|
|
232
302
|
The plugin can also export logs in OpenTelemetry format. Logs are sent to your
|
|
@@ -152,6 +152,54 @@ performing complex transformations.
|
|
|
152
152
|
|
|
153
153
|
:::
|
|
154
154
|
|
|
155
|
+
## Disable Tracing for Load Tests
|
|
156
|
+
|
|
157
|
+
[OpenTelemetry tracing](./opentelemetry.mdx) instruments every request, opening
|
|
158
|
+
spans for the request, each policy, the handler, and any `fetch` subrequests.
|
|
159
|
+
Spans are exported after the response is sent, so tracing adds little to any
|
|
160
|
+
single response time, but the CPU and memory it consumes per request reduce the
|
|
161
|
+
headroom available for the next one. Under sustained load, that shows up as
|
|
162
|
+
lower peak throughput and higher tail latency.
|
|
163
|
+
|
|
164
|
+
Zuplo recommends disabling OpenTelemetry before you run a load test. Otherwise
|
|
165
|
+
you measure the gateway and the telemetry pipeline together, and the results
|
|
166
|
+
understate the gateway's capacity.
|
|
167
|
+
|
|
168
|
+
To disable tracing, remove the `OpenTelemetryPlugin` from `zuplo.runtime.ts`, or
|
|
169
|
+
gate it on an [environment variable](./environment-variables.mdx) so you can
|
|
170
|
+
control it per environment without changing code:
|
|
171
|
+
|
|
172
|
+
```ts title="zuplo.runtime.ts"
|
|
173
|
+
import { OpenTelemetryPlugin } from "@zuplo/otel";
|
|
174
|
+
import { RuntimeExtensions, environment } from "@zuplo/runtime";
|
|
175
|
+
|
|
176
|
+
export function runtimeInit(runtime: RuntimeExtensions) {
|
|
177
|
+
if (environment.TRACING_ENABLED === "true") {
|
|
178
|
+
runtime.addPlugin(new OpenTelemetryPlugin());
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Tracing runs only where you set `TRACING_ENABLED` to `true`, so leave the
|
|
184
|
+
variable unset on the environment you test against.
|
|
185
|
+
|
|
186
|
+
The same applies to any third-party logging or monitoring plugin you've added.
|
|
187
|
+
Each one adds per-request work.
|
|
188
|
+
|
|
189
|
+
:::tip
|
|
190
|
+
|
|
191
|
+
Disable tracing in production too for environments that need the most
|
|
192
|
+
performance, such as high-RPS traffic or latency-sensitive service-to-service
|
|
193
|
+
calls. Where you want to keep some visibility, sample instead of disabling — see
|
|
194
|
+
[Performance and Sampling](./opentelemetry.mdx#performance-and-sampling).
|
|
195
|
+
|
|
196
|
+
:::
|
|
197
|
+
|
|
198
|
+
If you plan to run tracing in production, run one test pass with the same
|
|
199
|
+
configuration you'll deploy, including the same sampling ratio. Comparing that
|
|
200
|
+
pass against an untraced one tells you what the telemetry costs you at your
|
|
201
|
+
target request rate.
|
|
202
|
+
|
|
155
203
|
## Performance Testing Best Practices
|
|
156
204
|
|
|
157
205
|
### 1. Choose the Right Testing Tool
|
|
@@ -15,6 +15,14 @@ the TLS handshake. Routes protected by the `mtls-auth-inbound` policy only allow
|
|
|
15
15
|
clients that present a valid certificate chain anchored by a CA you uploaded to
|
|
16
16
|
Zuplo.
|
|
17
17
|
|
|
18
|
+
:::note
|
|
19
|
+
|
|
20
|
+
Client mTLS is enforced at the Zuplo **API gateway** only. It is not available
|
|
21
|
+
on the Zuplo developer portal — the portal cannot require client certificates
|
|
22
|
+
from visitors.
|
|
23
|
+
|
|
24
|
+
:::
|
|
25
|
+
|
|
18
26
|
## How client mTLS works
|
|
19
27
|
|
|
20
28
|
When a client calls your Zuplo gateway:
|
package/docs/cli/dev.mdx
CHANGED
|
@@ -17,6 +17,17 @@ sidebar_label: dev
|
|
|
17
17
|
"hidden": true,
|
|
18
18
|
"normalize": true
|
|
19
19
|
},
|
|
20
|
+
{
|
|
21
|
+
"name": "mtls-certs",
|
|
22
|
+
"type": "string",
|
|
23
|
+
"description": "Directory containing mTLS client certificates to load for local development (layout: <dir>/<cert-name>/tls.crt and tls.key). Only supported for managed-dedicated projects. If not provided, certificates are loaded from ./.zuplo-local/mtls when that directory exists.",
|
|
24
|
+
"required": false,
|
|
25
|
+
"deprecated": false,
|
|
26
|
+
"hidden": false,
|
|
27
|
+
"alias": [
|
|
28
|
+
"mtls-certificates-dir"
|
|
29
|
+
]
|
|
30
|
+
},
|
|
20
31
|
{
|
|
21
32
|
"name": "start-editor",
|
|
22
33
|
"type": "boolean",
|
package/docs/policies/_index.md
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
| --- | --- | --- | --- |
|
|
5
5
|
| set-query-params-inbound | Add or Set Query Parameters | Adds or sets query parameters on the incoming request. | api-gateway |
|
|
6
6
|
| set-headers-inbound | Add or Set Request Headers | Adds or sets headers on the incoming request. | api-gateway |
|
|
7
|
-
| ai-gateway-model-
|
|
7
|
+
| ai-gateway-fallback-model-v2-inbound | AI Gateway Fallback Model (v2) | Adds failure and quota fallbacks to an existing AI Gateway model selection. Place this policy after AI Gateway Model Filtering (v2). It never creates a model selection, so a misplaced policy cannot bypass filtering. | ai-gateway |
|
|
8
|
+
| ai-gateway-model-filtering-v2-inbound | AI Gateway Model Filtering (v2) | Matches AI Gateway requests against curated allow lists or open block lists, then stores the winning model reference for the route handler. | ai-gateway |
|
|
8
9
|
| akamai-ai-firewall | Akamai AI Firewall | Akamai AI Firewall Inbound Policy | ai-gateway |
|
|
9
10
|
| akamai-firewall-for-ai-outbound | Akamai Firewall for AI | Inspects each upstream response with Akamai's Firewall for AI detect API and replaces the response with a `403 Forbidden` if Akamai returns a `deny` rule. Useful behind AI-powered APIs to filter unsafe completions, sensitive data exposure, and toxic content before they reach the client. The body, headers, URL, and query string sent to Akamai are configurable; by default only the response body is captured. Bodies are read from a clone so the client still receives the original. | api-gateway |
|
|
10
11
|
| akamai-firewall-for-ai-inbound | Akamai Firewall for AI | Inspects each incoming request with Akamai's Firewall for AI detect API and blocks the request if Akamai returns a `deny` rule. Useful in front of AI-powered APIs to filter prompt injection, jailbreaks, and other unsafe inputs before they reach the model. The body, headers, URL, and query string sent to Akamai are configurable; by default only the request body is captured. Bodies are read from a clone so the upstream handler still sees the original. | api-gateway |
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# AI Gateway Fallback Model (v2) Policy
|
|
2
|
+
|
|
3
|
+
Use this policy to add resilience to a model selection created by Model
|
|
4
|
+
Filtering or a custom routing policy. Place it after Model Filtering:
|
|
5
|
+
|
|
6
|
+
```text
|
|
7
|
+
Model Filtering -> Fallback Model -> AI Gateway handler
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Fallback Model never creates a primary selection. If it runs without a prior
|
|
11
|
+
selection, it logs a warning and leaves the request unchanged.
|
|
12
|
+
|
|
13
|
+
## Options
|
|
14
|
+
|
|
15
|
+
`models` must contain `completions`, `embeddings`, or both. Each configured
|
|
16
|
+
capability must set at least one of:
|
|
17
|
+
|
|
18
|
+
- `fallback`: the `providerName/model` attempted after a retryable error or
|
|
19
|
+
after the primary exceeds `fallbackTimeoutSeconds`.
|
|
20
|
+
- `quotaFallback`: the `providerName/model` selected after a
|
|
21
|
+
usage-limit-exceeded signal. This path is independent of retry and timeout
|
|
22
|
+
fallback.
|
|
23
|
+
|
|
24
|
+
`fallbackTimeoutSeconds` applies to every configured `fallback`. It defaults to
|
|
25
|
+
60 seconds and accepts values from 1 through 300.
|
|
26
|
+
|
|
27
|
+
## Cross-provider fallback example
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"name": "ai-gateway-fallback-model-v2-inbound",
|
|
32
|
+
"policyType": "ai-gateway-fallback-model-v2",
|
|
33
|
+
"handler": {
|
|
34
|
+
"export": "AIGatewayFallbackModelV2InboundPolicy",
|
|
35
|
+
"module": "$import(@zuplo/runtime)",
|
|
36
|
+
"options": {
|
|
37
|
+
"models": {
|
|
38
|
+
"completions": {
|
|
39
|
+
"fallback": "anthropic/claude-haiku-4-5",
|
|
40
|
+
"quotaFallback": "openai/gpt-4o-mini"
|
|
41
|
+
},
|
|
42
|
+
"embeddings": {
|
|
43
|
+
"fallback": "openai/text-embedding-3-small"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"fallbackTimeoutSeconds": 30
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
This configuration preserves the primary model selected earlier in the chain.
|
|
53
|
+
For completions, Anthropic becomes the retry/timeout backup and OpenAI becomes
|
|
54
|
+
the independent quota fallback. The gateway validates every reference against
|
|
55
|
+
the live provider catalog and resolves credentials when the selection is set.
|
|
56
|
+
|
|
57
|
+
## Composition behavior
|
|
58
|
+
|
|
59
|
+
Only capabilities already present in the current selection are modified.
|
|
60
|
+
Capabilities not configured here are preserved, as are fields this policy does
|
|
61
|
+
not override. A configured `fallback` replaces an existing backup and timeout; a
|
|
62
|
+
configured `quotaFallback` replaces an existing quota fallback.
|
|
63
|
+
|
|
64
|
+
Retry and timeout fallback is available for translated Chat Completions and
|
|
65
|
+
Embeddings requests. Native `/v1/messages` and `/v1/responses` requests are
|
|
66
|
+
passed through without retrying a backup. Quota fallback remains a separate,
|
|
67
|
+
metering-driven path.
|
|
68
|
+
|
|
69
|
+
## Write your own fallback policy
|
|
70
|
+
|
|
71
|
+
Fallback Model also uses the public routing primitives, so custom code can
|
|
72
|
+
choose fallbacks dynamically:
|
|
73
|
+
|
|
74
|
+
- `AIGatewayModels.load(context)` returns the cached provider catalog, including
|
|
75
|
+
each model's capability, status, and per-token pricing.
|
|
76
|
+
- `AIGatewayModelRouting.get(context)` returns the sanitized, normalized routing
|
|
77
|
+
selected earlier in the chain. It never returns provider credentials.
|
|
78
|
+
- `AIGatewayModelRouting.set(context, routing)` validates every target, resolves
|
|
79
|
+
provider credentials internally, and replaces the complete stored selection.
|
|
80
|
+
|
|
81
|
+
Because `set()` replaces rather than partially updates routing, a custom
|
|
82
|
+
fallback policy must read, merge, and set. It should leave the request unchanged
|
|
83
|
+
when no earlier policy created a selection:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
import {
|
|
87
|
+
AIGatewayModelRouting,
|
|
88
|
+
AIGatewayModels,
|
|
89
|
+
type ZuploContext,
|
|
90
|
+
type ZuploRequest,
|
|
91
|
+
} from "@zuplo/runtime";
|
|
92
|
+
|
|
93
|
+
export default async function addFallback(
|
|
94
|
+
request: ZuploRequest,
|
|
95
|
+
context: ZuploContext
|
|
96
|
+
) {
|
|
97
|
+
const routing = AIGatewayModelRouting.get(context);
|
|
98
|
+
const current = routing?.completions;
|
|
99
|
+
if (!routing || !current) {
|
|
100
|
+
return request;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const providers = await AIGatewayModels.load(context);
|
|
104
|
+
const anthropic = providers.find(
|
|
105
|
+
(provider) => provider.providerName === "anthropic"
|
|
106
|
+
);
|
|
107
|
+
const fallback = anthropic?.models.find(
|
|
108
|
+
(candidate) =>
|
|
109
|
+
candidate.capability === "completions" && candidate.status === "active"
|
|
110
|
+
);
|
|
111
|
+
if (!fallback) {
|
|
112
|
+
return request;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
await AIGatewayModelRouting.set(context, {
|
|
116
|
+
...routing,
|
|
117
|
+
completions: {
|
|
118
|
+
...(typeof current === "string" ? { main: current } : current),
|
|
119
|
+
backup: `anthropic/${fallback.model}`,
|
|
120
|
+
fallbackTimeoutSeconds: 30,
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
return request;
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Place this custom policy after Model Filtering, replacing Fallback Model in the
|
|
128
|
+
inbound chain. Keeping the no-selection guard prevents a misordered custom
|
|
129
|
+
policy from creating a primary selection and bypassing filtering.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft-07/schema",
|
|
3
|
+
"$id": "https://cdn.zuplo.com/policies/runtime/schemas/ai-gateway-fallback-model-v2-inbound.json",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"title": "AI Gateway Fallback Model (v2)",
|
|
6
|
+
"isDeprecated": false,
|
|
7
|
+
"isPaidAddOn": false,
|
|
8
|
+
"isEnterprise": false,
|
|
9
|
+
"isInternal": false,
|
|
10
|
+
"isBeta": true,
|
|
11
|
+
"isHidden": false,
|
|
12
|
+
"requiresAI": true,
|
|
13
|
+
"products": ["ai-gateway"],
|
|
14
|
+
"description": "Adds failure and quota fallbacks to an existing AI Gateway model selection.\n\nPlace this policy after AI Gateway Model Filtering (v2). It never creates a model selection, so a misplaced policy cannot bypass filtering.",
|
|
15
|
+
"deprecatedMessage": "",
|
|
16
|
+
"required": ["handler"],
|
|
17
|
+
"properties": {
|
|
18
|
+
"handler": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"default": {},
|
|
21
|
+
"required": ["export", "module", "options"],
|
|
22
|
+
"properties": {
|
|
23
|
+
"export": {
|
|
24
|
+
"const": "AIGatewayFallbackModelV2InboundPolicy",
|
|
25
|
+
"description": "The name of the exported type"
|
|
26
|
+
},
|
|
27
|
+
"module": {
|
|
28
|
+
"const": "$import(@zuplo/runtime)",
|
|
29
|
+
"description": "The module containing the policy"
|
|
30
|
+
},
|
|
31
|
+
"options": {
|
|
32
|
+
"x-zuplo-policy-type": "ai-gateway-fallback-model-v2",
|
|
33
|
+
"type": "object",
|
|
34
|
+
"title": "AIGatewayFallbackModelV2InboundPolicyOptions",
|
|
35
|
+
"description": "Options for adding failure and quota fallbacks to an existing AI Gateway model selection.",
|
|
36
|
+
"additionalProperties": false,
|
|
37
|
+
"required": ["models"],
|
|
38
|
+
"properties": {
|
|
39
|
+
"models": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"title": "Fallback Models",
|
|
42
|
+
"description": "Fallbacks grouped by AI Gateway capability.",
|
|
43
|
+
"additionalProperties": false,
|
|
44
|
+
"minProperties": 1,
|
|
45
|
+
"properties": {
|
|
46
|
+
"completions": {
|
|
47
|
+
"title": "Completions Fallbacks",
|
|
48
|
+
"type": "object",
|
|
49
|
+
"additionalProperties": false,
|
|
50
|
+
"minProperties": 1,
|
|
51
|
+
"properties": {
|
|
52
|
+
"fallback": {
|
|
53
|
+
"title": "Fallback Model",
|
|
54
|
+
"description": "The model attempted after a retryable error or timeout.",
|
|
55
|
+
"type": "string",
|
|
56
|
+
"pattern": "^[^/\\s]+/.+$"
|
|
57
|
+
},
|
|
58
|
+
"quotaFallback": {
|
|
59
|
+
"title": "Quota Fallback Model",
|
|
60
|
+
"description": "The model selected after a usage-limit-exceeded signal.",
|
|
61
|
+
"type": "string",
|
|
62
|
+
"pattern": "^[^/\\s]+/.+$"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"embeddings": {
|
|
67
|
+
"title": "Embeddings Fallbacks",
|
|
68
|
+
"type": "object",
|
|
69
|
+
"additionalProperties": false,
|
|
70
|
+
"minProperties": 1,
|
|
71
|
+
"properties": {
|
|
72
|
+
"fallback": {
|
|
73
|
+
"title": "Fallback Model",
|
|
74
|
+
"description": "The model attempted after a retryable error or timeout.",
|
|
75
|
+
"type": "string",
|
|
76
|
+
"pattern": "^[^/\\s]+/.+$"
|
|
77
|
+
},
|
|
78
|
+
"quotaFallback": {
|
|
79
|
+
"title": "Quota Fallback Model",
|
|
80
|
+
"description": "The model selected after a usage-limit-exceeded signal.",
|
|
81
|
+
"type": "string",
|
|
82
|
+
"pattern": "^[^/\\s]+/.+$"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"fallbackTimeoutSeconds": {
|
|
89
|
+
"type": "number",
|
|
90
|
+
"title": "Fallback Timeout Seconds",
|
|
91
|
+
"description": "How long a primary model may take before the failure fallback is attempted.",
|
|
92
|
+
"minimum": 1,
|
|
93
|
+
"maximum": 300,
|
|
94
|
+
"default": 60
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"examples": [
|
|
100
|
+
{
|
|
101
|
+
"export": "AIGatewayFallbackModelV2InboundPolicy",
|
|
102
|
+
"module": "$import(@zuplo/runtime)",
|
|
103
|
+
"options": {
|
|
104
|
+
"fallbackTimeoutSeconds": 60,
|
|
105
|
+
"models": {
|
|
106
|
+
"completions": {},
|
|
107
|
+
"embeddings": {}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# AI Gateway Model Filtering (v2) Policy
|
|
2
|
+
|
|
3
|
+
Use this policy to control which models clients may select on AI Gateway routes.
|
|
4
|
+
Model references use `providerName/model`; the first slash separates the
|
|
5
|
+
Provider Name configured in the Zuplo Portal from the provider-specific model
|
|
6
|
+
ID.
|
|
7
|
+
|
|
8
|
+
Place Model Filtering before Fallback Model in the inbound policy chain:
|
|
9
|
+
|
|
10
|
+
```text
|
|
11
|
+
Model Filtering -> Fallback Model -> AI Gateway handler
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Model Filtering accepts or rejects the request and creates the primary model
|
|
15
|
+
selection. Fallback Model can then enrich that allowed selection without
|
|
16
|
+
bypassing the filter.
|
|
17
|
+
|
|
18
|
+
## Options
|
|
19
|
+
|
|
20
|
+
`models` must contain `completions`, `embeddings`, or both. Each capability
|
|
21
|
+
chooses exactly one mode:
|
|
22
|
+
|
|
23
|
+
- `allowList` creates a curated catalog. Only listed models are accepted, and
|
|
24
|
+
the first entry is used when a request omits `model`.
|
|
25
|
+
- `blockList` leaves the catalog open except for named models. Every request
|
|
26
|
+
must include `model`.
|
|
27
|
+
|
|
28
|
+
Every list entry is a plain `providerName/model` string. Matching is
|
|
29
|
+
case-insensitive, while configured casing is preserved for the upstream request.
|
|
30
|
+
Route-target objects and fallback fields belong in the Fallback Model policy.
|
|
31
|
+
|
|
32
|
+
## Allow-list example
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"name": "ai-gateway-model-filtering-v2-inbound",
|
|
37
|
+
"policyType": "ai-gateway-model-filtering-v2",
|
|
38
|
+
"handler": {
|
|
39
|
+
"export": "AIGatewayModelFilteringV2InboundPolicy",
|
|
40
|
+
"module": "$import(@zuplo/runtime)",
|
|
41
|
+
"options": {
|
|
42
|
+
"models": {
|
|
43
|
+
"completions": {
|
|
44
|
+
"allowList": ["openai/gpt-4o-mini", "anthropic/claude-haiku-4-5"]
|
|
45
|
+
},
|
|
46
|
+
"embeddings": {
|
|
47
|
+
"allowList": ["openai/text-embedding-3-small"]
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Block-list example
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"models": {
|
|
60
|
+
"completions": {
|
|
61
|
+
"blockList": ["openai/deprecated-model", "anthropic/deprecated-model"]
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
In block-list mode, an unknown Provider Name is rejected even if the model is
|
|
68
|
+
not listed. Entries whose Provider Names are absent from the live catalog are
|
|
69
|
+
reported as warnings because they cannot match a request.
|
|
70
|
+
|
|
71
|
+
## Request behavior
|
|
72
|
+
|
|
73
|
+
`/v1/embeddings` uses `embeddings`; Chat Completions, Responses, and Anthropic
|
|
74
|
+
Messages use `completions`. Native routes also enforce their provider type:
|
|
75
|
+
`/v1/responses` requires an OpenAI-backed Provider Name and `/v1/messages`
|
|
76
|
+
requires an Anthropic-backed Provider Name.
|
|
77
|
+
|
|
78
|
+
A bare model name receives a 400 response. An unlisted allow-list model or a
|
|
79
|
+
blocked model receives a 403 response. If another policy has already selected a
|
|
80
|
+
model, Model Filtering leaves that selection unchanged.
|
|
81
|
+
|
|
82
|
+
## Adding fallbacks
|
|
83
|
+
|
|
84
|
+
Declare AI Gateway Fallback Model (v2) separately and place it immediately after
|
|
85
|
+
this policy. Its `fallback` handles retryable errors and timeouts;
|
|
86
|
+
`quotaFallback` handles usage-limit signals independently.
|
|
87
|
+
|
|
88
|
+
## Write your own routing policy
|
|
89
|
+
|
|
90
|
+
Everything Model Filtering does is built on two public primitives, so a custom
|
|
91
|
+
inbound policy can replace it entirely. The policy's job is to choose one
|
|
92
|
+
allowed target and store it:
|
|
93
|
+
|
|
94
|
+
- `AIGatewayModels.load(context)` returns the cached provider catalog, including
|
|
95
|
+
each model's capability, status, and per-token pricing.
|
|
96
|
+
- `AIGatewayModelRouting.set(context, routing)` validates the routing, resolves
|
|
97
|
+
provider credentials internally, and stores the selection that the AI Gateway
|
|
98
|
+
handler consumes.
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import {
|
|
102
|
+
AIGatewayModelRouting,
|
|
103
|
+
AIGatewayModels,
|
|
104
|
+
type ZuploContext,
|
|
105
|
+
type ZuploRequest,
|
|
106
|
+
} from "@zuplo/runtime";
|
|
107
|
+
|
|
108
|
+
export default async function selectModel(
|
|
109
|
+
request: ZuploRequest,
|
|
110
|
+
context: ZuploContext
|
|
111
|
+
) {
|
|
112
|
+
const providers = await AIGatewayModels.load(context);
|
|
113
|
+
const openAI = providers.find(
|
|
114
|
+
(provider) => provider.providerName === "openai"
|
|
115
|
+
);
|
|
116
|
+
const model = openAI?.models.find(
|
|
117
|
+
(candidate) =>
|
|
118
|
+
candidate.capability === "completions" && candidate.status === "active"
|
|
119
|
+
);
|
|
120
|
+
if (!model) {
|
|
121
|
+
throw new Error("No active OpenAI completions model is available");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
await AIGatewayModelRouting.set(context, {
|
|
125
|
+
completions: `openai/${model.model}`,
|
|
126
|
+
});
|
|
127
|
+
return request;
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Attach the module as an ordinary inbound policy instead of Model Filtering:
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{
|
|
135
|
+
"name": "my-model-filtering-inbound",
|
|
136
|
+
"policyType": "custom-code-inbound",
|
|
137
|
+
"handler": {
|
|
138
|
+
"export": "default",
|
|
139
|
+
"module": "$import(./modules/my-model-filtering)"
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`AIGatewayModelRouting.get(context)` returns the sanitized, normalized routing
|
|
145
|
+
for the current request and never returns credentials. The AI Gateway handler
|
|
146
|
+
consumes a custom selection even when Model Filtering is not attached. Prefer
|
|
147
|
+
one policy that creates the primary selection: Model Filtering preserves an
|
|
148
|
+
existing selection, while a later custom policy can replace it.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
AI Gateway Model Filtering (v2) controls which `providerName/model` references
|
|
2
|
+
clients may select. Use an `allowList` for a curated catalog with a default, or
|
|
3
|
+
a `blockList` for an open catalog with explicit exclusions. Put this policy
|
|
4
|
+
before AI Gateway Fallback Model (v2), which adds fallback behavior only after a
|
|
5
|
+
primary model has passed filtering.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft-07/schema",
|
|
3
|
+
"$id": "https://cdn.zuplo.com/policies/runtime/schemas/ai-gateway-model-filtering-v2-inbound.json",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"title": "AI Gateway Model Filtering (v2)",
|
|
6
|
+
"isDeprecated": false,
|
|
7
|
+
"isPaidAddOn": false,
|
|
8
|
+
"isEnterprise": false,
|
|
9
|
+
"isInternal": false,
|
|
10
|
+
"isBeta": true,
|
|
11
|
+
"isHidden": false,
|
|
12
|
+
"requiresAI": true,
|
|
13
|
+
"products": ["ai-gateway"],
|
|
14
|
+
"description": "Matches AI Gateway requests against curated allow lists or open block lists, then stores the winning model reference for the route handler.",
|
|
15
|
+
"deprecatedMessage": "",
|
|
16
|
+
"required": ["handler"],
|
|
17
|
+
"properties": {
|
|
18
|
+
"handler": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"default": {},
|
|
21
|
+
"required": ["export", "module", "options"],
|
|
22
|
+
"properties": {
|
|
23
|
+
"export": {
|
|
24
|
+
"const": "AIGatewayModelFilteringV2InboundPolicy",
|
|
25
|
+
"description": "The name of the exported type"
|
|
26
|
+
},
|
|
27
|
+
"module": {
|
|
28
|
+
"const": "$import(@zuplo/runtime)",
|
|
29
|
+
"description": "The module containing the policy"
|
|
30
|
+
},
|
|
31
|
+
"options": {
|
|
32
|
+
"x-zuplo-policy-type": "ai-gateway-model-filtering-v2",
|
|
33
|
+
"type": "object",
|
|
34
|
+
"title": "AIGatewayModelFilteringV2InboundPolicyOptions",
|
|
35
|
+
"description": "Options for allowing or blocking providerName/model references for each AI Gateway capability.",
|
|
36
|
+
"additionalProperties": false,
|
|
37
|
+
"required": ["models"],
|
|
38
|
+
"properties": {
|
|
39
|
+
"models": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"title": "Models",
|
|
42
|
+
"description": "Model filtering rules grouped by AI Gateway capability.",
|
|
43
|
+
"additionalProperties": false,
|
|
44
|
+
"minProperties": 1,
|
|
45
|
+
"properties": {
|
|
46
|
+
"completions": {
|
|
47
|
+
"title": "Completions Models",
|
|
48
|
+
"description": "Rules for chat completions, Responses, and Anthropic Messages requests.",
|
|
49
|
+
"oneOf": [
|
|
50
|
+
{
|
|
51
|
+
"type": "object",
|
|
52
|
+
"additionalProperties": false,
|
|
53
|
+
"required": ["allowList"],
|
|
54
|
+
"properties": {
|
|
55
|
+
"allowList": {
|
|
56
|
+
"type": "array",
|
|
57
|
+
"title": "Allowed Models",
|
|
58
|
+
"description": "Models clients may select; the first entry is the default when model is omitted.",
|
|
59
|
+
"minItems": 1,
|
|
60
|
+
"items": {
|
|
61
|
+
"type": "string",
|
|
62
|
+
"title": "Provider and Model",
|
|
63
|
+
"description": "A model reference in providerName/model form, split on the first slash.",
|
|
64
|
+
"pattern": "^[^/\\s]+/.+$"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"type": "object",
|
|
71
|
+
"additionalProperties": false,
|
|
72
|
+
"required": ["blockList"],
|
|
73
|
+
"properties": {
|
|
74
|
+
"blockList": {
|
|
75
|
+
"type": "array",
|
|
76
|
+
"title": "Blocked Models",
|
|
77
|
+
"description": "Models rejected in open-but-filtered mode.",
|
|
78
|
+
"minItems": 1,
|
|
79
|
+
"items": {
|
|
80
|
+
"type": "string",
|
|
81
|
+
"title": "Provider and Model",
|
|
82
|
+
"description": "A model reference in providerName/model form, split on the first slash.",
|
|
83
|
+
"pattern": "^[^/\\s]+/.+$"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
},
|
|
90
|
+
"embeddings": {
|
|
91
|
+
"title": "Embeddings Models",
|
|
92
|
+
"description": "Rules for embedding requests.",
|
|
93
|
+
"oneOf": [
|
|
94
|
+
{
|
|
95
|
+
"type": "object",
|
|
96
|
+
"additionalProperties": false,
|
|
97
|
+
"required": ["allowList"],
|
|
98
|
+
"properties": {
|
|
99
|
+
"allowList": {
|
|
100
|
+
"type": "array",
|
|
101
|
+
"title": "Allowed Models",
|
|
102
|
+
"minItems": 1,
|
|
103
|
+
"items": {
|
|
104
|
+
"type": "string",
|
|
105
|
+
"title": "Provider and Model",
|
|
106
|
+
"description": "A model reference in providerName/model form, split on the first slash.",
|
|
107
|
+
"pattern": "^[^/\\s]+/.+$"
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"type": "object",
|
|
114
|
+
"additionalProperties": false,
|
|
115
|
+
"required": ["blockList"],
|
|
116
|
+
"properties": {
|
|
117
|
+
"blockList": {
|
|
118
|
+
"type": "array",
|
|
119
|
+
"title": "Blocked Models",
|
|
120
|
+
"minItems": 1,
|
|
121
|
+
"items": {
|
|
122
|
+
"type": "string",
|
|
123
|
+
"title": "Provider and Model",
|
|
124
|
+
"description": "A model reference in providerName/model form, split on the first slash.",
|
|
125
|
+
"pattern": "^[^/\\s]+/.+$"
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
"examples": [
|
|
138
|
+
{
|
|
139
|
+
"export": "AIGatewayModelFilteringV2InboundPolicy",
|
|
140
|
+
"module": "$import(@zuplo/runtime)",
|
|
141
|
+
"options": {
|
|
142
|
+
"models": {}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
]
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zuplo",
|
|
3
|
-
"version": "6.73.
|
|
3
|
+
"version": "6.73.18",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The programmable API Gateway",
|
|
6
6
|
"author": "Zuplo, Inc.",
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
"zuplo": "zuplo.js"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@zuplo/cli": "6.73.
|
|
23
|
-
"@zuplo/core": "6.73.
|
|
24
|
-
"@zuplo/runtime": "6.73.
|
|
22
|
+
"@zuplo/cli": "6.73.18",
|
|
23
|
+
"@zuplo/core": "6.73.18",
|
|
24
|
+
"@zuplo/runtime": "6.73.18",
|
|
25
25
|
"@zuplo/test": "1.4.0"
|
|
26
26
|
}
|
|
27
27
|
}
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
# AI Gateway Model Routing (v2) Policy
|
|
2
|
-
|
|
3
|
-
The AI Gateway Model Routing (v2) policy applies declarative allow or block
|
|
4
|
-
rules to AI requests. The Provider Name configured in the Zuplo Portal is its
|
|
5
|
-
routing address: configured targets use `providerName/model` form and split on
|
|
6
|
-
the first slash, so provider-specific model IDs can contain additional slashes.
|
|
7
|
-
|
|
8
|
-
## Configuration
|
|
9
|
-
|
|
10
|
-
Set a non-empty `models` object containing `completions`, `embeddings`, or both.
|
|
11
|
-
Each capability uses exactly one mode:
|
|
12
|
-
|
|
13
|
-
- `allowList` is curated mode. Only listed `main` models are accepted, entry 0
|
|
14
|
-
is the default when the request omits `model`, and entries can include
|
|
15
|
-
`backup`, `fallbackTimeoutSeconds`, and `quotaFallback`.
|
|
16
|
-
- `blockList` is open-but-filtered mode. Any unblocked target can be requested,
|
|
17
|
-
but every request must include `model`; this mode has no defaults or
|
|
18
|
-
fallbacks.
|
|
19
|
-
|
|
20
|
-
Because Responses management operations such as `GET /v1/responses/{id}` and
|
|
21
|
-
`DELETE /v1/responses/{id}` have no request body, attach curated `allowList`
|
|
22
|
-
rules (or a custom routing policy that supplies an OpenAI selection) to those
|
|
23
|
-
operations. A `blockList` policy cannot select credentials for a bodyless
|
|
24
|
-
request because that mode intentionally has no default.
|
|
25
|
-
|
|
26
|
-
An allow-list entry can be a string shorthand or a full target object:
|
|
27
|
-
|
|
28
|
-
```json
|
|
29
|
-
{
|
|
30
|
-
"name": "ai-gateway-model-routing-v2-inbound",
|
|
31
|
-
"policyType": "ai-gateway-model-routing-v2",
|
|
32
|
-
"handler": {
|
|
33
|
-
"export": "AIGatewayModelRoutingV2InboundPolicy",
|
|
34
|
-
"module": "$import(@zuplo/runtime)",
|
|
35
|
-
"options": {
|
|
36
|
-
"models": {
|
|
37
|
-
"completions": {
|
|
38
|
-
"allowList": [
|
|
39
|
-
"openai/gpt-5",
|
|
40
|
-
{
|
|
41
|
-
"main": "anthropic/claude-sonnet-4-6",
|
|
42
|
-
"backup": "openai/gpt-5-mini",
|
|
43
|
-
"fallbackTimeoutSeconds": 30,
|
|
44
|
-
"quotaFallback": "anthropic/claude-haiku-4-5"
|
|
45
|
-
}
|
|
46
|
-
]
|
|
47
|
-
},
|
|
48
|
-
"embeddings": {
|
|
49
|
-
"blockList": ["openai/legacy-embedding-model"]
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
The first slash separates the Provider Name from the model. Model IDs may
|
|
58
|
-
contain additional slashes, such as
|
|
59
|
-
`my-fireworks/accounts/fireworks/models/llama-v3`. Provider Name and model
|
|
60
|
-
matching is case-insensitive. In curated mode the configured casing is preserved
|
|
61
|
-
when the request is sent upstream.
|
|
62
|
-
|
|
63
|
-
## Request behavior
|
|
64
|
-
|
|
65
|
-
Every non-empty request model must use `providerName/model`. A bare model name
|
|
66
|
-
receives an actionable 400 response. When the model is omitted, curated mode
|
|
67
|
-
uses allow-list entry zero; block-list mode rejects the request because it has
|
|
68
|
-
no default.
|
|
69
|
-
|
|
70
|
-
`/v1/embeddings` uses the `embeddings` rules; Chat Completions, Responses, and
|
|
71
|
-
Anthropic Messages use `completions` rules. The first slash separates the
|
|
72
|
-
Provider Name, so model IDs containing slashes remain intact after that
|
|
73
|
-
separator.
|
|
74
|
-
|
|
75
|
-
The pass-through routes are provider-native: `/v1/messages` accepts Anthropic
|
|
76
|
-
targets and `/v1/responses` accepts OpenAI targets. A request that explicitly
|
|
77
|
-
selects the wrong provider receives a 400 response. An incompatible entry-0
|
|
78
|
-
default is a policy configuration error.
|
|
79
|
-
|
|
80
|
-
## Fallback behavior
|
|
81
|
-
|
|
82
|
-
When `backup` is configured, the gateway fails over after a retryable error or
|
|
83
|
-
after `fallbackTimeoutSeconds` (60 seconds by default). The timeout must be from
|
|
84
|
-
1 through 300 seconds. `quotaFallback` independently selects another model after
|
|
85
|
-
a usage-limit-exceeded signal. Fallbacks do not chain through another allow-list
|
|
86
|
-
entry.
|
|
87
|
-
|
|
88
|
-
Retry and timeout backups apply to translated Chat Completions and Embeddings
|
|
89
|
-
requests. Native `/v1/messages` and `/v1/responses` requests are passed through
|
|
90
|
-
without retrying a `backup`; `AIGatewayModelRouting.set` still validates every
|
|
91
|
-
configured target and credential eagerly. A same-native-provider `quotaFallback`
|
|
92
|
-
can still serve those routes after a usage-limit-exceeded signal.
|
|
93
|
-
|
|
94
|
-
## Write your own routing policy
|
|
95
|
-
|
|
96
|
-
Everything this policy does is built on two public primitives, so a custom
|
|
97
|
-
inbound policy can replace it entirely. The policy's job is to collapse its
|
|
98
|
-
allow list to one winning target and store it; custom code makes the same call
|
|
99
|
-
with whatever logic it wants:
|
|
100
|
-
|
|
101
|
-
- `AIGatewayModels.load(context)` returns the cached provider catalog, including
|
|
102
|
-
each model's capability, status, and per-token pricing.
|
|
103
|
-
- `AIGatewayModelRouting.set(context, routing)` validates the routing, resolves
|
|
104
|
-
provider credentials internally, and stores the selection that the AI Gateway
|
|
105
|
-
handler consumes.
|
|
106
|
-
|
|
107
|
-
A per-capability target is the same `AIGatewayRouteTarget` shape used by
|
|
108
|
-
`allowList` entries: a `"providerName/model"` string, or an object with `main`
|
|
109
|
-
and optional `backup`, `fallbackTimeoutSeconds`, and `quotaFallback`.
|
|
110
|
-
|
|
111
|
-
```typescript
|
|
112
|
-
import {
|
|
113
|
-
AIGatewayModelRouting,
|
|
114
|
-
AIGatewayModels,
|
|
115
|
-
type ZuploContext,
|
|
116
|
-
type ZuploRequest,
|
|
117
|
-
} from "@zuplo/runtime";
|
|
118
|
-
|
|
119
|
-
export default async function routeModel(
|
|
120
|
-
request: ZuploRequest,
|
|
121
|
-
context: ZuploContext
|
|
122
|
-
) {
|
|
123
|
-
const providers = await AIGatewayModels.load(context);
|
|
124
|
-
const openAI = providers.find(
|
|
125
|
-
(provider) => provider.providerName === "openai"
|
|
126
|
-
);
|
|
127
|
-
const model = openAI?.models.find(
|
|
128
|
-
(candidate) =>
|
|
129
|
-
candidate.capability === "completions" && candidate.status === "active"
|
|
130
|
-
);
|
|
131
|
-
if (!model) {
|
|
132
|
-
throw new Error("No active OpenAI completions model is available");
|
|
133
|
-
}
|
|
134
|
-
await AIGatewayModelRouting.set(context, {
|
|
135
|
-
completions: {
|
|
136
|
-
main: `openai/${model.model}`,
|
|
137
|
-
backup: "anthropic/claude-sonnet-4-6",
|
|
138
|
-
fallbackTimeoutSeconds: 30,
|
|
139
|
-
},
|
|
140
|
-
});
|
|
141
|
-
return request;
|
|
142
|
-
}
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
Attach the module as an ordinary inbound policy on the AI Gateway route instead
|
|
146
|
-
of (or before) the built-in model-routing policy:
|
|
147
|
-
|
|
148
|
-
```json
|
|
149
|
-
{
|
|
150
|
-
"name": "my-model-routing-inbound",
|
|
151
|
-
"policyType": "custom-code-inbound",
|
|
152
|
-
"handler": {
|
|
153
|
-
"export": "default",
|
|
154
|
-
"module": "$import(./modules/my-model-routing)"
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
`AIGatewayModelRouting.get(context)` returns the sanitized, normalized routing
|
|
160
|
-
set for the current request; it never returns credentials. The AI Gateway
|
|
161
|
-
handler consumes this selection even when the built-in model-routing policy is
|
|
162
|
-
not attached to the route. Prefer one routing policy per route. If you compose
|
|
163
|
-
policies intentionally, a custom policy before the built-in policy wins because
|
|
164
|
-
the built-in policy preserves an existing selection. A custom policy after it
|
|
165
|
-
can replace an allowed selection, while a built-in rejection stops the chain.
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
The AI Gateway Model Routing (v2) policy selects models with either a curated
|
|
2
|
-
`allowList` or an open-but-filtered `blockList`. Allow-list entries accept the
|
|
3
|
-
string shorthand `"providerName/model"` or an object with `main`, `backup`,
|
|
4
|
-
`fallbackTimeoutSeconds`, and `quotaFallback`; entry zero is the default when a
|
|
5
|
-
curated request omits `model`, while block-list mode requires every request to
|
|
6
|
-
name a model.
|
|
7
|
-
|
|
8
|
-
Request model references use `providerName/model`, split on the first slash,
|
|
9
|
-
match provider and model case-insensitively, and preserve configured casing
|
|
10
|
-
upstream. A non-empty request model without a Provider Name is rejected with an
|
|
11
|
-
actionable 400. Bodyless Responses management operations need curated or custom
|
|
12
|
-
routing because block-list mode has no default. Retry backups apply to
|
|
13
|
-
translated Chat Completions and Embeddings, not native pass-through routes;
|
|
14
|
-
quota fallbacks remain independent and fallbacks do not chain.
|
|
15
|
-
|
|
16
|
-
Custom inbound policies can inspect `AIGatewayModels.load(context)` and call
|
|
17
|
-
`AIGatewayModelRouting.set(context, routing)` to select the same target without
|
|
18
|
-
attaching the built-in policy. This policy is in beta.
|
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://json-schema.org/draft-07/schema",
|
|
3
|
-
"$id": "https://cdn.zuplo.com/policies/runtime/schemas/ai-gateway-model-routing-v2-inbound.json",
|
|
4
|
-
"type": "object",
|
|
5
|
-
"title": "AI Gateway Model Routing (v2)",
|
|
6
|
-
"isDeprecated": false,
|
|
7
|
-
"isPaidAddOn": false,
|
|
8
|
-
"isEnterprise": false,
|
|
9
|
-
"isInternal": false,
|
|
10
|
-
"isBeta": true,
|
|
11
|
-
"isHidden": false,
|
|
12
|
-
"requiresAI": true,
|
|
13
|
-
"products": ["ai-gateway"],
|
|
14
|
-
"description": "Matches AI Gateway requests against curated allow lists or open block lists, then stores the winning `AIGatewayRouteTarget` for the route handler.",
|
|
15
|
-
"deprecatedMessage": "",
|
|
16
|
-
"required": ["handler"],
|
|
17
|
-
"properties": {
|
|
18
|
-
"handler": {
|
|
19
|
-
"type": "object",
|
|
20
|
-
"default": {},
|
|
21
|
-
"required": ["export", "module", "options"],
|
|
22
|
-
"properties": {
|
|
23
|
-
"export": {
|
|
24
|
-
"const": "AIGatewayModelRoutingV2InboundPolicy",
|
|
25
|
-
"description": "The name of the exported type"
|
|
26
|
-
},
|
|
27
|
-
"module": {
|
|
28
|
-
"const": "$import(@zuplo/runtime)",
|
|
29
|
-
"description": "The module containing the policy"
|
|
30
|
-
},
|
|
31
|
-
"options": {
|
|
32
|
-
"type": "object",
|
|
33
|
-
"title": "AIGatewayModelRoutingV2InboundPolicyOptions",
|
|
34
|
-
"description": "Options for allowing or blocking providerName/model routes for each AI Gateway capability.",
|
|
35
|
-
"additionalProperties": false,
|
|
36
|
-
"required": ["models"],
|
|
37
|
-
"properties": {
|
|
38
|
-
"models": {
|
|
39
|
-
"type": "object",
|
|
40
|
-
"title": "Models",
|
|
41
|
-
"description": "Declarative model-routing rules, grouped by AI Gateway capability.",
|
|
42
|
-
"additionalProperties": false,
|
|
43
|
-
"minProperties": 1,
|
|
44
|
-
"properties": {
|
|
45
|
-
"completions": {
|
|
46
|
-
"title": "Completions Models",
|
|
47
|
-
"description": "Rules for chat completions, Responses, and Anthropic Messages requests.",
|
|
48
|
-
"allOf": [
|
|
49
|
-
{
|
|
50
|
-
"title": "Capability Model Rules",
|
|
51
|
-
"description": "Choose curated allow-list mode or open-but-filtered block-list mode.",
|
|
52
|
-
"oneOf": [
|
|
53
|
-
{
|
|
54
|
-
"type": "object",
|
|
55
|
-
"title": "Curated Model Rules",
|
|
56
|
-
"description": "Allow only listed targets and use entry zero as the default.",
|
|
57
|
-
"additionalProperties": false,
|
|
58
|
-
"required": ["allowList"],
|
|
59
|
-
"not": {
|
|
60
|
-
"required": ["blockList"]
|
|
61
|
-
},
|
|
62
|
-
"properties": {
|
|
63
|
-
"allowList": {
|
|
64
|
-
"type": "array",
|
|
65
|
-
"title": "Allowed Models",
|
|
66
|
-
"description": "Curated targets clients may select; the first entry is the default when model is omitted.",
|
|
67
|
-
"minItems": 1,
|
|
68
|
-
"items": {
|
|
69
|
-
"title": "Route Target",
|
|
70
|
-
"description": "A providerName/model string shorthand or a full model route target.",
|
|
71
|
-
"oneOf": [
|
|
72
|
-
{
|
|
73
|
-
"type": "string",
|
|
74
|
-
"title": "Provider and Model",
|
|
75
|
-
"description": "A model reference in providerName/model form, split on the first slash.",
|
|
76
|
-
"pattern": "^[^/\\s]+/.+$"
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
"type": "object",
|
|
80
|
-
"title": "Model Route Target",
|
|
81
|
-
"description": "A primary model and optional failure or quota fallbacks.",
|
|
82
|
-
"additionalProperties": false,
|
|
83
|
-
"required": ["main"],
|
|
84
|
-
"properties": {
|
|
85
|
-
"main": {
|
|
86
|
-
"type": "string",
|
|
87
|
-
"title": "Provider and Model",
|
|
88
|
-
"description": "A model reference in providerName/model form, split on the first slash.",
|
|
89
|
-
"pattern": "^[^/\\s]+/.+$"
|
|
90
|
-
},
|
|
91
|
-
"backup": {
|
|
92
|
-
"type": "string",
|
|
93
|
-
"title": "Provider and Model",
|
|
94
|
-
"description": "A model reference in providerName/model form, split on the first slash.",
|
|
95
|
-
"pattern": "^[^/\\s]+/.+$"
|
|
96
|
-
},
|
|
97
|
-
"fallbackTimeoutSeconds": {
|
|
98
|
-
"type": "number",
|
|
99
|
-
"title": "Fallback Timeout Seconds",
|
|
100
|
-
"description": "How long the main model may take before the backup is attempted.",
|
|
101
|
-
"minimum": 1,
|
|
102
|
-
"maximum": 300,
|
|
103
|
-
"default": 60
|
|
104
|
-
},
|
|
105
|
-
"quotaFallback": {
|
|
106
|
-
"type": "string",
|
|
107
|
-
"title": "Provider and Model",
|
|
108
|
-
"description": "A model reference in providerName/model form, split on the first slash.",
|
|
109
|
-
"pattern": "^[^/\\s]+/.+$"
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
]
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
"type": "object",
|
|
120
|
-
"title": "Open-but-Filtered Model Rules",
|
|
121
|
-
"description": "Allow any requested target except those explicitly blocked.",
|
|
122
|
-
"additionalProperties": false,
|
|
123
|
-
"required": ["blockList"],
|
|
124
|
-
"not": {
|
|
125
|
-
"required": ["allowList"]
|
|
126
|
-
},
|
|
127
|
-
"properties": {
|
|
128
|
-
"blockList": {
|
|
129
|
-
"type": "array",
|
|
130
|
-
"title": "Blocked Models",
|
|
131
|
-
"description": "Configured providerName/model targets rejected in open-but-filtered mode.",
|
|
132
|
-
"minItems": 1,
|
|
133
|
-
"items": {
|
|
134
|
-
"type": "string",
|
|
135
|
-
"title": "Provider and Model",
|
|
136
|
-
"description": "A model reference in providerName/model form, split on the first slash.",
|
|
137
|
-
"pattern": "^[^/\\s]+/.+$"
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
]
|
|
143
|
-
}
|
|
144
|
-
]
|
|
145
|
-
},
|
|
146
|
-
"embeddings": {
|
|
147
|
-
"title": "Embeddings Models",
|
|
148
|
-
"description": "Rules for embedding requests.",
|
|
149
|
-
"allOf": [
|
|
150
|
-
{
|
|
151
|
-
"title": "Capability Model Rules",
|
|
152
|
-
"description": "Choose curated allow-list mode or open-but-filtered block-list mode.",
|
|
153
|
-
"oneOf": [
|
|
154
|
-
{
|
|
155
|
-
"type": "object",
|
|
156
|
-
"title": "Curated Model Rules",
|
|
157
|
-
"description": "Allow only listed targets and use entry zero as the default.",
|
|
158
|
-
"additionalProperties": false,
|
|
159
|
-
"required": ["allowList"],
|
|
160
|
-
"not": {
|
|
161
|
-
"required": ["blockList"]
|
|
162
|
-
},
|
|
163
|
-
"properties": {
|
|
164
|
-
"allowList": {
|
|
165
|
-
"type": "array",
|
|
166
|
-
"title": "Allowed Models",
|
|
167
|
-
"description": "Curated targets clients may select; the first entry is the default when model is omitted.",
|
|
168
|
-
"minItems": 1,
|
|
169
|
-
"items": {
|
|
170
|
-
"title": "Route Target",
|
|
171
|
-
"description": "A providerName/model string shorthand or a full model route target.",
|
|
172
|
-
"oneOf": [
|
|
173
|
-
{
|
|
174
|
-
"type": "string",
|
|
175
|
-
"title": "Provider and Model",
|
|
176
|
-
"description": "A model reference in providerName/model form, split on the first slash.",
|
|
177
|
-
"pattern": "^[^/\\s]+/.+$"
|
|
178
|
-
},
|
|
179
|
-
{
|
|
180
|
-
"type": "object",
|
|
181
|
-
"title": "Model Route Target",
|
|
182
|
-
"description": "A primary model and optional failure or quota fallbacks.",
|
|
183
|
-
"additionalProperties": false,
|
|
184
|
-
"required": ["main"],
|
|
185
|
-
"properties": {
|
|
186
|
-
"main": {
|
|
187
|
-
"type": "string",
|
|
188
|
-
"title": "Provider and Model",
|
|
189
|
-
"description": "A model reference in providerName/model form, split on the first slash.",
|
|
190
|
-
"pattern": "^[^/\\s]+/.+$"
|
|
191
|
-
},
|
|
192
|
-
"backup": {
|
|
193
|
-
"type": "string",
|
|
194
|
-
"title": "Provider and Model",
|
|
195
|
-
"description": "A model reference in providerName/model form, split on the first slash.",
|
|
196
|
-
"pattern": "^[^/\\s]+/.+$"
|
|
197
|
-
},
|
|
198
|
-
"fallbackTimeoutSeconds": {
|
|
199
|
-
"type": "number",
|
|
200
|
-
"title": "Fallback Timeout Seconds",
|
|
201
|
-
"description": "How long the main model may take before the backup is attempted.",
|
|
202
|
-
"minimum": 1,
|
|
203
|
-
"maximum": 300,
|
|
204
|
-
"default": 60
|
|
205
|
-
},
|
|
206
|
-
"quotaFallback": {
|
|
207
|
-
"type": "string",
|
|
208
|
-
"title": "Provider and Model",
|
|
209
|
-
"description": "A model reference in providerName/model form, split on the first slash.",
|
|
210
|
-
"pattern": "^[^/\\s]+/.+$"
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
]
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
},
|
|
219
|
-
{
|
|
220
|
-
"type": "object",
|
|
221
|
-
"title": "Open-but-Filtered Model Rules",
|
|
222
|
-
"description": "Allow any requested target except those explicitly blocked.",
|
|
223
|
-
"additionalProperties": false,
|
|
224
|
-
"required": ["blockList"],
|
|
225
|
-
"not": {
|
|
226
|
-
"required": ["allowList"]
|
|
227
|
-
},
|
|
228
|
-
"properties": {
|
|
229
|
-
"blockList": {
|
|
230
|
-
"type": "array",
|
|
231
|
-
"title": "Blocked Models",
|
|
232
|
-
"description": "Configured providerName/model targets rejected in open-but-filtered mode.",
|
|
233
|
-
"minItems": 1,
|
|
234
|
-
"items": {
|
|
235
|
-
"type": "string",
|
|
236
|
-
"title": "Provider and Model",
|
|
237
|
-
"description": "A model reference in providerName/model form, split on the first slash.",
|
|
238
|
-
"pattern": "^[^/\\s]+/.+$"
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
]
|
|
244
|
-
}
|
|
245
|
-
]
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
},
|
|
252
|
-
"examples": [
|
|
253
|
-
{
|
|
254
|
-
"export": "AIGatewayModelRoutingV2InboundPolicy",
|
|
255
|
-
"module": "$import(@zuplo/runtime)",
|
|
256
|
-
"options": {
|
|
257
|
-
"models": {}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
]
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|