Skip to content

fix(azure_ai): add passthrough config so router-model relays reach the deployment's own endpoint - #39863

Open
mateo-berri wants to merge 12 commits into
litellm_internal_stagingfrom
litellm_lit_7022_azure_ai_passthrough_config
Open

fix(azure_ai): add passthrough config so router-model relays reach the deployment's own endpoint#39863
mateo-berri wants to merge 12 commits into
litellm_internal_stagingfrom
litellm_lit_7022_azure_ai_passthrough_config

Conversation

@mateo-berri

@mateo-berri mateo-berri commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • /azure_ai/<router model>/<native path> relays failed on every call with HTTP 500
  • The proxy had no Azure AI passthrough config, so the relay never reached Foundry
  • An OpenAI-family model on a Foundry resource was mis-routed as plain Azure OpenAI

How it solves it:

  • Adds AzureAIPassthroughConfig: strips the router-model prefix, forwards to the deployment's api_base
  • Sends the deployment's own key as api-key on Foundry/Azure OpenAI hosts, Bearer elsewhere, Entra as fallback
  • The router keeps a model's own provider prefix for generic SDK calls, so azure_ai/gpt-* deployments keep working on Foundry and Azure OpenAI hosts alike
  • Streaming chat relays now build the usage they log, counting prompt tokens from the relayed messages (remote high-detail images are charged at the high-detail upper bound, nothing is downloaded) when Azure sends no usage chunk; relays keep the JSON body under a charset Content-Type, return the upstream status instead of a 500, and forward the caller's api-version
  • Chat/completions relays still get cost logging through the existing Azure passthrough logic; non-chat relays (Cohere Parse) now log the relayed body too, so a deployment with pricing gets a spend row
  • Relays land on the Foundry root even when api_base ends in /models, the deployment's api_version fills in when the caller sends none, and an azure_ai/gpt-* deployment on a <resource>.openai.azure.com host relays to /openai/deployments/...
  • The router rewrites the model group as a whole path segment, so a group named gpt no longer corrupts a gpt-5.4-mini deployment segment

User Flow

Before: a developer relaying Cohere Parse through the gateway's Azure AI passthrough gets a 500 on every call, so no document ever gets parsed

  1. The proxy admin adds a Foundry deployment to the config, model_name: Cohere-parse-v5 with model: azure_ai/Cohere-parse-v5, the resource's api_base and api_key, and restarts the proxy
  2. The developer sends POST https://litellm-domain/azure_ai/Cohere-parse-v5/providers/cohere/v2/parse with their virtual key and a JSON body carrying the document as a data URI plus "output_format": "markdown"
  3. They get HTTP 500 {"error":{"message":"Internal server error","type":"internal_server_error"}} and nothing reaches Azure
  4. A chat deployment on the same kind of resource fails the same way: POST https://litellm-domain/azure_ai/foundry-gpt-5.4-mini/models/chat/completions?api-version=2024-05-01-preview returns the same 500, streaming or not, and so does the same path under /azure/
  5. Only the unified route works: POST https://litellm-domain/v1/chat/completions with "model": "foundry-gpt-5.4-mini" returns 200

After: the same relay reaches the deployment's own Foundry endpoint with its own credential and returns the parsed document

  1. The proxy admin adds the same Foundry deployment to the config and restarts the proxy
  2. The developer sends the same POST https://litellm-domain/azure_ai/Cohere-parse-v5/providers/cohere/v2/parse with the same body
  3. They get HTTP 200 with Cohere's parse response: the document's text as markdown pages plus usage
  4. POST https://litellm-domain/azure_ai/foundry-gpt-5.4-mini/models/chat/completions?api-version=2024-05-01-preview returns 200 with the model's completion, "stream": true streams SSE data: chunks, and the same path under /azure/ works too
  5. Both chat calls show up under GET https://litellm-domain/spend/logs?api_key= with a spend greater than zero, a parse call shows up once its deployment carries input_cost_per_token/output_cost_per_token (Cohere Parse has no built-in cost entry yet, LIT-6992), and GET https://litellm-domain/key/info shows the total
  6. A typo in the native path comes back with Azure's own 404 body instead of a 500, and a Content-Type: application/json; charset=utf-8 request is relayed with its body
  7. A deployment whose api_base ends in /models, one pointing at <resource>.openai.azure.com with api_version set, and one whose router name is a prefix of the deployment name (gpt for gpt-5.4-mini) all relay with HTTP 200 on the same routes
  8. POST https://litellm-domain/v1/chat/completions with "model": "foundry-gpt-5.4-mini" still returns 200

Relevant issues

Linear ticket

Resolves LIT-7022

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • The handful of test files covering my change pass locally, e.g. uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*, make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

Screenshots / Proof of Fix

Both legs ran the same proxy config with --num_workers 2 (two uvicorn workers, one process) against real Azure resources: a Cohere Parse v5 Foundry deployment and a gpt-5.4-mini deployment reached both through its Foundry host and through the same resource's <resource>.openai.azure.com host. Before ran at 2151dcb, the litellm_internal_staging tip when the run started (the merge base, aea5358, has no Azure AI passthrough config either), on port 59723 without a database, After at this PR's tip (4706ace, which merges litellm_internal_staging at aea5358) on port 46237 with a local Postgres behind DATABASE_URL so spend logs could be read back. The parse payload (lit7022_parse_req.json) is a one-page PNG as a data URI

