Search

↑↓ to move↵ to openesc to closetop 6 documentation results

Book a demoTry TokenTriage

In development · Explore the product preview. Join the launch list or book a walkthrough.

Browse docs

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.

How-to guideLast updated

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.