diff --git a/litellm/llms/azure/azure.py b/litellm/llms/azure/azure.py index b84f24622f..9291269d15 100644 --- a/litellm/llms/azure/azure.py +++ b/litellm/llms/azure/azure.py @@ -44,6 +44,7 @@ from .common_utils import ( select_azure_base_url_or_endpoint, ) from .image_generation import get_azure_image_generation_config +from .image_generation.http_utils import azure_deployment_image_generation_json_body class AzureOpenAIAssistantsAPIConfig: @@ -133,22 +134,6 @@ class AzureChatCompletion(BaseAzureLLM, BaseLLM): def __init__(self) -> None: super().__init__() - @staticmethod - def azure_deployment_image_generation_json_body(api_base: str, data: dict) -> dict: - """ - JSON body for Azure OpenAI image generation HTTP calls. - - For ``.../openai/deployments/{deployment}/images/generations``, routing uses - the deployment in the URL only; sending ``model`` in the body (especially the - deployment name) breaks some models (e.g. gpt-image-2). See LiteLLM #26316. - - Provider-style URLs (e.g. ``/providers/...`` for FLUX on Azure AI) keep all - keys so non–OpenAI-deployment payloads still work. - """ - if "images/generations" in api_base and "/openai/deployments/" in api_base: - return {k: v for k, v in data.items() if k != "model"} - return data - def make_sync_azure_openai_chat_completion_request( self, azure_client: Union[AzureOpenAI, OpenAI], @@ -982,9 +967,7 @@ class AzureChatCompletion(BaseAzureLLM, BaseLLM): content=json.dumps(result).encode("utf-8"), request=httpx.Request(method="POST", url="https://api.openai.com/v1"), ) - request_json = AzureChatCompletion.azure_deployment_image_generation_json_body( - api_base, data - ) + request_json = azure_deployment_image_generation_json_body(api_base, data) return await async_handler.post( url=api_base, json=request_json, @@ -1104,9 +1087,7 @@ class AzureChatCompletion(BaseAzureLLM, BaseLLM): content=json.dumps(result).encode("utf-8"), request=httpx.Request(method="POST", url="https://api.openai.com/v1"), ) - request_json = AzureChatCompletion.azure_deployment_image_generation_json_body( - api_base, data - ) + request_json = azure_deployment_image_generation_json_body(api_base, data) return sync_handler.post( url=api_base, json=request_json, diff --git a/litellm/llms/azure/image_edit/transformation.py b/litellm/llms/azure/image_edit/transformation.py index 321c2dee4e..0b6ecfb076 100644 --- a/litellm/llms/azure/image_edit/transformation.py +++ b/litellm/llms/azure/image_edit/transformation.py @@ -97,7 +97,7 @@ class AzureImageEditConfig(OpenAIImageEditConfig): return str(final_url) - def finalize_image_edit_multipart_data( + def finalize_image_edit_request_data( self, data: dict, resolved_request_url: str ) -> dict: return self.azure_deployment_image_edit_form_data(data, resolved_request_url) diff --git a/litellm/llms/azure/image_generation/__init__.py b/litellm/llms/azure/image_generation/__init__.py index a9cf151464..f60e446f0c 100644 --- a/litellm/llms/azure/image_generation/__init__.py +++ b/litellm/llms/azure/image_generation/__init__.py @@ -6,11 +6,13 @@ from litellm.llms.base_llm.image_generation.transformation import ( from .dall_e_2_transformation import AzureDallE2ImageGenerationConfig from .dall_e_3_transformation import AzureDallE3ImageGenerationConfig from .gpt_transformation import AzureGPTImageGenerationConfig +from .http_utils import azure_deployment_image_generation_json_body __all__ = [ "AzureDallE2ImageGenerationConfig", "AzureDallE3ImageGenerationConfig", "AzureGPTImageGenerationConfig", + "azure_deployment_image_generation_json_body", ] diff --git a/litellm/llms/azure/image_generation/http_utils.py b/litellm/llms/azure/image_generation/http_utils.py new file mode 100644 index 0000000000..03c425eeff --- /dev/null +++ b/litellm/llms/azure/image_generation/http_utils.py @@ -0,0 +1,17 @@ +"""HTTP helpers for Azure OpenAI image generation (REST, not SDK).""" + + +def azure_deployment_image_generation_json_body(api_base: str, data: dict) -> dict: + """ + Build the JSON body for Azure OpenAI image generation POSTs. + + For ``.../openai/deployments/{deployment}/images/generations``, routing uses the + deployment in the URL only; sending ``model`` in the body (especially the deployment + name) breaks some models (e.g. gpt-image-2). See LiteLLM #26316. + + Provider-style URLs (e.g. ``/providers/...`` for FLUX on Azure AI) keep all keys + so non–OpenAI-deployment payloads still work. + """ + if "images/generations" in api_base and "/openai/deployments/" in api_base: + return {k: v for k, v in data.items() if k != "model"} + return data diff --git a/litellm/llms/base_llm/image_edit/transformation.py b/litellm/llms/base_llm/image_edit/transformation.py index 71710f5b66..92429573ff 100644 --- a/litellm/llms/base_llm/image_edit/transformation.py +++ b/litellm/llms/base_llm/image_edit/transformation.py @@ -102,12 +102,15 @@ class BaseImageEditConfig(ABC): ) -> Tuple[Dict, RequestFiles]: pass - def finalize_image_edit_multipart_data( + def finalize_image_edit_request_data( self, data: dict, resolved_request_url: str ) -> dict: """ - Adjust non-file form fields after ``transform_image_edit_request`` using the - exact URL that will be used for the HTTP POST (same string as ``get_complete_url``). + Last pass on the request dict after ``transform_image_edit_request``, using the + exact URL string used for the HTTP POST (same as ``get_complete_url`` output). + + The handler sends this dict as ``data=`` for multipart providers or ``json=`` + for JSON-only providers; default implementation returns ``data`` unchanged. """ return data diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index de476c2fb0..2ffc7acbfb 100644 --- a/litellm/llms/custom_httpx/llm_http_handler.py +++ b/litellm/llms/custom_httpx/llm_http_handler.py @@ -5579,7 +5579,7 @@ class BaseLLMHTTPHandler: litellm_params=litellm_params, headers=headers, ) - data = image_edit_provider_config.finalize_image_edit_multipart_data( + data = image_edit_provider_config.finalize_image_edit_request_data( data, api_base ) @@ -5680,7 +5680,7 @@ class BaseLLMHTTPHandler: litellm_params=litellm_params, headers=headers, ) - data = image_edit_provider_config.finalize_image_edit_multipart_data( + data = image_edit_provider_config.finalize_image_edit_request_data( data, 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 d80e0aa90a..96cbd88da1 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 @@ -44,7 +44,7 @@ def test_azure_finalize_image_edit_strips_model_after_openai_transform(): api_base=litellm_params.api_base, litellm_params=litellm_params.model_dump(exclude_none=True), ) - data_out = config.finalize_image_edit_multipart_data(data, resolved) + data_out = config.finalize_image_edit_request_data(data, resolved) assert "model" not in data_out assert data_out.get("prompt") == prompt assert data_out.get("n") == 1 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 6e91fdf865..f49b3f09d6 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 @@ -12,6 +12,9 @@ sys.path.insert( ) # Adds the parent directory to the system path import litellm from litellm.llms.azure.azure import AzureChatCompletion +from litellm.llms.azure.image_generation.http_utils import ( + azure_deployment_image_generation_json_body, +) from litellm.llms.custom_httpx.http_handler import HTTPHandler from litellm.llms.azure.image_generation import ( AzureDallE3ImageGenerationConfig, @@ -41,7 +44,7 @@ def test_azure_deployment_image_generation_json_body(): "images/generations?api-version=2025-04-01-preview" ) data = {"model": "my-dep", "prompt": "x", "n": 1} - out = AzureChatCompletion.azure_deployment_image_generation_json_body(api, data) + out = azure_deployment_image_generation_json_body(api, data) assert "model" not in out assert out == {"prompt": "x", "n": 1} @@ -50,7 +53,7 @@ def test_azure_providers_image_generation_json_body_keeps_model(): """Non-deployment routes (e.g. FLUX on Azure AI) keep the payload unchanged.""" api = "https://example.services.ai.azure.com/providers/blackforestlabs/v1/flux-2-pro?api-version=preview" data = {"model": "flux.2-pro", "prompt": "x"} - out = AzureChatCompletion.azure_deployment_image_generation_json_body(api, data) + out = azure_deployment_image_generation_json_body(api, data) assert out == data