fix(litellm_proxy): add fake-api-key fallback when LITELLM_PROXY_API_KEY is not set

This commit is contained in:
ritsuki1227 2026-02-11 12:50:04 +09:00
parent b8cef1a4e5
commit cdc76f8b50
8 changed files with 245 additions and 8 deletions

View file

@ -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(

View file

@ -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

View file

@ -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

View file

@ -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. "

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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"