zuplo 6.73.29 → 6.73.31

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.
@@ -76,17 +76,24 @@ const { text } = await generateText({
76
76
 
77
77
  ### Google
78
78
 
79
+ The `@ai-sdk/google` provider speaks Gemini's native protocol: it sends requests
80
+ to `/models/{model}:generateContent` and authenticates with the `x-goog-api-key`
81
+ header, neither of which the AI Gateway serves. Use the
82
+ [OpenAI-compatible provider](https://ai-sdk.dev/providers/openai-compatible-providers)
83
+ instead — the AI Gateway translates OpenAI-format requests to Google upstream.
84
+
79
85
  ```typescript
80
- import { createGoogleGenerativeAI } from "@ai-sdk/google";
86
+ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
81
87
  import { generateText } from "ai";
82
88
 
83
- const google = createGoogleGenerativeAI({
89
+ const gateway = createOpenAICompatible({
90
+ name: "zuplo-ai-gateway",
84
91
  apiKey: process.env.ZUPLO_AI_GATEWAY_API_KEY,
85
92
  baseURL: "https://my-ai-gateway.zuplo.app/v1",
86
93
  });
87
94
 
88
95
  const { text } = await generateText({
89
- model: google("gemini-2.5-flash"),
96
+ model: gateway("gemini-2.5-flash"),
90
97
  prompt: "Write a one-sentence bedtime story about a unicorn.",
91
98
  });
92
99
  ```
@@ -113,3 +113,20 @@ request. If the property already exists, it will be overwritten.
113
113
  The log properties will be exported in a format native to the plugin you are
114
114
  using. For example, in the case of the OpenTelemetry plugin, the properties will
115
115
  be included in the `attributes` field of the log record.
116
+
117
+ ## Performance Under High Load
118
+
119
+ Logging costs compute and memory on every request. That's rarely noticeable on a
120
+ single response, but on an API running at a high request rate it shows up as
121
+ lower peak throughput and higher tail latency.
122
+
123
+ For high-traffic APIs, and before a [load test](./performance-testing.mdx):
124
+
125
+ - Log the events you'd act on rather than everything that happens. Verbose
126
+ `debug` logging on a hot path is the usual culprit.
127
+ - Keep custom log properties small. Identifiers cost far less than whole request
128
+ or response bodies.
129
+ - Add logging plugins deliberately. Each one is another copy of every record.
130
+
131
+ [OpenTelemetry tracing](./opentelemetry.mdx) carries a similar cost and supports
132
+ [sampling](./opentelemetry.mdx#performance-and-sampling) to bring it down.
@@ -60,6 +60,9 @@ export function runtimeInit(runtime: RuntimeExtensions) {
60
60
  }
61
61
  ```
62
62
 
63
+ When you created your project, Zuplo added this plugin with a
64
+ [sampling ratio](#sample-a-percentage-of-traces) already set.
65
+
63
66
  Deploy your project and open the **Observability** tab to see traces.
64
67
 
65
68
  ![Trace visualization](../../public/media/opentelemetry/image-1.png)
@@ -237,32 +240,45 @@ it consumes per request reduce the headroom available for the next one. On
237
240
  high-throughput APIs, that shows up as lower peak throughput and higher tail
238
241
  latency.
239
242
 
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
+ Sample traces on any API that runs at a high request rate — high-RPS production
244
+ traffic, latency-sensitive service-to-service calls, or an API you're about to
245
+ [load test](./performance-testing.mdx). A ratio of `0.1`, which keeps 10% of
246
+ traces, is the standard starting point: enough traffic to see how the API
247
+ behaves, without paying the export cost on every request. Tracing every request
248
+ is a reasonable default during development and for APIs at moderate request
249
+ rates.
243
250
 
244
251
  ### Sample a Percentage of Traces
245
252
 
246
- Set `sampling.headSampler.ratio` to export a fraction of traces. The ratio
247
- applies to every destination, including Zuplo's built-in storage:
253
+ Set `sampling.headSampler.ratio` to the fraction of traces to keep. The ratio
254
+ applies to every destination, including Zuplo's built-in storage. When you
255
+ created your project, Zuplo set it up this way: every request traced in your
256
+ working copy, 10% everywhere else.
248
257
 
249
- ```ts title="zuplo.runtime.ts"
258
+ ```ts title="modules/zuplo.runtime.ts"
250
259
  import { OpenTelemetryPlugin } from "@zuplo/otel";
251
- import { RuntimeExtensions } from "@zuplo/runtime";
260
+ import { environment, RuntimeExtensions } from "@zuplo/runtime";
252
261
 
253
262
  export function runtimeInit(runtime: RuntimeExtensions) {
263
+ const isWorkingCopy = environment.ZUPLO_ENVIRONMENT_STAGE === "working-copy";
264
+
254
265
  runtime.addPlugin(
255
266
  new OpenTelemetryPlugin({
256
267
  sampling: {
257
- headSampler: {
258
- ratio: 0.05, // Export 5% of traces
259
- },
268
+ headSampler: { ratio: isWorkingCopy ? 1 : 0.1 },
260
269
  },
261
270
  }),
262
271
  );
263
272
  }
264
273
  ```
265
274
 
275
+ Set `ratio` to a constant to use one value everywhere. Zuplo populates
276
+ `ZUPLO_ENVIRONMENT_STAGE`, which reads `working-copy` in your working copy and
277
+ `production`, `preview`, or `local` elsewhere, so local development
278
+ (`zuplo dev`) samples at 10% along with deployed environments. See
279
+ [system environment variables](./environment-variables.mdx#system-environment-variables)
280
+ for the full list.
281
+
266
282
  By default, traces whose root span ends in an error are exported whatever the
267
283
  ratio, so errors stay debuggable on a heavily sampled API. Supplying your own
268
284
  `sampling.tailSampler` replaces that behavior.
@@ -273,6 +289,10 @@ request, so sampling reduces the cost of tracing rather than removing it.
273
289
 
274
290
  ### Disable Tracing
275
291
 
292
+ Sampling leaves some per-request work in place. Where you need none of it —
293
+ measuring the gateway's raw capacity, or squeezing the last of the headroom out
294
+ of a high-RPS environment — turn tracing off instead.
295
+
276
296
  To remove the overhead entirely, don't register the plugin. Gate it on an
277
297
  [environment variable](./environment-variables.mdx) so you can control tracing
278
298
  per environment without changing code:
@@ -297,6 +317,14 @@ Where the plugin doesn't run, traces stop appearing in the **Observability** tab
297
317
  for that environment. Request analytics and logs come from a separate pipeline,
298
318
  so they aren't affected.
299
319
 
320
+ ### Account for Logging Too
321
+
322
+ [Logging](./logging.mdx) costs compute and memory per request the same way
323
+ tracing does. On a high-throughput API, log the events you'd act on rather than
324
+ everything that happens, and keep custom log properties small. Sampling traces
325
+ while logging verbosely on every request moves the cost rather than removing it.
326
+ See [Performance Under High Load](./logging.mdx#performance-under-high-load).
327
+
300
328
  ## Logging
301
329
 
302
330
  The plugin can also export logs in OpenTelemetry format. Logs are sent to your
@@ -152,7 +152,7 @@ performing complex transformations.
152
152
 
153
153
  :::
154
154
 
155
- ## Disable Tracing for Load Tests
155
+ ## Sample Tracing for Load Tests and High-Traffic APIs
156
156
 
157
157
  [OpenTelemetry tracing](./opentelemetry.mdx) instruments every request, opening
158
158
  spans for the request, each policy, the handler, and any `fetch` subrequests.
@@ -161,12 +161,42 @@ single response time, but the CPU and memory it consumes per request reduce the
161
161
  headroom available for the next one. Under sustained load, that shows up as
162
162
  lower peak throughput and higher tail latency.
163
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.
164
+ Zuplo recommends sampling tracing down before you run a load test, and on any
165
+ API that serves a high request rate in production. A ratio of `0.1` keeps 10% of
166
+ traces, enough to see how the API behaves without spending most of the gateway's
167
+ headroom on telemetry:
167
168
 
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
169
+ ```ts title="zuplo.runtime.ts"
170
+ import { OpenTelemetryPlugin } from "@zuplo/otel";
171
+ import { RuntimeExtensions } from "@zuplo/runtime";
172
+
173
+ export function runtimeInit(runtime: RuntimeExtensions) {
174
+ runtime.addPlugin(
175
+ new OpenTelemetryPlugin({
176
+ sampling: {
177
+ headSampler: {
178
+ ratio: 0.1, // Keep 10% of traces
179
+ },
180
+ },
181
+ }),
182
+ );
183
+ }
184
+ ```
185
+
186
+ When you created your project, Zuplo set this ratio for everything outside the
187
+ working copy, so check `modules/zuplo.runtime.ts` before you assume your test
188
+ environment traces every request.
189
+
190
+ Test with the ratio you plan to deploy. A pass that traces every request tells
191
+ you little about an API that will run at 10%, and the reverse understates what
192
+ tracing costs you. For the full sampling reference, see
193
+ [Performance and Sampling](./opentelemetry.mdx#performance-and-sampling).
194
+
195
+ ### Turn Tracing Off to Measure Raw Capacity
196
+
197
+ Sampling lowers the cost of tracing rather than removing it. To measure what the
198
+ gateway alone can do, remove the `OpenTelemetryPlugin` from `zuplo.runtime.ts`,
199
+ or gate it on an [environment variable](./environment-variables.mdx) so you can
170
200
  control it per environment without changing code:
171
201
 
172
202
  ```ts title="zuplo.runtime.ts"
@@ -181,24 +211,16 @@ export function runtimeInit(runtime: RuntimeExtensions) {
181
211
  ```
182
212
 
183
213
  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).
214
+ variable unset on the environment you test against. Running one untraced pass
215
+ next to your sampled pass tells you what telemetry costs at your target request
216
+ rate.
195
217
 
196
- :::
218
+ ### Account for Logging Too
197
219
 
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.
220
+ The same applies to any logging or monitoring plugin you've added: each one adds
221
+ per-request work that eats into the throughput you're trying to measure. Trim
222
+ [logging](./logging.mdx) to the events you'd act on, and run the test with the
223
+ same logging configuration you plan to deploy.
202
224
 
203
225
  ## Performance Testing Best Practices
204
226
 
@@ -0,0 +1,49 @@
1
+ ---
2
+ title: Zuplo Branding
3
+ sidebar_label: Zuplo Branding
4
+ description:
5
+ Learn which plans show the "Powered by Zuplo" badge on the Developer Portal
6
+ and how to remove it.
7
+ ---
8
+
9
+ Developer Portals on Zuplo's self-serve plans display a "Powered by Zuplo"
10
+ badge. Whether the badge appears is controlled by your account's plan — it isn't
11
+ a setting in `zudoku.config.tsx`.
12
+
13
+ ## Which plans show the badge
14
+
15
+ | Plan | "Powered by Zuplo" badge |
16
+ | ------------- | ------------------------ |
17
+ | Free | Shown |
18
+ | Pay-as-you-go | Shown |
19
+ | Builder | Shown |
20
+ | Business | Shown |
21
+ | Enterprise | Hidden |
22
+
23
+ Upgrading from Free to a paid self-serve plan such as Builder doesn't remove the
24
+ badge. Removing Zuplo branding is an Enterprise entitlement.
25
+
26
+ ## Remove the badge
27
+
28
+ <EnterpriseFeature name="Developer Portal white-labeling" />
29
+
30
+ To remove Zuplo branding from your Developer Portal, contact
31
+ [sales@zuplo.com](mailto:sales@zuplo.com) or reach out to your account manager.
32
+ Your account team enables the entitlement on your account — you don't need to
33
+ change your project configuration.
34
+
35
+ :::note
36
+
37
+ The badge comes from your account's entitlements, not your portal configuration.
38
+ There's no configuration option, environment variable, or Developer API endpoint
39
+ that overrides it — editing your `zudoku.config.tsx` footer won't remove it.
40
+
41
+ :::
42
+
43
+ ## Customize the rest of the footer
44
+
45
+ The Zuplo badge is separate from the footer you control. On every plan you can
46
+ set your own columns, social links, copyright notice, and logo — see
47
+ [Footer Configuration](./zudoku/configuration/footer.mdx). To change colors,
48
+ fonts, and the overall look of the portal, see
49
+ [Colors and Theme](./zudoku/customization/colors-theme.mdx).
@@ -5,6 +5,7 @@
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
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-metering-v2-inbound | AI Gateway Metering (v2) | Meters AI Gateway v2 usage and enforces options-driven usage limits. The authentication policy must run before this policy so the app configuration id is available for meter storage and analytics. | ai-gateway |
8
9
  | 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 |
9
10
  | akamai-ai-firewall | Akamai AI Firewall | Akamai AI Firewall Inbound Policy | ai-gateway |
10
11
  | 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 |
@@ -0,0 +1,121 @@
1
+ # AI Gateway Metering (v2)
2
+
3
+ `ai-gateway-metering-v2-inbound` records AI Gateway usage and enforces daily or
4
+ monthly limits before the provider request runs. Declare the policy in
5
+ `policies.json`, then add it to an application's `inboundPolicyChain`.
6
+
7
+ ## Example
8
+
9
+ ```json
10
+ {
11
+ "name": "ai-gateway-metering-v2-inbound",
12
+ "options": {
13
+ "throwOnFailure": false,
14
+ "limits": {
15
+ "costs": {
16
+ "monthly": {
17
+ "enabled": true,
18
+ "limit": 100,
19
+ "warning": {
20
+ "enabled": true,
21
+ "threshold": 80
22
+ }
23
+ }
24
+ },
25
+ "requests": {
26
+ "daily": {
27
+ "enabled": true,
28
+ "limit": 1000
29
+ }
30
+ }
31
+ }
32
+ }
33
+ }
34
+ ```
35
+
36
+ The supported meters are `costs`, `tokens`, and `requests`. Each meter can have
37
+ `daily` and `monthly` settings. A period is enforced only when `enabled` is
38
+ `true` and `limit` is present. Usage equal to the limit is blocked.
39
+
40
+ When `throwOnFailure` is `false`, metering failures pass the request through.
41
+ Set it to `true` to fail the request instead.
42
+
43
+ ## Set limits from custom code
44
+
45
+ Use a custom inbound policy to vary limits for individual requests. For example,
46
+ the following policy gives callers with a `pro` plan a higher daily request
47
+ limit:
48
+
49
+ ```typescript
50
+ import {
51
+ AIGatewayMeteringV2InboundPolicy,
52
+ type ZuploContext,
53
+ type ZuploRequest,
54
+ } from "@zuplo/runtime";
55
+
56
+ export default function applyPlanLimit(
57
+ request: ZuploRequest,
58
+ context: ZuploContext
59
+ ) {
60
+ const dailyRequestLimit = request.user?.data.plan === "pro" ? 10_000 : 1_000;
61
+
62
+ AIGatewayMeteringV2InboundPolicy.setLimits(context, {
63
+ requests: {
64
+ daily: {
65
+ enabled: true,
66
+ limit: dailyRequestLimit,
67
+ },
68
+ },
69
+ });
70
+
71
+ return request;
72
+ }
73
+ ```
74
+
75
+ Register the module as a custom policy:
76
+
77
+ ```json
78
+ {
79
+ "name": "plan-based-ai-limits",
80
+ "policyType": "custom-code-inbound",
81
+ "handler": {
82
+ "export": "default",
83
+ "module": "$import(./modules/plan-based-ai-limits)"
84
+ }
85
+ }
86
+ ```
87
+
88
+ Place it before metering in the application's inbound policy chain:
89
+
90
+ ```json
91
+ {
92
+ "inboundPolicyChain": [
93
+ {
94
+ "name": "plan-based-ai-limits"
95
+ },
96
+ {
97
+ "name": "ai-gateway-metering-v2-inbound"
98
+ }
99
+ ]
100
+ }
101
+ ```
102
+
103
+ `setLimits` changes only the fields supplied for the current request. Other
104
+ settings from the metering policy remain unchanged. For example, setting only
105
+ `requests.daily.limit` preserves `requests.daily.enabled`, its warning settings,
106
+ and all monthly limits. It does not update the policy configuration or affect
107
+ later requests.
108
+
109
+ The exported `AIGatewayMeteringV2LimitOverrides` interface can be used when a
110
+ custom policy builds the override object separately. Its shape matches the
111
+ `limits` option. These values are called overrides because they temporarily
112
+ replace matching policy settings for the current request:
113
+
114
+ - Choose a meter: `costs`, `tokens`, or `requests`.
115
+ - Choose a period: `daily` or `monthly`.
116
+ - Set one or more values: `enabled`, `limit`, or `warning`.
117
+
118
+ If model filtering and fallback-model policies selected a `quotaFallback`, an
119
+ exceeded limit activates that fallback instead of returning 429. The fallback
120
+ request is still metered. Order the chain as model filtering, fallback model,
121
+ metering, then policies that may short-circuit such as semantic cache.
@@ -0,0 +1,292 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft-07/schema",
3
+ "$id": "https://cdn.zuplo.com/policies/runtime/schemas/ai-gateway-metering-v2-inbound.json",
4
+ "type": "object",
5
+ "title": "AI Gateway Metering (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": "Meters AI Gateway v2 usage and enforces options-driven usage limits.\n\nThe authentication policy must run before this policy so the app configuration id is available for meter storage and analytics.",
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": "AIGatewayMeteringV2InboundPolicy",
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-metering-v2",
33
+ "title": "AIGatewayMeteringV2InboundPolicyOptions",
34
+ "type": "object",
35
+ "description": "Options for metering AI Gateway v2 usage and enforcing request-time limits.",
36
+ "additionalProperties": false,
37
+ "required": [],
38
+ "properties": {
39
+ "throwOnFailure": {
40
+ "type": "boolean",
41
+ "default": false,
42
+ "description": "Throw when the metering service is unavailable instead of failing open."
43
+ },
44
+ "limits": {
45
+ "type": "object",
46
+ "description": "Usage limits grouped by meter.",
47
+ "additionalProperties": false,
48
+ "properties": {
49
+ "costs": {
50
+ "type": "object",
51
+ "minProperties": 1,
52
+ "additionalProperties": false,
53
+ "properties": {
54
+ "daily": {
55
+ "type": "object",
56
+ "additionalProperties": false,
57
+ "required": ["enabled"],
58
+ "properties": {
59
+ "enabled": {
60
+ "type": "boolean"
61
+ },
62
+ "limit": {
63
+ "type": "number",
64
+ "minimum": 0
65
+ },
66
+ "warning": {
67
+ "type": "object",
68
+ "additionalProperties": false,
69
+ "required": ["enabled"],
70
+ "properties": {
71
+ "enabled": {
72
+ "type": "boolean"
73
+ },
74
+ "threshold": {
75
+ "type": "number",
76
+ "minimum": 0,
77
+ "maximum": 100,
78
+ "description": "Percentage of the limit at which to emit a warning event."
79
+ }
80
+ }
81
+ }
82
+ }
83
+ },
84
+ "monthly": {
85
+ "type": "object",
86
+ "additionalProperties": false,
87
+ "required": ["enabled"],
88
+ "properties": {
89
+ "enabled": {
90
+ "type": "boolean"
91
+ },
92
+ "limit": {
93
+ "type": "number",
94
+ "minimum": 0
95
+ },
96
+ "warning": {
97
+ "type": "object",
98
+ "additionalProperties": false,
99
+ "required": ["enabled"],
100
+ "properties": {
101
+ "enabled": {
102
+ "type": "boolean"
103
+ },
104
+ "threshold": {
105
+ "type": "number",
106
+ "minimum": 0,
107
+ "maximum": 100,
108
+ "description": "Percentage of the limit at which to emit a warning event."
109
+ }
110
+ }
111
+ }
112
+ }
113
+ }
114
+ }
115
+ },
116
+ "tokens": {
117
+ "type": "object",
118
+ "minProperties": 1,
119
+ "additionalProperties": false,
120
+ "properties": {
121
+ "daily": {
122
+ "type": "object",
123
+ "additionalProperties": false,
124
+ "required": ["enabled"],
125
+ "properties": {
126
+ "enabled": {
127
+ "type": "boolean"
128
+ },
129
+ "limit": {
130
+ "type": "number",
131
+ "minimum": 0
132
+ },
133
+ "warning": {
134
+ "type": "object",
135
+ "additionalProperties": false,
136
+ "required": ["enabled"],
137
+ "properties": {
138
+ "enabled": {
139
+ "type": "boolean"
140
+ },
141
+ "threshold": {
142
+ "type": "number",
143
+ "minimum": 0,
144
+ "maximum": 100,
145
+ "description": "Percentage of the limit at which to emit a warning event."
146
+ }
147
+ }
148
+ }
149
+ }
150
+ },
151
+ "monthly": {
152
+ "type": "object",
153
+ "additionalProperties": false,
154
+ "required": ["enabled"],
155
+ "properties": {
156
+ "enabled": {
157
+ "type": "boolean"
158
+ },
159
+ "limit": {
160
+ "type": "number",
161
+ "minimum": 0
162
+ },
163
+ "warning": {
164
+ "type": "object",
165
+ "additionalProperties": false,
166
+ "required": ["enabled"],
167
+ "properties": {
168
+ "enabled": {
169
+ "type": "boolean"
170
+ },
171
+ "threshold": {
172
+ "type": "number",
173
+ "minimum": 0,
174
+ "maximum": 100,
175
+ "description": "Percentage of the limit at which to emit a warning event."
176
+ }
177
+ }
178
+ }
179
+ }
180
+ }
181
+ }
182
+ },
183
+ "requests": {
184
+ "type": "object",
185
+ "minProperties": 1,
186
+ "additionalProperties": false,
187
+ "properties": {
188
+ "daily": {
189
+ "type": "object",
190
+ "additionalProperties": false,
191
+ "required": ["enabled"],
192
+ "properties": {
193
+ "enabled": {
194
+ "type": "boolean"
195
+ },
196
+ "limit": {
197
+ "type": "number",
198
+ "minimum": 0
199
+ },
200
+ "warning": {
201
+ "type": "object",
202
+ "additionalProperties": false,
203
+ "required": ["enabled"],
204
+ "properties": {
205
+ "enabled": {
206
+ "type": "boolean"
207
+ },
208
+ "threshold": {
209
+ "type": "number",
210
+ "minimum": 0,
211
+ "maximum": 100,
212
+ "description": "Percentage of the limit at which to emit a warning event."
213
+ }
214
+ }
215
+ }
216
+ }
217
+ },
218
+ "monthly": {
219
+ "type": "object",
220
+ "additionalProperties": false,
221
+ "required": ["enabled"],
222
+ "properties": {
223
+ "enabled": {
224
+ "type": "boolean"
225
+ },
226
+ "limit": {
227
+ "type": "number",
228
+ "minimum": 0
229
+ },
230
+ "warning": {
231
+ "type": "object",
232
+ "additionalProperties": false,
233
+ "required": ["enabled"],
234
+ "properties": {
235
+ "enabled": {
236
+ "type": "boolean"
237
+ },
238
+ "threshold": {
239
+ "type": "number",
240
+ "minimum": 0,
241
+ "maximum": 100,
242
+ "description": "Percentage of the limit at which to emit a warning event."
243
+ }
244
+ }
245
+ }
246
+ }
247
+ }
248
+ }
249
+ }
250
+ }
251
+ }
252
+ }
253
+ }
254
+ },
255
+ "examples": [
256
+ {
257
+ "export": "AIGatewayMeteringV2InboundPolicy",
258
+ "module": "$import(@zuplo/runtime)",
259
+ "options": {
260
+ "limits": {
261
+ "costs": {
262
+ "daily": {
263
+ "warning": {}
264
+ },
265
+ "monthly": {
266
+ "warning": {}
267
+ }
268
+ },
269
+ "requests": {
270
+ "daily": {
271
+ "warning": {}
272
+ },
273
+ "monthly": {
274
+ "warning": {}
275
+ }
276
+ },
277
+ "tokens": {
278
+ "daily": {
279
+ "warning": {}
280
+ },
281
+ "monthly": {
282
+ "warning": {}
283
+ }
284
+ }
285
+ },
286
+ "throwOnFailure": false
287
+ }
288
+ }
289
+ ]
290
+ }
291
+ }
292
+ }
@@ -1,9 +1,73 @@
1
1
  # AI Gateway Model Filtering (v2) Policy
2
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.
3
+ Use this policy when an AI Gateway route must restrict which models clients may
4
+ select. The policy is optional.
5
+
6
+ Choose the setup that matches the route:
7
+
8
+ - Omit Model Filtering to let each request select any available model. Every
9
+ request must provide `model` as `providerName/model`.
10
+ - Use an `allowList` to expose a curated set of models and provide a default
11
+ model.
12
+ - Use a `blockList` to permit available models except for specific exclusions.
13
+ - Use a custom routing policy when model selection depends on request data or
14
+ application logic that an allow list or block list cannot express.
15
+
16
+ `providerName` is the Provider Name configured in the Zuplo Portal. The text
17
+ after the first slash is the provider-specific model ID, so model IDs may
18
+ contain additional slashes.
19
+
20
+ ## Routing without Model Filtering
21
+
22
+ When neither Model Filtering nor a custom routing policy selects a model, the AI
23
+ Gateway handler reads the request body's `model` and uses it as the primary
24
+ routing target:
25
+
26
+ ```json
27
+ {
28
+ "model": "openai/gpt-4o-mini",
29
+ "messages": [{ "role": "user", "content": "Hello" }]
30
+ }
31
+ ```
32
+
33
+ The handler validates that:
34
+
35
+ - `model` is a string in `providerName/model` form;
36
+ - the Provider Name is configured;
37
+ - the model is available for the route's capability;
38
+ - the Provider Assignment has usable credentials.
39
+
40
+ Missing, bare, non-string, or malformed model values receive an
41
+ OpenAI-compatible 400 `invalid_request_error`. A validly formatted target that
42
+ cannot be fulfilled is reported as a routing configuration error with a
43
+ suggested fix.
44
+
45
+ When Semantic Cache is attached, a cache hit also validates request-derived
46
+ routing before returning the cached response. A removed, deprecated, or
47
+ otherwise unavailable target therefore follows the same routing configuration
48
+ error path instead of bypassing validation with a cached response. Native route
49
+ requirements are checked too, so an Anthropic Messages cache hit cannot be
50
+ served by an OpenAI-backed Provider Name.
51
+
52
+ Attach Model Filtering only when the gateway must enforce model-selection rules.
53
+ An attached policy must always have a valid, non-empty `models` configuration.
54
+
55
+ ### Responses management operations
56
+
57
+ These Responses API operations do not have a request body:
58
+
59
+ - `GET /v1/responses/:responseId`
60
+ - `GET /v1/responses/:responseId/input_items`
61
+ - `DELETE /v1/responses/:responseId`
62
+
63
+ Because they cannot supply `model`, they require routing to be selected before
64
+ the handler runs. Configure a `completions.allowList` in Model Filtering so its
65
+ first entry supplies the default, or use a custom inbound policy that calls
66
+ `AIGatewayModelRouting.set(context, { completions: "providerName/model" })`.
67
+ Without preselected routing, the handler returns an OpenAI-compatible 400
68
+ `invalid_request_error` with this configuration guidance.
69
+
70
+ ## Policy order
7
71
 
8
72
  Place Model Filtering before Fallback Model in the inbound policy chain:
9
73
 
@@ -13,7 +77,8 @@ Model Filtering -> Fallback Model -> AI Gateway handler
13
77
 
14
78
  Model Filtering accepts or rejects the request and creates the primary model
15
79
  selection. Fallback Model can then enrich that allowed selection without
16
- bypassing the filter.
80
+ bypassing the filter. Fallback Model does not create a primary selection by
81
+ itself.
17
82
 
18
83
  ## Options
19
84
 
@@ -25,9 +90,17 @@ chooses exactly one mode:
25
90
  - `blockList` leaves the catalog open except for named models. Every request
26
91
  must include `model`.
27
92
 
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.
93
+ Each configured list must contain at least one entry. A capability cannot define
94
+ both `allowList` and `blockList`, and unsupported fields are rejected. Every
95
+ list entry is a plain `providerName/model` string. Matching is case-insensitive,
96
+ while configured casing is preserved for the upstream request. Route-target
97
+ objects and fallback fields belong in the Fallback Model policy.
98
+
99
+ Configure every capability served by a route using this policy. A route using
100
+ `/v1/embeddings` needs `embeddings`; Chat Completions, Responses, and Anthropic
101
+ Messages routes need `completions`. If the policy is attached but the active
102
+ capability has no rules, the request receives a 403 response explaining which
103
+ capability to add.
31
104
 
32
105
  ## Allow-list example
33
106
 
@@ -70,14 +143,30 @@ reported as warnings because they cannot match a request.
70
143
 
71
144
  ## Request behavior
72
145
 
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.
146
+ | Situation | Result |
147
+ | --------------------------------------------------- | ------------------------------------------------------ |
148
+ | Allow list, request omits `model` | The first allow-list entry is selected. |
149
+ | Allow list, request names a listed model | The matching configured entry is selected. |
150
+ | Allow list, request names an unlisted model | 403 response listing the allowed models. |
151
+ | Block list, request omits `model` | 400 response asking for `providerName/model`. |
152
+ | Block list, request names a blocked model | 403 response. |
153
+ | Either mode, request uses a bare or malformed model | 400 response explaining the required format. |
154
+ | Policy has no rules for the route capability | 403 response explaining which capability to configure. |
155
+ | Another inbound policy already selected routing | Model Filtering leaves that selection unchanged. |
77
156
 
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.
157
+ Native routes also enforce provider type. `/v1/responses` requires a Provider
158
+ Name backed by OpenAI, and `/v1/messages` requires a Provider Name backed by
159
+ Anthropic. Provider Names may be custom labels; validation uses the provider
160
+ type configured for that Provider Assignment.
161
+
162
+ For example, this embedding request is evaluated against `models.embeddings`:
163
+
164
+ ```json
165
+ {
166
+ "model": "openai/text-embedding-3-small",
167
+ "input": "Search text"
168
+ }
169
+ ```
81
170
 
82
171
  ## Adding fallbacks
83
172
 
@@ -143,6 +232,18 @@ Attach the module as an ordinary inbound policy instead of Model Filtering:
143
232
 
144
233
  `AIGatewayModelRouting.get(context)` returns the sanitized, normalized routing
145
234
  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.
235
+ consumes a custom selection even when Model Filtering is not attached. If
236
+ neither kind of policy creates a selection, the handler derives one from the
237
+ request's required `providerName/model`.
238
+
239
+ Policy order determines precedence:
240
+
241
+ 1. Routing selected before Model Filtering remains authoritative because Model
242
+ Filtering leaves an existing selection unchanged.
243
+ 2. Model Filtering creates routing when no earlier policy selected it.
244
+ 3. A custom policy placed after Model Filtering may deliberately replace that
245
+ selection.
246
+ 4. If no policy selects routing, the handler derives it from the request.
247
+
248
+ Prefer one policy as the primary selector so the route's intent is easy to
249
+ understand.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zuplo",
3
- "version": "6.73.29",
3
+ "version": "6.73.31",
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.29",
23
- "@zuplo/core": "6.73.29",
24
- "@zuplo/runtime": "6.73.29",
25
- "@zuplo/test": "1.4.0"
22
+ "@zuplo/cli": "6.73.31",
23
+ "@zuplo/core": "6.73.31",
24
+ "@zuplo/runtime": "6.73.31",
25
+ "@zuplo/test": "1.4.4"
26
26
  }
27
27
  }