From 5802cf0d8d8772cfe401b3eab8a5680110de22a3 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 26 Aug 2026 01:40:59 +0000 Subject: [PATCH] fix(azure): drop deployment path from api_base when building v1 image routes Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/llms/azure/azure.py | 14 +++++------ litellm/llms/azure/common_utils.py | 23 +++++++++++++++++++ .../llms/azure/image_edit/transformation.py | 14 +++++------ .../test_azure_image_edit_transformation.py | 22 +++++++++++------- .../test_azure_image_generation_init.py | 12 ++++++++++ 5 files changed, 63 insertions(+), 22 deletions(-) diff --git a/litellm/llms/azure/azure.py b/litellm/llms/azure/azure.py index c23a4dd04fa..ce65d62ca45 100644 --- a/litellm/llms/azure/azure.py +++ b/litellm/llms/azure/azure.py @@ -2,7 +2,6 @@ import asyncio import json import time from collections.abc import Callable, Coroutine -from types import MappingProxyType from typing import Any, Final import httpx @@ -1114,12 +1113,13 @@ class AzureChatCompletion(BaseAzureLLM, BaseLLM): api_version=api_version, ) - if BaseAzureLLM._is_azure_v1_api_version(api_version): - return BaseAzureLLM._get_base_azure_url( - api_base=api_base, - litellm_params=MappingProxyType({"api_version": api_version}), - route="/openai/images/generations", - ) + v1_url: Final = BaseAzureLLM.get_azure_v1_image_url( + api_base=api_base, + api_version=api_version, + route="/openai/images/generations", + ) + if v1_url is not None: + return v1_url if "/openai/deployments/" in api_base: base_url_with_deployment = api_base diff --git a/litellm/llms/azure/common_utils.py b/litellm/llms/azure/common_utils.py index 2c34851d275..ae9a43a540c 100644 --- a/litellm/llms/azure/common_utils.py +++ b/litellm/llms/azure/common_utils.py @@ -4,6 +4,7 @@ import json import os from collections.abc import Callable, Mapping from functools import lru_cache +from types import MappingProxyType from typing import Any, Final, Literal, NamedTuple, cast import httpx @@ -789,6 +790,28 @@ class BaseAzureLLM(BaseOpenAILLM): return str(final_url) + @staticmethod + def get_azure_v1_image_url(api_base: str, api_version: str | None, route: str) -> str | None: + """ + Azure's v1 surface serves images at ``/openai/v1/images/{generations,edits}`` and routes by + ``model`` in the request body, so any deployment path in ``api_base`` has to be dropped. + + Returns None when ``api_version`` is a dated one, which still uses the deployment route. + """ + if not BaseAzureLLM._is_azure_v1_api_version(api_version): + return None + + base_url: Final = httpx.URL(api_base) + openai_path_start: Final = base_url.path.find("/openai") + resource_base: Final = ( + api_base if openai_path_start == -1 else str(base_url.copy_with(path=base_url.path[:openai_path_start])) + ) + return BaseAzureLLM._get_base_azure_url( + api_base=resource_base, + litellm_params=MappingProxyType({"api_version": api_version}), + route=route, + ) + @staticmethod def _is_azure_v1_api_version(api_version: str | None) -> bool: if api_version is None: diff --git a/litellm/llms/azure/image_edit/transformation.py b/litellm/llms/azure/image_edit/transformation.py index 274bb49f400..e4716289a34 100644 --- a/litellm/llms/azure/image_edit/transformation.py +++ b/litellm/llms/azure/image_edit/transformation.py @@ -1,4 +1,3 @@ -from types import MappingProxyType from typing import Final, cast import httpx @@ -104,12 +103,13 @@ class AzureImageEditConfig(OpenAIImageEditConfig): or litellm.AZURE_DEFAULT_API_VERSION ) - if BaseAzureLLM._is_azure_v1_api_version(api_version): - return BaseAzureLLM._get_base_azure_url( - api_base=api_base, - litellm_params=MappingProxyType({"api_version": api_version}), - route="/openai/images/edits", - ) + v1_url: Final = BaseAzureLLM.get_azure_v1_image_url( + api_base=api_base, + api_version=api_version, + route="/openai/images/edits", + ) + if v1_url is not None: + return v1_url original_url: Final = httpx.URL(api_base) diff --git a/tests/test_litellm/llms/azure/image_edit/test_azure_image_edit_transformation.py b/tests/test_litellm/llms/azure/image_edit/test_azure_image_edit_transformation.py index 251b98aa8d4..48fa7221dff 100644 --- a/tests/test_litellm/llms/azure/image_edit/test_azure_image_edit_transformation.py +++ b/tests/test_litellm/llms/azure/image_edit/test_azure_image_edit_transformation.py @@ -235,14 +235,6 @@ def test_api_version_in_api_base_query_is_preserved(monkeypatch): assert _query_params(url) == {"api-version": "2024-05-01-preview"} -# --------------------------------------------------------------------------- -# Azure v1 API surface (api_version in {"v1", "preview", "latest"}) -# -# The v1 surface exposes /openai/v1/images/edits and routes by ``model`` in the -# multipart form. Building the deployment-scoped path instead makes Azure 404. -# --------------------------------------------------------------------------- - - def test_v1_api_version_uses_v1_route_and_keeps_model(monkeypatch): monkeypatch.setattr(litellm, "api_version", None, raising=False) monkeypatch.delenv("AZURE_API_VERSION", raising=False) @@ -286,3 +278,17 @@ def test_dated_api_version_still_uses_deployment_route(monkeypatch): ) assert urllib.parse.urlparse(url).path == f"/openai/deployments/{_FALLBACK_MODEL}/images/edits" + + +def test_v1_api_version_replaces_deployment_scoped_api_base(monkeypatch): + monkeypatch.setattr(litellm, "api_version", None, raising=False) + monkeypatch.delenv("AZURE_API_VERSION", raising=False) + + url = AzureImageEditConfig().get_complete_url( + model=_FALLBACK_MODEL, + api_base=f"{_FALLBACK_API_BASE}/openai/deployments/{_FALLBACK_MODEL}/images/edits", + litellm_params={"api_version": "preview"}, + ) + + assert urllib.parse.urlparse(url).path == "/openai/v1/images/edits" + assert _query_params(url) == {"api-version": "preview"} diff --git a/tests/test_litellm/llms/azure/image_generation/test_azure_image_generation_init.py b/tests/test_litellm/llms/azure/image_generation/test_azure_image_generation_init.py index ebb6c672f96..cfec4eea2f4 100644 --- a/tests/test_litellm/llms/azure/image_generation/test_azure_image_generation_init.py +++ b/tests/test_litellm/llms/azure/image_generation/test_azure_image_generation_init.py @@ -465,3 +465,15 @@ def test_azure_image_generation_dated_api_version_uses_deployment_route(): == "https://my-resource.openai.azure.com/openai/deployments/gpt-image-1/images/generations?api-version=2024-10-21" ) assert "model" not in azure_deployment_image_generation_json_body(url, {"model": "gpt-image-1", "prompt": "x"}) + + +def test_azure_image_generation_v1_api_version_replaces_deployment_scoped_api_base(): + url = AzureChatCompletion().create_azure_base_url( + azure_client_params={ + "azure_endpoint": "https://my-resource.openai.azure.com/openai/deployments/gpt-image-1/images/generations", + "api_version": "preview", + }, + model="gpt-image-1", + base_model=None, + ) + assert url == "https://my-resource.openai.azure.com/openai/v1/images/generations?api-version=preview"