1010import litellm
1111from litellm ._logging import verbose_proxy_logger
1212from litellm .constants import (
13+ LITELLM_PROXY_MASTER_KEY_ALIAS ,
1314 LITELLM_TRUNCATED_PAYLOAD_FIELD ,
1415 LITELLM_TRUNCATION_DB_SAFEGUARD_NOTE ,
1516 REDACTED_BY_LITELM_STRING ,
2122 get_litellm_metadata_from_kwargs ,
2223 reconstruct_model_name ,
2324)
25+ from litellm .litellm_core_utils .litellm_logging import is_valid_sha256_hash
2426from litellm .litellm_core_utils .safe_json_dumps import safe_dumps , strip_null_bytes
2527from litellm .proxy ._types import SpendLogsMetadata , SpendLogsPayload
2628from litellm .proxy .spend_tracking .spend_log_error_logger import spend_log_error
@@ -53,13 +55,6 @@ def _get_max_string_length_prompt_in_db() -> int:
5355 return DEFAULT_MAX_STRING_LENGTH_PROMPT_IN_DB
5456
5557
56- def _hash_api_key_for_spend_log (api_key : str ) -> str :
57- stripped : Final = api_key [7 :] if api_key [:7 ].lower () == "bearer " else api_key
58- if stripped .startswith ("sk-" ):
59- return hash_token (stripped )
60- return stripped
61-
62-
6358def _is_master_key (api_key : str | None , _master_key : str | None ) -> bool :
6459 """
6560 Raw-only constant-time master-key comparison. The hashed form is never
@@ -70,6 +65,28 @@ def _is_master_key(api_key: str | None, _master_key: str | None) -> bool:
7065 return secrets .compare_digest (api_key , _master_key )
7166
7267
68+ _HASHED_JWT_RE = re .compile (r"hashed-jwt-[a-fA-F0-9]{64}" )
69+
70+
71+ def _is_non_secret_key_value (value : str ) -> bool :
72+ return (
73+ value == LITELLM_PROXY_MASTER_KEY_ALIAS
74+ or is_valid_sha256_hash (value )
75+ or _HASHED_JWT_RE .fullmatch (value ) is not None
76+ )
77+
78+
79+ def _redact_logged_api_key (value : str | None , * , already_redacted : bool = False ) -> str | None :
80+ if not isinstance (value , str ) or not value :
81+ return None
82+ stripped : Final = re .sub (r"(?i)^bearer " , "" , value )
83+ if not stripped :
84+ return None
85+ if already_redacted and _is_non_secret_key_value (stripped ):
86+ return stripped
87+ return hash_token (stripped )
88+
89+
7390def _get_spend_logs_metadata (
7491 metadata : dict | None ,
7592 applied_guardrails : list [str ] | None = None ,
@@ -123,9 +140,12 @@ def _get_spend_logs_metadata(
123140
124141 # Filter the metadata dictionary to include only the specified keys
125142 clean_metadata : Final = SpendLogsMetadata (** {key : metadata .get (key ) for key in SpendLogsMetadata .__annotations__ })
126- raw_user_api_key : Final = clean_metadata .get ("user_api_key" )
127- if raw_user_api_key is not None and isinstance (raw_user_api_key , str ):
128- clean_metadata ["user_api_key" ] = _hash_api_key_for_spend_log (raw_user_api_key )
143+ _raw_key : Final = clean_metadata .get ("user_api_key" )
144+ _trusted_hash : Final = metadata .get ("user_api_key_hash" )
145+ _already_redacted : Final = (
146+ isinstance (_trusted_hash , str ) and _is_non_secret_key_value (_trusted_hash ) and _trusted_hash == _raw_key
147+ )
148+ clean_metadata ["user_api_key" ] = _redact_logged_api_key (_raw_key , already_redacted = _already_redacted )
129149 clean_metadata ["applied_guardrails" ] = applied_guardrails
130150 clean_metadata ["batch_models" ] = batch_models
131151 clean_metadata ["mcp_tool_call_metadata" ] = mcp_tool_call_metadata
@@ -281,16 +301,23 @@ def get_logging_payload(kwargs, response_obj, start_time, end_time) -> SpendLogs
281301 standard_logging_prompt_tokens = standard_logging_payload .get ("prompt_tokens" , 0 )
282302 standard_logging_completion_tokens = standard_logging_payload .get ("completion_tokens" , 0 )
283303 standard_logging_total_tokens = standard_logging_payload .get ("total_tokens" , 0 )
284- if api_key is not None and isinstance (api_key , str ):
285- api_key = _hash_api_key_for_spend_log (api_key )
304+ _trusted_hash = metadata .get ("user_api_key_hash" )
305+ _key_already_redacted = (
306+ isinstance (_trusted_hash , str ) and _is_non_secret_key_value (_trusted_hash ) and _trusted_hash == api_key
307+ )
308+ api_key = _redact_logged_api_key (api_key , already_redacted = _key_already_redacted ) or ""
286309
287310 if (
288311 standard_logging_payload is not None
289312 ): # [TODO] migrate completely to sl payload. currently missing pass-through endpoint data
290- api_key = api_key or standard_logging_payload ["metadata" ].get ("user_api_key_hash" ) or ""
313+ api_key = (
314+ api_key
315+ or _redact_logged_api_key (
316+ standard_logging_payload ["metadata" ].get ("user_api_key_hash" ), already_redacted = True
317+ )
318+ or ""
319+ )
291320 end_user_id = end_user_id or standard_logging_payload ["metadata" ].get ("user_api_key_end_user_id" )
292- # BUG FIX: Don't overwrite api_key when standard_logging_payload is None
293- # The api_key was already extracted from metadata (line 243) and hashed (lines 256-259)
294321 request_tags = safe_dumps (metadata .get ("tags" , [])) if isinstance (metadata .get ("tags" , []), list ) else "[]"
295322 if (
296323 standard_logging_payload is not None and standard_logging_payload .get ("request_tags" ) is not None
0 commit comments