From 82204d3e97efdef8ca905be7cd6b1f6446876c19 Mon Sep 17 00:00:00 2001 From: shivam Date: Thu, 12 Feb 2026 17:39:08 -0800 Subject: [PATCH] removed jwt tokens from detailed_debug logging --- litellm/proxy/litellm_pre_call_utils.py | 44 +++++++++++++++++++++++-- litellm/proxy/proxy_server.py | 16 +++++++-- litellm/utils.py | 32 +++++++++++++++++- 3 files changed, 85 insertions(+), 7 deletions(-) diff --git a/litellm/proxy/litellm_pre_call_utils.py b/litellm/proxy/litellm_pre_call_utils.py index 49d31c1efec..b434a87868a 100644 --- a/litellm/proxy/litellm_pre_call_utils.py +++ b/litellm/proxy/litellm_pre_call_utils.py @@ -252,6 +252,34 @@ def clean_headers( return clean_headers +def _mask_secret_fields_for_logging(secret_fields: dict) -> dict: + """ + Masks sensitive values (like JWT tokens) in secret_fields before logging. + Keeps the field structure intact but redacts authorization header values. + """ + _sensitive_header_keys = frozenset( + {"authorization", "x-api-key", "api-key", "x-litellm-api-key"} + ) + if not isinstance(secret_fields, dict): + return secret_fields + masked = {} + for field_key, field_value in secret_fields.items(): + if isinstance(field_value, dict): + masked_inner = {} + for k, v in field_value.items(): + if k.lower() in _sensitive_header_keys and isinstance(v, str): + if len(v) > 20: + masked_inner[k] = v[:10] + "****" + v[-4:] + else: + masked_inner[k] = "****" + else: + masked_inner[k] = v + masked[field_key] = masked_inner + else: + masked[field_key] = field_value + return masked + + class LiteLLMProxyRequestSetup: @staticmethod def _get_timeout_from_request(headers: dict) -> Optional[float]: @@ -899,13 +927,19 @@ async def add_litellm_data_to_request( # noqa: PLR0915 ## Cache Controls headers = request.headers - verbose_proxy_logger.debug("Request Headers: %s", headers) + verbose_proxy_logger.debug("Request Headers: %s", clean_headers(headers)) cache_control_header = headers.get("Cache-Control", None) if cache_control_header: cache_dict = parse_cache_control(cache_control_header) data["ttl"] = cache_dict.get("s-maxage") - verbose_proxy_logger.debug("receiving data: %s", data) + verbose_proxy_logger.debug( + "receiving data: %s", + { + k: (_mask_secret_fields_for_logging(v) if k == "secret_fields" else v) + for k, v in data.items() + }, + ) # Parse metadata if it's a string (e.g., from multipart/form-data) if "metadata" in data and data["metadata"] is not None: @@ -1133,7 +1167,11 @@ async def add_litellm_data_to_request( # noqa: PLR0915 ) verbose_proxy_logger.debug( - "[PROXY] returned data from litellm_pre_call_utils: %s", data + "[PROXY] returned data from litellm_pre_call_utils: %s", + { + k: (_mask_secret_fields_for_logging(v) if k == "secret_fields" else v) + for k, v in data.items() + }, ) ## ENFORCED PARAMS CHECK diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 6286d6dd1ca..dbdf1785e7d 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -328,7 +328,11 @@ from litellm.proxy.hooks.prompt_injection_detection import ( ) from litellm.proxy.hooks.proxy_track_cost_callback import _ProxyDBLogger from litellm.proxy.image_endpoints.endpoints import router as image_router -from litellm.proxy.litellm_pre_call_utils import add_litellm_data_to_request +from litellm.proxy.litellm_pre_call_utils import ( + _mask_secret_fields_for_logging, + add_litellm_data_to_request, + clean_headers, +) from litellm.proxy.management_endpoints.budget_management_endpoints import ( router as budget_management_router, ) @@ -9838,11 +9842,17 @@ async def async_queue_request( data["proxy_server_request"] = { "url": str(request.url), "method": request.method, - "headers": dict(request.headers), + "headers": clean_headers(request.headers), "body": copy.copy(data), # use copy instead of deepcopy } - verbose_proxy_logger.debug("receiving data: %s", data) + verbose_proxy_logger.debug( + "receiving data: %s", + { + k: (_mask_secret_fields_for_logging(v) if k == "secret_fields" else v) + for k, v in data.items() + }, + ) data["model"] = ( general_settings.get("completion_model", None) # server default or user_model # model name passed via cli args diff --git a/litellm/utils.py b/litellm/utils.py index 0fa5436d98d..f8b80b89420 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -7267,6 +7267,32 @@ def get_valid_models( return [] # NON-Blocking +def _mask_secret_fields_for_logging(secret_fields: dict) -> dict: + """ + Masks sensitive values (like JWT tokens) in secret_fields before logging. + Keeps the field structure intact but redacts authorization header values. + """ + if not isinstance(secret_fields, dict): + return secret_fields + masked = {} + for field_key, field_value in secret_fields.items(): + if isinstance(field_value, dict): + masked_inner = {} + for k, v in field_value.items(): + if k.lower() in ("authorization", "x-api-key", "api-key", "x-litellm-api-key") and isinstance(v, str): + # Show first 10 and last 4 chars, mask the rest + if len(v) > 20: + masked_inner[k] = v[:10] + "****" + v[-4:] + else: + masked_inner[k] = "****" + else: + masked_inner[k] = v + masked[field_key] = masked_inner + else: + masked[field_key] = field_value + return masked + + def print_args_passed_to_litellm(original_function, args, kwargs): if not _is_debugging_on(): return @@ -7292,7 +7318,11 @@ def print_args_passed_to_litellm(original_function, args, kwargs): return args_str = ", ".join(map(repr, args)) - kwargs_str = ", ".join(f"{key}={repr(value)}" for key, value in kwargs.items()) + # Mask JWT tokens / auth headers in secret_fields before logging + kwargs_str = ", ".join( + f"{key}={repr(_mask_secret_fields_for_logging(value)) if key == 'secret_fields' else repr(value)}" + for key, value in kwargs.items() + ) print_verbose( "\n", ) # new line before