diff --git a/litellm/llms/litellm_proxy/chat/transformation.py b/litellm/llms/litellm_proxy/chat/transformation.py index cf6a6ed7a54..63248752bd6 100644 --- a/litellm/llms/litellm_proxy/chat/transformation.py +++ b/litellm/llms/litellm_proxy/chat/transformation.py @@ -1,5 +1,5 @@ """ -Translate from OpenAI's `/v1/chat/completions` to VLLM's `/v1/chat/completions` +Translate from OpenAI's `/v1/chat/completions` to LiteLLM Proxy's `/v1/chat/completions` """ from typing import TYPE_CHECKING, List, Optional, Tuple @@ -39,7 +39,9 @@ class LiteLLMProxyChatConfig(OpenAIGPTConfig): self, api_base: Optional[str], api_key: Optional[str] ) -> Tuple[Optional[str], Optional[str]]: api_base = api_base or get_secret_str("LITELLM_PROXY_API_BASE") # type: ignore - dynamic_api_key = api_key or get_secret_str("LITELLM_PROXY_API_KEY") + dynamic_api_key = ( + api_key or get_secret_str("LITELLM_PROXY_API_KEY") or "fake-api-key" + ) # litellm_proxy does not require an api key, but OpenAI client requires non-None value return api_base, dynamic_api_key def get_models( @@ -55,7 +57,9 @@ class LiteLLMProxyChatConfig(OpenAIGPTConfig): @staticmethod def get_api_key(api_key: Optional[str] = None) -> Optional[str]: - return api_key or get_secret_str("LITELLM_PROXY_API_KEY") + return ( + api_key or get_secret_str("LITELLM_PROXY_API_KEY") or "fake-api-key" + ) # litellm_proxy does not require an api key @staticmethod def _should_use_litellm_proxy_by_default( diff --git a/litellm/llms/litellm_proxy/image_edit/transformation.py b/litellm/llms/litellm_proxy/image_edit/transformation.py index 5f5e2bdb24d..92edbf7f4ed 100644 --- a/litellm/llms/litellm_proxy/image_edit/transformation.py +++ b/litellm/llms/litellm_proxy/image_edit/transformation.py @@ -10,7 +10,9 @@ class LiteLLMProxyImageEditConfig(OpenAIImageEditConfig): def validate_environment( self, headers: dict, model: str, api_key: Optional[str] = None ) -> dict: - api_key = api_key or get_secret_str("LITELLM_PROXY_API_KEY") + api_key = ( + api_key or get_secret_str("LITELLM_PROXY_API_KEY") or "fake-api-key" + ) # litellm_proxy does not require an api key headers.update({"Authorization": f"Bearer {api_key}"}) return headers diff --git a/litellm/llms/litellm_proxy/image_generation/transformation.py b/litellm/llms/litellm_proxy/image_generation/transformation.py index 6174424154d..0da8613a1ff 100644 --- a/litellm/llms/litellm_proxy/image_generation/transformation.py +++ b/litellm/llms/litellm_proxy/image_generation/transformation.py @@ -8,6 +8,7 @@ from litellm.secret_managers.main import get_secret_str class LiteLLMProxyImageGenerationConfig(GPTImageGenerationConfig): """Configuration for image generation requests routed through LiteLLM Proxy.""" + def validate_environment( self, headers: dict, @@ -18,7 +19,9 @@ class LiteLLMProxyImageGenerationConfig(GPTImageGenerationConfig): api_key: Optional[str] = None, api_base: Optional[str] = None, ) -> dict: - api_key = api_key or get_secret_str("LITELLM_PROXY_API_KEY") + api_key = ( + api_key or get_secret_str("LITELLM_PROXY_API_KEY") or "fake-api-key" + ) # litellm_proxy does not require an api key headers.update({"Authorization": f"Bearer {api_key}"}) return headers diff --git a/litellm/llms/litellm_proxy/responses/transformation.py b/litellm/llms/litellm_proxy/responses/transformation.py index 0b81d8be7d8..d3fbc919926 100644 --- a/litellm/llms/litellm_proxy/responses/transformation.py +++ b/litellm/llms/litellm_proxy/responses/transformation.py @@ -9,13 +9,14 @@ from typing import Optional from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig from litellm.secret_managers.main import get_secret_str +from litellm.types.router import GenericLiteLLMParams from litellm.types.utils import LlmProviders class LiteLLMProxyResponsesAPIConfig(OpenAIResponsesAPIConfig): """ Configuration for LiteLLM Proxy Responses API support. - + Extends OpenAI's config since the proxy follows OpenAI's API spec, but uses LITELLM_PROXY_API_BASE for the base URL. """ @@ -24,6 +25,21 @@ class LiteLLMProxyResponsesAPIConfig(OpenAIResponsesAPIConfig): def custom_llm_provider(self) -> LlmProviders: return LlmProviders.LITELLM_PROXY + def validate_environment( + self, + headers: dict, + model: str, + litellm_params: Optional[GenericLiteLLMParams], + ) -> dict: + litellm_params = litellm_params or GenericLiteLLMParams() + api_key = ( + litellm_params.api_key + or get_secret_str("LITELLM_PROXY_API_KEY") + or "fake-api-key" # litellm_proxy does not require an api key + ) + headers.update({"Authorization": f"Bearer {api_key}"}) + return headers + def get_complete_url( self, api_base: Optional[str], @@ -31,11 +47,11 @@ class LiteLLMProxyResponsesAPIConfig(OpenAIResponsesAPIConfig): ) -> str: """ Get the endpoint for LiteLLM Proxy responses API. - + Uses LITELLM_PROXY_API_BASE environment variable if api_base is not provided. """ api_base = api_base or get_secret_str("LITELLM_PROXY_API_BASE") - + if api_base is None: raise ValueError( "api_base not set for LiteLLM Proxy responses API. " diff --git a/tests/test_litellm/llms/litellm_proxy/chat/test_litellm_proxy_chat_transformation.py b/tests/test_litellm/llms/litellm_proxy/chat/test_litellm_proxy_chat_transformation.py index 3c2f22dca9e..795942bc5a0 100644 --- a/tests/test_litellm/llms/litellm_proxy/chat/test_litellm_proxy_chat_transformation.py +++ b/tests/test_litellm/llms/litellm_proxy/chat/test_litellm_proxy_chat_transformation.py @@ -40,3 +40,101 @@ def test_litellm_gateway_from_sdk_with_user_param(): ) print(f"supported_params: {supported_params}") assert "user" in supported_params + + +@pytest.mark.parametrize( + "input_api_key, env_api_key, expected_api_key", + [ + ("user-provided-key", "secret-key", "user-provided-key"), + (None, "secret-key", "secret-key"), + (None, None, "fake-api-key"), + ("", "secret-key", "secret-key"), + ("", None, "fake-api-key"), + ], +) +def test_get_openai_compatible_provider_info_api_key( + input_api_key, env_api_key, expected_api_key +): + config = LiteLLMProxyChatConfig() + env = {} + if env_api_key is not None: + env["LITELLM_PROXY_API_KEY"] = env_api_key + + with patch.dict("os.environ", env, clear=True): + _, result_key = config._get_openai_compatible_provider_info( + api_base=None, api_key=input_api_key + ) + assert result_key == expected_api_key + + +@pytest.mark.parametrize( + "input_api_key, env_api_key, expected_api_key", + [ + ("user-provided-key", "secret-key", "user-provided-key"), + (None, "secret-key", "secret-key"), + (None, None, "fake-api-key"), + ("", "secret-key", "secret-key"), + ("", None, "fake-api-key"), + ], +) +def test_get_api_key(input_api_key, env_api_key, expected_api_key): + env = {} + if env_api_key is not None: + env["LITELLM_PROXY_API_KEY"] = env_api_key + + with patch.dict("os.environ", env, clear=True): + result = LiteLLMProxyChatConfig.get_api_key(input_api_key) + assert result == expected_api_key + + +def test_completion_with_litellm_proxy_no_api_key(): + """ + E2E mock test: USE_LITELLM_PROXY=true with no LITELLM_PROXY_API_KEY + should use "fake-api-key" as fallback. + """ + with patch( + "litellm.main.openai_chat_completions.completion" + ) as mock_completion_func: + mock_completion_func.return_value = {} + + env = { + "USE_LITELLM_PROXY": "true", + "LITELLM_PROXY_API_BASE": "http://localhost:4000", + } + with patch.dict("os.environ", env, clear=True): + _ = litellm.completion( + model="openai/gpt-4o", + messages=[{"role": "user", "content": "Hello"}], + ) + + mock_completion_func.assert_called_once() + _, call_kwargs = mock_completion_func.call_args + assert call_kwargs.get("api_key") == "fake-api-key" + assert call_kwargs.get("custom_llm_provider") == "litellm_proxy" + + +def test_completion_with_litellm_proxy_does_not_use_openai_key(): + """ + OPENAI_API_KEY should NOT be sent to litellm_proxy. + Even when OPENAI_API_KEY is in the environment, the proxy should use + "fake-api-key" (truthy value stops the or-chain in main.py:2366-2371). + """ + with patch( + "litellm.main.openai_chat_completions.completion" + ) as mock_completion_func: + mock_completion_func.return_value = {} + + env = { + "USE_LITELLM_PROXY": "true", + "LITELLM_PROXY_API_BASE": "http://localhost:4000", + "OPENAI_API_KEY": "sk-real-openai-key", + } + with patch.dict("os.environ", env, clear=True): + _ = litellm.completion( + model="openai/gpt-4o", + messages=[{"role": "user", "content": "Hello"}], + ) + + _, call_kwargs = mock_completion_func.call_args + assert call_kwargs.get("api_key") == "fake-api-key" + assert call_kwargs.get("api_key") != "sk-real-openai-key" diff --git a/tests/test_litellm/llms/litellm_proxy/image_edit/test_litellm_proxy_image_edit_transformation.py b/tests/test_litellm/llms/litellm_proxy/image_edit/test_litellm_proxy_image_edit_transformation.py new file mode 100644 index 00000000000..e8669b1ac23 --- /dev/null +++ b/tests/test_litellm/llms/litellm_proxy/image_edit/test_litellm_proxy_image_edit_transformation.py @@ -0,0 +1,30 @@ +from unittest.mock import patch + +import pytest + +from litellm.llms.litellm_proxy.image_edit.transformation import ( + LiteLLMProxyImageEditConfig, +) + + +@pytest.mark.parametrize( + "input_api_key, env_api_key, expected_bearer", + [ + ("user-provided-key", "secret-key", "Bearer user-provided-key"), + (None, "secret-key", "Bearer secret-key"), + (None, None, "Bearer fake-api-key"), + ("", "secret-key", "Bearer secret-key"), + ("", None, "Bearer fake-api-key"), + ], +) +def test_validate_environment(input_api_key, env_api_key, expected_bearer): + config = LiteLLMProxyImageEditConfig() + env = {} + if env_api_key is not None: + env["LITELLM_PROXY_API_KEY"] = env_api_key + + with patch.dict("os.environ", env, clear=True): + headers = config.validate_environment( + headers={}, model="dall-e-3", api_key=input_api_key + ) + assert headers.get("Authorization") == expected_bearer diff --git a/tests/test_litellm/llms/litellm_proxy/image_generation/test_litellm_proxy_image_generation_transformation.py b/tests/test_litellm/llms/litellm_proxy/image_generation/test_litellm_proxy_image_generation_transformation.py new file mode 100644 index 00000000000..82dc83d37a2 --- /dev/null +++ b/tests/test_litellm/llms/litellm_proxy/image_generation/test_litellm_proxy_image_generation_transformation.py @@ -0,0 +1,35 @@ +from unittest.mock import patch + +import pytest + +from litellm.llms.litellm_proxy.image_generation.transformation import ( + LiteLLMProxyImageGenerationConfig, +) + + +@pytest.mark.parametrize( + "input_api_key, env_api_key, expected_bearer", + [ + ("user-provided-key", "secret-key", "Bearer user-provided-key"), + (None, "secret-key", "Bearer secret-key"), + (None, None, "Bearer fake-api-key"), + ("", "secret-key", "Bearer secret-key"), + ("", None, "Bearer fake-api-key"), + ], +) +def test_validate_environment(input_api_key, env_api_key, expected_bearer): + config = LiteLLMProxyImageGenerationConfig() + env = {} + if env_api_key is not None: + env["LITELLM_PROXY_API_KEY"] = env_api_key + + with patch.dict("os.environ", env, clear=True): + headers = config.validate_environment( + headers={}, + model="dall-e-3", + messages=[], + optional_params={}, + litellm_params={}, + api_key=input_api_key, + ) + assert headers.get("Authorization") == expected_bearer diff --git a/tests/test_litellm/llms/litellm_proxy/responses/test_litellm_proxy_responses_transformation.py b/tests/test_litellm/llms/litellm_proxy/responses/test_litellm_proxy_responses_transformation.py new file mode 100644 index 00000000000..8c8bd903ddd --- /dev/null +++ b/tests/test_litellm/llms/litellm_proxy/responses/test_litellm_proxy_responses_transformation.py @@ -0,0 +1,49 @@ +from unittest.mock import patch + +import pytest + +from litellm.llms.litellm_proxy.responses.transformation import ( + LiteLLMProxyResponsesAPIConfig, +) +from litellm.types.router import GenericLiteLLMParams + + +@pytest.mark.parametrize( + "litellm_params_api_key, env_api_key, expected_bearer", + [ + ("user-provided-key", "secret-key", "Bearer user-provided-key"), + (None, "secret-key", "Bearer secret-key"), + (None, None, "Bearer fake-api-key"), + ("", "secret-key", "Bearer secret-key"), + ("", None, "Bearer fake-api-key"), + ], +) +def test_validate_environment(litellm_params_api_key, env_api_key, expected_bearer): + config = LiteLLMProxyResponsesAPIConfig() + env = {} + if env_api_key is not None: + env["LITELLM_PROXY_API_KEY"] = env_api_key + + litellm_params = GenericLiteLLMParams(api_key=litellm_params_api_key) + + with patch.dict("os.environ", env, clear=True): + headers = config.validate_environment( + headers={}, model="gpt-4o", litellm_params=litellm_params + ) + assert headers.get("Authorization") == expected_bearer + + +def test_validate_environment_does_not_use_openai_key(): + """ + OPENAI_API_KEY should NOT be used for litellm_proxy requests. + The proxy should use LITELLM_PROXY_API_KEY or fall back to fake-api-key. + """ + config = LiteLLMProxyResponsesAPIConfig() + env = {"OPENAI_API_KEY": "sk-real-openai-key"} + litellm_params = GenericLiteLLMParams() + + with patch.dict("os.environ", env, clear=True): + headers = config.validate_environment( + headers={}, model="gpt-4o", litellm_params=litellm_params + ) + assert headers.get("Authorization") == "Bearer fake-api-key"