Skip to content

Commit f651d51

Browse files
Litellm dev 02 07 2025 p2 (#8377)
* fix(caching_routes.py): mask redis password on `/cache/ping` route * fix(caching_routes.py): fix linting erro * fix(caching_routes.py): fix linting error on caching routes * fix: fix test - ignore mask_dict - has a breakpoint * fix(azure.py): add timeout param + elapsed time in azure timeout error * fix(http_handler.py): add elapsed time to http timeout request makes it easier to debug how long request took before failing
1 parent 5a42be4 commit f651d51

File tree

7 files changed

+126
-25
lines changed

7 files changed

+126
-25
lines changed

Diff for: litellm/litellm_core_utils/sensitive_data_masker.py

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
from typing import Any, Dict, Optional, Set
2+
3+
4+
class SensitiveDataMasker:
5+
def __init__(
6+
self,
7+
sensitive_patterns: Optional[Set[str]] = None,
8+
visible_prefix: int = 4,
9+
visible_suffix: int = 4,
10+
mask_char: str = "*",
11+
):
12+
self.sensitive_patterns = sensitive_patterns or {
13+
"password",
14+
"secret",
15+
"key",
16+
"token",
17+
"auth",
18+
"credential",
19+
"access",
20+
"private",
21+
"certificate",
22+
}
23+
24+
self.visible_prefix = visible_prefix
25+
self.visible_suffix = visible_suffix
26+
self.mask_char = mask_char
27+
28+
def _mask_value(self, value: str) -> str:
29+
if not value or len(str(value)) < (self.visible_prefix + self.visible_suffix):
30+
return value
31+
32+
value_str = str(value)
33+
masked_length = len(value_str) - (self.visible_prefix + self.visible_suffix)
34+
return f"{value_str[:self.visible_prefix]}{self.mask_char * masked_length}{value_str[-self.visible_suffix:]}"
35+
36+
def is_sensitive_key(self, key: str) -> bool:
37+
key_lower = str(key).lower()
38+
result = any(pattern in key_lower for pattern in self.sensitive_patterns)
39+
return result
40+
41+
def mask_dict(
42+
self, data: Dict[str, Any], depth: int = 0, max_depth: int = 10
43+
) -> Dict[str, Any]:
44+
if depth >= max_depth:
45+
return data
46+
47+
masked_data: Dict[str, Any] = {}
48+
for k, v in data.items():
49+
try:
50+
if isinstance(v, dict):
51+
masked_data[k] = self.mask_dict(v, depth + 1)
52+
elif hasattr(v, "__dict__") and not isinstance(v, type):
53+
masked_data[k] = self.mask_dict(vars(v), depth + 1)
54+
elif self.is_sensitive_key(k):
55+
str_value = str(v) if v is not None else ""
56+
masked_data[k] = self._mask_value(str_value)
57+
else:
58+
masked_data[k] = (
59+
v if isinstance(v, (int, float, bool, str)) else str(v)
60+
)
61+
except Exception:
62+
masked_data[k] = "<unable to serialize>"
63+
64+
return masked_data
65+
66+
67+
# Usage example:
68+
"""
69+
masker = SensitiveDataMasker()
70+
data = {
71+
"api_key": "sk-1234567890abcdef",
72+
"redis_password": "very_secret_pass",
73+
"port": 6379
74+
}
75+
masked = masker.mask_dict(data)
76+
# Result: {
77+
# "api_key": "sk-1****cdef",
78+
# "redis_password": "very****pass",
79+
# "port": 6379
80+
# }
81+
"""

Diff for: litellm/llms/azure/azure.py

+11-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from typing import Any, Callable, Dict, List, Literal, Optional, Union
66

77
import httpx # type: ignore
8-
from openai import AsyncAzureOpenAI, AzureOpenAI
8+
from openai import APITimeoutError, AsyncAzureOpenAI, AzureOpenAI
99

1010
import litellm
1111
from litellm.caching.caching import DualCache
@@ -305,6 +305,7 @@ async def make_azure_openai_chat_completion_request(
305305
- call chat.completions.create.with_raw_response when litellm.return_response_headers is True
306306
- call chat.completions.create by default
307307
"""
308+
start_time = time.time()
308309
try:
309310
raw_response = await azure_client.chat.completions.with_raw_response.create(
310311
**data, timeout=timeout
@@ -313,6 +314,11 @@ async def make_azure_openai_chat_completion_request(
313314
headers = dict(raw_response.headers)
314315
response = raw_response.parse()
315316
return headers, response
317+
except APITimeoutError as e:
318+
end_time = time.time()
319+
time_delta = round(end_time - start_time, 2)
320+
e.message += f" - timeout value={timeout}, time taken={time_delta} seconds"
321+
raise e
316322
except Exception as e:
317323
raise e
318324

@@ -642,6 +648,7 @@ async def acompletion(
642648
)
643649
raise AzureOpenAIError(status_code=500, message=str(e))
644650
except Exception as e:
651+
message = getattr(e, "message", str(e))
645652
## LOGGING
646653
logging_obj.post_call(
647654
input=data["messages"],
@@ -652,7 +659,7 @@ async def acompletion(
652659
if hasattr(e, "status_code"):
653660
raise e
654661
else:
655-
raise AzureOpenAIError(status_code=500, message=str(e))
662+
raise AzureOpenAIError(status_code=500, message=message)
656663

657664
def streaming(
658665
self,
@@ -797,10 +804,11 @@ async def async_streaming(
797804
status_code = getattr(e, "status_code", 500)
798805
error_headers = getattr(e, "headers", None)
799806
error_response = getattr(e, "response", None)
807+
message = getattr(e, "message", str(e))
800808
if error_headers is None and error_response:
801809
error_headers = getattr(error_response, "headers", None)
802810
raise AzureOpenAIError(
803-
status_code=status_code, message=str(e), headers=error_headers
811+
status_code=status_code, message=message, headers=error_headers
804812
)
805813

806814
async def aembedding(

Diff for: litellm/llms/custom_httpx/http_handler.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import asyncio
22
import os
3+
import time
34
from typing import TYPE_CHECKING, Any, Callable, List, Mapping, Optional, Union
45

56
import httpx
@@ -179,6 +180,7 @@ async def post(
179180
stream: bool = False,
180181
logging_obj: Optional[LiteLLMLoggingObject] = None,
181182
):
183+
start_time = time.time()
182184
try:
183185
if timeout is None:
184186
timeout = self.timeout
@@ -207,14 +209,16 @@ async def post(
207209
finally:
208210
await new_client.aclose()
209211
except httpx.TimeoutException as e:
212+
end_time = time.time()
213+
time_delta = round(end_time - start_time, 3)
210214
headers = {}
211215
error_response = getattr(e, "response", None)
212216
if error_response is not None:
213217
for key, value in error_response.headers.items():
214218
headers["response_headers-{}".format(key)] = value
215219

216220
raise litellm.Timeout(
217-
message=f"Connection timed out after {timeout} seconds.",
221+
message=f"Connection timed out. Timeout passed={timeout}, time taken={time_delta} seconds",
218222
model="default-model-name",
219223
llm_provider="litellm-httpx-handler",
220224
headers=headers,

Diff for: litellm/proxy/_experimental/out/onboarding.html

-1
This file was deleted.

Diff for: litellm/proxy/_new_secret_config.yaml

+15-14
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,20 @@ model_list:
55
- model_name: gpt-4
66
litellm_params:
77
model: gpt-3.5-turbo
8+
- model_name: azure-gpt-35-turbo
9+
litellm_params:
10+
model: azure/chatgpt-v-2
11+
api_key: os.environ/AZURE_API_KEY
12+
api_base: os.environ/AZURE_API_BASE
13+
timeout: 0.000000001
814
- model_name: o3-mini
915
litellm_params:
1016
model: o3-mini
1117
rpm: 3
1218
- model_name: anthropic-claude
1319
litellm_params:
1420
model: claude-3-5-haiku-20241022
15-
mock_response: Hi!
21+
timeout: 0.000000001
1622
- model_name: groq/*
1723
litellm_params:
1824
model: groq/*
@@ -28,16 +34,11 @@ model_list:
2834
api_key: fake-key
2935
api_base: https://exampleopenaiendpoint-production.up.railway.app/
3036

31-
general_settings:
32-
enable_jwt_auth: True
33-
litellm_jwtauth:
34-
team_id_jwt_field: "client_id"
35-
team_id_upsert: true
36-
scope_mappings:
37-
- scope: litellm.api.consumer
38-
models: ["anthropic-claude"]
39-
routes: ["/v1/chat/completions"]
40-
- scope: litellm.api.gpt_3_5_turbo
41-
models: ["gpt-3.5-turbo-testing"]
42-
enforce_scope_based_access: true
43-
enforce_rbac: true
37+
litellm_settings:
38+
cache: true
39+
40+
41+
router_settings:
42+
redis_host: os.environ/REDIS_HOST
43+
redis_password: os.environ/REDIS_PASSWORD
44+
redis_port: os.environ/REDIS_PORT

Diff for: litellm/proxy/caching_routes.py

+13-6
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
import copy
1+
from typing import Any, Dict
22

33
from fastapi import APIRouter, Depends, HTTPException, Request
44

55
import litellm
66
from litellm._logging import verbose_proxy_logger
77
from litellm.caching.caching import RedisCache
8+
from litellm.litellm_core_utils.sensitive_data_masker import SensitiveDataMasker
89
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
910

11+
masker = SensitiveDataMasker()
12+
1013
router = APIRouter(
1114
prefix="/cache",
1215
tags=["caching"],
@@ -21,27 +24,30 @@ async def cache_ping():
2124
"""
2225
Endpoint for checking if cache can be pinged
2326
"""
24-
litellm_cache_params = {}
25-
specific_cache_params = {}
27+
litellm_cache_params: Dict[str, Any] = {}
28+
specific_cache_params: Dict[str, Any] = {}
2629
try:
2730

2831
if litellm.cache is None:
2932
raise HTTPException(
3033
status_code=503, detail="Cache not initialized. litellm.cache is None"
3134
)
32-
35+
litellm_cache_params = {}
36+
specific_cache_params = {}
3337
for k, v in vars(litellm.cache).items():
3438
try:
3539
if k == "cache":
3640
continue
37-
litellm_cache_params[k] = str(copy.deepcopy(v))
41+
litellm_cache_params[k] = v
3842
except Exception:
3943
litellm_cache_params[k] = "<unable to copy or convert>"
4044
for k, v in vars(litellm.cache.cache).items():
4145
try:
42-
specific_cache_params[k] = str(v)
46+
specific_cache_params[k] = v
4347
except Exception:
4448
specific_cache_params[k] = "<unable to copy or convert>"
49+
litellm_cache_params = masker.mask_dict(litellm_cache_params)
50+
specific_cache_params = masker.mask_dict(specific_cache_params)
4551
if litellm.cache.type == "redis":
4652
# ping the redis cache
4753
ping_response = await litellm.cache.ping()
@@ -56,6 +62,7 @@ async def cache_ping():
5662
messages=[{"role": "user", "content": "test from litellm"}],
5763
)
5864
verbose_proxy_logger.debug("/cache/ping: done with set_cache()")
65+
5966
return {
6067
"status": "healthy",
6168
"cache_type": litellm.cache.type,

Diff for: tests/code_coverage_tests/recursive_detector.py

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"add_object_type",
1414
"strip_field",
1515
"_transform_prompt",
16+
"mask_dict",
1617
]
1718

1819

0 commit comments

Comments
 (0)