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.
@@ -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",
@@ -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-routing-v2-inbound | AI Gateway Model Routing (v2) | Matches AI Gateway requests against curated allow lists or open block lists, then stores the winning `AIGatewayRouteTarget` for the route handler. | ai-gateway |
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,4 @@
1
+ AI Gateway Fallback Model (v2) adds retry, timeout, and quota fallbacks to an
2
+ existing model selection. Place it after AI Gateway Model Filtering (v2). It
3
+ preserves the primary selection and never creates one when the policy chain is
4
+ misordered.
@@ -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.16",
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.16",
23
- "@zuplo/core": "6.73.16",
24
- "@zuplo/runtime": "6.73.16",
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
- }