diff --git a/litellm/integrations/langfuse/langfuse.py b/litellm/integrations/langfuse/langfuse.py index 7bf97665fd2..04edc81d9b9 100644 --- a/litellm/integrations/langfuse/langfuse.py +++ b/litellm/integrations/langfuse/langfuse.py @@ -20,16 +20,16 @@ from packaging.version import Version import litellm from litellm._logging import verbose_logger from litellm.constants import MAX_LANGFUSE_INITIALIZED_CLIENTS -from litellm.litellm_core_utils.core_helpers import ( - safe_deep_copy, - reconstruct_model_name, - filter_exceptions_from_params, -) -from litellm.litellm_core_utils.redact_messages import redact_user_api_key_info from litellm.integrations.langfuse.langfuse_mock_client import ( create_mock_langfuse_client, should_use_langfuse_mock, ) +from litellm.litellm_core_utils.core_helpers import ( + filter_exceptions_from_params, + reconstruct_model_name, + safe_deep_copy, +) +from litellm.litellm_core_utils.redact_messages import redact_user_api_key_info from litellm.llms.custom_httpx.http_handler import _get_httpx_client from litellm.secret_managers.main import str_to_bool from litellm.types.integrations.langfuse import * @@ -272,7 +272,9 @@ class LangFuseLogger: """ try: verbose_logger.debug( - f"Langfuse Logging - Enters logging function for model {kwargs}" + "Langfuse Logging - Enters logging function for model call_type=%s litellm_call_id=%s", + kwargs.get("call_type"), + kwargs.get("litellm_call_id"), ) # set default values for input/output for langfuse logging diff --git a/litellm/litellm_core_utils/initialize_dynamic_callback_params.py b/litellm/litellm_core_utils/initialize_dynamic_callback_params.py index ff521d47804..68fa69dbda7 100644 --- a/litellm/litellm_core_utils/initialize_dynamic_callback_params.py +++ b/litellm/litellm_core_utils/initialize_dynamic_callback_params.py @@ -1,36 +1,60 @@ from typing import Dict, Optional + from litellm.secret_managers.main import get_secret_str from litellm.types.utils import StandardCallbackDynamicParams -# Hardcoded list of supported callback params to avoid runtime inspection issues with TypedDict +# Callback config params - never send to external loggers (credential leak) +_SUPPORTED_CALLBACK_PARAMS_FROZEN = frozenset( + [ + "langfuse_public_key", + "langfuse_secret", + "langfuse_secret_key", + "langfuse_host", + "langfuse_prompt_version", + "gcs_bucket_name", + "gcs_path_service_account", + "langsmith_api_key", + "langsmith_project", + "langsmith_base_url", + "langsmith_sampling_rate", + "langsmith_tenant_id", + "humanloop_api_key", + "arize_api_key", + "arize_space_key", + "arize_space_id", + "posthog_api_key", + "posthog_host", + "braintrust_api_key", + "braintrust_project", + "braintrust_host", + "slack_webhook_url", + "lunary_public_key", + "turn_off_message_logging", + "litellm_logging_obj", + "environment_variables", + ] +) + +# List form for iteration (excludes litellm_logging_obj, environment_variables - config only) _supported_callback_params = [ - "langfuse_public_key", - "langfuse_secret", - "langfuse_secret_key", - "langfuse_host", - "langfuse_prompt_version", - "gcs_bucket_name", - "gcs_path_service_account", - "langsmith_api_key", - "langsmith_project", - "langsmith_base_url", - "langsmith_sampling_rate", - "langsmith_tenant_id", - "humanloop_api_key", - "arize_api_key", - "arize_space_key", - "arize_space_id", - "posthog_api_key", - "posthog_host", - "braintrust_api_key", - "braintrust_project", - "braintrust_host", - "slack_webhook_url", - "lunary_public_key", - "turn_off_message_logging", + p for p in _SUPPORTED_CALLBACK_PARAMS_FROZEN if p not in ("litellm_logging_obj", "environment_variables") ] +def scrub_callback_config_params_from_dict(data: Dict) -> Dict: + if not data: + return data + keys_to_remove = [ + k for k in data + if k in _SUPPORTED_CALLBACK_PARAMS_FROZEN + or k.endswith("_secret_key") + or k.endswith("_secret") + or k.endswith("_api_key") + ] + result = {k: v for k, v in data.items() if k not in keys_to_remove} + return result + + def initialize_standard_callback_dynamic_params( kwargs: Optional[Dict] = None, ) -> StandardCallbackDynamicParams: diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 6f587abcdf1..0e4a9010c38 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -61,6 +61,9 @@ from litellm.integrations.mlflow import MlflowLogger from litellm.integrations.sqs import SQSLogger from litellm.litellm_core_utils.core_helpers import reconstruct_model_name from litellm.litellm_core_utils.get_litellm_params import get_litellm_params +from litellm.litellm_core_utils.initialize_dynamic_callback_params import ( + scrub_callback_config_params_from_dict, +) from litellm.litellm_core_utils.llm_cost_calc.tool_call_cost_tracking import ( StandardBuiltInToolCostTracking, ) @@ -537,11 +540,14 @@ class Logging(LiteLLMLoggingBaseClass): if _is_debugging_on() or self.litellm_request_debug: verbose_logger.debug(f"self.optional_params: {self.optional_params}") + sanitized_optional_params = scrub_callback_config_params_from_dict( + dict(optional_params) + ) self.model_call_details.update( { "model": self.model, "messages": self.messages, - "optional_params": self.optional_params, + "optional_params": sanitized_optional_params, "litellm_params": self.litellm_params, "start_time": self.start_time, "stream": self.stream, @@ -550,7 +556,7 @@ class Logging(LiteLLMLoggingBaseClass): "litellm_call_id": self.litellm_call_id, "completion_start_time": self.completion_start_time, "standard_callback_dynamic_params": self.standard_callback_dynamic_params, - **self.optional_params, + **sanitized_optional_params, **additional_params, } ) diff --git a/repro_secret_leak/before_after/after_secret_fixed.png b/repro_secret_leak/before_after/after_secret_fixed.png new file mode 100644 index 00000000000..f3b95ef812e Binary files /dev/null and b/repro_secret_leak/before_after/after_secret_fixed.png differ diff --git a/repro_secret_leak/before_after/before_secret_leak.png b/repro_secret_leak/before_after/before_secret_leak.png new file mode 100644 index 00000000000..a4efa497e35 Binary files /dev/null and b/repro_secret_leak/before_after/before_secret_leak.png differ diff --git a/tests/logging_callback_tests/test_dynamic_otel_keys.py b/tests/logging_callback_tests/test_dynamic_otel_keys.py index 2a463fddc0d..93e63534288 100644 --- a/tests/logging_callback_tests/test_dynamic_otel_keys.py +++ b/tests/logging_callback_tests/test_dynamic_otel_keys.py @@ -5,6 +5,7 @@ sys.path.insert(0, os.path.abspath("../..")) from litellm.litellm_core_utils.initialize_dynamic_callback_params import ( initialize_standard_callback_dynamic_params, + scrub_callback_config_params_from_dict, ) @@ -47,6 +48,24 @@ def test_dynamic_key_extraction_from_litellm_params_metadata(): assert params.get("langfuse_secret_key") == "sk-litellm" +def test_scrub_callback_config_params_removes_credentials(): + """Scrub removes callback config/credentials before passing to loggers.""" + data = { + "messages": "[{'role': 'user', 'content': 'Hi'}]", + "langfuse_public_key": "pk-lf-xxx", + "langfuse_secret": "sk-lf-4be1b432-61f4-4771-82a2-a33d3822ad65", + "langfuse_host": "http://localhost:9999", + "litellm_logging_obj": "", + } + result = scrub_callback_config_params_from_dict(data) + assert "messages" in result + assert "langfuse_secret" not in result + assert "langfuse_public_key" not in result + assert "langfuse_host" not in result + assert "litellm_logging_obj" not in result + + if __name__ == "__main__": test_dynamic_key_extraction_from_metadata() test_dynamic_key_extraction_from_litellm_params_metadata() + test_scrub_callback_config_params_removes_credentials()