model_list:
  - model_name: Cohere-parse-v5
    litellm_params:
      model: azure_ai/Cohere-parse-v5
      api_base: os.environ/AZURE_DEVIN_FOUNDRY_QA_API_BASE
      api_key: os.environ/AZURE_DEVIN_FOUNDRY_QA_API_KEY
  - model_name: parse-priced
    litellm_params:
      model: azure_ai/Cohere-parse-v5
      api_base: os.environ/AZURE_DEVIN_FOUNDRY_QA_API_BASE
      api_key: os.environ/AZURE_DEVIN_FOUNDRY_QA_API_KEY
      input_cost_per_token: 0
      output_cost_per_token: 0
  - model_name: foundry-gpt-5.4-mini
    litellm_params:
      model: azure_ai/gpt-5.4-mini
      api_base: os.environ/AZURE_FOUNDRY_API_BASE
      api_key: os.environ/AZURE_FOUNDRY_API_KEY
  - model_name: foundry-versioned
    litellm_params:
      model: azure_ai/gpt-5.4-mini
      api_base: os.environ/AZURE_FOUNDRY_API_BASE
      api_key: os.environ/AZURE_FOUNDRY_API_KEY
      api_version: 2024-05-01-preview
  - model_name: foundry-models-suffix
    litellm_params:
      model: azure_ai/gpt-5.4-mini
      api_base: os.environ/LIT7022_FOUNDRY_MODELS_BASE
      api_key: os.environ/AZURE_FOUNDRY_API_KEY
  - model_name: gpt
    litellm_params:
      model: azure_ai/gpt-5.4-mini
      api_base: os.environ/AZURE_FOUNDRY_API_BASE
      api_key: os.environ/AZURE_FOUNDRY_API_KEY
  - model_name: aoai-mini
    litellm_params:
      model: azure_ai/gpt-5.4-mini
      api_base: os.environ/LIT7022_AOAI_BASE
      api_key: os.environ/AZURE_FOUNDRY_API_KEY
      api_version: "2024-10-21"
general_settings:
  master_key: sk-lit7022
{"model": "Cohere-parse-v5", "document": {"type": "image_url", "image_url": "data:image/png;base64,..."}, "output_format": "markdown"}

Before (2151dcb)

1. Cohere Parse relay (Foundry-native path)

  1. Command
curl -sS -X POST http://localhost:59723/azure_ai/Cohere-parse-v5/providers/cohere/v2/parse -H 'Authorization: Bearer sk-lit7022' -H 'Content-Type: application/json' -d @lit7022_parse_req.json -w '
HTTP %{http_code}
'
  1. Observed output
{"error":{"message":"Internal server error","type":"internal_server_error"}}
HTTP 500

2. Chat relay via the model-inference path

  1. Command
curl -sS -X POST 'http://localhost:59723/azure_ai/foundry-gpt-5.4-mini/models/chat/completions?api-version=2024-05-01-preview' -H 'Authorization: Bearer sk-lit7022' -H 'Content-Type: application/json' -d '{"model":"foundry-gpt-5.4-mini","messages":[{"role":"user","content":"Say hi in three words"}]}' -w '
HTTP %{http_code}
'
  1. Observed output
{"error":{"message":"Internal server error","type":"internal_server_error"}}
HTTP 500

3. Chat relay via the Azure OpenAI deployments path

  1. Command
curl -sS -X POST 'http://localhost:59723/azure_ai/foundry-gpt-5.4-mini/openai/deployments/foundry-gpt-5.4-mini/chat/completions?api-version=2024-10-21' -H 'Authorization: Bearer sk-lit7022' -H 'Content-Type: application/json' -d '{"model":"foundry-gpt-5.4-mini","messages":[{"role":"user","content":"Say hi in three words"}]}' -w '
HTTP %{http_code}
'
  1. Observed output
{"error":{"message":"Internal server error","type":"internal_server_error"}}
HTTP 500

4. Streaming chat relay

  1. Command
curl -sS -N -X POST 'http://localhost:59723/azure_ai/foundry-gpt-5.4-mini/models/chat/completions?api-version=2024-05-01-preview' -H 'Authorization: Bearer sk-lit7022' -H 'Content-Type: application/json' -d '{"model":"foundry-gpt-5.4-mini","messages":[{"role":"user","content":"Say hi in three words"}],"stream":true}' -o "lit7022_qa_before.log.stream" -w 'HTTP %{http_code}
'; head -c 700 "lit7022_qa_before.log.stream"; echo; echo '... (truncated to the first 700 bytes)'
  1. Observed output
HTTP 500
{"error":{"message":"Internal server error","type":"internal_server_error"}}
... (truncated to the first 700 bytes)

5. Same router model through /azure/ (shares the route)

  1. Command
curl -sS -X POST 'http://localhost:59723/azure/foundry-gpt-5.4-mini/models/chat/completions?api-version=2024-05-01-preview' -H 'Authorization: Bearer sk-lit7022' -H 'Content-Type: application/json' -d '{"model":"foundry-gpt-5.4-mini","messages":[{"role":"user","content":"Say hi in three words"}]}' -w '
HTTP %{http_code}
'
  1. Observed output
{"error":{"message":"Internal server error","type":"internal_server_error"}}
HTTP 500

6. Regression: unified /v1/chat/completions on the same azure_ai deployment

  1. Command
