Azure OpenAI and Azure AI Foundry
Three Azure descriptors, three price namespaces, and one URL shape that needs a deployment name you chose rather than a model id.
On this page
Azure is the family most likely to be misconfigured, for two reasons. Its URL carries a deployment name you chose rather than a model id, so the path and the body disagree about what is being called. And “Azure” is not one product: three descriptors ship for it, over two URL shapes and three price namespaces that no single descriptor could represent.
This page sets up all three. You need the resource host, an API key, and, for the two deployment-path descriptors, the name of the deployment you created in the portal.
Which of the three you have
| You are calling | Descriptor | Host looks like | Prices under |
|---|---|---|---|
| a chat or embedding model you deployed in Azure OpenAI | azure_openai |
https://<resource>.openai.azure.com |
azure/, 216 priced keys |
| Llama, Mistral, DeepSeek or another catalog model in AI Foundry | azure_ai |
https://<project>.services.ai.azure.com |
azure_ai/, 91 priced keys |
a legacy instruct deployment on /completions |
azure_text |
https://<resource>.openai.azure.com |
azure_text/, 3 priced keys |
Those three figures count keys in the vendored litellm@2026-07-14#179affb price file. They are
statistics about a price table rather than claims about what the gateway can reach, and
tokentriage prices show reads the same file back to you.
Getting the descriptor wrong does not produce a wrong dollar. It produces no dollar: the model you
sent is not found under the other product’s namespace, and the row comes back model_unknown. The
pricing-key page covers how to read that.
Azure OpenAI
The URL is …/openai/deployments/{deployment}/{operation}?api-version=…. The descriptor holds that
template; you supply the two variables.
upstreams:
azure:
provider: azure_openai
base_url: https://my-resource.openai.azure.com
api_key_env: AZURE_OPENAI_API_KEY
params:
deployment: gpt5-prod
api_version: "2025-04-01-preview"
tiers:
- name: frontier
upstream: azure
model: gpt-4o
$ tokentriage validate --config tokentriage.yaml
mode: ledger
listen: 127.0.0.1:8787 admin: 127.0.0.1:9090
upstreams: 1
- azure https://my-resource.openai.azure.com
tiers: 1 (cheapest -> most capable)
0. frontier -> azure/gpt-4o
rules: 0
OK: configuration is valid
The upstream name, the host and the tier in that block are yours, read back from the file above. The
mode, listen and admin lines are the shipped defaults, printed because this configuration sets
none of them.
Authentication is the api-key header, not a bearer token. That is descriptor data, so
api_key_env is all you write. TokenTriage puts 10 of its 95 built-in provider descriptors on that
header scheme, and 3 of the 10 are Azure’s. What a test gates is one descriptor’s one endpoint
rather than a descriptor as a whole, and the six Azure cells are tabled below with the gate on each.
Quote the API version. 2025-04-01-preview is a string; YAML will read an unquoted
2025-04-01 as a date and the value will not be what you meant.
That is the fact that decides how you lay out tiers. If your cheap and frontier models live in two Azure deployments, you need two upstreams:
upstreams:
azure-mini:
provider: azure_openai
base_url: https://my-resource.openai.azure.com
api_key_env: AZURE_OPENAI_API_KEY
params: { deployment: gpt5-mini-prod, api_version: "2025-04-01-preview" }
azure-frontier:
provider: azure_openai
base_url: https://my-resource.openai.azure.com
api_key_env: AZURE_OPENAI_API_KEY
params: { deployment: gpt5-prod, api_version: "2025-04-01-preview" }
Endpoints
The endpoint vocabulary registers 17 kinds. azure_openai declares three of them: chat, embeddings
and the models listing. Azure sells image and audio surfaces as well, and no Azure descriptor
declares those. Image generation is declared by 7 descriptors and image editing by 3; audio
transcription by 4, speech by 3 and translation by 1. None of those declarers is an Azure
descriptor, so calls to those surfaces take the passthrough path: forwarded and ledgered, without a
cost figure. The count of descriptors declaring
each kind is generated, and the generator prints a zero rather than dropping the row, so a
registered but undeclared kind is a stated fact instead of a hole in a table.
Four of the 17 kinds are recognition-only across every descriptor, not only on Azure: files,
fine_tuning, vector_stores and realtime. A request to one of those is scoped by virtual key,
matchable by governance limits and cache rules, and stamped on the ledger row, which is worth
having. What it is not is a proxy for file content or for lifecycle state, so Azure OpenAI’s own
file and fine-tuning surfaces sit outside what this gateway models. The
endpoints page carries the whole vocabulary.
What a test gates, and what it does not
The generated matrix declares 186 provider-by-endpoint cells and prints an evidence level against each one, so you can tell a gated cell from a declared one. These are the six Azure cells:
| Cell | Evidence | Gate you can run |
|---|---|---|
azure_openai chat |
row-stamp | TestAzureCostGoldenDP2 |
azure_openai embeddings |
endpoint-stamp | TestAzureEmbeddingsCost |
azure_openai models |
declared (no e2e gate) | none |
azure_ai chat |
row-stamp | TestAzureAIFoundry |
azure_ai embeddings |
declared (no e2e gate) | none |
azure_text completions |
declared (no e2e gate) | none |
A row-stamp means a test drives a request through the descriptor against a mock shaped like that
provider and asserts the ledger row’s own provider and endpoint stamps. An endpoint-stamp is weaker:
the test pins the endpoint stamp and identifies one descriptor without asserting the provider stamp,
and the matrix marks it as such. declared (no e2e gate) means the descriptor declares the endpoint
and the engine will serve it, with no harness test driving that cell; the matrix prints those rather
than hiding them and calls them “not a support claim”. Those 186 are declared cells, so the total is
not a verified count and should not be read as one.
Legacy instruct deployments
azure_text is the third descriptor and the one fewest readers need. It is derived from the Azure
OpenAI one, so the api-key header, the required api-version query and the inbound handling
cannot drift apart from it. What differs is the path,
/openai/deployments/{deployment}/completions, the single endpoint kind it declares, and the price
namespace it binds.
upstreams:
azure-instruct:
provider: azure_text
base_url: https://my-resource.openai.azure.com
api_key_env: AZURE_OPENAI_API_KEY
params:
deployment: gpt35-instruct-prod
api_version: "2024-10-21"
tiers:
- name: legacy
upstream: azure-instruct
model: gpt-35-turbo-instruct
TokenTriage declares the completions kind on 3 of its 95 built-in provider descriptors:
azure_text, openai and text_completion_openai. No harness test drives any of those three
cells, so all three read declared (no e2e gate) in the matrix, and azure_text is one of the 10
descriptors carrying no gated cell at all. Read this section as a shape the engine will serve rather
than as a tested path. The capability claim on the descriptor is narrowed to match the endpoint:
tools, tool choice, response format, vision parts and reasoning effort are all absent from the
legacy request schema, so the descriptor claims none of them.
All 3 keys under azure_text/ are gpt-35-turbo-instruct variants, and Microsoft’s model
retirement schedule, read on 2026-08-23 while the descriptor was written, lists no instruct row at
all. Absence from a schedule is not a retirement notice, so nothing here says the deployment is gone
and nothing says it is still available; the descriptor carries a dated note to look again on
2026-11-01. The /completions path itself stays in Azure’s stable data-plane specification either
way.
Azure AI Foundry
Foundry uses one endpoint for every model in its catalog, so there is no deployment in the path, only an API version.
upstreams:
foundry:
provider: azure_ai
base_url: https://my-project.services.ai.azure.com
api_key_env: AZURE_AI_API_KEY
params:
api_version: "2024-05-01-preview"
tiers:
- name: cheap
upstream: foundry
model: deepseek-r1
Requests go to /models/chat/completions on the OpenAI wire, with the api-key header. The
descriptor declares chat and embeddings.
The price difference between the two products is the argument for keeping them apart. In the
committed snapshot, deepseek-r1 through Foundry resolves at $1.35 per million input tokens and
$5.40 per million output; the same model bought direct from DeepSeek resolves at $0.55 and $2.19.
Both are correct figures for what you bought, out of one price file.
Reaching Azure from a client that is not OpenAI-shaped
All three Azure descriptors ride the openai wire driver, so an Anthropic-format client reaches an
Azure upstream through translation, streaming and not. The grid of which client format reaches which
upstream dialect is drawn once, on the
dialect matrix; what a translated row records about the
fields that could not cross is on
translation loss.
Checking it worked
Point a client at the proxy, send one request, and read cost.pricing_key on the row. It should
begin azure/, azure_ai/ or azure_text/ according to which product you configured.
You can check a rate first without sending anything:
$ tokentriage prices show azure/gpt-4o
model: azure/gpt-4o
resolved: azure/azure/gpt-4o
provider: azure
snapshot: litellm@2026-07-14#179affb
source: litellm fetched 2026-07-14 generator pricegen v2
upstream_sha256: 179affb29103fed8b02a83e22e93209c2c65a393cbf9dd785dbaec0f8748d6f7
RATE USD / TOKEN USD / 1M TOKENS
input 2.5e-06 $2.5000
output 1e-05 $10.0000
cache_read 1.25e-06 $1.2500
cache_write (fallback = input) $2.5000
reasoning (fallback = output) $10.0000
Every line there comes from the price file compiled into the binary, so your copy prints the same figures for as long as you run the same build. The snapshot id and the digest are printed to tell you which build that was.
Why the doubled azure/ in the resolved key
The vendored catalog namespaces each provider’s models by the provider prefix, and Azure’s own
published model ids already carry azure/. So the stored key is azure/azure/gpt-4o, and two
different lookups reach it.
The proxy scopes its search to the descriptor’s catalog prefix, which is what keeps an Azure request
from picking up OpenAI’s direct rate: a model absent from the azure/ namespace resolves
model_unknown rather than borrowing a neighbour. prices show takes no prefix argument, so it
matches your argument against the tail of every key in the file and prefers the shortest hit. That
is why azure/gpt-4o finds the doubled key, and why the azure_text rows want their stored key in
full: prices show azure_text/azure/gpt-35-turbo-instruct resolves, and
azure_text/gpt-35-turbo-instruct does not.
What can go wrong
| Symptom | Cause | Fix |
|---|---|---|
"deployment_name" is not a template variable of provider "azure_openai" (allowed: api_version, deployment) |
wrong key name | the message names both allowed keys |
requires template variable "api_version" (missing) |
a params: block missing one |
both are required for azure_openai and for azure_text |
| provider returns 404 | the deployment name does not exist on that resource | check the portal; the path carries it |
row is model_unknown |
the model is not in this product’s namespace | check which of the three descriptors you configured |
| API version reads as a date | unquoted YAML | quote it |
A validation failure prints twice. The command returns the error and the binary prints it again on its way out, so a doubled message is the tool talking rather than two separate faults.