zuplo 7.7.8 → 7.7.9

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.
@@ -0,0 +1,410 @@
1
+ ---
2
+ title: Using Azure AI
3
+ sidebar_label: Azure AI
4
+ description:
5
+ Serve Azure OpenAI and Microsoft Foundry models through one provider
6
+ configuration. Map each deployment to the model it serves so the gateway
7
+ prices usage.
8
+ ---
9
+
10
+ **Azure AI** serves models from your own Azure resource through one provider
11
+ configuration—one resource endpoint and one resource key. Adding it gives your
12
+ [apps](./apps.mdx) the models you have deployed, through the
13
+ [Universal API](./universal-api.mdx), on your Azure subscription and Azure
14
+ billing.
15
+
16
+ One provider type covers both kinds of Azure resource:
17
+
18
+ - An **Azure OpenAI** resource deploys OpenAI models and serves them on an
19
+ OpenAI-compatible API.
20
+ - A **Microsoft Foundry** resource serves that same API for every Foundry model
21
+ family—Grok, DeepSeek, Llama, Mistral, Phi, Kimi and more—_and_ serves Claude
22
+ on the native Anthropic Messages API.
23
+
24
+ Apps reference models as `providerName/model`, where `providerName` is the name
25
+ you give the provider configuration. A provider named `azureai` serves
26
+ `azureai/my-gpt`.
27
+
28
+ ## Azure serves deployments, not model names
29
+
30
+ Of the ways Azure differs from other providers, this is the one that changes
31
+ what you put in the `model` field.
32
+
33
+ Other providers accept a published model ID. Azure resolves the `model` field
34
+ against the **deployments** in your resource, and you choose deployment names
35
+ when you create them. If you deploy `gpt-4.1-mini` under the name `my-gpt`, then
36
+ `my-gpt` is the only name Azure accepts for it.
37
+
38
+ The gateway forwards your deployment name to Azure unchanged, because it's the
39
+ only address Azure answers to. What the gateway needs to know separately is
40
+ which catalog model each deployment serves—that's what prices your usage.
41
+
42
+ :::caution{title="An unmapped deployment is rejected"}
43
+
44
+ The gateway prices a request by looking up the model name in its catalog. A
45
+ deployment named after something outside the catalog, such as `my-gpt`, matches
46
+ nothing—so the gateway returns **400** rather than serving a request it can't
47
+ bill. Azure would have served it, and the usage would have recorded no cost.
48
+
49
+ Map every deployment whose name differs from the model it serves. You do this
50
+ when you add or edit the provider. The error names the deployment and lists the
51
+ names that do work.
52
+
53
+ :::
54
+
55
+ Mapping is optional when it isn't needed. Deploy `gpt-4.1-mini` under the name
56
+ `gpt-4.1-mini`—the Azure portal's own default—and it already matches the
57
+ catalog.
58
+
59
+ ### Deployment name rules
60
+
61
+ Azure accepts 2 to 64 characters of letters, digits, hyphens, underscores and
62
+ periods, and won't accept a name ending in a period. Leading hyphens and
63
+ periods, doubled periods, trailing hyphens and uppercase letters are all fine.
64
+
65
+ Deployment names match case-insensitively, so a deployment named `MyGpt` answers
66
+ to `mygpt`, and one named `my-gpt` answers to `MY-GPT`. Map each deployment once
67
+ using whichever spelling you prefer; requests in any casing resolve to it.
68
+
69
+ ## How the gateway routes Azure AI models
70
+
71
+ A Foundry resource serves two API formats on the same host with the same
72
+ resource key:
73
+
74
+ - An **OpenAI-compatible API** at `/openai/v1`, serving every non-Claude model.
75
+ - The **native Anthropic Messages API** at `/anthropic/v1/messages`, serving the
76
+ Claude models.
77
+
78
+ <Diagram height="h-64">
79
+ <DiagramNode id="app">Your app</DiagramNode>
80
+ <DiagramNode id="gateway" variant="zuplo">
81
+ AI Gateway
82
+ </DiagramNode>
83
+ <DiagramGroup id="azure" label="Your Azure resource">
84
+ <DiagramNode id="openai-surface" variant="blue">
85
+ OpenAI-compatible API
86
+ </DiagramNode>
87
+ <DiagramNode id="messages-surface" variant="green">
88
+ Anthropic Messages API
89
+ </DiagramNode>
90
+ </DiagramGroup>
91
+ <DiagramEdge from="app" to="gateway" label="Universal API" />
92
+ <DiagramEdge
93
+ from="gateway"
94
+ to="openai-surface"
95
+ label="OpenAI-compatible models"
96
+ />
97
+ <DiagramEdge from="gateway" to="messages-surface" label="Claude models" />
98
+ </Diagram>
99
+
100
+ The gateway's model catalog records which API serves each model, and routes
101
+ every request accordingly. Your clients always call your app's URL and never see
102
+ the Azure endpoint.
103
+
104
+ Claude needs a Foundry resource, because an Azure OpenAI resource can't deploy
105
+ Claude at all. Which endpoint host you configure doesn't affect this—a Foundry
106
+ resource serves Claude on either host family the provider accepts.
107
+
108
+ ## Supported endpoints
109
+
110
+ | Endpoint | OpenAI-compatible models | Claude models |
111
+ | ---------------------- | ------------------------ | ------------- |
112
+ | `/v1/chat/completions` | ✅ Forwarded | ✅ Translated |
113
+ | `/v1/embeddings` | ✅ Embedding models | ❌ |
114
+ | `/v1/messages` | ❌ | ✅ Native |
115
+ | `/v1/responses` | ❌ | ❌ |
116
+
117
+ Streaming (`stream: true`) works on chat completions and messages.
118
+
119
+ The catalog carries three embedding models—`text-embedding-3-small`,
120
+ `text-embedding-3-large` and `text-embedding-ada-002`—so `/v1/embeddings` works
121
+ once you deploy one of them and map your deployment to it.
122
+
123
+ `/v1/responses` isn't served for Azure AI, and a Responses request fails with a
124
+ `400` error before any upstream call. Azure does serve a Responses API on its
125
+ own endpoint; the gateway doesn't route to it yet. Use `/v1/chat/completions`
126
+ instead.
127
+
128
+ Claude models work on `/v1/chat/completions` through the gateway's translation
129
+ to the Messages API, which supports the core chat parameters: `messages`,
130
+ `max_tokens`, `temperature`, `top_p`, `stop`, and `stream`. For tool use or
131
+ other Anthropic-specific features, call
132
+ [`/v1/messages`](#call-claude-models-on-the-messages-api) instead.
133
+
134
+ This is the gateway's standard handling for Claude, not something specific to
135
+ Azure—[Bedrock Mantle](./bedrock-mantle.mdx#supported-endpoints-by-model-family)
136
+ serves Claude the same way. You get an ordinary `chat.completion` object back,
137
+ with `choices` and `prompt_tokens`/`completion_tokens`. The one visible trace of
138
+ the conversion is the response `id`, which keeps Anthropic's `msg_` prefix.
139
+ Azure's OpenAI-compatible models are forwarded instead of translated, so their
140
+ responses carry Azure's own fields, such as `content_filter_results`.
141
+
142
+ ## Before you begin
143
+
144
+ You need:
145
+
146
+ - An Azure subscription with an Azure OpenAI or Microsoft Foundry resource. Note
147
+ the resource name—it's the first label of the resource's endpoint host.
148
+ - **At least one model deployed in that resource**, and its deployment name. The
149
+ gateway addresses deployments, so a resource with no deployments serves
150
+ nothing. Deploy models in the Azure portal under your resource's **Model
151
+ deployments**.
152
+ - One of the resource's API keys, from the resource's **Keys and Endpoint** page
153
+ in the Azure portal.
154
+ - An AI Gateway project in the Zuplo Portal.
155
+ - An AI Gateway [app](./apps.mdx) to call the models from. The app page shows
156
+ the app's API URL, and its API key lives on the app's **API Key** tab.
157
+
158
+ :::note
159
+
160
+ Use a resource key, not a Microsoft Entra ID token. Azure accepts both on its
161
+ own API, but the gateway stores long-lived resource keys, and the provider
162
+ dialog rejects a pasted Entra token—those expire within hours.
163
+
164
+ :::
165
+
166
+ ### Deploying Claude on Foundry
167
+
168
+ Claude deployments carry extra requirements that Azure applies only to them, and
169
+ the failures are easy to misread:
170
+
171
+ - Azure asks for your **industry, organization name and country** when you
172
+ create an Anthropic-format deployment. Give a plain organization name;
173
+ punctuation in it has been enough to fail the deployment.
174
+ - Azure validates none of it up front. A deployment can report success, spend a
175
+ few minutes provisioning, and then land in a **Failed** state with an
176
+ internal-error message. Check the deployment's provisioning state before you
177
+ configure the provider, and delete a failed deployment before reusing its
178
+ name.
179
+ - If you deploy through the Azure REST API or a template rather than the portal,
180
+ use API version `2025-12-01` or later. Earlier versions silently ignore those
181
+ fields and then reject the request for not providing them.
182
+
183
+ ## Add the provider
184
+
185
+ Adding or editing providers requires the **Edit** permission, granted to Zuplo
186
+ account and project **Admins**—see
187
+ [Managing Providers](./managing-providers.mdx).
188
+
189
+ <Stepper>
190
+
191
+ 1. Open
192
+ [**Settings → AI Providers**](https://portal.zuplo.com/+/account/project/ai/settings/data-models)
193
+ in your AI Gateway project in the Zuplo Portal.
194
+
195
+ 1. Click the **Add Provider** button.
196
+
197
+ 1. In the **AI Provider** list, select **Azure AI** from the Default Providers
198
+ group.
199
+
200
+ 1. Review the **Provider Name**, which fills in as `azureai` (a second
201
+ configuration becomes `azureai-2`). You can replace it with your own name,
202
+ but only now—the name is permanent after creation, and it's the prefix in
203
+ every model reference: a provider named `azureai` serves `azureai/my-gpt`.
204
+
205
+ 1. In **Azure Resource Name**, enter your resource's name, such as
206
+ `my-resource`. Use the host family selector beside the field to pick
207
+ `openai.azure.com` or `services.ai.azure.com`, matching your resource's
208
+ endpoint. The gateway sends this provider's requests to
209
+ `https://<resource>.<host family>`.
210
+
211
+ 1. In **API Key**, paste one of the resource's keys.
212
+
213
+ 1. Select the models to enable, or click **Select All**. The picker shows
214
+ per-token prices, which the gateway uses to track cost per app. You can
215
+ change the selection later.
216
+
217
+ 1. Under **Deployment Names**, click **Add deployment** for each deployment
218
+ whose name differs from the model it serves. Enter the deployment name, then
219
+ pick the model it serves from the list. Skip this for deployments already
220
+ named after their model.
221
+
222
+ 1. Click **Create**.
223
+
224
+ </Stepper>
225
+
226
+ :::note
227
+
228
+ Saving provider settings triggers an automatic production deployment of your
229
+ gateway, because provider credentials are part of the deployed gateway. The
230
+ change is live once the deployment completes.
231
+
232
+ :::
233
+
234
+ When you edit the provider later—see
235
+ [Managing Providers](./managing-providers.mdx)—the **Azure Resource Name**, host
236
+ family and deployment mappings all stay editable, and you can replace the API
237
+ key. The **Provider Name** doesn't change.
238
+
239
+ :::caution{title="Which endpoint form to enter"}
240
+
241
+ Enter the resource name and pick a host family; don't paste a full URL. The
242
+ provider accepts the `openai.azure.com` and `services.ai.azure.com` families
243
+ only.
244
+
245
+ A Foundry resource also has a `cognitiveservices.azure.com` address, and that's
246
+ the one the Azure portal shows as the resource's endpoint. It isn't accepted
247
+ here—use the resource name with one of the two families above instead. The
248
+ resource is the same either way.
249
+
250
+ :::
251
+
252
+ ## Verify the provider
253
+
254
+ Once the deployment completes, send a chat completions request to your app's
255
+ [Universal API](./universal-api.mdx) URL—shown at the top of the
256
+ [app page](./apps.mdx)—with the app's API key as the bearer token and one of
257
+ your deployments:
258
+
259
+ ```bash
260
+ curl https://my-gateway-main-2e18f50.zuplo.app/config_fe0a04972d2848e0a94ae4b8bcd1497e/v1/chat/completions \
261
+ -H "Authorization: Bearer $ZUPLO_APP_API_KEY" \
262
+ -H "Content-Type: application/json" \
263
+ -d '{
264
+ "model": "azureai/my-gpt",
265
+ "messages": [{ "role": "user", "content": "Say hi" }]
266
+ }'
267
+ ```
268
+
269
+ The URL is a sample—replace it with your app's API URL plus
270
+ `/v1/chat/completions`, set `ZUPLO_APP_API_KEY` to the app's API key, and
271
+ substitute your own deployment name for `my-gpt`.
272
+
273
+ A `200` response confirms the provider works. If the request fails immediately
274
+ after you save the provider, the deployment may not have finished—retry before
275
+ debugging further.
276
+
277
+ ## Call OpenAI-compatible models
278
+
279
+ Use any OpenAI client with your app's URL plus `/v1` as the base URL and the
280
+ app's API key:
281
+
282
+ ```ts
283
+ import OpenAI from "openai";
284
+
285
+ const client = new OpenAI({
286
+ apiKey: process.env.ZUPLO_APP_API_KEY,
287
+ baseURL:
288
+ "https://my-gateway-main-2e18f50.zuplo.app/config_fe0a04972d2848e0a94ae4b8bcd1497e/v1",
289
+ });
290
+
291
+ const response = await client.chat.completions.create({
292
+ model: "azureai/my-gpt",
293
+ messages: [{ role: "user", content: "Summarize this ticket." }],
294
+ });
295
+ ```
296
+
297
+ The same call works with a Claude deployment—the gateway translates it to the
298
+ Messages API—within the [translation's parameter subset](#supported-endpoints).
299
+
300
+ ## Call Claude models on the Messages API
301
+
302
+ Claude deployments on a Foundry resource serve the native
303
+ [Anthropic Messages API](./universal-api.mdx#supported-endpoints) at
304
+ `/v1/messages`. With the Anthropic SDK, set `baseURL` to the app's URL _without_
305
+ `/v1`—the SDK appends `/v1/messages` itself—and pass the app's API key as
306
+ `authToken`, not `apiKey`:
307
+
308
+ ```ts
309
+ import Anthropic from "@anthropic-ai/sdk";
310
+
311
+ const client = new Anthropic({
312
+ baseURL:
313
+ "https://my-gateway-main-2e18f50.zuplo.app/config_fe0a04972d2848e0a94ae4b8bcd1497e",
314
+ authToken: process.env.ZUPLO_APP_API_KEY, // apiKey would send x-api-key, which the gateway ignores
315
+ });
316
+
317
+ const message = await client.messages.create({
318
+ model: "azureai/my-claude",
319
+ max_tokens: 1024,
320
+ messages: [{ role: "user", content: "Say hi" }],
321
+ });
322
+ ```
323
+
324
+ The gateway forwards the request body to Azure verbatim, so tool use, system
325
+ prompts and streaming work as they do against Anthropic directly.
326
+
327
+ `anthropic-beta` headers are the exception. The gateway forwards the
328
+ long-context and prompt-caching betas and drops every other value, because Azure
329
+ rejects an unrecognized beta by failing the whole request. A client asking for a
330
+ different beta still gets an answer, without that feature.
331
+
332
+ ## What the model field reports in responses
333
+
334
+ The `model` field in a response doesn't always say what you might expect, and
335
+ what it says depends on the model you called.
336
+
337
+ **Claude responses report your deployment name.** Every other response reports
338
+ Azure's own model ID, which usually carries a version date the short name
339
+ doesn't.
340
+
341
+ | Endpoint | Deployment `my-gpt` / `my-embed` / `my-claude` serves | `model` in the response |
342
+ | ---------------------- | ----------------------------------------------------- | ------------------------- |
343
+ | `/v1/chat/completions` | `gpt-4.1-mini` | `gpt-4.1-mini-2025-04-14` |
344
+ | `/v1/embeddings` | `text-embedding-3-small` | `text-embedding-3-small` |
345
+ | `/v1/messages` | `claude-haiku-4-5` | `my-claude` |
346
+ | `/v1/chat/completions` | `claude-haiku-4-5` | `my-claude` |
347
+
348
+ A Claude model reports your deployment name on both endpoints—the native
349
+ Messages API and the chat completions translation—because the gateway builds
350
+ those responses itself and echoes back the model you asked for.
351
+
352
+ One exception, if you stream Claude: the `message_start` event reports Azure's
353
+ ID (`claude-haiku-4-5-20251001`) rather than your deployment name, because the
354
+ gateway forwards each event as it arrives instead of rebuilding the response. So
355
+ the same request reports two different values depending on whether you streamed
356
+ it.
357
+
358
+ Treat this field as informational either way. If you need to know which
359
+ deployment served a request, use your own request metadata rather than parsing
360
+ this.
361
+
362
+ ## Troubleshooting
363
+
364
+ **A request fails with a deployment-not-found error.** The `model` value after
365
+ the provider prefix must be a deployment name in your Azure resource, not a
366
+ published model ID. Check the deployment list in the Azure portal under your
367
+ resource's **Model deployments**, and confirm the deployment finished
368
+ provisioning.
369
+
370
+ **A request fails saying the deployment isn't mapped.** The deployment exists in
371
+ Azure, but the gateway can't tell which model it serves, so it can't price the
372
+ request. Edit the provider, add the deployment under **Deployment Names**, and
373
+ pick its model. If the deployment is named after its model, selecting that model
374
+ is enough. The error lists the names that currently work.
375
+
376
+ **The dialog rejects your endpoint.** Enter the resource name, not a URL, and
377
+ pick `openai.azure.com` or `services.ai.azure.com`. A Foundry resource's
378
+ `cognitiveservices.azure.com` address isn't accepted—use the resource name with
379
+ one of those two families. Resource names are 2 to 63 characters of lowercase
380
+ letters, digits and hyphens, starting and ending with a letter or digit.
381
+
382
+ **The dialog rejects your API key.** The gateway takes a resource key from the
383
+ resource's **Keys and Endpoint** page. A Microsoft Entra ID access token is
384
+ rejected—it expires within hours, so the connection would break the same day.
385
+
386
+ **A Claude model returns an error on an Azure OpenAI resource.** Only Foundry
387
+ resources deploy Claude. Confirm the deployment exists and is Anthropic-format,
388
+ and see [Deploying Claude on Foundry](#deploying-claude-on-foundry) for the
389
+ requirements Azure applies to those deployments.
390
+
391
+ **A `400` error names `/v1/responses`.** The gateway doesn't serve the Responses
392
+ API for Azure AI. Use `/v1/chat/completions`.
393
+
394
+ **A model in the picker fails at request time.** The picker lists the full Azure
395
+ catalog, but a model only works once you deploy it in your resource, and model
396
+ availability varies by region and resource kind. Deploy the model, then map your
397
+ deployment name to it.
398
+
399
+ ## Next steps
400
+
401
+ - [AI Providers](./providers.mdx)—the capability matrix across every supported
402
+ provider.
403
+ - [Universal API](./universal-api.mdx)—the endpoints every app serves and how
404
+ model references work.
405
+ - [Managing Providers](./managing-providers.mdx)—edit models, keys, and the
406
+ endpoint, and understand when changes deploy.
407
+ - [AI Gateway Apps](./apps.mdx)—create the apps that call your Azure-backed
408
+ models.
409
+ - [Model Filtering policy](../policies/ai-gateway-model-filtering-v2-inbound.mdx)—control
410
+ which models each app can call.
@@ -53,9 +53,12 @@ To add a new AI provider to your Zuplo AI Gateway, follow these steps:
53
53
  [Zuplo Demo provider](./providers.mdx#zuplo-demo) asks for no API key—your
54
54
  gateway authenticates to the demo service itself,
55
55
  [Bedrock Mantle](./bedrock-mantle.mdx) additionally asks for an **AWS
56
- Region** and accepts only long-term Bedrock API keys, and
56
+ Region** and accepts only long-term Bedrock API keys,
57
57
  [Vertex AI](./vertex-ai.mdx) asks for a **Location** and a **Google Cloud
58
- Project ID** and takes a service account JSON key file instead of an API key.
58
+ Project ID** and takes a service account JSON key file instead of an API key,
59
+ and [Azure AI](./azure-ai.mdx) asks for an **Azure Resource Name** and a host
60
+ family, plus a mapping for any deployment named differently from the model it
61
+ serves.
59
62
 
60
63
  1. Select the model or models you want to use with this provider. The available
61
64
  models will depend on the selected provider. This can be changed later.
@@ -85,10 +88,11 @@ To modify an existing provider, open
85
88
  and click the **Edit** button next to the provider you want to modify.
86
89
 
87
90
  You can modify the API key and selected models for the provider—and, for
88
- [Bedrock Mantle](./bedrock-mantle.mdx), the AWS Region, or for
89
- [Vertex AI](./vertex-ai.mdx), the Location and Google Cloud Project ID. The
90
- **Provider Name** isn't editable: it's the routing address in every
91
- `providerName/model` reference, so renaming it would orphan each stored
91
+ [Bedrock Mantle](./bedrock-mantle.mdx), the AWS Region, for
92
+ [Vertex AI](./vertex-ai.mdx), the Location and Google Cloud Project ID, or for
93
+ [Azure AI](./azure-ai.mdx), the Azure Resource Name, host family, and deployment
94
+ mappings. The **Provider Name** isn't editable: it's the routing address in
95
+ every `providerName/model` reference, so renaming it would orphan each stored
92
96
  reference to this provider. After making your changes, click **Save** to apply
93
97
  them.
94
98
 
@@ -75,8 +75,9 @@ usage limits still apply.
75
75
  ### Multi-Provider Support
76
76
 
77
77
  Configure multiple LLM providers within a single AI Gateway project. Supported
78
- providers include OpenAI, Anthropic, Google, Mistral, xAI, Amazon Bedrock
79
- (through [Bedrock Mantle](./bedrock-mantle.mdx)), Google Cloud
78
+ providers include OpenAI, Anthropic, Google, Mistral, xAI, Microsoft Azure
79
+ (through [Azure AI](./azure-ai.mdx)), Amazon Bedrock (through
80
+ [Bedrock Mantle](./bedrock-mantle.mdx)), Google Cloud
80
81
  ([Vertex AI](./vertex-ai.mdx)), and OpenAI-compatible custom providers. See
81
82
  [AI Providers](./providers.mdx) for the full list of providers and supported
82
83
  capabilities. Apps reference models as `providerName/model`—for example
@@ -19,6 +19,8 @@ Zuplo currently supports the following AI providers:
19
19
  - Google
20
20
  - Mistral
21
21
  - xAI (Grok)
22
+ - [Azure AI](./azure-ai.mdx)—Azure OpenAI and Microsoft Foundry resources,
23
+ serving Claude and every Foundry model family from your own Azure subscription
22
24
  - [Bedrock Mantle](./bedrock-mantle.mdx)—Amazon Bedrock's compatible-APIs
23
25
  endpoint, serving Claude models and models from many other vendors
24
26
  - [Vertex AI](./vertex-ai.mdx)—Google Cloud's managed model platform, serving
@@ -36,6 +38,7 @@ The following capabilities are supported across providers:
36
38
  | Google | ✅ | ✅ | ❌ | ❌ |
37
39
  | Mistral | ✅ | ✅ | ❌ | ❌ |
38
40
  | xAI | ✅ | ✅ | ❌ | ❌ |
41
+ | Azure AI | ✅ | ✅ | ❌ | ✅ |
39
42
  | Bedrock Mantle | ✅ | ❌ | ✅ | ✅ |
40
43
  | Vertex AI | ✅ | ✅ | ❌ | ✅ |
41
44
  | Zuplo Demo | ✅ | ❌ | ❌ | ❌ |
@@ -51,6 +54,12 @@ Messages (plus chat completions through translation), while its other models
51
54
  serve chat completions and—per model—Responses. See
52
55
  [Using Bedrock Mantle](./bedrock-mantle.mdx#supported-endpoints-by-model-family).
53
56
 
57
+ Azure AI's Messages support needs a Microsoft Foundry resource, since Claude
58
+ can't be deployed on an Azure OpenAI resource. Azure also addresses models by
59
+ **deployment name** rather than by published model ID, so a deployment named
60
+ differently from the model it serves needs mapping for the gateway to price it.
61
+ See [Using Azure AI](./azure-ai.mdx#azure-serves-deployments-not-model-names).
62
+
54
63
  A custom provider must serve chat completions and embeddings under a `/v1` path
55
64
  segment on its API URL, and you enter that URL as an origin root—without the
56
65
  `/v1` suffix vendors usually publish. See
@@ -61,6 +70,32 @@ Apps reference a provider's models as `providerName/model`—for example
61
70
  name you give the provider configuration. See the
62
71
  [Universal API](./universal-api.mdx).
63
72
 
73
+ ## Azure AI
74
+
75
+ **Azure AI** serves models from your own Azure resource. One provider
76
+ configuration covers both kinds of resource, and which capabilities apply
77
+ depends on the resource and the model:
78
+
79
+ - An **Azure OpenAI** resource deploys OpenAI models and serves chat completions
80
+ and embeddings.
81
+ - A **Microsoft Foundry** resource serves those plus every Foundry model
82
+ family—Grok, DeepSeek, Llama, Mistral, Phi, Kimi and more—and serves **Claude
83
+ models** on the native Anthropic Messages API, with chat completions through
84
+ the gateway's translation.
85
+
86
+ Two things make Azure different from every other provider:
87
+
88
+ - **It addresses deployments, not published model IDs.** You name deployments
89
+ when you create them in Azure, and that name is what goes in the `model`
90
+ field. A deployment whose name differs from the model it serves needs a
91
+ mapping, or the gateway can't price its usage.
92
+ - **Its endpoint host embeds your resource name.** The provider dialog asks for
93
+ an **Azure Resource Name** and a host family rather than a full URL, and it
94
+ takes a resource key—not a Microsoft Entra ID token.
95
+
96
+ For prerequisites, setup steps, code examples, and troubleshooting, see
97
+ [Using Azure AI](./azure-ai.mdx).
98
+
64
99
  ## Bedrock Mantle
65
100
 
66
101
  **Bedrock Mantle** is Amazon Bedrock's compatible-APIs endpoint. One regional
@@ -63,9 +63,9 @@ list—so clients that can't set a model still work.
63
63
 
64
64
  ## Supported endpoints
65
65
 
66
- | Endpoint | Notes |
67
- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
68
- | `/v1/chat/completions` | Chat completions for every provider |
69
- | `/v1/embeddings` | Embeddings for every provider except Anthropic |
70
- | `/v1/responses` | OpenAI Responses API: OpenAI, and [Bedrock Mantle](./bedrock-mantle.mdx) OpenAI-compatible models that serve it |
71
- | `/v1/messages` | Anthropic Messages API: Anthropic, and the Claude models of [Bedrock Mantle](./bedrock-mantle.mdx) and [Vertex AI](./vertex-ai.mdx) |
66
+ | Endpoint | Notes |
67
+ | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
68
+ | `/v1/chat/completions` | Chat completions for every provider |
69
+ | `/v1/embeddings` | Embeddings for the providers marked in the [capability matrix](./providers.mdx#supported-providers) |
70
+ | `/v1/responses` | OpenAI Responses API: OpenAI, and [Bedrock Mantle](./bedrock-mantle.mdx) OpenAI-compatible models that serve it |
71
+ | `/v1/messages` | Anthropic Messages API: Anthropic, and the Claude models of [Azure AI](./azure-ai.mdx), [Bedrock Mantle](./bedrock-mantle.mdx) and [Vertex AI](./vertex-ai.mdx) |
@@ -4,7 +4,7 @@
4
4
  | --- | --- | --- | --- |
5
5
  | set-query-params-inbound | Add or Set Query Parameters | Adds or sets query parameters on the incoming request. | api-gateway |
6
6
  | set-headers-inbound | Add or Set Request Headers | Adds or sets headers on the incoming request. | api-gateway |
7
- | ai-gateway-auth-v2-inbound | AI Gateway Authentication | Authenticates requests to an AI Gateway endpoint with application API keys. Add this policy to an application's `inboundPolicyChain` to require a key for that app only, or place it on the route before the configuration executor to require a key for every application on the route. The policies that follow can read the authenticated application from `request.user` (`sub` is the application name, `data` its metadata), and the application's AI Gateway configuration takes effect for the request. Use `authHeader` and `authScheme` when clients send their app key somewhere other than the default `Authorization: Bearer` header. When the matched route captures an `app_id` path parameter (platform catch-all `/:app_id/(.*)`), this policy also requires `configuration.id === request.params.app_id` and returns 403 on mismatch. | ai-gateway |
7
+ | ai-gateway-auth-v2-inbound | AI Gateway Authentication | Authenticates requests to an AI Gateway endpoint with application API keys. Add this policy to an application's `inboundPolicyChain` to require a key for that app only, or place it on the route before the configuration executor to require a key for every application on the route. The policies that follow can read the authenticated application from `request.user` (`sub` is the application name, `data` its metadata), and the application's AI Gateway configuration takes effect for the request. Use `authHeader` and `authScheme` when clients send their app key somewhere other than the default `Authorization: Bearer` header. Enable `credentialPassthrough`, with the application key in a separate header, to forward the caller's `Authorization` header to the primary provider as its credential. When the matched route captures an `app_id` path parameter (platform catch-all `/:app_id/(.*)`), this policy also requires `configuration.id === request.params.app_id` and returns 403 on mismatch. | ai-gateway |
8
8
  | ai-gateway-configuration-executor-v2-inbound | AI Gateway Configuration Executor | Loads the app configuration for the request (when auth or the configuration loader has not already), runs the inbound policy chain from that configuration, and enforces limits inherited from parent teams or the gateway root. Place this policy on AI Gateway routes after optional authentication and optional `ai-gateway-configuration-loader-v2-inbound`. When either of those already populated the app-configuration channel, this policy reuses it. Otherwise it loads the configuration with the route's `app_id` path parameter. Applications select from policies pre-declared by the gateway. Applications without a `inboundPolicyChain`, or with an empty chain, run no application-selected policies. Entry options replace the declaration's options as a complete object; omit them to inherit the declaration, including environment-backed credentials. Each occurrence receives a private deep copy of its entry options, so a policy mutating its options cannot corrupt the cached app configuration. | ai-gateway |
9
9
  | ai-gateway-configuration-loader-v2-inbound | AI Gateway Configuration Loader | Loads the AI Gateway app configuration for the request into the request-scoped channel and does nothing else. Place this policy on AI Gateway routes before `ai-gateway-configuration-executor-v2-inbound` when you want configuration loading separated from chain execution. When `ai-gateway-auth-v2-inbound` already populated the channel, this policy reuses it. Otherwise it loads the configuration with the route's `app_id` path parameter. If this policy is omitted, the configuration executor still loads configuration itself before running the application chain. | ai-gateway |
10
10
  | ai-gateway-fallback-model-v2-inbound | AI Gateway Fallback Model | Adds failure and quota fallbacks to an existing AI Gateway model selection. Place this policy after AI Gateway Model Filtering. It never creates a model selection, so a misplaced policy cannot bypass filtering. | ai-gateway |
@@ -147,6 +147,64 @@ curl https://gateway.example.com/v1/chat/completions \
147
147
  }'
148
148
  ```
149
149
 
150
+ ## Pass through a caller's provider credential
151
+
152
+ Set `credentialPassthrough` to `true` and configure a separate header for the
153
+ Zuplo application key. For a bare key in `zp-gateway-api-key`, use these policy
154
+ options:
155
+
156
+ ```json
157
+ {
158
+ "credentialPassthrough": true,
159
+ "authHeader": "zp-gateway-api-key",
160
+ "authScheme": ""
161
+ }
162
+ ```
163
+
164
+ Send the provider credential in `Authorization`. `authHeader` and `authScheme`
165
+ remain authoritative; sending a special header does not enable passthrough or
166
+ override your configuration. The defaults remain `Authorization` and `Bearer`,
167
+ so enabling passthrough without configuring a separate header is a configuration
168
+ error.
169
+
170
+ For example:
171
+
172
+ ```bash
173
+ curl https://gateway.example.com/v1/chat/completions \
174
+ --header "zp-gateway-api-key: YOUR_ZUPLO_APP_KEY" \
175
+ --header "Authorization: Bearer YOUR_PROVIDER_CREDENTIAL" \
176
+ --header "Content-Type: application/json" \
177
+ --data '{
178
+ "model": "provider/model",
179
+ "messages": [{ "role": "user", "content": "Hello" }]
180
+ }'
181
+ ```
182
+
183
+ After the Zuplo application key succeeds, the policy moves the provider
184
+ credential into private request-scoped state and removes both authentication
185
+ headers from the mutable inbound request. The primary provider receives the
186
+ original `Authorization` value exactly as supplied, instead of its configured
187
+ credential. Retries of the primary use that same caller credential. Error and
188
+ quota fallbacks use their own configured credentials, never the caller's token.
189
+ Fallbacks therefore require configured credentials and can incur charges on the
190
+ gateway owner's provider account. On native create requests at `/v1/messages`
191
+ and `/v1/responses`, a compatible, explicitly configured backup runs only after
192
+ the primary exhausts upstream 429 retries. It uses the backup's configured
193
+ credential while preserving the native response and stream format; these
194
+ endpoints do not use timeout, transport-error, or 5xx fallback. Semantic-cache
195
+ policies skip passthrough requests so a cache hit cannot bypass upstream
196
+ authentication. ZuploDemo is excluded because it must continue to use the
197
+ gateway's deployment credential.
198
+
199
+ A missing, empty, or invalid dedicated key fails authentication; the gateway
200
+ never retries with `Authorization` as the Zuplo key. A missing or empty
201
+ `Authorization` also returns 401 in this mode.
202
+
203
+ With `credentialPassthrough` omitted or `false`, existing authentication
204
+ applies: `authHeader` and `authScheme` select the Zuplo key, and providers use
205
+ their configured credentials. Moving the gateway key to an arbitrary custom
206
+ header alone does not enable passthrough.
207
+
150
208
  ## Choose a cache duration
151
209
 
152
210
  `cacheTtlSeconds` controls how long an authentication result can be reused. The
@@ -163,6 +221,10 @@ A revoked key can continue to work until its cached result expires.
163
221
  - `cacheTtlSeconds`: Number of seconds to cache an authentication result.
164
222
  Defaults to `10` and must be at least `10`.
165
223
  - `authHeader`: Header containing the application key. Defaults to
166
- `Authorization`.
167
- - `authScheme`: Scheme before the key. Defaults to `Bearer`. Use `""` for a
168
- header containing the key without a scheme.
224
+ `Authorization`. With `credentialPassthrough`, use a different header, such as
225
+ `zp-gateway-api-key`.
226
+ - `authScheme`: Scheme before the key. Defaults to `Bearer`. Use `""` when the
227
+ header contains only the key.
228
+ - `credentialPassthrough`: Forward the caller's `Authorization` header to the
229
+ primary provider as its credential while Zuplo reads the application key from
230
+ `authHeader`. Defaults to `false`.
@@ -12,7 +12,7 @@
12
12
  "requiresAI": true,
13
13
  "policyType": "ai-gateway-auth-v2",
14
14
  "products": ["ai-gateway"],
15
- "description": "Authenticates requests to an AI Gateway endpoint with application API keys.\n\nAdd this policy to an application's `inboundPolicyChain` to require a key for that app only, or place it on the route before the configuration executor to require a key for every application on the route. The policies that follow can read the authenticated application from `request.user` (`sub` is the application name, `data` its metadata), and the application's AI Gateway configuration takes effect for the request. Use `authHeader` and `authScheme` when clients send their app key somewhere other than the default `Authorization: Bearer` header.\n\nWhen the matched route captures an `app_id` path parameter (platform catch-all `/:app_id/(.*)`), this policy also requires `configuration.id === request.params.app_id` and returns 403 on mismatch.",
15
+ "description": "Authenticates requests to an AI Gateway endpoint with application API keys.\n\nAdd this policy to an application's `inboundPolicyChain` to require a key for that app only, or place it on the route before the configuration executor to require a key for every application on the route. The policies that follow can read the authenticated application from `request.user` (`sub` is the application name, `data` its metadata), and the application's AI Gateway configuration takes effect for the request. Use `authHeader` and `authScheme` when clients send their app key somewhere other than the default `Authorization: Bearer` header. Enable `credentialPassthrough`, with the application key in a separate header, to forward the caller's `Authorization` header to the primary provider as its credential.\n\nWhen the matched route captures an `app_id` path parameter (platform catch-all `/:app_id/(.*)`), this policy also requires `configuration.id === request.params.app_id` and returns 403 on mismatch.",
16
16
  "deprecatedMessage": "",
17
17
  "required": ["handler"],
18
18
  "properties": {
@@ -36,6 +36,25 @@
36
36
  "description": "The options for this policy.",
37
37
  "additionalProperties": false,
38
38
  "required": [],
39
+ "if": {
40
+ "required": ["credentialPassthrough"],
41
+ "properties": {
42
+ "credentialPassthrough": {
43
+ "const": true
44
+ }
45
+ }
46
+ },
47
+ "then": {
48
+ "required": ["authHeader"],
49
+ "properties": {
50
+ "authHeader": {
51
+ "minLength": 1,
52
+ "not": {
53
+ "pattern": "^\\s*[Aa][Uu][Tt][Hh][Oo][Rr][Ii][Zz][Aa][Tt][Ii][Oo][Nn]\\s*$"
54
+ }
55
+ }
56
+ }
57
+ },
39
58
  "properties": {
40
59
  "cacheTtlSeconds": {
41
60
  "type": "number",
@@ -48,14 +67,19 @@
48
67
  "default": "Authorization",
49
68
  "x-show-example": false,
50
69
  "x-advanced": true,
51
- "description": "The name of the header with the key."
70
+ "description": "The request header that carries the Zuplo application key. When credentialPassthrough is enabled, use a header other than Authorization, such as zp-gateway-api-key."
52
71
  },
53
72
  "authScheme": {
54
73
  "type": "string",
55
74
  "default": "Bearer",
56
75
  "x-show-example": false,
57
76
  "x-advanced": true,
58
- "description": "The scheme used on the header."
77
+ "description": "The scheme that prefixes the key in the authHeader header, for example Bearer. Set to an empty string when the header contains only the key."
78
+ },
79
+ "credentialPassthrough": {
80
+ "type": "boolean",
81
+ "default": false,
82
+ "description": "Forward the caller's Authorization header to the primary provider as its credential. Zuplo reads the application key from authHeader, which must be a header other than Authorization."
59
83
  }
60
84
  }
61
85
  }
@@ -65,7 +89,8 @@
65
89
  "export": "AIGatewayAuthV2InboundPolicy",
66
90
  "module": "$import(@zuplo/runtime)",
67
91
  "options": {
68
- "cacheTtlSeconds": 10
92
+ "cacheTtlSeconds": 10,
93
+ "credentialPassthrough": false
69
94
  }
70
95
  }
71
96
  ]
@@ -27,13 +27,38 @@ The gateway owner remains in control:
27
27
  | `inboundPolicyChain` contains entries | Entries run in the listed order |
28
28
  | An entry has `enabled: false` | That entry is skipped |
29
29
 
30
- If a policy returns a response, that response is sent immediately and later
31
- entries do not run. If a chain is invalid, the request fails closed with an
32
- error that identifies the entry to fix.
30
+ If a policy returns a response, later entries do not run. Successful 2xx
31
+ responses still receive the budget check described below before they are sent.
32
+ Other responses, including authentication failures, remain unchanged. If a chain
33
+ is invalid, the request fails closed with an error that identifies the entry to
34
+ fix.
33
35
 
34
36
  This inbound executor does not run `outboundPolicyChain`. That field is reserved
35
37
  for an outbound configuration executor on the response pipeline.
36
38
 
39
+ ## Automatic budget enforcement
40
+
41
+ The executor enforces budgets configured on the application, its parent teams,
42
+ and the gateway. This does not require a Metering policy and applies even when
43
+ the application's policy chain is empty. An application cannot override a shared
44
+ budget with a higher limit of its own.
45
+
46
+ Request logs indicate when automatic budget checks run, including when the
47
+ application has no Metering policy. Warnings identify exceeded budgets, blocked
48
+ requests, fallback decisions, and checks that could not be completed.
49
+
50
+ When a budget blocks a provider request, the gateway uses the configured quota
51
+ fallback model or returns `429 Too Many Requests`. Successful responses from
52
+ policies, including Semantic Cache hits, are also subject to budgets. A blocked
53
+ response returns `429 Too Many Requests` without using a fallback model.
54
+ Warning-only budgets do not block responses. Authentication failures and other
55
+ non-2xx policy responses are preserved.
56
+
57
+ Place Metering before Semantic Cache to count cache hits toward request limits.
58
+ Budget changes can take time to take effect, and concurrent requests can exceed
59
+ a budget before further requests are blocked. If a budget check is unavailable,
60
+ requests can proceed unless a budget has already been determined to block them.
61
+
37
62
  ## Build an AI Gateway from scratch
38
63
 
39
64
  ### 1. Declare the policies
@@ -171,8 +196,7 @@ chain:
171
196
  }
172
197
  ```
173
198
 
174
- Routes that list only the executor keep working — the executor loads
175
- configuration when the channel is empty.
199
+ If you omit the loader, the executor loads the application configuration.
176
200
 
177
201
  Authentication is optional and placement controls its scope:
178
202
 
@@ -205,14 +229,19 @@ An application can inherit the options from `policies.json`. Include
205
229
  {
206
230
  "name": "ai-gateway-metering-v2-inbound",
207
231
  "options": {
208
- "limits": {
209
- "requests": {
210
- "daily": {
211
- "enabled": true,
212
- "limit": 1000
213
- }
232
+ "budgetRules": [
233
+ {
234
+ "budgetBy": "app",
235
+ "meters": [
236
+ {
237
+ "meter": "requests",
238
+ "period": "daily",
239
+ "value": 1000,
240
+ "action": "block"
241
+ }
242
+ ]
214
243
  }
215
- }
244
+ ]
216
245
  }
217
246
  },
218
247
  {
@@ -347,10 +376,10 @@ canonical forms when writing application configuration:
347
376
  | A key requiring brackets | `request.user.data["team-id"]` |
348
377
 
349
378
  The `expression` field contains the complete expression. Do not wrap it in an
350
- interpolation marker such as `${...}`. The runtime resolves it for every request
351
- after the application's inbound policy chain finishes. Updating an application's
352
- configuration changes which value later requests select without rebuilding or
353
- redeploying the gateway.
379
+ interpolation marker such as `${...}`. Expressions use values from the request
380
+ and context, including values set by earlier policies. Place policies that set
381
+ these values before Semantic Cache. You can change an expression in the
382
+ application configuration without rebuilding or redeploying the gateway.
354
383
 
355
384
  JSON encoding and expression syntax are separate. When writing raw JSON, escape
356
385
  the double quotes required by a bracket segment:
@@ -393,10 +422,8 @@ The selectable data model is:
393
422
  | `request` | `url`, `method`, `headers`, `user.sub`, `user.data`, `query`, `searchParams`, `params`, and the scalar Fetch request metadata `bodyUsed`, `cache`, `credentials`, `destination`, `integrity`, `keepalive`, `mode`, `redirect`, `referrer`, and `referrerPolicy` |
394
423
  | `context` | `contextId`, `requestId`, `custom`, `route`, and `incomingRequestProperties` |
395
424
 
396
- `request.body`, `context.log`, policy invocation methods, and every other host
397
- object are outside the selectable data model. The evaluator receives a
398
- plain-data snapshot of the selected root. It never receives the live request or
399
- context object.
425
+ Expressions cannot read the request body, call logging or policy methods, or
426
+ access properties outside this table.
400
427
 
401
428
  Expressions have these limits:
402
429
 
@@ -413,9 +440,8 @@ Expressions have these limits:
413
440
  It does not traverse arrays, class instances, inherited properties, or the
414
441
  property names `__proto__`, `constructor`, and `prototype`.
415
442
 
416
- Expressions observe values after the application's inbound policy chain
417
- finishes. An authentication policy can populate `request.user`. A custom policy
418
- earlier in the chain can derive a value and place it in a request header:
443
+ An authentication policy can populate `request.user`. A custom policy earlier in
444
+ the chain can derive a value and place it in a request header:
419
445
 
420
446
  ```ts
421
447
  import { ZuploContext, ZuploRequest } from "@zuplo/runtime";
@@ -446,14 +472,12 @@ value to well-formed NFC Unicode and ignores it when it exceeds 256 UTF-8 bytes
446
472
  or contains control characters, U+2028, or U+2029. A missing or invalid value
447
473
  does not contribute to the rule for that request.
448
474
 
449
- The exact stored expression is the rule identity and analytics dimension name.
450
- Store its authored bytes unchanged. Different accepted spellings, such as single
451
- quotes and double quotes, identify different rules even when they select the
452
- same value.
475
+ Use consistent spelling for each expression. Different spellings, such as single
476
+ quotes and double quotes, identify different budget rules even when they select
477
+ the same value.
453
478
 
454
- Malformed budget rules are logged and ignored individually. An unsupported
455
- expression is also logged and omitted from dimension capture and quota tokens;
456
- other valid rules continue to run.
479
+ Invalid budget rules and unsupported expressions are logged and skipped. Other
480
+ valid rules continue to apply.
457
481
 
458
482
  ## Write a custom policy for the chain
459
483
 
@@ -70,9 +70,12 @@ not override. A configured `fallback` replaces an existing backup and timeout; a
70
70
  configured `quotaFallback` replaces an existing quota fallback.
71
71
 
72
72
  Retry and timeout fallback is available for translated Chat Completions and
73
- Embeddings requests. Native `/v1/messages` and `/v1/responses` requests are
74
- passed through without retrying a backup. Quota fallback remains a separate,
75
- metering-driven path.
73
+ Embeddings requests. Native create requests at `/v1/messages` and
74
+ `/v1/responses` use a compatible completions backup only after the primary
75
+ exhausts its upstream 429 retries; they do not inherit `fallbackTimeoutSeconds`
76
+ or transport/5xx fallback. Responses management operations do not retry a backup
77
+ because stored response IDs belong to the provider that created them. Quota
78
+ fallback remains a separate, metering-driven path.
76
79
 
77
80
  ## Write your own fallback policy
78
81
 
@@ -5,13 +5,19 @@ budgets before the provider request runs. It meters spend, tokens, and requests.
5
5
  Budget rules can cover the whole application or each distinct value of an
6
6
  expression.
7
7
 
8
- When a limit is exceeded, the policy activates the model selection's
9
- `quotaFallback` when AI Gateway Fallback Model supplied one. Otherwise it
10
- returns `429 Too Many Requests`.
8
+ Application, team, and gateway budgets are enforced even when this policy is not
9
+ included in the application's policy chain.
11
10
 
12
- Place Metering after Model Filtering and Fallback Model so an exceeded budget
13
- can activate the quota fallback. Put policies that may answer early, such as
14
- Semantic Cache, after Metering so cache hits still count toward request limits.
11
+ Request logs indicate when automatic budget checks run and when a budget blocks
12
+ a request or selects a fallback model.
13
+
14
+ When a budget blocks a provider request, the gateway uses the configured quota
15
+ fallback model or returns `429 Too Many Requests`. Cached responses are also
16
+ subject to budgets: a blocked cache hit returns `429 Too Many Requests` without
17
+ using a fallback model.
18
+
19
+ Place Metering after Model Filtering and Fallback Model. Place it before
20
+ Semantic Cache so cache hits count toward request limits.
15
21
 
16
22
  ## Example
17
23
 
@@ -62,19 +68,26 @@ value of its expression. An action of `"warn"` notifies without blocking. An
62
68
  action of `"block"` activates the configured quota fallback or returns
63
69
  `429 Too Many Requests` when usage reaches the value.
64
70
 
65
- > **Budgets fail open by default.** When `throwOnFailure` is `false`, a metering
66
- > service failure lets the request proceed unmetered and no limit is checked.
67
- > Set it to `true` to reject the request instead.
68
-
69
71
  ## Team limits
70
72
 
71
- Budgets configured on this application govern only this app. Limits configured
72
- on a parent team or the gateway root are enforced centrally after the
73
- application's policy chain, whether or not this policy appears in that chain. An
74
- inherited limit activates the selected model's quota fallback when available and
75
- otherwise returns `429 Too Many Requests`.
73
+ Application budgets apply only to that application. Parent team and gateway
74
+ budgets apply across their applications, even when an application does not
75
+ include Metering. An application cannot override a shared budget with a higher
76
+ limit of its own.
77
+
78
+ For example, two applications that spend $40 and $10 exhaust their team's $50
79
+ monthly budget. An application outside that team does not share its budget.
80
+
81
+ Team policy templates provide defaults for application policies. To set a shared
82
+ team budget, configure the budget on the team itself.
83
+
84
+ ## Budget availability
85
+
86
+ Budget changes can take time to take effect. Usage is recorded asynchronously,
87
+ so concurrent requests can exceed a budget before further requests are blocked.
76
88
 
77
- An application cannot disable inherited enforcement through its policy chain. If
78
- the central hierarchical check is unavailable, the request proceeds. The
79
- policy's `throwOnFailure` option controls failures while checking or recording
80
- the app's own limits; it does not change inherited-limit behavior.
89
+ If a budget check is unavailable, requests can proceed unless a budget has
90
+ already been determined to block them. The `throwOnFailure` option controls
91
+ whether this policy rejects requests when its metering operations fail; it does
92
+ not change how application, team, or gateway `budgetRules` handle unavailable
93
+ checks.
@@ -253,3 +253,58 @@ Policy order determines precedence:
253
253
 
254
254
  Prefer one policy as the primary selector so the route's intent is easy to
255
255
  understand.
256
+
257
+ ## Model discovery
258
+
259
+ Authenticated `GET /v1/models` and `GET /v1/models/{providerName/model}` use the
260
+ same app URL as inference. Select the inference endpoint with the optional
261
+ `x-zuplo-models-endpoint` request header:
262
+
263
+ | Value | Eligible models | Response format |
264
+ | ------------------ | ----------------------------------------------------------------------------------------------- | --------------- |
265
+ | `chat-completions` | Active completion models supported by Chat Completions adapters, including existing translation | OpenAI |
266
+ | `messages` | Active completion models supported by native Messages passthrough | Anthropic |
267
+ | `responses` | Active completion models supported by the resolved Responses adapter | OpenAI |
268
+ | `embeddings` | Active embedding models supported by the resolved embedding adapter | OpenAI |
269
+
270
+ An explicit header wins. Otherwise, the presence of `anthropic-version` selects
271
+ `messages`; other requests default to `chat-completions`. Empty, unknown, and
272
+ multiple selector values return an actionable 400. The selector affects
273
+ discovery only. Embedding discovery is opt-in. Messages never translates to Chat
274
+ Completions.
275
+
276
+ Discovery applies the first Model Filtering policy's parsed rules, matching
277
+ inference's selection precedence. A capability omitted from that policy lists no
278
+ models. Without Model Filtering, discovery lists the eligible catalog. Inactive
279
+ models, unavailable adapters, and assignments without locally resolvable
280
+ credentials are excluded. List and retrieve use identical eligibility rules;
281
+ unknown, filtered-out, and wrong-endpoint IDs return the same 404.
282
+
283
+ IDs retain the exact model-name casing; only the provider assignment label is
284
+ normalized. OpenAI entries contain `id`, `object`, `created`, and `owned_by`.
285
+ `owned_by` means the provider assignment namespace, not the model's developer.
286
+ Anthropic entries contain `id`, `type`, `display_name`, and `created_at`, with
287
+ unknown required nullable metadata set to `null`. Unknown creation dates use the
288
+ epoch; optional unknown metadata is omitted.
289
+
290
+ OpenAI lists return every eligible model and ignore pagination parameters.
291
+ Anthropic lists default to 20 entries, accept `limit` from 1–1000, and accept
292
+ either `after_id` or `before_id` from a previous page. Follow `has_more` and
293
+ `last_id` for forward pagination. Invalid cursors return 400; restart without a
294
+ cursor.
295
+
296
+ Discovery uses the existing 60-second catalog cache. An uncached lookup,
297
+ including legacy fallback, has a two-second budget; failure returns a
298
+ sanitized 503. A successfully loaded catalog with no eligible models returns an
299
+ empty 200. Responses use `Cache-Control: private, no-store` and vary on both
300
+ selector headers. Only GET is supported. POST, PUT, PATCH, DELETE, and HEAD
301
+ return 405 with `Allow: GET`. OPTIONS is handled by the gateway’s existing CORS
302
+ preflight handler before app routing.
303
+
304
+ Authentication and general abuse protections still apply. Discovery skips prompt
305
+ inspection, semantic caching, and inference-budget blocking, and records one
306
+ baseline request. Eligibility describes the catalog, adapters, and declared
307
+ filtering rules; it does not guarantee upstream availability, provider
308
+ acceptance of credentials, or acceptance by prompt-dependent custom policies.
309
+ Existing Chat Completions translation does not promise full feature parity. Keep
310
+ a qualified model configured when a client's picker does not consume discovery.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zuplo",
3
- "version": "7.7.8",
3
+ "version": "7.7.9",
4
4
  "type": "module",
5
5
  "description": "The official Zuplo CLI for local development and platform management",
6
6
  "homepage": "https://zuplo.com/docs/cli/overview",
@@ -32,9 +32,9 @@
32
32
  "zuplo": "zuplo.js"
33
33
  },
34
34
  "dependencies": {
35
- "@zuplo/cli": "7.7.8",
36
- "@zuplo/core": "7.7.8",
37
- "@zuplo/runtime": "7.7.8",
38
- "@zuplo/test": "7.7.8"
35
+ "@zuplo/cli": "7.7.9",
36
+ "@zuplo/core": "7.7.9",
37
+ "@zuplo/runtime": "7.7.9",
38
+ "@zuplo/test": "7.7.9"
39
39
  }
40
40
  }