curl -sS -X POST http://localhost:59723/v1/chat/completions -H 'Authorization: Bearer sk-lit7022' -H 'Content-Type: application/json' -d '{"model":"foundry-gpt-5.4-mini","messages":[{"role":"user","content":"Say hi in three words"}]}' -w '
HTTP %{http_code}
'
  1. Observed output
{"id":"chatcmpl-EKgzvk8kLJkCqyrfSTndhWoBo5EmF","created":1788600551,"model":"foundry-gpt-5.4-mini","object":"chat.completion","choices":[{"finish_reason":"stop","index":0,"message":{"content":"Hi there, friend.","role":"assistant","provider_specific_fields":{"refusal":null},"annotations":[]},"provider_specific_fields":{"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"protected_material_code":{"detected":false,"filtered":false},"protected_material_text":{"detected":false,"filtered":false},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}}}}],"usage":{"completion_tokens":9,"prompt
... (truncated, 1531 chars total)
HTTP 200

7. Relay to an Azure OpenAI host (api_base ends in .openai.azure.com, api_version on the deployment)

  1. Command
curl -sS -X POST 'http://localhost:59723/azure_ai/aoai-mini/openai/deployments/gpt-5.4-mini/chat/completions' -H 'Authorization: Bearer sk-lit7022' -H 'Content-Type: application/json' -d '{"model":"aoai-mini","messages":[{"role":"user","content":"Say hi in three words"}]}' -w '
HTTP %{http_code}
'
  1. Observed output
{"error":{"message":"Internal server error","type":"internal_server_error"}}
HTTP 500

8. Deployment whose api_base already ends in /models

  1. Command
curl -sS -X POST 'http://localhost:59723/azure_ai/foundry-models-suffix/models/chat/completions?api-version=2024-05-01-preview' -H 'Authorization: Bearer sk-lit7022' -H 'Content-Type: application/json' -d '{"model":"foundry-models-suffix","messages":[{"role":"user","content":"Say hi in three words"}]}' -w '
HTTP %{http_code}
'
  1. Observed output
{"error":{"message":"Internal server error","type":"internal_server_error"}}
HTTP 500

9. Router model name that is a prefix of the deployment name (gpt vs gpt-5.4-mini)

  1. Command
curl -sS -X POST 'http://localhost:59723/azure_ai/gpt/openai/deployments/gpt-5.4-mini/chat/completions?api-version=2024-10-21' -H 'Authorization: Bearer sk-lit7022' -H 'Content-Type: application/json' -d '{"model":"gpt","messages":[{"role":"user","content":"Say hi in three words"}]}' -w '
HTTP %{http_code}
'
  1. Observed output
{"error":{"message":"Internal server error","type":"internal_server_error"}}
HTTP 500

10. Deployment api_version fills in when the caller sends none

  1. Command
curl -sS -X POST 'http://localhost:59723/azure_ai/foundry-versioned/models/chat/completions' -H 'Authorization: Bearer sk-lit7022' -H 'Content-Type: application/json' -d '{"model":"foundry-versioned","messages":[{"role":"user","content":"Say hi in three words"}]}' -w '
HTTP %{http_code}
'
  1. Observed output
{"error":{"message":"Internal server error","type":"internal_server_error"}}
HTTP 500

After (4706ace)

1. Cohere Parse relay (Foundry-native path)

  1. Command
curl -sS -X POST http://localhost:46237/azure_ai/Cohere-parse-v5/providers/cohere/v2/parse -H 'Authorization: Bearer sk-lit7022' -H 'Content-Type: application/json' -d @lit7022_parse_req.json -w '
HTTP %{http_code}
'
  1. Observed output
{"id":"665a5509-cf07-41fd-a12a-1f32367a258d","pages":[{"index":0,"type":"markdown","markdown":{"content":"![The image displays three distinct, roughly circular or oval shapes arranged in a triangular formation. The top shape is the largest and colored dark green. Below it to the left is a smaller shape in orange-red, and below it to the right is a medium-sized shape in purple. There are no visible labels, axes, text, or data annotations within the image. The background is plain white, and no additional context or scale is provided.](img-0)","images":[{"id":"img-0","description":"The image displays three distinct, roughly circular or oval shapes arranged in a triangular formation. The top sha
... (truncated, 1290 chars total)
HTTP 200

2. Chat relay via the model-inference path

  1. Command
curl -sS -X POST 'http://localhost:46237/azure_ai/foundry-gpt-5.4-mini/models/chat/completions?api-version=2024-05-01-preview' -H 'Authorization: Bearer sk-lit7022' -H 'Content-Type: application/json' -d '{"model":"foundry-gpt-5.4-mini","messages":[{"role":"user","content":"Say hi in three words"}]}' -w '
HTTP %{http_code}
'
  1. Observed output
{"choices":[{"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"protected_material_code":{"detected":false,"filtered":false},"protected_material_text":{"detected":false,"filtered":false},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}},"finish_reason":"stop","index":0,"logprobs":null,"message":{"annotations":[],"content":"Hi there, friend.","refusal":null,"role":"assistant"}}],"created":1788605341,"id":"chatcmpl-EKiFB8NevCz5GcWRZ4fvu7pXShWKz","model":"gpt-5.4-mini-2026-03-17","object":"chat.completion","prompt_filter_results":[{"prompt_index":0,"content_filter_results":{"hate":{
... (truncated, 1520 chars total)
HTTP 200

3. Chat relay via the Azure OpenAI deployments path

  1. Command
curl -sS -X POST 'http://localhost:46237/azure_ai/foundry-gpt-5.4-mini/openai/deployments/foundry-gpt-5.4-mini/chat/completions?api-version=2024-10-21' -H 'Authorization: Bearer sk-lit7022' -H 'Content-Type: application/json' -d '{"model":"foundry-gpt-5.4-mini","messages":[{"role":"user","content":"Say hi in three words"}]}' -w '
HTTP %{http_code}
'
  1. Observed output
{"choices":[{"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"protected_material_code":{"detected":false,"filtered":false},"protected_material_text":{"detected":false,"filtered":false},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}},"finish_reason":"stop","index":0,"logprobs":null,"message":{"annotations":[],"content":"Hi there, friend.","refusal":null,"role":"assistant"}}],"created":1788605342,"id":"chatcmpl-EKiFC9puN8u5CcOIoEDKFw3NuVrfH","model":"gpt-5.4-mini-2026-03-17","object":"chat.completion","prompt_filter_results":[{"prompt_index":0,"content_filter_results":{"hate":{
... (truncated, 1520 chars total)
HTTP 200

4. Streaming chat relay

  1. Command
curl -sS -N -X POST 'http://localhost:46237/azure_ai/foundry-gpt-5.4-mini/models/chat/completions?api-version=2024-05-01-preview' -H 'Authorization: Bearer sk-lit7022' -H 'Content-Type: application/json' -d '{"model":"foundry-gpt-5.4-mini","messages":[{"role":"user","content":"Say hi in three words"}],"stream":true}' -o "lit7022_qa_after.log.stream" -w 'HTTP %{http_code}
'; head -c 700 "lit7022_qa_after.log.stream"; echo; echo '... (truncated to the first 700 bytes)'
  1. Observed output
HTTP 200
data: {"choices":[],"created":0,"id":"","model":"","object":"","prompt_filter_results":[{"prompt_index":0,"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"jailbreak":{"detected":false,"filtered":false},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}}}]}

data: {"choices":[{"content_filter_results":{},"delta":{"content":"","refusal":null,"role":"assistant"},"finish_reason":null,"index":0,"logprobs":null}],"created":1788605344,"id":"chatcmpl-EKiFENTlFPP7C1Sb73LMvSOOvVpC3","model":"gpt-5.4-mini-2026-03-17","obfuscation":"","object":"chat.completion.chunk","service_tier":
... (truncated, 748 chars total)

5. Same router model through /azure/ (shares the route)

  1. Command
curl -sS -X POST 'http://localhost:46237/azure/foundry-gpt-5.4-mini/models/chat/completions?api-version=2024-05-01-preview' -H 'Authorization: Bearer sk-lit7022' -H 'Content-Type: application/json' -d '{"model":"foundry-gpt-5.4-mini","messages":[{"role":"user","content":"Say hi in three words"}]}' -w '
HTTP %{http_code}
'
  1. Observed output
{"choices":[{"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"protected_material_code":{"detected":false,"filtered":false},"protected_material_text":{"detected":false,"filtered":false},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}},"finish_reason":"stop","index":0,"logprobs":null,"message":{"annotations":[],"content":"Hi there, friend.","refusal":null,"role":"assistant"}}],"created":1788605345,"id":"chatcmpl-EKiFF25YeyWOg3QvpfyA0zMscD1bR","model":"gpt-5.4-mini-2026-03-17","object":"chat.completion","prompt_filter_results":[{"prompt_index":0,"content_filter_results":{"hate":{
... (truncated, 1519 chars total)
HTTP 200

6. Regression: unified /v1/chat/completions on the same azure_ai deployment

  1. Command
curl -sS -X POST http://localhost:46237/v1/chat/completions -H 'Authorization: Bearer sk-lit7022' -H 'Content-Type: application/json' -d '{"model":"foundry-gpt-5.4-mini","messages":[{"role":"user","content":"Say hi in three words"}]}' -w '
HTTP %{http_code}
'
  1. Observed output
{"id":"chatcmpl-EKiFH7FliWqC5lLm1hJDQPioy0j87","created":1788605347,"model":"foundry-gpt-5.4-mini","object":"chat.completion","choices":[{"finish_reason":"stop","index":0,"message":{"content":"Hi there, friend.","role":"assistant","provider_specific_fields":{"refusal":null},"annotations":[]},"provider_specific_fields":{"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"protected_material_code":{"detected":false,"filtered":false},"protected_material_text":{"detected":false,"filtered":false},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}}}}],"usage":{"completion_tokens":9,"prompt
... (truncated, 1532 chars total)
HTTP 200

7. Relay to an Azure OpenAI host (api_base ends in .openai.azure.com, api_version on the deployment)

  1. Command
curl -sS -X POST 'http://localhost:46237/azure_ai/aoai-mini/openai/deployments/gpt-5.4-mini/chat/completions' -H 'Authorization: Bearer sk-lit7022' -H 'Content-Type: application/json' -d '{"model":"aoai-mini","messages":[{"role":"user","content":"Say hi in three words"}]}' -w '
HTTP %{http_code}
'
  1. Observed output
{"choices":[{"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"protected_material_code":{"detected":false,"filtered":false},"protected_material_text":{"detected":false,"filtered":false},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}},"finish_reason":"stop","index":0,"logprobs":null,"message":{"annotations":[],"content":"Hi there, friend.","refusal":null,"role":"assistant"}}],"created":1788605348,"id":"chatcmpl-EKiFIr3o5QmjSnhrYuvCpJtVPqHFO","model":"gpt-5.4-mini-2026-03-17","object":"chat.completion","prompt_filter_results":[{"prompt_index":0,"content_filter_results":{"hate":{
... (truncated, 1519 chars total)
HTTP 200

8. Deployment whose api_base already ends in /models

  1. Command
curl -sS -X POST 'http://localhost:46237/azure_ai/foundry-models-suffix/models/chat/completions?api-version=2024-05-01-preview' -H 'Authorization: Bearer sk-lit7022' -H 'Content-Type: application/json' -d '{"model":"foundry-models-suffix","messages":[{"role":"user","content":"Say hi in three words"}]}' -w '
HTTP %{http_code}
'
  1. Observed output
{"choices":[{"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"protected_material_code":{"detected":false,"filtered":false},"protected_material_text":{"detected":false,"filtered":false},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}},"finish_reason":"stop","index":0,"logprobs":null,"message":{"annotations":[],"content":"Hi there!","refusal":null,"role":"assistant"}}],"created":1788605351,"id":"chatcmpl-EKiFLSQjPgickZIJBQ1ZoupTDMPGh","model":"gpt-5.4-mini-2026-03-17","object":"chat.completion","prompt_filter_results":[{"prompt_index":0,"content_filter_results":{"hate":{"filtere
... (truncated, 1511 chars total)
HTTP 200

9. Router model name that is a prefix of the deployment name (gpt vs gpt-5.4-mini)

  1. Command
curl -sS -X POST 'http://localhost:46237/azure_ai/gpt/openai/deployments/gpt-5.4-mini/chat/completions?api-version=2024-10-21' -H 'Authorization: Bearer sk-lit7022' -H 'Content-Type: application/json' -d '{"model":"gpt","messages":[{"role":"user","content":"Say hi in three words"}]}' -w '
HTTP %{http_code}
'
  1. Observed output
{"choices":[{"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"protected_material_code":{"detected":false,"filtered":false},"protected_material_text":{"detected":false,"filtered":false},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}},"finish_reason":"stop","index":0,"logprobs":null,"message":{"annotations":[],"content":"Hi there, friend.","refusal":null,"role":"assistant"}}],"created":1788605352,"id":"chatcmpl-EKiFMmSUZVPdjZLwZ6tXp4cQEnh9f","model":"gpt-5.4-mini-2026-03-17","object":"chat.completion","prompt_filter_results":[{"prompt_index":0,"content_filter_results":{"hate":{
... (truncated, 1519 chars total)
HTTP 200

10. Deployment api_version fills in when the caller sends none

  1. Command
curl -sS -X POST 'http://localhost:46237/azure_ai/foundry-versioned/models/chat/completions' -H 'Authorization: Bearer sk-lit7022' -H 'Content-Type: application/json' -d '{"model":"foundry-versioned","messages":[{"role":"user","content":"Say hi in three words"}]}' -w '
HTTP %{http_code}
'
  1. Observed output
{"choices":[{"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"protected_material_code":{"detected":false,"filtered":false},"protected_material_text":{"detected":false,"filtered":false},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}},"finish_reason":"stop","index":0,"logprobs":null,"message":{"annotations":[],"content":"Hi, how are you?","refusal":null,"role":"assistant"}}],"created":1788605353,"id":"chatcmpl-EKiFNXBnEYfxi6fUw52ut8Te9n6Xa","model":"gpt-5.4-mini-2026-03-17","object":"chat.completion","prompt_filter_results":[{"prompt_index":0,"content_filter_results":{"hate":{"
... (truncated, 1519 chars total)
HTTP 200

After, spend logging with a generated virtual key (4706ace)

S1. Generate a virtual key limited to the deployment

  1. Command
echo 'POST /key/generate {"key_alias":"lit7022-spend-1788605362","models":["foundry-gpt-5.4-mini","parse-priced"]} -> key sk-...8M4w'
POST /key/generate {"key_alias":"lit7022-spend-1788605362","models":["foundry-gpt-5.4-mini","parse-priced"]} -> key sk-...8M4w
  1. Observed output

S2. Non-streaming chat relay with that key

  1. Command
curl -sS -X POST 'http://localhost:46237/azure_ai/foundry-gpt-5.4-mini/models/chat/completions?api-version=2024-05-01-preview' -H 'Authorization: Bearer sk-...8M4w' -H 'Content-Type: application/json' -d '{"model":"foundry-gpt-5.4-mini","messages":[{"role":"user","content":"Say hi in three words"}]}' -w '
HTTP %{http_code}
'
  1. Observed output
{"choices":[{"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"protected_material_code":{"detected":false,"filtered":false},"protected_material_text":{"detected":false,"filtered":false},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}},"finish_reason":"stop","index":0,"logprobs":null,"message":{"annotations":[],"content":"Hi there, friend!","refusal":null,"role":"assistant"}}],"created":1788605363,"id":"chatcmpl-EKiFXyWCCpC0Cj7clH6MHCX8nGI1V","model":"gpt-5.4-mini-2026-03-17","object":"chat.completion","prompt_filter_results":[{"prompt_index":0,"content_filter_results":{"hate":{
... (truncated, 1519 chars total)
HTTP 200

S3. Streaming chat relay with that key

  1. Command
curl -sS -N -X POST 'http://localhost:46237/azure_ai/foundry-gpt-5.4-mini/models/chat/completions?api-version=2024-05-01-preview' -H 'Authorization: Bearer sk-...8M4w' -H 'Content-Type: application/json' -d '{"model":"foundry-gpt-5.4-mini","messages":[{"role":"user","content":"Say hi in three words"}],"stream":true}' -o "lit7022_qa_spend_after.log.stream" -w 'HTTP %{http_code}
'; tail -c 300 "lit7022_qa_spend_after.log.stream"; echo; echo '... (last 300 bytes of the stream)'
  1. Observed output
HTTP 200
lts":{},"delta":{},"finish_reason":"stop","index":0,"logprobs":null}],"created":1788605364,"id":"chatcmpl-EKiFYthqBHr76feT7LNUljv29ouFR","model":"gpt-5.4-mini-2026-03-17","obfuscation":"UCAJEXaCiCRA","object":"chat.completion.chunk","service_tier":"default","system_fingerprint":null}

data: [DONE]


... (last 300 bytes of the stream)

S3b. Non-chat relay (Cohere Parse with custom pricing on the deployment) with that key

  1. Command
curl -sS -X POST http://localhost:46237/azure_ai/parse-priced/providers/cohere/v2/parse -H 'Authorization: Bearer sk-...8M4w' -H 'Content-Type: application/json' -d @lit7022_parse_req_priced.json -o "lit7022_qa_spend_after.log.parse" -w 'HTTP %{http_code}
'; python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); print(json.dumps({"id": d.get("id"), "pages": len(d.get("pages", [])), "meta": d.get("meta")}))' "lit7022_qa_spend_after.log.parse"
  1. Observed output
HTTP 200
{"id": "7c63ceab-59be-4cf7-a0b8-9fe27b43a607", "pages": 1, "meta": {"api_version": {"version": "2"}, "billed_units": {"pages": 1}}}

S4. Spend logs for the key (after the proxy's batch write)

  1. Command
curl -sS 'http://localhost:46237/spend/logs?api_key=sk-...8M4w' -H 'Authorization: Bearer sk-lit7022' | python3 -c 'import sys,json; rows=json.load(sys.stdin); print(json.dumps([{k: r.get(k) for k in ("model","call_type","spend","prompt_tokens","completion_tokens","total_tokens")} for r in rows], indent=1))'
[
 {
  "model": "azure_ai/Cohere-parse-v5",
  "call_type": "allm_passthrough_route",
  "spend": 0.0,
  "prompt_tokens": 0,
  "completion_tokens": 0,
  "total_tokens": 0
 },
 {
  "model": "azure_ai/gpt-5.4-mini",
  "call_type": "allm_passthrough_route",
  "spend": 3.15e-05,
  "prompt_tokens": 12,
  "completion_tokens": 5,
  "total_tokens": 17
 },
 {
  "model": "azure_ai/gpt-5.4-mini",
  "call_type": "allm_passthrough_route",
  "spend": 4.875000000000001e-05,
  "prompt_tokens": 11,
  "completion_tokens": 9,
  "total_tokens": 20
 }
]
  1. Observed output

S5. Key info shows the accumulated spend

  1. Command
curl -sS 'http://localhost:46237/key/info?key=sk-...8M4w' -H 'Authorization: Bearer sk-lit7022' | python3 -c 'import sys,json; i=json.load(sys.stdin)["info"]; print(json.dumps({k: i.get(k) for k in ("key_alias","models","spend")}))'
{"key_alias": "lit7022-spend-1788605362", "models": ["foundry-gpt-5.4-mini", "parse-priced"], "spend": 8.025000000000001e-05}
  1. Observed output

After, relay edge cases (4706ace)

C. Bad native path returns the deployment's own error

  1. Command
curl -sS -X POST 'http://localhost:46237/azure_ai/foundry-gpt-5.4-mini/models/no-such-route?api-version=2024-05-01-preview' -H 'Authorization: Bearer sk-lit7022' -H 'Content-Type: application/json' -d '{"model":"foundry-gpt-5.4-mini","messages":[{"role":"user","content":"hi"}]}' -w '
HTTP %{http_code}
'
  1. Observed output
{"error":{"code":"404","message": "Resource not found"}}
HTTP 404

D. Content-Type with a charset still carries the body

  1. Command
curl -sS -X POST 'http://localhost:46237/azure_ai/foundry-gpt-5.4-mini/models/chat/completions?api-version=2024-05-01-preview' -H 'Authorization: Bearer sk-lit7022' -H 'Content-Type: application/json; charset=utf-8' -d '{"model":"foundry-gpt-5.4-mini","messages":[{"role":"user","content":"Say hi in three words"}]}' -w '
HTTP %{http_code}
'
  1. Observed output
{"choices":[{"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"protected_material_code":{"detected":false,"filtered":false},"protected_material_text":{"detected":false,"filtered":false},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}},"finish_reason":"stop","index":0,"logprobs":null,"message":{"annotations":[],"content":"Hi there! Hello","refusal":null,"role":"assistant"}}],"created":1788605360,"id":"chatcmpl-EKiFUszoleN9UwJyyvz2i9OrtnafL","model":"gpt-5.4-mini-2026-03-17","object":"chat.completion","prompt_filter_results":[{"prompt_index":0,"content_filter_results":{"hate":{"f
... (truncated, 1518 chars total)
HTTP 200

E. Caller's api-version reaches an Azure OpenAI deployments path

  1. Command
curl -sS -X POST 'http://localhost:46237/azure_ai/foundry-gpt-5.4-mini/openai/deployments/foundry-gpt-5.4-mini/chat/completions?api-version=2025-04-01-preview' -H 'Authorization: Bearer sk-lit7022' -H 'Content-Type: application/json' -d '{"model":"foundry-gpt-5.4-mini","messages":[{"role":"user","content":"Say hi in three words"}]}' -w '
HTTP %{http_code}
'
  1. Observed output
{"choices":[{"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"protected_material_code":{"detected":false,"filtered":false},"protected_material_text":{"detected":false,"filtered":false},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}},"finish_reason":"stop","index":0,"logprobs":null,"message":{"annotations":[],"content":"Hi there, friend.","refusal":null,"role":"assistant"}}],"created":1788605361,"id":"chatcmpl-EKiFVDPASRl3AkAlYLzeGph4tv2g0","model":"gpt-5.4-mini-2026-03-17","object":"chat.completion","prompt_filter_results":[{"prompt_index":0,"content_filter_results":{"hate":{
... (truncated, 1519 chars total)
HTTP 200

F. Wrong virtual key

  1. Command
curl -sS -X POST 'http://localhost:46237/azure_ai/foundry-gpt-5.4-mini/models/chat/completions?api-version=2024-05-01-preview' -H 'Authorization: Bearer sk-wrong' -H 'Content-Type: application/json' -d '{"model":"foundry-gpt-5.4-mini","messages":[{"role":"user","content":"hi"}]}' -w '
HTTP %{http_code}
'
  1. Observed output
{"error":{"message":"Authentication Error, Invalid proxy server token passed. Received API Key = sk-..., Key Hash (Token) =2ab06cfadcdcc1bb1a1ff9edefb028084a837b3ed7b95f97e602b960ee127471. Unable to find token in cache or `LiteLLM_VerificationTokenTable`","type":"token_not_found_in_db","param":"key","code":"401"}}
HTTP 401

At the Before commit none of the relays reached Azure (HTTP 500 on every case above), so there was nothing to bill; the spend section therefore has no Before leg

Observations from the run:

  • Azure streams carry no usage chunk unless the client sends stream_options.include_usage, so the streaming spend row counts prompt tokens from the relayed messages and completion tokens from the assembled text
  • Only remote high-detail image_url parts in relayed messages are charged at the high-detail upper bound (1445 tokens, the largest image high-res mode keeps) during the log flush; data URLs and low/auto detail images are counted exactly and nothing is downloaded, so a slow image host cannot stall the event loop
  • Passthrough relays return no cost header; the PR leaves that alone
  • Cohere Parse has no built-in cost entry yet (LIT-6992); a parse deployment with custom pricing gets a spend row (S3b), an unpriced one hits the proxy-wide Cost tracking failed ... Add custom pricing policy and writes no row, like any unmapped model
  • Non-router /azure_ai/providers/... paths still 500 at base; out of scope, left alone
  • A router model named like a literal native path word (chat, models, deployments, parse) is rewritten wherever that word appears in the relayed path, because the Azure OpenAI deployments pattern needs every occurrence rewritten; pick router model names that are not path words
  • The router still runs its retries before an upstream 4xx is returned to the caller; every provider's router relay shares that shape, left alone

Type

🐛 Bug Fix

Caveats (if any)

Caveats (if any)

Low

  • api_key_header_for_base allowlist branching is effectively dead for Foundry hosts: it mirrors the pre-existing chat _should_use_api_key_header decision and Azure's documented auth (api-key on Foundry and Azure OpenAI hosts, Bearer for Entra tokens and non-Azure hosts); the chat path now shares the helper so the two cannot drift
  • A router model named like a literal native path word (chat, models, deployments, parse) is rewritten wherever that word appears in the relayed path, because the Azure OpenAI deployments pattern (/azure_ai/<group>/openai/deployments/<group>/...) needs every occurrence rewritten; the whole-segment rewrite fixes the substring case (gpt vs gpt-5.4-mini) only, so name router models distinctly from path words
  • Acknowledged gaps, all pre-existing and outside the ticket's relay path: non-router /azure_ai/... paths still fall back to the global AZURE_API_BASE, and Cohere Parse has no built-in cost entry yet (LIT-6992), so an unpriced parse deployment writes no spend row until it carries custom pricing
  • The router still runs its retries before an upstream 4xx is returned to the caller, because the retry gate reads error.status_code, which httpx.HTTPStatusError lacks; every provider's router relay shares that shape
  • Entra (DefaultAzureCredential) fallback on a keyless Foundry deployment is unit-tested only; no keyless deployment was available for the live legs

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

…e deployment's own endpoint

Every /azure_ai/<router model>/<native path> relay failed with HTTP 500 because
azure_ai had no passthrough config. The new AzureAIPassthroughConfig strips the
router-model prefix from the relayed path, forwards to the deployment's api_base
with its own credential (api-key on Foundry and Azure OpenAI hosts, Bearer
elsewhere, Entra as the fallback), and delegates chat/completions cost logging
to the Azure passthrough config.

The router's provider inference now receives the deployment's api_base so an
OpenAI-family model on a Foundry resource stays azure_ai instead of flipping to
azure through the AZURE_AI_API_BASE env var.
@mateo-berri
mateo-berri requested a review from a team September 5, 2026 04:33
@codspeed-hq

codspeed-hq Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_lit_7022_azure_ai_passthrough_config (4706ace) with litellm_internal_staging (c52b537)

Open in CodSpeed

@greptile-apps

greptile-apps Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds Azure AI passthrough routing so router-model relays use each deployment’s endpoint and credentials.

  • Strips only leading router-model path segments while preserving native Foundry paths and query parameters.
  • Adds Azure AI authentication-header selection and passthrough response handling.
  • Preserves upstream status codes and JSON bodies with charset-qualified content types.
  • Reconstructs streaming chat usage for cost logging when Azure omits usage.
  • Refines remote high-detail image accounting to use a conservative token upper bound without downloading the image.
  • Adds regression coverage for routing, authentication, streaming, response propagation, and token accounting.

Confidence Score: 5/5

The PR appears safe to merge, with no outstanding blocking or non-blocking findings.

The latest changes address remote high-detail image accounting without fetching untrusted remote resources, and no new correctness, security, or repository-rule failures were established. The previous thread was manually resolved without an explanatory reply and is therefore not outstanding.

Important Files Changed

Filename Overview
litellm/llms/azure_ai/passthrough/transformation.py Implements deployment-aware Azure AI passthrough URL construction, authentication, and logging integration.
litellm/llms/azure/passthrough/transformation.py Corrects Azure relay path handling and adds streaming chat response reconstruction.
litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py Preserves upstream HTTP responses and accepts charset-qualified JSON request bodies.
litellm/proxy/pass_through_endpoints/llm_provider_handlers/openai_passthrough_logging_handler.py Reconstructs missing streaming prompt usage while avoiding remote high-detail image downloads.
litellm/litellm_core_utils/token_counter.py Adds a bounded high-detail image token estimate used when exact remote dimensions are unavailable.
litellm/router_utils/common_utils.py Preserves explicit provider prefixes for generic router calls.
litellm/router.py Uses segment-aware endpoint replacement and centralized generic-call provider resolution.

Reviews (7): Last reviewed commit: "fix(passthrough): charge remote high-det..." | Re-trigger Greptile

@codecov

codecov Bot commented Sep 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.07602% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...itellm/llms/azure_ai/passthrough/transformation.py 96.92% 2 Missing ⚠️
...der_handlers/openai_passthrough_logging_handler.py 92.00% 2 Missing ⚠️
...itellm/llms/base_llm/passthrough/transformation.py 92.30% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@mateo-berri mateo-berri added run-ci and removed run-ci labels Sep 5, 2026
@mateo-berri mateo-berri added run-ci and removed run-ci labels Sep 5, 2026
@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

Generic passthrough calls inferred the provider from the bare model name, so an
azure_ai/gpt-* deployment on an Azure OpenAI host flipped to azure and
get_llm_provider re-prefixed the deployment name into azure_ai/gpt-5.4-mini, a
404 DeploymentNotFound. provider_for_generic_call takes the declared
custom_llm_provider first, then the model's own prefix, and only infers for
unprefixed models
…router relays

Streaming chat relays on Azure and azure_ai deployments rebuild the response from
the SSE chunks through the OpenAI passthrough assembler, so the spend log carries
usage. The router relays keep the JSON body when the Content-Type carries a
charset, return the upstream status and body instead of a 500 when the deployment
rejects the call, and fall back to the caller's api-version when the deployment
sets none. Lint budgets ratcheted to the measured totals
@mateo-berri mateo-berri added run-ci and removed run-ci labels Sep 5, 2026
@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@mateo-berri mateo-berri added run-ci and removed run-ci labels Sep 5, 2026
@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

Comment thread litellm/llms/azure/passthrough/transformation.py
@veria-ai

veria-ai Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

PR overview

This pull request adds passthrough configuration for Azure AI router-model relays so requests reach the deployment’s own endpoint, including handling for non-chat responses.

Two issues have been addressed, but non-chat parse or OCR relays can still omit provider usage data from budget accounting. An authenticated caller could repeatedly consume billable Azure AI operations without debiting key or team budgets, creating a direct but financially bounded abuse path.

Open issues (1)

Fixed/addressed: 2 · PR risk: 6/10

@mateo-berri mateo-berri added run-ci and removed run-ci labels Sep 5, 2026
@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@mateo-berri mateo-berri added run-ci and removed run-ci labels Sep 5, 2026
@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

)
if chat_result is not None:
return chat_result
return StandardPassThroughResponseObject(response=relayed_body(httpx_response))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium: Non-chat relays bypass budget accounting

This wrapper discards provider billing fields such as processed pages by placing the response inside a generic object with no recognized usage. An authenticated caller can repeatedly invoke billable parse or OCR routes while response_cost remains zero or unset, so key and team budgets are not debited. Transform supported responses into their typed LiteLLM response with usage information, or explicitly calculate and attach the provider-reported cost before dispatching success callbacks.

@mateo-berri mateo-berri added run-ci and removed run-ci labels Sep 5, 2026
@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant