This commit is contained in:
Rick 2026-11-04 00:19:56 +00:00 • committed by GitHub
commit 1d19a0a365
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 226 additions and 1 deletions

View file

@ -9,12 +9,14 @@ from litellm.llms.azure_ai.image_generation.mai_transformation import (
from litellm.llms.base_llm.image_edit.transformation import BaseImageEditConfig
from .flux2_transformation import AzureFoundryFlux2ImageEditConfig
from .gpt_transformation import AzureFoundryGPTImageEditConfig
from .mai_transformation import AzureFoundryMAIImageEditConfig
from .transformation import AzureFoundryFluxImageEditConfig
__all__ = [
"AzureFoundryFlux2ImageEditConfig",
"AzureFoundryFluxImageEditConfig",
"AzureFoundryGPTImageEditConfig",
"AzureFoundryMAIImageEditConfig",
]
@ -23,6 +25,7 @@ def get_azure_ai_image_edit_config(model: str) -> BaseImageEditConfig:
"""
Get the appropriate image edit config for an Azure AI model.
- gpt-image models use /openai/deployments/{deployment}/images/edits with multipart form data
- MAI models use /mai/v1/images/edits with multipart form data
- FLUX 2 models use JSON with base64 image
- FLUX 1 models use multipart/form-data
@ -30,12 +33,16 @@ def get_azure_ai_image_edit_config(model: str) -> BaseImageEditConfig:
if AzureFoundryMAIImageGenerationConfig.is_mai_model(model):
return AzureFoundryMAIImageEditConfig()
model_normalized: Final = model.lower().replace("-", "").replace("_", "")
if "gptimage" in model_normalized:
return AzureFoundryGPTImageEditConfig()
# Check if it's a FLUX 2 model
if AzureFoundryFluxImageGenerationConfig.is_flux2_model(model):
return AzureFoundryFlux2ImageEditConfig()
# Default to FLUX 1 config for other FLUX models
model_normalized: Final = model.lower().replace("-", "").replace("_", "")
if model_normalized == "" or "flux" in model_normalized:
return AzureFoundryFluxImageEditConfig()

View file

@ -0,0 +1,57 @@
from typing import Final
from litellm.llms.azure.image_edit.transformation import AzureImageEditConfig
from litellm.llms.azure_ai.common_utils import (
AzureFoundryModelInfo,
get_azure_ai_auth_headers,
)
from litellm.secret_managers.main import get_secret_str
from litellm.types.router import GenericLiteLLMParams
class AzureFoundryGPTImageEditConfig(AzureImageEditConfig):
"""
Azure AI Foundry gpt-image edit config.
Foundry serves gpt-image deployments on the same
``/openai/deployments/{deployment}/images/edits`` route as Azure OpenAI, so the
request and URL handling is shared with ``AzureImageEditConfig``. Only the
credential source is Foundry-specific.
"""
def validate_environment(
self,
headers: dict[str, str],
model: str,
api_key: str | None = None,
litellm_params: dict[str, object] | None = None,
api_base: str | None = None,
) -> dict[str, str]:
params: Final = GenericLiteLLMParams(**(litellm_params or {}))
resolved_api_key: Final = params.api_key or api_key
headers.update(
get_azure_ai_auth_headers(
api_key=AzureFoundryModelInfo.get_api_key(resolved_api_key),
litellm_params=litellm_params,
api_key_header="api-key",
)
)
return headers
def get_complete_url(
self,
model: str,
api_base: str | None,
litellm_params: dict[str, object],
) -> str:
azure_ai_api_version: Final = litellm_params.get("api_version") or get_secret_str("AZURE_AI_API_VERSION")
resolved_params: Final = (
{**litellm_params, "api_version": azure_ai_api_version}
if azure_ai_api_version is not None
else litellm_params
)
return super().get_complete_url( # pyright: ignore[reportUnknownMemberType] # bare dict in base
model=model,
api_base=AzureFoundryModelInfo.get_api_base(api_base),
litellm_params=resolved_params,
)

View file

@ -0,0 +1,161 @@
from typing import Final
import httpx
import pytest
import litellm
from litellm.llms.azure_ai.image_edit import get_azure_ai_image_edit_config
from litellm.llms.azure_ai.image_edit.gpt_transformation import (
AzureFoundryGPTImageEditConfig,
)
from litellm.llms.custom_httpx.http_handler import HTTPHandler
FOUNDRY_API_BASE: Final = "https://example.services.ai.azure.com"
DEPLOYMENT_EDIT_URL: Final = (
f"{FOUNDRY_API_BASE}/openai/deployments/gpt-image-2.5-flare/images/edits?api-version=2025-04-01-preview"
)
@pytest.mark.parametrize(
"model",
[
"gpt-image-1",
"gpt-image-2",
"gpt-image-2.5-flare",
"gpt-image-2.5-flare-2026-09-08",
"gpt-image-2.5-sunburst",
"gpt-image-2.5-sunburst-2026-09-08",
],
)
def test_gpt_image_models_get_gpt_image_edit_config(model: str):
assert isinstance(get_azure_ai_image_edit_config(model), AzureFoundryGPTImageEditConfig)
def test_gpt_image_edit_url_uses_foundry_deployment_route():
config: Final = AzureFoundryGPTImageEditConfig()
url: Final = config.get_complete_url(
model="gpt-image-2.5-flare",
api_base=FOUNDRY_API_BASE,
litellm_params={"api_version": "2025-04-01-preview"},
)
assert url == DEPLOYMENT_EDIT_URL
def test_gpt_image_edit_url_uses_azure_ai_api_base_and_version_env(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv("AZURE_AI_API_BASE", FOUNDRY_API_BASE)
monkeypatch.setenv("AZURE_AI_API_VERSION", "2025-04-01-preview")
monkeypatch.delenv("AZURE_API_VERSION", raising=False)
monkeypatch.setattr(litellm, "api_base", None)
monkeypatch.setattr(litellm, "api_version", None)
url: Final = AzureFoundryGPTImageEditConfig().get_complete_url(
model="gpt-image-2.5-flare",
api_base=None,
litellm_params={},
)
assert url == DEPLOYMENT_EDIT_URL
def test_gpt_image_edit_url_uses_v1_surface_for_preview_version():
url: Final = AzureFoundryGPTImageEditConfig().get_complete_url(
model="gpt-image-2.5-flare",
api_base=FOUNDRY_API_BASE,
litellm_params={"api_version": "preview"},
)
assert url == f"{FOUNDRY_API_BASE}/openai/v1/images/edits?api-version=preview"
def test_gpt_image_edit_sends_api_key_header():
headers: Final = AzureFoundryGPTImageEditConfig().validate_environment(
{},
"gpt-image-2.5-flare",
api_key="test-key",
)
assert headers == {"api-key": "test-key"}
def test_gpt_image_edit_prefers_litellm_params_api_key():
headers: Final = AzureFoundryGPTImageEditConfig().validate_environment(
{},
"gpt-image-2.5-flare",
api_key="positional-key",
litellm_params={"api_key": "params-key"},
)
assert headers["api-key"] == "params-key"
def test_gpt_image_edit_reads_azure_ai_api_key_env(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv("AZURE_AI_API_KEY", "env-key")
monkeypatch.setattr(litellm, "api_key", None)
headers: Final = AzureFoundryGPTImageEditConfig().validate_environment(
{},
"gpt-image-2.5-flare",
)
assert headers["api-key"] == "env-key"
def test_gpt_image_edit_falls_back_to_entra_token(monkeypatch: pytest.MonkeyPatch, no_ambient_azure_credentials: None):
monkeypatch.delenv("AZURE_AI_API_KEY", raising=False)
monkeypatch.setattr(litellm, "api_key", None)
headers: Final = AzureFoundryGPTImageEditConfig().validate_environment(
{},
"gpt-image-2.5-flare",
litellm_params={"azure_ad_token": "entra-token"},
)
assert headers == {"Authorization": "Bearer entra-token"}
def test_gpt_image_edit_strips_model_from_deployment_multipart():
data: Final = AzureFoundryGPTImageEditConfig().finalize_image_edit_request_data(
{"model": "gpt-image-2.5-flare", "prompt": "Add a hat"},
DEPLOYMENT_EDIT_URL,
)
assert data == {"prompt": "Add a hat"}
def test_gpt_image_edit_keeps_model_on_v1_multipart():
data: Final = AzureFoundryGPTImageEditConfig().finalize_image_edit_request_data(
{"model": "gpt-image-2.5-flare", "prompt": "Add a hat"},
f"{FOUNDRY_API_BASE}/openai/v1/images/edits?api-version=preview",
)
assert data == {"model": "gpt-image-2.5-flare", "prompt": "Add a hat"}
def test_gpt_image_edit_end_to_end_posts_multipart_to_deployment_route(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv("AZURE_AI_API_VERSION", "2025-04-01-preview")
monkeypatch.delenv("AZURE_API_VERSION", raising=False)
captured: Final[list[httpx.Request]] = []
def respond(request: httpx.Request) -> httpx.Response:
captured.append(request)
return httpx.Response(200, json={"created": 1, "data": [{"b64_json": "aW1n"}]})
client: Final = HTTPHandler(client=httpx.Client(transport=httpx.MockTransport(respond)))
response: Final = litellm.image_edit(
model="azure_ai/gpt-image-2.5-flare",
image=b"image",
prompt="Add a hat",
api_key="test-key",
api_base=FOUNDRY_API_BASE,
client=client,
)
assert response.data[0]["b64_json"] == "aW1n"
assert len(captured) == 1
request: Final = captured[0]
assert str(request.url) == DEPLOYMENT_EDIT_URL
assert request.headers["api-key"] == "test-key"
assert b'name="prompt"' in request.content
assert b'name="model"' not in request.content