mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-14 09:03:16 +00:00
Merge 4c29e1846a into aeede53112
This commit is contained in:
commit
24d6a65d33
3 changed files with 226 additions and 1 deletions
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
57
litellm/llms/azure_ai/image_edit/gpt_transformation.py
Normal file
57
litellm/llms/azure_ai/image_edit/gpt_transformation.py
Normal 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,
|
||||
)
|
||||
|
|
@ -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
|
||||
Loading…
Add table
Reference in a new issue