This commit is contained in:
devin-ai-integration[bot] 2026-08-26 09:44:16 -07:00 committed by GitHub
commit 352d641e77
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 164 additions and 5 deletions

View file

@ -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 = ""
@ -1113,6 +1114,14 @@ class AzureChatCompletion(BaseAzureLLM, BaseLLM):
api_version=api_version,
)
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
else:

View file

@ -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,32 @@ 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 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.
"""
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 = str(
base_url.copy_with(
path=base_url.path if openai_path_start == -1 else base_url.path[:openai_path_start],
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(
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:

View file

@ -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,16 @@ class AzureImageEditConfig(OpenAIImageEditConfig):
or litellm.AZURE_DEFAULT_API_VERSION
)
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)
# Create a new dictionary with existing params
query_params: Final = dict(original_url.params)

View file

@ -233,3 +233,62 @@ def test_api_version_in_api_base_query_is_preserved(monkeypatch):
)
assert _query_params(url) == {"api-version": "2024-05-01-preview"}
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"
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?api-version=2024-10-21",
litellm_params={"api_version": "preview"},
)
assert urllib.parse.urlparse(url).path == "/openai/v1/images/edits"
assert _query_params(url) == {"api-version": "preview"}

View file

@ -433,3 +433,59 @@ 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"})
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"
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"