zuplo 7.9.17 → 7.9.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/ai-gateway/integrations/claude-code.mdx +4 -4
- package/docs/ai-gateway/integrations/claude-desktop.mdx +9 -9
- package/docs/ai-gateway/integrations/codex.mdx +15 -15
- package/docs/ai-gateway/integrations/github-copilot.mdx +10 -10
- package/docs/ai-gateway/integrations/goose.mdx +2 -2
- package/docs/ai-gateway/user-apps.mdx +8 -8
- package/docs/dev-portal/zudoku/configuration/api-reference.md +16 -3
- package/docs/dev-portal/zudoku/configuration/build-configuration.mdx +7 -0
- package/docs/dev-portal/zudoku/configuration/overview.md +1 -1
- package/docs/dev-portal/zudoku/markdown/frontmatter.md +19 -0
- package/docs/dev-portal/zudoku/openapi-extensions/x-zudoku-request-box-enabled.md +47 -0
- package/docs/dev-portal/zudoku/openapi-extensions/x-zudoku-seo.md +3 -0
- package/docs/policies/_index.md +1 -0
- package/docs/policies/ai-gateway-load-balancing-inbound/doc.md +522 -0
- package/docs/policies/ai-gateway-load-balancing-inbound/intro.md +6 -0
- package/docs/policies/ai-gateway-load-balancing-inbound/schema.json +175 -0
- package/docs/policies/ai-gateway-smart-router-inbound/doc.md +19 -19
- package/docs/policies/ai-gateway-smart-router-inbound/schema.json +10 -10
- package/package.json +5 -5
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
Use this policy to spread requests across several deployments of a model to
|
|
2
|
+
combine their rate limits, across providers that serve the same model, or across
|
|
3
|
+
different models. List the models, and every request the policy handles goes to
|
|
4
|
+
one of them.
|
|
5
|
+
|
|
6
|
+
## How it works
|
|
7
|
+
|
|
8
|
+
For each request, the policy:
|
|
9
|
+
|
|
10
|
+
1. Takes the list for the request's type: `completions` for chat completions,
|
|
11
|
+
Responses, Messages, and System One, or `embeddings` for embeddings.
|
|
12
|
+
2. Drops models that can't serve the request's endpoint, such as an OpenAI model
|
|
13
|
+
on `/v1/messages`. The policy decides this by the provider and by whether the
|
|
14
|
+
model is Claude, not by which APIs each model serves. See
|
|
15
|
+
[Endpoints](#endpoints).
|
|
16
|
+
3. Picks a model with the `strategy`, plus a retry model: the model the strategy
|
|
17
|
+
would pick next.
|
|
18
|
+
4. Hands both to the AI Gateway. The gateway calls the chosen model, and moves
|
|
19
|
+
to the retry model when that call fails in a way the endpoint retries. See
|
|
20
|
+
[Failures and retries](#failures-and-retries).
|
|
21
|
+
|
|
22
|
+
The gateway sends the policy's choice to the provider in place of the request's
|
|
23
|
+
`model`. Clients still send a `model`, because most SDKs require one, but any
|
|
24
|
+
value works. The request itself keeps the client's `model`, so policies that run
|
|
25
|
+
later, such as Semantic Cache, still see that value. Because the policy picks
|
|
26
|
+
the model for every request it handles, the list also limits which models
|
|
27
|
+
clients can reach.
|
|
28
|
+
|
|
29
|
+
If an earlier policy already chose a model for the request, this policy leaves
|
|
30
|
+
that choice alone. Requests of a type with no list, such as embeddings when you
|
|
31
|
+
set only `completions`, pass through unchanged.
|
|
32
|
+
|
|
33
|
+
## Choose a strategy
|
|
34
|
+
|
|
35
|
+
| Strategy | How it picks a model | Use it to |
|
|
36
|
+
| ------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ |
|
|
37
|
+
| `round-robin` | Cycles through the list in order. | Spread load evenly across deployments, accounts, or providers. |
|
|
38
|
+
| `sticky` | Sends every request with the same key to the same model. The key is the caller, the conversation, or a value you select. | Keep each caller or end user on one model, or keep each conversation on the account whose prompt cache already holds it. |
|
|
39
|
+
|
|
40
|
+
Here is how four requests could go with the example configuration, on one
|
|
41
|
+
gateway instance:
|
|
42
|
+
|
|
43
|
+
| Request | Caller | `round-robin` | `sticky` by `user` |
|
|
44
|
+
| ------- | ------- | -------------------- | -------------------- |
|
|
45
|
+
| 1 | `app-a` | `azure-eastus/gpt-5` | `azure-eastus/gpt-5` |
|
|
46
|
+
| 2 | `app-b` | `azure-westus/gpt-5` | `openai/gpt-5` |
|
|
47
|
+
| 3 | `app-a` | `azure-westus/gpt-5` | `azure-eastus/gpt-5` |
|
|
48
|
+
| 4 | `app-a` | `openai/gpt-5` | `azure-eastus/gpt-5` |
|
|
49
|
+
|
|
50
|
+
Round robin moves each app to the next model on each of its requests. Sticky
|
|
51
|
+
keeps `app-a` on `azure-eastus/gpt-5`, whichever gateway instance serves it.
|
|
52
|
+
|
|
53
|
+
## Examples
|
|
54
|
+
|
|
55
|
+
### Serve one model from several providers
|
|
56
|
+
|
|
57
|
+
Claude Sonnet is available directly from Anthropic and through Amazon Bedrock.
|
|
58
|
+
Cycle between them to use both quotas. Use a Bedrock Mantle provider for the
|
|
59
|
+
Bedrock entry, because the policy skips models on Bedrock Runtime providers:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"models": {
|
|
64
|
+
"completions": [
|
|
65
|
+
"anthropic/claude-sonnet-4-6",
|
|
66
|
+
"bedrockmantle/anthropic.claude-sonnet-4-6"
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Keep conversations on one deployment
|
|
73
|
+
|
|
74
|
+
Providers cache the prompt prefixes they've already processed, and cached input
|
|
75
|
+
tokens cost less and return faster. Each account or deployment has its own
|
|
76
|
+
cache, so a conversation that moves between deployments loses the benefit:
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"strategy": "sticky",
|
|
81
|
+
"stickyBy": "conversation",
|
|
82
|
+
"models": {
|
|
83
|
+
"completions": [
|
|
84
|
+
"azure-eastus/gpt-5",
|
|
85
|
+
"azure-westus/gpt-5",
|
|
86
|
+
"azure-swedencentral/gpt-5"
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
This works for clients that send the whole conversation on every turn, as Chat
|
|
93
|
+
Completions clients do. A Responses API client that continues a stored response
|
|
94
|
+
with `previous_response_id` sends only the new turn, so with this list, its
|
|
95
|
+
requests to read or continue a response fail. Use sticky routing by `user` or
|
|
96
|
+
`expression` for those clients. See [Stored responses](#stored-responses).
|
|
97
|
+
|
|
98
|
+
### Keep each customer on one model
|
|
99
|
+
|
|
100
|
+
When each of your customers calls through its own app, sticky routing by user
|
|
101
|
+
keeps every customer on one model while customers spread across the list:
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"strategy": "sticky",
|
|
106
|
+
"stickyBy": "user",
|
|
107
|
+
"models": {
|
|
108
|
+
"completions": ["openai/gpt-5", "anthropic/claude-sonnet-4-6"]
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Balance embeddings
|
|
114
|
+
|
|
115
|
+
```json
|
|
116
|
+
{
|
|
117
|
+
"models": {
|
|
118
|
+
"embeddings": [
|
|
119
|
+
"openai/text-embedding-3-large",
|
|
120
|
+
"azure-eastus/text-embedding-3-large"
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
:::caution Use one embedding model per list
|
|
127
|
+
|
|
128
|
+
Every entry in the `embeddings` list must be the same embedding model. Vectors
|
|
129
|
+
from different models can't be compared, so mixing models breaks similarity
|
|
130
|
+
search.
|
|
131
|
+
|
|
132
|
+
:::
|
|
133
|
+
|
|
134
|
+
## Round robin
|
|
135
|
+
|
|
136
|
+
Round robin gives every model in the list an equal share of requests. Each
|
|
137
|
+
gateway instance keeps a separate place in the list for each app, starting from
|
|
138
|
+
a random point. Instances never have to coordinate, and a busy app can't skew a
|
|
139
|
+
quiet app's rotation. Across all instances, requests spread evenly, but two
|
|
140
|
+
requests in a row can still go to the same model.
|
|
141
|
+
|
|
142
|
+
## Sticky routing
|
|
143
|
+
|
|
144
|
+
Sticky routing uses a hash, not a stored mapping, so it gives the same answer on
|
|
145
|
+
every gateway instance. For each request, the policy scores every model by
|
|
146
|
+
hashing the sticky key together with the model name, and picks the model with
|
|
147
|
+
the highest score. The same key always produces the same scores, so it always
|
|
148
|
+
gets the same model.
|
|
149
|
+
|
|
150
|
+
| Model | Score for `app-a` | Result |
|
|
151
|
+
| -------------------- | ----------------- | ----------- |
|
|
152
|
+
| `azure-eastus/gpt-5` | 0.74 | Chosen |
|
|
153
|
+
| `azure-westus/gpt-5` | 0.72 | Retry model |
|
|
154
|
+
| `openai/gpt-5` | 0.25 | |
|
|
155
|
+
|
|
156
|
+
- The retry model is the one with the second-highest score, so it's stable too.
|
|
157
|
+
- Adding a model moves only the keys it now wins. Removing a model moves only
|
|
158
|
+
the keys it was winning. Every other key stays where it was.
|
|
159
|
+
|
|
160
|
+
Requests without a key go round robin:
|
|
161
|
+
|
|
162
|
+
| `stickyBy` | A request has no key when |
|
|
163
|
+
| -------------- | ------------------------------------------------------------------------------- |
|
|
164
|
+
| `user` | It has no authenticated user |
|
|
165
|
+
| `conversation` | It has no user message with text, such as an embeddings or System One request |
|
|
166
|
+
| `expression` | The expression resolves to nothing, to an empty string, or to an unusable value |
|
|
167
|
+
|
|
168
|
+
Stored-response requests don't go round robin when the models that store
|
|
169
|
+
responses have more than one provider name. They fail with `400` unless a `user`
|
|
170
|
+
or `expression` key picks their model, so under `conversation` they fail even
|
|
171
|
+
with a key. See [Stored responses](#stored-responses).
|
|
172
|
+
|
|
173
|
+
### Sticky by user
|
|
174
|
+
|
|
175
|
+
`"stickyBy": "user"` keys on `request.user.sub`, the identity your
|
|
176
|
+
authentication policy sets. It works like an expression of `request.user.sub`.
|
|
177
|
+
|
|
178
|
+
:::note With AI Gateway API keys, the user is the app
|
|
179
|
+
|
|
180
|
+
AI Gateway Authentication sets `request.user.sub` to the app's name. Sticky
|
|
181
|
+
routing by user then keeps each app on one model, and spreads load only when
|
|
182
|
+
many apps share the policy. To keep each end user on one model, have the app
|
|
183
|
+
send an end-user ID and select it with an [expression](#sticky-by-expression).
|
|
184
|
+
On User App routes, where callers use personal API keys, `request.user.sub` is
|
|
185
|
+
the key owner's user ID, so each person stays on one model.
|
|
186
|
+
|
|
187
|
+
:::
|
|
188
|
+
|
|
189
|
+
### Sticky by conversation
|
|
190
|
+
|
|
191
|
+
`"stickyBy": "conversation"` keys on the conversation's system prompt and its
|
|
192
|
+
first user message with text. A client that sends the whole conversation on
|
|
193
|
+
every turn, as Chat Completions and Messages clients do, sends the same key on
|
|
194
|
+
every turn. On the Responses API, the system prompt is `instructions` plus any
|
|
195
|
+
system or developer items before the first user message. Keeping a conversation
|
|
196
|
+
on one model keeps it on the account whose prompt cache already holds its
|
|
197
|
+
earlier turns.
|
|
198
|
+
|
|
199
|
+
Only text counts, including plain-text documents. Conversations that open with
|
|
200
|
+
the same system prompt and the same first message share a model, and images and
|
|
201
|
+
other files don't make them different. A conversation that opens with an image
|
|
202
|
+
on its own goes round robin until a turn adds text. If your clients open every
|
|
203
|
+
conversation the same way, send a conversation ID in a header and key on it with
|
|
204
|
+
an [expression](#sticky-by-expression) instead.
|
|
205
|
+
|
|
206
|
+
A Responses API request that continues a stored response with
|
|
207
|
+
`previous_response_id` or `conversation` sends only the new turn, so its key
|
|
208
|
+
changes on every turn. See [Stored responses](#stored-responses).
|
|
209
|
+
|
|
210
|
+
The policy hashes this text in memory. It never stores or logs it.
|
|
211
|
+
|
|
212
|
+
### Sticky by expression
|
|
213
|
+
|
|
214
|
+
`"stickyBy": "expression"` keys on one value that `expression` selects from the
|
|
215
|
+
request or its context. Expressions use the same syntax as
|
|
216
|
+
[Metering budget expressions](/docs/policies/ai-gateway-metering-inbound#budget-expressions),
|
|
217
|
+
so a value you budget on can also keep requests on one model. For example, to
|
|
218
|
+
keep each end user on one model when your app sends an end-user ID in a header:
|
|
219
|
+
|
|
220
|
+
```json
|
|
221
|
+
{
|
|
222
|
+
"strategy": "sticky",
|
|
223
|
+
"stickyBy": "expression",
|
|
224
|
+
"expression": "request.headers.get(\"x-end-user-id\")",
|
|
225
|
+
"models": {
|
|
226
|
+
"completions": ["azure-eastus/gpt-5", "azure-westus/gpt-5"]
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
In JSON, escape the double quotes inside an expression, as the example does.
|
|
232
|
+
Common expressions:
|
|
233
|
+
|
|
234
|
+
| Expression | Selects |
|
|
235
|
+
| ------------------------------- | --------------------------------------------------------- |
|
|
236
|
+
| `request.headers.get("<name>")` | One request header, such as an end-user ID your app sends |
|
|
237
|
+
| `request.user.data.<property>` | A property of the caller's metadata |
|
|
238
|
+
| `request.params.<name>` | A path parameter from the matched route |
|
|
239
|
+
| `context.custom.<property>` | A value your own policy puts on `context.custom` |
|
|
240
|
+
|
|
241
|
+
See
|
|
242
|
+
[Supported expressions](/docs/policies/ai-gateway-metering-inbound#supported-expressions)
|
|
243
|
+
in the Metering docs for the full list. An expression is a selector, not code,
|
|
244
|
+
so it can't compare values, call string methods, or read the request body.
|
|
245
|
+
|
|
246
|
+
- The value follows Metering's value rules. It must be a string or a safe
|
|
247
|
+
integer, and `-0` doesn't count. The gateway converts a string to well-formed
|
|
248
|
+
NFC Unicode, then rejects it when it's longer than 256 UTF-8 bytes or contains
|
|
249
|
+
a control character, U+2028, or U+2029.
|
|
250
|
+
- Values are case-sensitive, so `Acme` and `acme` can land on different models.
|
|
251
|
+
Two strings that normalize to the same NFC text share a model.
|
|
252
|
+
- When the expression resolves to nothing, to an empty string, or to a value
|
|
253
|
+
these rules reject, such as an object, a fractional number, or an overlong
|
|
254
|
+
string, the request goes round robin. A stored-response request fails with
|
|
255
|
+
`400` instead when the models that store responses have more than one provider
|
|
256
|
+
name. The request log says why.
|
|
257
|
+
- Per-request identifiers, such as `context.requestId`, aren't allowed, because
|
|
258
|
+
a key that never repeats can't keep requests together.
|
|
259
|
+
- An expression outside the syntax is an options error, and the error shows
|
|
260
|
+
where parsing stopped.
|
|
261
|
+
|
|
262
|
+
The policy hashes the value in memory. It never stores or logs it.
|
|
263
|
+
|
|
264
|
+
## Failures and retries
|
|
265
|
+
|
|
266
|
+
Each request gets a retry model, except stored-response requests,
|
|
267
|
+
`/v1/messages/count_tokens` requests, `/v1/systemone` requests, and requests
|
|
268
|
+
that only one model in the list can serve. The AI Gateway treats the retry model
|
|
269
|
+
like any other backup model. When it moves there depends on the endpoint:
|
|
270
|
+
|
|
271
|
+
| When the chosen model | Chat completions and embeddings | Responses and Messages |
|
|
272
|
+
| ------------------------------------------------------------ | ----------------------------------------------- | ----------------------------------------------- |
|
|
273
|
+
| Returns `429` | Retries it twice, then moves to the retry model | Retries it twice, then moves to the retry model |
|
|
274
|
+
| Returns a `5xx`, `408`, or `425` status, or can't be reached | Moves to the retry model | Fails the request |
|
|
275
|
+
| Doesn't start responding within `fallbackTimeoutSeconds` | Moves to the retry model | Keeps waiting |
|
|
276
|
+
| Returns any other error, such as `400` or `401` | Fails the request | Fails the request |
|
|
277
|
+
|
|
278
|
+
Responses and Messages requests use the provider's native API. On those
|
|
279
|
+
endpoints the AI Gateway moves to another model only for rate limits, as it does
|
|
280
|
+
without this policy.
|
|
281
|
+
|
|
282
|
+
- Each request tries at most two models. The gateway retries a rate-limited
|
|
283
|
+
model twice, after 250 ms and then 500 ms. If every attempt returns `429`, the
|
|
284
|
+
request makes six calls, three to each model, before the gateway returns
|
|
285
|
+
`429`.
|
|
286
|
+
- Once a model starts streaming a response, the request stays on that model.
|
|
287
|
+
- A request that doesn't stream gets its response only when the model finishes,
|
|
288
|
+
so on chat completions and embeddings `fallbackTimeoutSeconds` limits the
|
|
289
|
+
whole response. A model that's still working when it runs out moves the
|
|
290
|
+
request to the retry model, which starts over with twice the time, and the
|
|
291
|
+
request fails if that runs out too. Raise `fallbackTimeoutSeconds` for slow
|
|
292
|
+
models, such as reasoning models, or stream the response. When Fallback Model
|
|
293
|
+
runs after this policy with a `fallback`, raise its `fallbackTimeoutSeconds`
|
|
294
|
+
too. Its timeout replaces this policy's on requests that get its `fallback`.
|
|
295
|
+
See [With Fallback Model](#with-fallback-model).
|
|
296
|
+
- When only one model in the list can serve a request, this policy sets no retry
|
|
297
|
+
model and no timeout. On `/v1/responses` and `/v1/messages`, a list that mixes
|
|
298
|
+
providers or vendors often has only one, so list at least two models for each
|
|
299
|
+
API your clients use. Fallback Model can also add a backup. See
|
|
300
|
+
[With Fallback Model](#with-fallback-model).
|
|
301
|
+
- The policy doesn't remember failures between requests. While a model is down,
|
|
302
|
+
every request that picks it tries it first: chat completions and embeddings
|
|
303
|
+
requests then move to the retry model, and Responses and Messages requests
|
|
304
|
+
fail. With `sticky` routing, the same callers pick the failing model every
|
|
305
|
+
time.
|
|
306
|
+
|
|
307
|
+
## Endpoints
|
|
308
|
+
|
|
309
|
+
| Endpoint | Behavior |
|
|
310
|
+
| ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
311
|
+
| `/v1/chat/completions` | Models the AI Gateway sends Chat Completions requests to are eligible, and the gateway translates the request for each provider. System One models are skipped. Claude on Vertex AI isn't skipped, and requests that pick it fail with `400`. |
|
|
312
|
+
| `/v1/responses` | Only models the AI Gateway sends Responses API requests to are eligible: OpenAI models, Azure AI and OpenRouter models other than Claude, and Bedrock Mantle models other than Claude. Other providers are skipped, even when their own API serves the Responses API. |
|
|
313
|
+
| `/v1/messages`, `/v1/messages/count_tokens` | Only models the AI Gateway sends Anthropic Messages API requests to are eligible: Anthropic models, and Claude on Azure AI, Bedrock Mantle, Vertex AI, and OpenRouter. `count_tokens` also skips providers that don't count tokens, such as OpenRouter. |
|
|
314
|
+
| `/v1/systemone` | Only TypeSafe System One (Jev) models are eligible, so you can balance several Jev models or TypeSafe accounts. Jev through OpenRouter isn't eligible. These requests get no retry model. |
|
|
315
|
+
| `/v1/embeddings` | Uses the `embeddings` list. |
|
|
316
|
+
| `GET` or `DELETE /v1/responses/{id}`, and `GET /v1/responses/{id}/input_items` | Routed to the account that holds the response, when the policy can tell which one it is. See [Stored responses](#stored-responses). |
|
|
317
|
+
| `GET /v1/models` | Passes through unchanged. |
|
|
318
|
+
| Bedrock Runtime (`/model/{modelId}/…`) | Not supported. An app whose policy chain includes this policy rejects Bedrock Runtime requests. A model on a Bedrock Runtime provider can't serve any `/v1` endpoint, so the policy skips it. |
|
|
319
|
+
|
|
320
|
+
If no model in the list can serve the request's endpoint, the policy returns
|
|
321
|
+
`400`. The message names the endpoint, and for an endpoint that uses a
|
|
322
|
+
provider's own API, such as `/v1/messages`, the API it requires.
|
|
323
|
+
|
|
324
|
+
These rules depend on the provider, and on whether the model is Claude. The
|
|
325
|
+
policy doesn't check which APIs each model serves. A request that picks a model
|
|
326
|
+
on an API the model doesn't serve fails, such as a `/v1/chat/completions`
|
|
327
|
+
request that picks a model that serves only the Responses API.
|
|
328
|
+
|
|
329
|
+
On `/v1/responses`:
|
|
330
|
+
|
|
331
|
+
- Bedrock Mantle serves the Responses API for only some of its models other than
|
|
332
|
+
Claude. Requests that pick a Mantle model without Responses support fail. To
|
|
333
|
+
check a model, read the **APIs supported** for the `bedrock-mantle` endpoint
|
|
334
|
+
on its
|
|
335
|
+
[AWS model card](https://docs.aws.amazon.com/bedrock/latest/userguide/model-cards.html).
|
|
336
|
+
The AI Gateway calls that endpoint, and the `bedrock-runtime` endpoint's list
|
|
337
|
+
can differ.
|
|
338
|
+
- OpenRouter stores no responses. When the list also has a model that does,
|
|
339
|
+
OpenRouter gets only creates that set `store` to `false`. See
|
|
340
|
+
[Stored responses](#stored-responses).
|
|
341
|
+
|
|
342
|
+
### Stored responses
|
|
343
|
+
|
|
344
|
+
A stored response lives on the provider account that created it. Requests that
|
|
345
|
+
read or continue one must reach that account: retrieving a response, listing its
|
|
346
|
+
input items, deleting it, and creating a response with `previous_response_id`, a
|
|
347
|
+
`conversation`, or an `item_reference` input item.
|
|
348
|
+
|
|
349
|
+
| When | Stored-response requests |
|
|
350
|
+
| --------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
|
|
351
|
+
| Every model in the list that stores responses has the same provider name | Go to that provider |
|
|
352
|
+
| `strategy` is `sticky` by `user` or `expression`, and the request has a key | Go to the model the key picks, where that caller's responses were created |
|
|
353
|
+
| Anything else | Fail with `400`, and the request log names the fix |
|
|
354
|
+
|
|
355
|
+
- The provider name is the part before the slash, compared without regard to
|
|
356
|
+
case. `openai/gpt-5` and `openai/gpt-5-mini` share one, but
|
|
357
|
+
`azure-eastus/gpt-5` and `azure-westus/gpt-5` are two.
|
|
358
|
+
- Stored-response requests get no retry model, because no other account holds
|
|
359
|
+
the response. Fallback Model placed after this policy still adds its
|
|
360
|
+
`fallback`, so a continuation that stays rate limited moves to the fallback,
|
|
361
|
+
and fails there if the fallback is on another account.
|
|
362
|
+
- A provider that stores nothing, such as OpenRouter, never gets stored-response
|
|
363
|
+
requests. It gets a create only when the create sets `store` to `false`, or
|
|
364
|
+
when no provider in the list stores responses, because a create stores its
|
|
365
|
+
response unless it opts out.
|
|
366
|
+
- A create that moves to the retry model stores its response on the retry
|
|
367
|
+
model's account. If that's another account, later requests for the response go
|
|
368
|
+
to the chosen model's account and fail.
|
|
369
|
+
- The retry model, a `fallback`, or a `quotaFallback` is on another account when
|
|
370
|
+
its provider name differs from the chosen model's. It's also on another
|
|
371
|
+
account when AI Gateway Authentication passes callers' own API keys through,
|
|
372
|
+
because the chosen model uses the caller's key and the others use your
|
|
373
|
+
configured key.
|
|
374
|
+
- The `400` tells the caller only that this API can't read or continue the
|
|
375
|
+
stored response. The fix is yours to make, so the request log carries it.
|
|
376
|
+
- `conversation` keys can't route them to one of several provider names. A
|
|
377
|
+
retrieve sends no messages, and a request with `previous_response_id` sends
|
|
378
|
+
only the new turn. With one provider name, each continuation is keyed on its
|
|
379
|
+
new turn, so it can run on a different model than the turn before.
|
|
380
|
+
- Changing the list can move a caller to another model. Responses stored before
|
|
381
|
+
the change stay on the old account.
|
|
382
|
+
|
|
383
|
+
## Policy order
|
|
384
|
+
|
|
385
|
+
Place Model Load Balancing after authentication and before the policies that
|
|
386
|
+
read or change model routing:
|
|
387
|
+
|
|
388
|
+
```json
|
|
389
|
+
{
|
|
390
|
+
"inboundPolicyChain": [
|
|
391
|
+
{ "name": "ai-gateway-auth-inbound" },
|
|
392
|
+
{ "name": "ai-gateway-load-balancing-inbound" },
|
|
393
|
+
{ "name": "ai-gateway-fallback-model-inbound" },
|
|
394
|
+
{ "name": "ai-gateway-metering-inbound" },
|
|
395
|
+
{ "name": "ai-gateway-semantic-cache-inbound" }
|
|
396
|
+
]
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
This chain shows the order only. Entries without `options` use the options
|
|
401
|
+
declared in `config/policies.json`. See
|
|
402
|
+
[Configure lists per app](#configure-lists-per-app).
|
|
403
|
+
|
|
404
|
+
| Policy | With Model Load Balancing |
|
|
405
|
+
| -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
406
|
+
| [Authentication](/docs/policies/ai-gateway-auth-inbound) | Place it before, so `request.user.sub` is set for sticky routing by user. |
|
|
407
|
+
| [Model Override](/docs/policies/ai-gateway-model-override-inbound) | Not needed, because this policy picks the model for every request it handles. If Model Override runs first, its model wins: always with `force`, and with `default` when a request omits `model`, as stored-response reads do. Placed after, it does nothing for request types this policy has a list for. |
|
|
408
|
+
| [Model Filtering](/docs/policies/ai-gateway-model-filtering-inbound) | Not needed for request types this policy has a list for, because the list already limits the models clients can reach. If Model Filtering runs first, it routes or refuses each request by the model the client asked for, and this policy does nothing. Placed after, it doesn't check this policy's choices. |
|
|
409
|
+
| [Fallback Model](/docs/policies/ai-gateway-fallback-model-inbound) | Place it after, where it changes the retry model, the timeout, and where over-quota requests go. Placed before, it does nothing. See [With Fallback Model](#with-fallback-model). |
|
|
410
|
+
| [Smart Router](/docs/policies/ai-gateway-smart-router-inbound) | When it routes a request, its model wins, whichever runs first. The request loses this policy's retry model, and Smart Router doesn't apply this policy's endpoint or stored-response rules. Fallback Model placed after Smart Router still adds its `fallback` and `quotaFallback`. Placed before Smart Router, they're lost too. |
|
|
411
|
+
| [Metering](/docs/policies/ai-gateway-metering-inbound) | Place it after this policy and Fallback Model, so a request over its quota can switch to Fallback Model's `quotaFallback`. Costs and usage use the model that served the request. |
|
|
412
|
+
| [Semantic Cache](/docs/policies/ai-gateway-semantic-cache-inbound) | Place it after. Its cache key uses the request's own `model`, not this policy's choice, so with a list of different models a cached answer can come from any of them. Placed before, it serves a cache hit without this policy when the request's `model` is a valid model, and skips the hit otherwise. |
|
|
413
|
+
|
|
414
|
+
### With Fallback Model
|
|
415
|
+
|
|
416
|
+
Fallback Model placed after this policy changes where failed and over-quota
|
|
417
|
+
requests go:
|
|
418
|
+
|
|
419
|
+
| Requests | Fallback Model's `fallback` | Its `quotaFallback`, over quota |
|
|
420
|
+
| ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
421
|
+
| Chat completions and embeddings | Becomes the retry model, and its `fallbackTimeoutSeconds`, 60 when unset, replaces this policy's. | Gets the request. A `quotaFallback` that the AI Gateway refuses on chat completions, such as Claude on Vertex AI, fails the request with `400`. |
|
|
422
|
+
| Responses and Messages creates | Becomes the retry model if it can serve the endpoint. Otherwise the request has no retry model. | Gets the request if it can serve the endpoint. Otherwise the gateway returns `429`. |
|
|
423
|
+
| `/v1/messages/count_tokens` | Never tried. | Gets the request if it can serve the endpoint. Otherwise the gateway returns `429`. |
|
|
424
|
+
| `/v1/systemone`, and requests that retrieve, delete, or list the input items of a stored response | Never tried. | Never used. The gateway returns `429`. |
|
|
425
|
+
|
|
426
|
+
- Requests this policy sends to the `fallback` model itself keep this policy's
|
|
427
|
+
retry model and timeout. Requests it sends to the `quotaFallback` model itself
|
|
428
|
+
get no quota fallback, so when they're over quota, the gateway returns `429`.
|
|
429
|
+
Use a `quotaFallback` that isn't in the list.
|
|
430
|
+
- When a `quotaFallback` call fails in a way the endpoint retries, the request
|
|
431
|
+
moves to the retry model, so an over-quota request can still run on a model in
|
|
432
|
+
the list. To keep over-quota requests off the list, give Fallback Model a
|
|
433
|
+
`fallback` that isn't in the list. To keep failed requests inside the list,
|
|
434
|
+
give Fallback Model only a `quotaFallback`.
|
|
435
|
+
- No model serves both `/v1/responses` and `/v1/messages`, so when your clients
|
|
436
|
+
call both, a `fallback` or a `quotaFallback` covers at most one of them.
|
|
437
|
+
- Over quota, a create that continues a stored response goes to the
|
|
438
|
+
`quotaFallback`, and fails there if the `quotaFallback` is on another account.
|
|
439
|
+
Other creates over quota go there too, so a response they store lives on the
|
|
440
|
+
`quotaFallback`'s account. If that's another account, requests for the
|
|
441
|
+
response fail once they're no longer over quota. See
|
|
442
|
+
[Stored responses](#stored-responses).
|
|
443
|
+
- For embeddings, set `fallback` to the same embedding model as the list. With a
|
|
444
|
+
one-model list, use that model on another provider name, because a `fallback`
|
|
445
|
+
equal to the chosen model is skipped.
|
|
446
|
+
- Unless you've changed them, Fallback Model's declared options set
|
|
447
|
+
`openai/gpt-4o-mini` as the `completions` `fallback`. A Fallback Model entry
|
|
448
|
+
without `options`, like the one in the chain above, makes that model the retry
|
|
449
|
+
model. On a gateway that can't use `openai/gpt-4o-mini`, such as one with no
|
|
450
|
+
provider named `openai`, every request that Fallback Model adds it to fails
|
|
451
|
+
with `500`, even requests that never try a backup. Give the entry its own
|
|
452
|
+
`options`.
|
|
453
|
+
|
|
454
|
+
## Configure lists per app
|
|
455
|
+
|
|
456
|
+
Declare the policy in `config/policies.json` with a default list. An app can
|
|
457
|
+
supply its own options in its chain entry. Entry options replace the declared
|
|
458
|
+
options entirely:
|
|
459
|
+
|
|
460
|
+
```json
|
|
461
|
+
{
|
|
462
|
+
"inboundPolicyChain": [
|
|
463
|
+
{
|
|
464
|
+
"name": "ai-gateway-load-balancing-inbound",
|
|
465
|
+
"options": {
|
|
466
|
+
"strategy": "sticky",
|
|
467
|
+
"stickyBy": "conversation",
|
|
468
|
+
"models": {
|
|
469
|
+
"completions": ["azure-eastus/gpt-5", "azure-westus/gpt-5"]
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
]
|
|
474
|
+
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
Unless you've changed them, the declared options are a one-model list,
|
|
478
|
+
`openai/gpt-4o-mini`. A chain entry without `options` then sends every chat
|
|
479
|
+
completions and Responses request to that model, and fails Messages and System
|
|
480
|
+
One requests with `400`, because that model can't serve them.
|
|
481
|
+
|
|
482
|
+
The gateway validates chain options when the policy runs. Invalid options fail
|
|
483
|
+
requests with an error that names the field to fix. So does a model the gateway
|
|
484
|
+
can't use, such as an unknown provider or an inactive model, on every request
|
|
485
|
+
that picks it as the chosen or the retry model. A provider whose API key isn't
|
|
486
|
+
on the gateway yet fails those requests with an error that says the provider
|
|
487
|
+
isn't ready, and the request log names the provider and its environment
|
|
488
|
+
variable.
|
|
489
|
+
|
|
490
|
+
## Monitoring
|
|
491
|
+
|
|
492
|
+
AI Gateway analytics attribute each request to the model that served it. Logging
|
|
493
|
+
plugins also receive these properties on the request's log entries after the
|
|
494
|
+
policy picks a model:
|
|
495
|
+
|
|
496
|
+
| Property | Value |
|
|
497
|
+
| --------------------------- | ---------------------------------- |
|
|
498
|
+
| `aiLoadBalancingModel` | The model the policy chose |
|
|
499
|
+
| `aiLoadBalancingRetryModel` | The retry model, when there is one |
|
|
500
|
+
|
|
501
|
+
The properties record this policy's picks. They don't change when Fallback Model
|
|
502
|
+
replaces the retry model or Smart Router replaces the choice.
|
|
503
|
+
|
|
504
|
+
## Limitations
|
|
505
|
+
|
|
506
|
+
- Each list can have up to 32 models, each listed once. The policy compares
|
|
507
|
+
entries without regard to case, so `openai/gpt-5` and `OpenAI/gpt-5` count as
|
|
508
|
+
one model. Provider names work in any case, but write each model ID exactly as
|
|
509
|
+
your provider spells it. A model ID in another case, such as `openai/GPT-5`,
|
|
510
|
+
can fail to route.
|
|
511
|
+
- Each request tries at most two models.
|
|
512
|
+
- An expression can be up to 1024 bytes and 32 property segments.
|
|
513
|
+
|
|
514
|
+
Lists that mix providers or models have limits of their own:
|
|
515
|
+
|
|
516
|
+
| When | What happens |
|
|
517
|
+
| ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
518
|
+
| A list mixes providers or vendors, on `/v1/responses` or `/v1/messages` | Only models the AI Gateway sends that API's requests to are eligible, and that depends on the model, not only its provider. On Azure AI, for example, Claude models are eligible on `/v1/messages`, and GPT models on `/v1/responses`. See [Endpoints](#endpoints). A mixed list often leaves one model and no retry model, so list at least two models for each API your clients use. |
|
|
519
|
+
| A list mixes vendors, on `/v1/chat/completions` | Each pick goes through its provider's translation, which drops what that provider doesn't support. Claude models ignore `response_format` and `seed`, use a `max_tokens` of 1,024 unless the request sets `max_tokens`, read `developer` messages as user messages, and fail requests that carry images, audio, or files. For the same behavior on every pick, list deployments of one model. |
|
|
520
|
+
| The models accept different parameters or context lengths | A request that one model rejects fails whenever the policy picks that model. The retry model isn't tried. |
|
|
521
|
+
| AI Gateway Authentication passes callers' own API keys through (`credentialPassthrough`) | The caller's key goes to whichever model the policy picks, so list only models that accept it. Every model that can be the retry model also needs its own configured key, and requests that move there run on your provider account. |
|
|
522
|
+
| You want different lists for Chat Completions, Responses, and Messages | One `completions` list serves them all. A second Model Load Balancing policy in the chain doesn't run for request types the first one has a list for. |
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
Spreads AI requests across a list of models. The policy cycles through the list,
|
|
2
|
+
or keeps requests that share a key, such as a user or a conversation, on the
|
|
3
|
+
same model. Most requests also get a retry model from the list. A request moves
|
|
4
|
+
there when the chosen model stays rate limited. Chat completions and embeddings
|
|
5
|
+
requests also move there when the model returns a server error, can't be
|
|
6
|
+
reached, or times out.
|