From 80fd9970c26ec1c81955dcba55e293ed73d4d69e 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:25:26 +0000 Subject: [PATCH 1/5] fix(azure): route image generation and edits through /openai/v1 for v1 api versions Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/llms/azure/azure.py | 7 +++ .../llms/azure/image_edit/transformation.py | 11 +++- .../test_azure_image_edit_transformation.py | 53 +++++++++++++++++++ .../test_azure_image_generation_init.py | 32 +++++++++++ 4 files changed, 101 insertions(+), 2 deletions(-) diff --git a/litellm/llms/azure/azure.py b/litellm/llms/azure/azure.py index 980b27cda55..3a30444f06d 100644 --- a/litellm/llms/azure/azure.py +++ b/litellm/llms/azure/azure.py @@ -1113,6 +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={"api_version": api_version}, + route="/openai/images/generations", + ) + if "/openai/deployments/" in api_base: base_url_with_deployment = api_base else: diff --git a/litellm/llms/azure/image_edit/transformation.py b/litellm/llms/azure/image_edit/transformation.py index 15592968bad..eb1a51ca3e8 100644 --- a/litellm/llms/azure/image_edit/transformation.py +++ b/litellm/llms/azure/image_edit/transformation.py @@ -93,8 +93,6 @@ class AzureImageEditConfig(OpenAIImageEditConfig): raise ValueError( f"api_base is required for Azure AI Studio. Please set the api_base parameter. Passed `api_base={api_base}`" ) - original_url: Final = httpx.URL(api_base) - # Resolve api_version: litellm_params > litellm.api_version > AZURE_API_VERSION env > default. # Mirrors the fallback chain used by the Azure chat path in common_utils.py, # so callers that set a global / env api_version don't get an unversioned URL. @@ -105,6 +103,15 @@ 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={"api_version": api_version}, + route="/openai/images/edits", + ) + + original_url: Final = httpx.URL(api_base) + # Create a new dictionary with existing params query_params: Final = dict(original_url.params) 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 59472d1a49d..251b98aa8d4 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 @@ -233,3 +233,56 @@ 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) + config = AzureImageEditConfig() + + for api_version in ("v1", "preview", "latest"): + url = config.get_complete_url( + model=_FALLBACK_MODEL, + api_base=_FALLBACK_API_BASE, + litellm_params={"api_version": api_version}, + ) + assert urllib.parse.urlparse(url).path == "/openai/v1/images/edits" + assert _query_params(url) == {"api-version": api_version} + assert config.finalize_image_edit_request_data({"model": _FALLBACK_MODEL, "prompt": "x"}, url) == { + "model": _FALLBACK_MODEL, + "prompt": "x", + } + + +def test_v1_api_version_from_global_uses_v1_route(monkeypatch): + monkeypatch.setattr(litellm, "api_version", "preview", raising=False) + monkeypatch.delenv("AZURE_API_VERSION", raising=False) + + url = AzureImageEditConfig().get_complete_url( + model=_FALLBACK_MODEL, + api_base=_FALLBACK_API_BASE, + litellm_params={}, + ) + + assert urllib.parse.urlparse(url).path == "/openai/v1/images/edits" + + +def test_dated_api_version_still_uses_deployment_route(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=_FALLBACK_API_BASE, + litellm_params={"api_version": "2024-10-21"}, + ) + + assert urllib.parse.urlparse(url).path == f"/openai/deployments/{_FALLBACK_MODEL}/images/edits" 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 560fee17328..ebb6c672f96 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 @@ -433,3 +433,35 @@ async def test_azure_aimage_generation_base_model_vs_deployment_name(): wire_json = post_kwargs.get("json") or {} assert "model" not in wire_json assert data.get("model") == base_model + + +@pytest.mark.parametrize("api_version", ["v1", "preview", "latest"]) +def test_azure_image_generation_v1_api_version_uses_v1_route(api_version): + """The v1 Azure surface exposes /openai/v1/images/generations and routes by body ``model``.""" + url = AzureChatCompletion().create_azure_base_url( + azure_client_params={ + "azure_endpoint": "https://my-resource.openai.azure.com", + "api_version": api_version, + }, + model="gpt-image-1", + base_model=None, + ) + assert url == f"https://my-resource.openai.azure.com/openai/v1/images/generations?api-version={api_version}" + data = {"model": "gpt-image-1", "prompt": "x"} + assert azure_deployment_image_generation_json_body(url, data) == data + + +def test_azure_image_generation_dated_api_version_uses_deployment_route(): + url = AzureChatCompletion().create_azure_base_url( + azure_client_params={ + "azure_endpoint": "https://my-resource.openai.azure.com", + "api_version": "2024-10-21", + }, + model="gpt-image-1", + base_model=None, + ) + assert ( + url + == "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"}) From a6e68b1b0ea9baceaf695eadc14a62163d2de2d6 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:29:57 +0000 Subject: [PATCH 2/5] fix(azure): wrap v1 image route litellm_params in MappingProxyType Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/llms/azure/azure.py | 3 ++- litellm/llms/azure/image_edit/transformation.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/litellm/llms/azure/azure.py b/litellm/llms/azure/azure.py index 3a30444f06d..c23a4dd04fa 100644 --- a/litellm/llms/azure/azure.py +++ b/litellm/llms/azure/azure.py @@ -2,6 +2,7 @@ import asyncio import json import time from collections.abc import Callable, Coroutine +from types import MappingProxyType from typing import Any, Final import httpx @@ -1116,7 +1117,7 @@ class AzureChatCompletion(BaseAzureLLM, BaseLLM): if BaseAzureLLM._is_azure_v1_api_version(api_version): return BaseAzureLLM._get_base_azure_url( api_base=api_base, - litellm_params={"api_version": api_version}, + litellm_params=MappingProxyType({"api_version": api_version}), route="/openai/images/generations", ) diff --git a/litellm/llms/azure/image_edit/transformation.py b/litellm/llms/azure/image_edit/transformation.py index eb1a51ca3e8..274bb49f400 100644 --- a/litellm/llms/azure/image_edit/transformation.py +++ b/litellm/llms/azure/image_edit/transformation.py @@ -1,3 +1,4 @@ +from types import MappingProxyType from typing import Final, cast import httpx @@ -106,7 +107,7 @@ class AzureImageEditConfig(OpenAIImageEditConfig): if BaseAzureLLM._is_azure_v1_api_version(api_version): return BaseAzureLLM._get_base_azure_url( api_base=api_base, - litellm_params={"api_version": api_version}, + litellm_params=MappingProxyType({"api_version": api_version}), route="/openai/images/edits", ) 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 3/5] 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" From 1cf683c9806246fa9af96d30194a0b68b5c83f2d 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:49:48 +0000 Subject: [PATCH 4/5] fix(azure): honor base_url client param and drop stale api-version on v1 image routes Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/llms/azure/azure.py | 7 ++++--- litellm/llms/azure/common_utils.py | 10 +++++++--- .../test_azure_image_edit_transformation.py | 2 +- .../test_azure_image_generation_init.py | 12 ++++++++++++ 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/litellm/llms/azure/azure.py b/litellm/llms/azure/azure.py index ce65d62ca45..c2d4d8b9306 100644 --- a/litellm/llms/azure/azure.py +++ b/litellm/llms/azure/azure.py @@ -1091,9 +1091,10 @@ class AzureChatCompletion(BaseAzureLLM, BaseLLM): AzureFoundryMAIImageGenerationConfig, ) - api_base: str = azure_client_params.get("azure_endpoint", "") # "https://example-endpoint.openai.azure.com" - if api_base.endswith("/"): - api_base = api_base.rstrip("/") + # deployment-scoped endpoints are moved to "base_url" by select_azure_base_url_or_endpoint + api_base: str = (azure_client_params.get("azure_endpoint") or azure_client_params.get("base_url") or "").rstrip( + "/" + ) api_version: Final[str] = azure_client_params.get("api_version", "") if model is None: model = "" diff --git a/litellm/llms/azure/common_utils.py b/litellm/llms/azure/common_utils.py index ae9a43a540c..8d5c7687ec8 100644 --- a/litellm/llms/azure/common_utils.py +++ b/litellm/llms/azure/common_utils.py @@ -794,7 +794,8 @@ class BaseAzureLLM(BaseOpenAILLM): 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. + ``model`` in the request body, so any deployment path and stale ``api-version`` in + ``api_base`` have to be dropped. Returns None when ``api_version`` is a dated one, which still uses the deployment route. """ @@ -803,8 +804,11 @@ class BaseAzureLLM(BaseOpenAILLM): 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])) + resource_base: Final = str( + base_url.copy_with( + path=base_url.path if openai_path_start == -1 else base_url.path[:openai_path_start], + params=httpx.QueryParams({k: v for k, v in base_url.params.items() if k != "api-version"}), + ) ) return BaseAzureLLM._get_base_azure_url( api_base=resource_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 48fa7221dff..5c7b249ae72 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 @@ -286,7 +286,7 @@ def test_v1_api_version_replaces_deployment_scoped_api_base(monkeypatch): url = AzureImageEditConfig().get_complete_url( model=_FALLBACK_MODEL, - api_base=f"{_FALLBACK_API_BASE}/openai/deployments/{_FALLBACK_MODEL}/images/edits", + api_base=f"{_FALLBACK_API_BASE}/openai/deployments/{_FALLBACK_MODEL}/images/edits?api-version=2024-10-21", litellm_params={"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 cfec4eea2f4..65beec2b707 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 @@ -477,3 +477,15 @@ def test_azure_image_generation_v1_api_version_replaces_deployment_scoped_api_ba base_model=None, ) assert url == "https://my-resource.openai.azure.com/openai/v1/images/generations?api-version=preview" + + +def test_azure_image_generation_v1_api_version_uses_base_url_client_param(): + url = AzureChatCompletion().create_azure_base_url( + azure_client_params={ + "base_url": "https://my-resource.openai.azure.com/openai/deployments/gpt-image-1?api-version=2024-10-21", + "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" From ed9575520b289b70986a3e9a3382c7638d1723a8 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:55:57 +0000 Subject: [PATCH 5/5] fix(azure): build v1 image query params without a mutable dict Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/llms/azure/common_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/llms/azure/common_utils.py b/litellm/llms/azure/common_utils.py index 8d5c7687ec8..6cb7d09cec4 100644 --- a/litellm/llms/azure/common_utils.py +++ b/litellm/llms/azure/common_utils.py @@ -807,7 +807,7 @@ class BaseAzureLLM(BaseOpenAILLM): resource_base: Final = str( base_url.copy_with( path=base_url.path if openai_path_start == -1 else base_url.path[:openai_path_start], - params=httpx.QueryParams({k: v for k, v in base_url.params.items() if k != "api-version"}), + params=httpx.QueryParams(tuple((k, v) for k, v in base_url.params.multi_items() if k != "api-version")), ) ) return BaseAzureLLM._get_base_azure_url(