diff --git a/litellm/images/main.py b/litellm/images/main.py index ca2d4e0b911..acdac166a77 100644 --- a/litellm/images/main.py +++ b/litellm/images/main.py @@ -778,6 +778,8 @@ def image_edit( model=model, image_edit_provider_config=image_edit_provider_config, image_edit_optional_params=image_edit_optional_params, + drop_params=kwargs.get("drop_params"), + additional_drop_params=kwargs.get("additional_drop_params"), ) ) diff --git a/litellm/images/utils.py b/litellm/images/utils.py index 7b1875c4932..fdf240ba2af 100644 --- a/litellm/images/utils.py +++ b/litellm/images/utils.py @@ -1,5 +1,5 @@ from io import BufferedReader, BytesIO -from typing import Any, Dict, cast, get_type_hints +from typing import Any, Dict, List, Optional, cast, get_type_hints import litellm from litellm.litellm_core_utils.token_counter import get_image_type @@ -14,41 +14,53 @@ def get_optional_params_image_edit( model: str, image_edit_provider_config: BaseImageEditConfig, image_edit_optional_params: ImageEditOptionalRequestParams, + drop_params: Optional[bool] = None, + additional_drop_params: Optional[List[str]] = None, ) -> Dict: """ Get optional parameters for the image edit API. Args: - params: Dictionary of all parameters model: The model name image_edit_provider_config: The provider configuration for image edit API + image_edit_optional_params: The optional parameters for the image edit API + drop_params: If True, silently drop unsupported parameters instead of raising + additional_drop_params: List of additional parameter names to drop Returns: A dictionary of supported parameters for the image edit API """ - # Remove None values and internal parameters - - # Get supported parameters for the model supported_params = image_edit_provider_config.get_supported_openai_params(model) - # Check for unsupported parameters + should_drop = litellm.drop_params is True or drop_params is True + + filtered_optional_params = dict(image_edit_optional_params) + if additional_drop_params: + for param in additional_drop_params: + filtered_optional_params.pop(param, None) + unsupported_params = [ param - for param in image_edit_optional_params + for param in filtered_optional_params if param not in supported_params ] if unsupported_params: - raise litellm.UnsupportedParamsError( - model=model, - message=f"The following parameters are not supported for model {model}: {', '.join(unsupported_params)}", - ) + if should_drop: + for param in unsupported_params: + filtered_optional_params.pop(param, None) + else: + raise litellm.UnsupportedParamsError( + model=model, + message=f"The following parameters are not supported for model {model}: {', '.join(unsupported_params)}", + ) - # Map parameters to provider-specific format mapped_params = image_edit_provider_config.map_openai_params( - image_edit_optional_params=image_edit_optional_params, + image_edit_optional_params=cast( + ImageEditOptionalRequestParams, filtered_optional_params + ), model=model, - drop_params=litellm.drop_params, + drop_params=should_drop, ) return mapped_params diff --git a/litellm/llms/vertex_ai/image_edit/vertex_gemini_transformation.py b/litellm/llms/vertex_ai/image_edit/vertex_gemini_transformation.py index 469340f6bba..df306002f53 100644 --- a/litellm/llms/vertex_ai/image_edit/vertex_gemini_transformation.py +++ b/litellm/llms/vertex_ai/image_edit/vertex_gemini_transformation.py @@ -114,19 +114,24 @@ def get_complete_url( """ Get the complete URL for Vertex AI Gemini generateContent API """ - vertex_project = self._resolve_vertex_project() - vertex_location = self._resolve_vertex_location() + vertex_project = ( + litellm_params.get("vertex_project") or self._resolve_vertex_project() + ) + vertex_location = ( + litellm_params.get("vertex_location") or self._resolve_vertex_location() + ) if not vertex_project or not vertex_location: raise ValueError("vertex_project and vertex_location are required for Vertex AI") - # Use the model name as provided, handling vertex_ai prefix model_name = model if model.startswith("vertex_ai/"): model_name = model.replace("vertex_ai/", "") if api_base: base_url = api_base.rstrip("/") + elif vertex_location == "global": + base_url = "https://aiplatform.googleapis.com" else: base_url = f"https://{vertex_location}-aiplatform.googleapis.com" diff --git a/tests/test_litellm/images/test_image_edit_utils.py b/tests/test_litellm/images/test_image_edit_utils.py new file mode 100644 index 00000000000..56d8e48405b --- /dev/null +++ b/tests/test_litellm/images/test_image_edit_utils.py @@ -0,0 +1,170 @@ +from typing import Any, Dict, List +from unittest.mock import MagicMock, patch + +import pytest + +import litellm +from litellm.images.utils import ImageEditRequestUtils +from litellm.llms.base_llm.image_edit.transformation import BaseImageEditConfig +from litellm.types.images.main import ImageEditOptionalRequestParams + + +class MockImageEditConfig(BaseImageEditConfig): + def get_supported_openai_params(self, model: str) -> List[str]: + return ["size", "quality"] + + def map_openai_params( + self, + image_edit_optional_params: ImageEditOptionalRequestParams, + model: str, + drop_params: bool, + ) -> Dict[str, Any]: + return dict(image_edit_optional_params) + + def get_complete_url( + self, model: str, api_base: str, litellm_params: dict + ) -> str: + return "https://example.com/api" + + def validate_environment( + self, headers: dict, model: str, api_key: str = None + ) -> dict: + return headers + + def transform_image_edit_request(self, *args, **kwargs): + return {}, [] + + def transform_image_edit_response(self, *args, **kwargs): + return MagicMock() + + +class TestImageEditRequestUtilsDropParams: + def setup_method(self): + self.config = MockImageEditConfig() + self.model = "test-model" + self._original_drop_params = getattr(litellm, "drop_params", None) + + def teardown_method(self): + if self._original_drop_params is None: + if hasattr(litellm, "drop_params"): + delattr(litellm, "drop_params") + else: + litellm.drop_params = self._original_drop_params + + def test_unsupported_params_raises_without_drop(self): + litellm.drop_params = False + optional_params: ImageEditOptionalRequestParams = { + "size": "1024x1024", + "unsupported_param": "value", + } + + with pytest.raises(litellm.UnsupportedParamsError) as exc_info: + ImageEditRequestUtils.get_optional_params_image_edit( + model=self.model, + image_edit_provider_config=self.config, + image_edit_optional_params=optional_params, + ) + + assert "unsupported_param" in str(exc_info.value) + + def test_drop_params_global_setting(self): + litellm.drop_params = True + optional_params: ImageEditOptionalRequestParams = { + "size": "1024x1024", + "unsupported_param": "value", + } + + result = ImageEditRequestUtils.get_optional_params_image_edit( + model=self.model, + image_edit_provider_config=self.config, + image_edit_optional_params=optional_params, + ) + + assert "size" in result + assert "unsupported_param" not in result + + def test_drop_params_explicit_parameter(self): + litellm.drop_params = False + optional_params: ImageEditOptionalRequestParams = { + "size": "1024x1024", + "unsupported_param": "value", + } + + result = ImageEditRequestUtils.get_optional_params_image_edit( + model=self.model, + image_edit_provider_config=self.config, + image_edit_optional_params=optional_params, + drop_params=True, + ) + + assert "size" in result + assert "unsupported_param" not in result + + def test_additional_drop_params(self): + litellm.drop_params = False + optional_params: ImageEditOptionalRequestParams = { + "size": "1024x1024", + "quality": "high", + } + + result = ImageEditRequestUtils.get_optional_params_image_edit( + model=self.model, + image_edit_provider_config=self.config, + image_edit_optional_params=optional_params, + additional_drop_params=["quality"], + ) + + assert "size" in result + assert "quality" not in result + + def test_drop_params_false_with_global_true(self): + litellm.drop_params = True + optional_params: ImageEditOptionalRequestParams = { + "size": "1024x1024", + "unsupported_param": "value", + } + + result = ImageEditRequestUtils.get_optional_params_image_edit( + model=self.model, + image_edit_provider_config=self.config, + image_edit_optional_params=optional_params, + drop_params=False, + ) + + assert "size" in result + assert "unsupported_param" not in result + + def test_supported_params_pass_through(self): + litellm.drop_params = False + optional_params: ImageEditOptionalRequestParams = { + "size": "1024x1024", + "quality": "high", + } + + result = ImageEditRequestUtils.get_optional_params_image_edit( + model=self.model, + image_edit_provider_config=self.config, + image_edit_optional_params=optional_params, + ) + + assert result["size"] == "1024x1024" + assert result["quality"] == "high" + + def test_additional_drop_params_with_unsupported_and_drop_true(self): + litellm.drop_params = True + optional_params: ImageEditOptionalRequestParams = { + "size": "1024x1024", + "quality": "high", + "unsupported_param": "value", + } + + result = ImageEditRequestUtils.get_optional_params_image_edit( + model=self.model, + image_edit_provider_config=self.config, + image_edit_optional_params=optional_params, + additional_drop_params=["quality"], + ) + + assert "size" in result + assert "quality" not in result + assert "unsupported_param" not in result diff --git a/tests/test_litellm/llms/vertex_ai/image_edit/test_vertex_ai_image_edit_transformation.py b/tests/test_litellm/llms/vertex_ai/image_edit/test_vertex_ai_image_edit_transformation.py index af07534eb57..8c1bd1545f9 100644 --- a/tests/test_litellm/llms/vertex_ai/image_edit/test_vertex_ai_image_edit_transformation.py +++ b/tests/test_litellm/llms/vertex_ai/image_edit/test_vertex_ai_image_edit_transformation.py @@ -140,6 +140,55 @@ def test_transform_image_edit_request_without_image_raises(self) -> None: headers={}, ) + def test_get_complete_url_from_litellm_params(self) -> None: + """Test vertex_project/vertex_location read from litellm_params first""" + url = self.config.get_complete_url( + model="gemini-2.5-flash", + api_base=None, + litellm_params={ + "vertex_project": "params-project", + "vertex_location": "us-east1", + }, + ) + assert "params-project" in url + assert "us-east1" in url + + def test_get_complete_url_global_location(self) -> None: + """Test global location uses correct base URL without region prefix""" + url = self.config.get_complete_url( + model="gemini-2.5-flash", + api_base=None, + litellm_params={ + "vertex_project": "test-project", + "vertex_location": "global", + }, + ) + assert "aiplatform.googleapis.com" in url + assert "global-aiplatform.googleapis.com" not in url + assert "/locations/global/" in url + + def test_get_complete_url_litellm_params_overrides_env(self) -> None: + """Test litellm_params takes precedence over environment variables""" + with patch.dict( + os.environ, + { + "VERTEXAI_PROJECT": "env-project", + "VERTEXAI_LOCATION": "us-central1", + }, + ): + url = self.config.get_complete_url( + model="gemini-2.5-flash", + api_base=None, + litellm_params={ + "vertex_project": "params-project", + "vertex_location": "eu-west1", + }, + ) + assert "params-project" in url + assert "eu-west1" in url + assert "env-project" not in url + assert "us-central1" not in url + class TestVertexAIImagenImageEditTransformation: def setup_method(self) -> None: