From 999ffabc39cd5175293245160d9a4e8e8283fd61 Mon Sep 17 00:00:00 2001 From: Cesar Garcia <128240629+Chesars@users.noreply.github.com> Date: Mon, 15 Dec 2025 23:11:02 -0300 Subject: [PATCH] fix(gemini): use JSON instead of form-data for image edit requests (#18012) * fix(gemini): use JSON instead of form-data for image edit requests Gemini's image edit API expects JSON body, not multipart/form-data. The handler was sending form-encoded data which caused 400 errors: "Invalid JSON payload received. Unexpected token." Changes: - Add use_multipart_form_data() method to BaseImageEditConfig (default True) - Modify image_edit_handler to use json= when use_multipart_form_data() is False - Override use_multipart_form_data() in GeminiImageEditConfig to return False * test(gemini): add test for use_multipart_form_data --- .../base_llm/image_edit/transformation.py | 9 ++++ litellm/llms/custom_httpx/llm_http_handler.py | 50 +++++++++++++------ .../llms/gemini/image_edit/transformation.py | 4 ++ .../test_gemini_image_edit_transformation.py | 11 ++++ 4 files changed, 60 insertions(+), 14 deletions(-) diff --git a/litellm/llms/base_llm/image_edit/transformation.py b/litellm/llms/base_llm/image_edit/transformation.py index f3ae2d32ea..d522675296 100644 --- a/litellm/llms/base_llm/image_edit/transformation.py +++ b/litellm/llms/base_llm/image_edit/transformation.py @@ -109,6 +109,15 @@ class BaseImageEditConfig(ABC): ) -> ImageResponse: pass + def use_multipart_form_data(self) -> bool: + """ + Return True if the provider uses multipart/form-data for image edit requests. + Return False if the provider uses JSON requests. + + Default is True for backwards compatibility with OpenAI-style providers. + """ + return True + def get_error_class( self, error_message: str, status_code: int, headers: Union[dict, httpx.Headers] ) -> BaseLLMException: diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index 4a7789a181..4b38f54215 100644 --- a/litellm/llms/custom_httpx/llm_http_handler.py +++ b/litellm/llms/custom_httpx/llm_http_handler.py @@ -3768,13 +3768,24 @@ class BaseLLMHTTPHandler: ) try: - response = sync_httpx_client.post( - url=api_base, - headers=headers, - data=data, - files=files, - timeout=timeout, - ) + # Check if provider uses multipart/form-data or JSON + if image_edit_provider_config.use_multipart_form_data(): + # Use form-data (OpenAI style) + response = sync_httpx_client.post( + url=api_base, + headers=headers, + data=data, + files=files, + timeout=timeout, + ) + else: + # Use JSON (Gemini style) + response = sync_httpx_client.post( + url=api_base, + headers=headers, + json=data, + timeout=timeout, + ) except Exception as e: raise self._handle_error( @@ -3853,13 +3864,24 @@ class BaseLLMHTTPHandler: ) try: - response = await async_httpx_client.post( - url=api_base, - headers=headers, - data=data, - files=files, - timeout=timeout, - ) + # Check if provider uses multipart/form-data or JSON + if image_edit_provider_config.use_multipart_form_data(): + # Use form-data (OpenAI style) + response = await async_httpx_client.post( + url=api_base, + headers=headers, + data=data, + files=files, + timeout=timeout, + ) + else: + # Use JSON (Gemini style) + response = await async_httpx_client.post( + url=api_base, + headers=headers, + json=data, + timeout=timeout, + ) except Exception as e: raise self._handle_error( diff --git a/litellm/llms/gemini/image_edit/transformation.py b/litellm/llms/gemini/image_edit/transformation.py index 830c58a006..78a7ff9546 100644 --- a/litellm/llms/gemini/image_edit/transformation.py +++ b/litellm/llms/gemini/image_edit/transformation.py @@ -63,6 +63,10 @@ class GeminiImageEditConfig(BaseImageEditConfig): headers["Content-Type"] = "application/json" return headers + def use_multipart_form_data(self) -> bool: + """Gemini uses JSON requests, not multipart/form-data.""" + return False + def get_complete_url( self, model: str, diff --git a/tests/test_litellm/llms/gemini/image_edit/test_gemini_image_edit_transformation.py b/tests/test_litellm/llms/gemini/image_edit/test_gemini_image_edit_transformation.py index 2732bf1595..021cfaeff5 100644 --- a/tests/test_litellm/llms/gemini/image_edit/test_gemini_image_edit_transformation.py +++ b/tests/test_litellm/llms/gemini/image_edit/test_gemini_image_edit_transformation.py @@ -147,3 +147,14 @@ class TestGeminiImageEditTransformation: headers={}, ) + def test_use_multipart_form_data_returns_false(self) -> None: + """ + Gemini uses JSON requests, not multipart/form-data. + This is critical because httpx sends data differently: + - data=dict sends form-encoded + - json=dict sends JSON + + Without this, Gemini returns: "Invalid JSON payload received. Unexpected token." + """ + assert self.config.use_multipart_form_data() is False +