From f155385e3e2d3c72f1498c8af6a57a497dd0dcb6 Mon Sep 17 00:00:00 2001 From: Josh Date: Mon, 13 Apr 2026 09:14:00 -0400 Subject: [PATCH] feat(security): add redact_sensitive_logging_metadata helper --- litellm/proxy/common_utils/callback_utils.py | 34 ++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/litellm/proxy/common_utils/callback_utils.py b/litellm/proxy/common_utils/callback_utils.py index 9ecae363ed7..a66a5e30e75 100644 --- a/litellm/proxy/common_utils/callback_utils.py +++ b/litellm/proxy/common_utils/callback_utils.py @@ -1,3 +1,4 @@ +import copy from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Literal, Optional import litellm @@ -524,3 +525,36 @@ def normalize_callback_names(callbacks: Iterable[Any]) -> List[Any]: if callbacks is None: return [] return [c.lower() if isinstance(c, str) else c for c in callbacks] + + +def redact_sensitive_logging_metadata(metadata: Optional[Dict]) -> Optional[Dict]: + """ + Return a copy of `metadata` with credential values inside + `metadata["logging"][*]["callback_vars"]` replaced by "***". + + Values that are just environment-variable references + (e.g. "os.environ/LANGFUSE_SECRET_KEY") are left as-is because they + don't expose the actual secret — they're just pointers. + """ + if not metadata: + return metadata + + metadata = copy.deepcopy(metadata) + + logging_configs = metadata.get("logging") + if not isinstance(logging_configs, list): + return metadata + + for entry in logging_configs: + if not isinstance(entry, dict): + continue + callback_vars = entry.get("callback_vars") + if not isinstance(callback_vars, dict): + continue + for key, value in callback_vars.items(): + # Keep env-var pointers; scrub anything that looks like a real secret + if isinstance(value, str) and value.startswith("os.environ/"): + continue + callback_vars[key] = "***" + + return metadata