Skip to content

Commit cdc76f8

Browse files
committed
fix(litellm_proxy): add fake-api-key fallback when LITELLM_PROXY_API_KEY is not set
1 parent b8cef1a commit cdc76f8

File tree

8 files changed

+245
-8
lines changed

8 files changed

+245
-8
lines changed

litellm/llms/litellm_proxy/chat/transformation.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""
2-
Translate from OpenAI's `/v1/chat/completions` to VLLM's `/v1/chat/completions`
2+
Translate from OpenAI's `/v1/chat/completions` to LiteLLM Proxy's `/v1/chat/completions`
33
"""
44

55
from typing import TYPE_CHECKING, List, Optional, Tuple
@@ -39,7 +39,9 @@ def _get_openai_compatible_provider_info(
3939
self, api_base: Optional[str], api_key: Optional[str]
4040
) -> Tuple[Optional[str], Optional[str]]:
4141
api_base = api_base or get_secret_str("LITELLM_PROXY_API_BASE") # type: ignore
42-
dynamic_api_key = api_key or get_secret_str("LITELLM_PROXY_API_KEY")
42+
dynamic_api_key = (
43+
api_key or get_secret_str("LITELLM_PROXY_API_KEY") or "fake-api-key"
44+
) # litellm_proxy does not require an api key, but OpenAI client requires non-None value
4345
return api_base, dynamic_api_key
4446

4547
def get_models(
@@ -55,7 +57,9 @@ def get_models(
5557

5658
@staticmethod
5759
def get_api_key(api_key: Optional[str] = None) -> Optional[str]:
58-
return api_key or get_secret_str("LITELLM_PROXY_API_KEY")
60+
return (
61+
api_key or get_secret_str("LITELLM_PROXY_API_KEY") or "fake-api-key"
62+
) # litellm_proxy does not require an api key
5963

6064
@staticmethod
6165
def _should_use_litellm_proxy_by_default(

litellm/llms/litellm_proxy/image_edit/transformation.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ class LiteLLMProxyImageEditConfig(OpenAIImageEditConfig):
1010
def validate_environment(
1111
self, headers: dict, model: str, api_key: Optional[str] = None
1212
) -> dict:
13-
api_key = api_key or get_secret_str("LITELLM_PROXY_API_KEY")
13+
api_key = (
14+
api_key or get_secret_str("LITELLM_PROXY_API_KEY") or "fake-api-key"
15+
) # litellm_proxy does not require an api key
1416
headers.update({"Authorization": f"Bearer {api_key}"})
1517
return headers
1618

litellm/llms/litellm_proxy/image_generation/transformation.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
class LiteLLMProxyImageGenerationConfig(GPTImageGenerationConfig):
1010
"""Configuration for image generation requests routed through LiteLLM Proxy."""
11+
1112
def validate_environment(
1213
self,
1314
headers: dict,
@@ -18,7 +19,9 @@ def validate_environment(
1819
api_key: Optional[str] = None,
1920
api_base: Optional[str] = None,
2021
) -> dict:
21-
api_key = api_key or get_secret_str("LITELLM_PROXY_API_KEY")
22+
api_key = (
23+
api_key or get_secret_str("LITELLM_PROXY_API_KEY") or "fake-api-key"
24+
) # litellm_proxy does not require an api key
2225
headers.update({"Authorization": f"Bearer {api_key}"})
2326
return headers
2427

litellm/llms/litellm_proxy/responses/transformation.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@
99

1010
from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig
1111
from litellm.secret_managers.main import get_secret_str
12+
from litellm.types.router import GenericLiteLLMParams
1213
from litellm.types.utils import LlmProviders
1314

1415

1516
class LiteLLMProxyResponsesAPIConfig(OpenAIResponsesAPIConfig):
1617
"""
1718
Configuration for LiteLLM Proxy Responses API support.
18-
19+
1920
Extends OpenAI's config since the proxy follows OpenAI's API spec,
2021
but uses LITELLM_PROXY_API_BASE for the base URL.
2122
"""
@@ -24,18 +25,33 @@ class LiteLLMProxyResponsesAPIConfig(OpenAIResponsesAPIConfig):
2425
def custom_llm_provider(self) -> LlmProviders:
2526
return LlmProviders.LITELLM_PROXY
2627

28+
def validate_environment(
29+
self,
30+
headers: dict,
31+
model: str,
32+
litellm_params: Optional[GenericLiteLLMParams],
33+
) -> dict:
34+
litellm_params = litellm_params or GenericLiteLLMParams()
35+
api_key = (
36+
litellm_params.api_key
37+
or get_secret_str("LITELLM_PROXY_API_KEY")
38+
or "fake-api-key" # litellm_proxy does not require an api key
39+
)
40+
headers.update({"Authorization": f"Bearer {api_key}"})
41+
return headers
42+
2743
def get_complete_url(
2844
self,
2945
api_base: Optional[str],
3046
litellm_params: dict,
3147
) -> str:
3248
"""
3349
Get the endpoint for LiteLLM Proxy responses API.
34-
50+
3551
Uses LITELLM_PROXY_API_BASE environment variable if api_base is not provided.
3652
"""
3753
api_base = api_base or get_secret_str("LITELLM_PROXY_API_BASE")
38-
54+
3955
if api_base is None:
4056
raise ValueError(
4157
"api_base not set for LiteLLM Proxy responses API. "

tests/test_litellm/llms/litellm_proxy/chat/test_litellm_proxy_chat_transformation.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,101 @@ def test_litellm_gateway_from_sdk_with_user_param():
4040
)
4141
print(f"supported_params: {supported_params}")
4242
assert "user" in supported_params
43+
44+
45+
@pytest.mark.parametrize(
46+
"input_api_key, env_api_key, expected_api_key",
47+
[
48+
("user-provided-key", "secret-key", "user-provided-key"),
49+
(None, "secret-key", "secret-key"),
50+
(None, None, "fake-api-key"),
51+
("", "secret-key", "secret-key"),
52+
("", None, "fake-api-key"),
53+
],
54+
)
55+
def test_get_openai_compatible_provider_info_api_key(
56+
input_api_key, env_api_key, expected_api_key
57+
):
58+
config = LiteLLMProxyChatConfig()
59+
env = {}
60+
if env_api_key is not None:
61+
env["LITELLM_PROXY_API_KEY"] = env_api_key
62+
63+
with patch.dict("os.environ", env, clear=True):
64+
_, result_key = config._get_openai_compatible_provider_info(
65+
api_base=None, api_key=input_api_key
66+
)
67+
assert result_key == expected_api_key
68+
69+
70+
@pytest.mark.parametrize(
71+
"input_api_key, env_api_key, expected_api_key",
72+
[
73+
("user-provided-key", "secret-key", "user-provided-key"),
74+
(None, "secret-key", "secret-key"),
75+
(None, None, "fake-api-key"),
76+
("", "secret-key", "secret-key"),
77+
("", None, "fake-api-key"),
78+
],
79+
)
80+
def test_get_api_key(input_api_key, env_api_key, expected_api_key):
81+
env = {}
82+
if env_api_key is not None:
83+
env["LITELLM_PROXY_API_KEY"] = env_api_key
84+
85+
with patch.dict("os.environ", env, clear=True):
86+
result = LiteLLMProxyChatConfig.get_api_key(input_api_key)
87+
assert result == expected_api_key
88+
89+
90+
def test_completion_with_litellm_proxy_no_api_key():
91+
"""
92+
E2E mock test: USE_LITELLM_PROXY=true with no LITELLM_PROXY_API_KEY
93+
should use "fake-api-key" as fallback.
94+
"""
95+
with patch(
96+
"litellm.main.openai_chat_completions.completion"
97+
) as mock_completion_func:
98+
mock_completion_func.return_value = {}
99+
100+
env = {
101+
"USE_LITELLM_PROXY": "true",
102+
"LITELLM_PROXY_API_BASE": "http://localhost:4000",
103+
}
104+
with patch.dict("os.environ", env, clear=True):
105+
_ = litellm.completion(
106+
model="openai/gpt-4o",
107+
messages=[{"role": "user", "content": "Hello"}],
108+
)
109+
110+
mock_completion_func.assert_called_once()
111+
_, call_kwargs = mock_completion_func.call_args
112+
assert call_kwargs.get("api_key") == "fake-api-key"
113+
assert call_kwargs.get("custom_llm_provider") == "litellm_proxy"
114+
115+
116+
def test_completion_with_litellm_proxy_does_not_use_openai_key():
117+
"""
118+
OPENAI_API_KEY should NOT be sent to litellm_proxy.
119+
Even when OPENAI_API_KEY is in the environment, the proxy should use
120+
"fake-api-key" (truthy value stops the or-chain in main.py:2366-2371).
121+
"""
122+
with patch(
123+
"litellm.main.openai_chat_completions.completion"
124+
) as mock_completion_func:
125+
mock_completion_func.return_value = {}
126+
127+
env = {
128+
"USE_LITELLM_PROXY": "true",
129+
"LITELLM_PROXY_API_BASE": "http://localhost:4000",
130+
"OPENAI_API_KEY": "sk-real-openai-key",
131+
}
132+
with patch.dict("os.environ", env, clear=True):
133+
_ = litellm.completion(
134+
model="openai/gpt-4o",
135+
messages=[{"role": "user", "content": "Hello"}],
136+
)
137+
138+
_, call_kwargs = mock_completion_func.call_args
139+
assert call_kwargs.get("api_key") == "fake-api-key"
140+
assert call_kwargs.get("api_key") != "sk-real-openai-key"
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from unittest.mock import patch
2+
3+
import pytest
4+
5+
from litellm.llms.litellm_proxy.image_edit.transformation import (
6+
LiteLLMProxyImageEditConfig,
7+
)
8+
9+
10+
@pytest.mark.parametrize(
11+
"input_api_key, env_api_key, expected_bearer",
12+
[
13+
("user-provided-key", "secret-key", "Bearer user-provided-key"),
14+
(None, "secret-key", "Bearer secret-key"),
15+
(None, None, "Bearer fake-api-key"),
16+
("", "secret-key", "Bearer secret-key"),
17+
("", None, "Bearer fake-api-key"),
18+
],
19+
)
20+
def test_validate_environment(input_api_key, env_api_key, expected_bearer):
21+
config = LiteLLMProxyImageEditConfig()
22+
env = {}
23+
if env_api_key is not None:
24+
env["LITELLM_PROXY_API_KEY"] = env_api_key
25+
26+
with patch.dict("os.environ", env, clear=True):
27+
headers = config.validate_environment(
28+
headers={}, model="dall-e-3", api_key=input_api_key
29+
)
30+
assert headers.get("Authorization") == expected_bearer
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from unittest.mock import patch
2+
3+
import pytest
4+
5+
from litellm.llms.litellm_proxy.image_generation.transformation import (
6+
LiteLLMProxyImageGenerationConfig,
7+
)
8+
9+
10+
@pytest.mark.parametrize(
11+
"input_api_key, env_api_key, expected_bearer",
12+
[
13+
("user-provided-key", "secret-key", "Bearer user-provided-key"),
14+
(None, "secret-key", "Bearer secret-key"),
15+
(None, None, "Bearer fake-api-key"),
16+
("", "secret-key", "Bearer secret-key"),
17+
("", None, "Bearer fake-api-key"),
18+
],
19+
)
20+
def test_validate_environment(input_api_key, env_api_key, expected_bearer):
21+
config = LiteLLMProxyImageGenerationConfig()
22+
env = {}
23+
if env_api_key is not None:
24+
env["LITELLM_PROXY_API_KEY"] = env_api_key
25+
26+
with patch.dict("os.environ", env, clear=True):
27+
headers = config.validate_environment(
28+
headers={},
29+
model="dall-e-3",
30+
messages=[],
31+
optional_params={},
32+
litellm_params={},
33+
api_key=input_api_key,
34+
)
35+
assert headers.get("Authorization") == expected_bearer
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from unittest.mock import patch
2+
3+
import pytest
4+
5+
from litellm.llms.litellm_proxy.responses.transformation import (
6+
LiteLLMProxyResponsesAPIConfig,
7+
)
8+
from litellm.types.router import GenericLiteLLMParams
9+
10+
11+
@pytest.mark.parametrize(
12+
"litellm_params_api_key, env_api_key, expected_bearer",
13+
[
14+
("user-provided-key", "secret-key", "Bearer user-provided-key"),
15+
(None, "secret-key", "Bearer secret-key"),
16+
(None, None, "Bearer fake-api-key"),
17+
("", "secret-key", "Bearer secret-key"),
18+
("", None, "Bearer fake-api-key"),
19+
],
20+
)
21+
def test_validate_environment(litellm_params_api_key, env_api_key, expected_bearer):
22+
config = LiteLLMProxyResponsesAPIConfig()
23+
env = {}
24+
if env_api_key is not None:
25+
env["LITELLM_PROXY_API_KEY"] = env_api_key
26+
27+
litellm_params = GenericLiteLLMParams(api_key=litellm_params_api_key)
28+
29+
with patch.dict("os.environ", env, clear=True):
30+
headers = config.validate_environment(
31+
headers={}, model="gpt-4o", litellm_params=litellm_params
32+
)
33+
assert headers.get("Authorization") == expected_bearer
34+
35+
36+
def test_validate_environment_does_not_use_openai_key():
37+
"""
38+
OPENAI_API_KEY should NOT be used for litellm_proxy requests.
39+
The proxy should use LITELLM_PROXY_API_KEY or fall back to fake-api-key.
40+
"""
41+
config = LiteLLMProxyResponsesAPIConfig()
42+
env = {"OPENAI_API_KEY": "sk-real-openai-key"}
43+
litellm_params = GenericLiteLLMParams()
44+
45+
with patch.dict("os.environ", env, clear=True):
46+
headers = config.validate_environment(
47+
headers={}, model="gpt-4o", litellm_params=litellm_params
48+
)
49+
assert headers.get("Authorization") == "Bearer fake-api-key"

0 commit comments

Comments
 (0)