zuplo 6.73.30 → 6.74.2

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
  ```
@@ -2,16 +2,175 @@
2
2
  title: How to check an incoming IP address
3
3
  sidebar_label: "Check IP Address"
4
4
  description:
5
- Learn how to access the true client IP address of requests using the
6
- true-client-ip header.
5
+ Read the client IP address of an incoming request, and understand when that
6
+ address can be trusted on each deployment type.
7
7
  tags:
8
8
  - request-handling
9
9
  - deployment
10
10
  ---
11
11
 
12
- Sometimes you want to access the true IP address of the gateway's client making
13
- the current request. To do this you can read the `true-client-ip` header:
12
+ To get the IP address of the client that made the current request, read it from
13
+ the request context:
14
14
 
15
15
  ```ts
16
- const ip = request.headers.get("true-client-ip");
16
+ export default async function (request: ZuploRequest, context: ZuploContext) {
17
+ const ip = context.incomingRequestProperties.ip;
18
+ return new Response(ip ?? "unknown");
19
+ }
17
20
  ```
21
+
22
+ This is the official way to read the client IP. Zuplo resolves the address from
23
+ the layer in front of your gateway, so you get the same property regardless of
24
+ where the gateway runs.
25
+
26
+ ## Handle an unknown address
27
+
28
+ The value is `undefined` when nothing identified the caller. Handle that case
29
+ explicitly:
30
+
31
+ ```ts
32
+ const ip = context.incomingRequestProperties.ip;
33
+ if (!ip) {
34
+ return HttpProblems.badRequest(request, context, {
35
+ detail: "Could not determine the client IP address",
36
+ });
37
+ }
38
+ ```
39
+
40
+ Don't substitute a placeholder such as `"unknown"` or `0.0.0.0` and then key on
41
+ it. Every unidentified caller collapses into that single value, so a rate limit
42
+ keyed on it throttles them as one client, and an allow list keyed on it either
43
+ admits all of them or none.
44
+
45
+ ## When the address can be trusted
46
+
47
+ An IP address is only as trustworthy as the layer that determined it. Zuplo
48
+ reports an address only where that layer establishes it, so the value is either
49
+ trustworthy or `undefined` — never one the caller chose.
50
+
51
+ | Deployment | Determined by | Trustworthy |
52
+ | ----------------- | ------------------------------------- | ---------------------------------------- |
53
+ | Zuplo Cloud | The Zuplo edge | Yes |
54
+ | Zuplo Dedicated | Your environment, configured by Zuplo | Yes |
55
+ | Self-hosted | The proxy you run | Only if that proxy overwrites the header |
56
+ | Local development | Nothing — the caller is loopback | Reports `127.0.0.1` |
57
+
58
+ Zuplo Cloud and Zuplo Dedicated need no configuration. Self-hosted deployments
59
+ depend on the proxy you run, covered in
60
+ [Self-hosted deployments](#self-hosted-deployments).
61
+
62
+ :::caution
63
+
64
+ Don't read an IP header from `request.headers` yourself. Which header carries
65
+ the caller's address depends on where the gateway runs, so a header that holds
66
+ it on one deployment may be absent on another — or may hold whatever the caller
67
+ put there. Use `context.incomingRequestProperties.ip`, which resolves the right
68
+ one for the deployment.
69
+
70
+ :::
71
+
72
+ ## The `x-forwarded-for` header
73
+
74
+ `x-forwarded-for` carries a client address through a chain of proxies. Each
75
+ proxy appends the address it received the request from, producing a list:
76
+
77
+ ```
78
+ X-Forwarded-For: 203.0.113.7, 198.51.100.4
79
+ ```
80
+
81
+ The header reaches your code and your backend unchanged, so you can read it
82
+ directly:
83
+
84
+ ```ts
85
+ const chain = (request.headers.get("x-forwarded-for") ?? "")
86
+ .split(",")
87
+ .map((entry) => entry.trim())
88
+ .filter(Boolean);
89
+ ```
90
+
91
+ To use it correctly, understand what the list contains. A caller can send an
92
+ `X-Forwarded-For` of its own, and every proxy after that appends to whatever
93
+ arrived. So the front of the list is whatever the caller wrote, and only entries
94
+ added by a hop you operate mean anything.
95
+
96
+ That leads to a few rules:
97
+
98
+ - Count positions from the **end**. The last entry was added by the hop nearest
99
+ your gateway; the first came from the caller.
100
+ - Decide in advance how many hops you operate, and ignore anything beyond them.
101
+ A caller can pad the front with as many entries as it likes.
102
+ - Don't search the list for an address you recognize — a caller can put one
103
+ there.
104
+ - Expect IPv6 entries, which may be bracketed and carry a port, such as
105
+ `[2001:db8::1]:8080`.
106
+
107
+ :::danger
108
+
109
+ An address is only as reliable as the hop that established it. If you key a rate
110
+ limit on a value the caller can choose, they escape the limit by rotating it.
111
+ The same value in an allow list can be used to impersonate an allowed client,
112
+ and in an audit record it produces entries that didn't happen.
113
+
114
+ :::
115
+
116
+ If you only need the caller's address, `context.incomingRequestProperties.ip`
117
+ already gives it to you without any of this.
118
+
119
+ ## Running your own CDN or proxy in front of Zuplo
120
+
121
+ If your own CDN, load balancer, or WAF sits in front of your gateway, then that
122
+ hop — not the end user — is the client Zuplo sees.
123
+ `incomingRequestProperties.ip` returns its address.
124
+
125
+ To identify the end user, have your CDN write the address into a header of your
126
+ own and read that header in your code:
127
+
128
+ ```ts
129
+ const endUserIp = request.headers.get("x-acme-client-ip");
130
+ ```
131
+
132
+ This works only if your CDN **overwrites** that header on every request. If it
133
+ merely adds the header when absent, a caller can supply their own value and it
134
+ passes straight through to your code.
135
+
136
+ ## Zuplo Dedicated
137
+
138
+ Zuplo configures your Dedicated environment to determine the client address, so
139
+ there's nothing for you to set up. `context.incomingRequestProperties.ip`
140
+ returns the caller's address the same way it does on Zuplo Cloud.
141
+
142
+ Dedicated environments aren't identical to one another — they differ by cloud,
143
+ region, and what sits in front of the gateway, such as a CDN or WAF you already
144
+ operate. If you need to know exactly how the address is determined for your
145
+ environment, or you're adding a network layer in front of it, contact your
146
+ account team or [support](mailto:support@zuplo.com). Adding a hop in front of
147
+ the gateway changes which address the gateway sees, as described in
148
+ [Running your own CDN or proxy in front of Zuplo](#running-your-own-cdn-or-proxy-in-front-of-zuplo).
149
+
150
+ ## Self-hosted deployments
151
+
152
+ On a self-hosted deployment you run the proxy in front of the gateway, so
153
+ determining the client address is yours to configure. The gateway reads it from
154
+ the `x-real-ip` request header.
155
+
156
+ Your proxy must **overwrite** that header rather than pass one through. If a
157
+ client-supplied `x-real-ip` reaches the gateway untouched, any caller can choose
158
+ its own address by sending it. NGINX's `real_ip` module does this correctly by
159
+ default. Confirm the behavior against your own configuration before relying on
160
+ the address for anything security-sensitive.
161
+
162
+ :::warning
163
+
164
+ A gateway reachable directly, with no proxy in front of it, has nothing
165
+ determining the client address. Don't expose a self-hosted gateway to the
166
+ internet directly if you rely on the client IP.
167
+
168
+ :::
169
+
170
+ ## Geolocation
171
+
172
+ To act on where a caller is rather than their address,
173
+ `incomingRequestProperties` already carries resolved geolocation — `country`,
174
+ `city`, `region`, `latitude`, `longitude`, `asn`, and more. There's no need to
175
+ geolocate the IP address yourself. See
176
+ [ZuploContext](../programmable-api/zuplo-context.mdx#incomingrequestproperties).
@@ -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.
@@ -48,9 +48,15 @@ const elapsed = Date.now() - context.custom.startTime;
48
48
 
49
49
  ### `incomingRequestProperties`
50
50
 
51
- Information about the incoming request such as geolocation data. This is a
52
- read-only object with the following properties:
53
-
51
+ Information about the incoming request such as the client IP address and
52
+ geolocation data. This is a read-only object with the following properties:
53
+
54
+ - `ip` - The IP address of the client that made the request, for example,
55
+ "203.0.113.7". This is the only supported way to read the client IP; don't
56
+ read an IP header from `request.headers` yourself. The value is `undefined`
57
+ when nothing identified the caller. See
58
+ [how to check an incoming IP address](../articles/check-ip-address.mdx) for
59
+ when the address can be trusted on each deployment type.
54
60
  - `asn` - ASN of the incoming request, for example, 395747.
55
61
  - `asOrganization` - The organization which owns the ASN of the incoming
56
62
  request, for example, Google Cloud.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zuplo",
3
- "version": "6.73.30",
3
+ "version": "6.74.2",
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.30",
23
- "@zuplo/core": "6.73.30",
24
- "@zuplo/runtime": "6.73.30",
22
+ "@zuplo/cli": "6.74.2",
23
+ "@zuplo/core": "6.74.2",
24
+ "@zuplo/runtime": "6.74.2",
25
25
  "@zuplo/test": "1.4.4"
26
26
  }
27
27
  }