diff --git a/litellm/utils.py b/litellm/utils.py index 13f22ddc36e..bf86ce234e8 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -586,7 +586,7 @@ last_fetched_at_keys: Final = None ############################################################ def print_verbose( - print_statement, + print_statement: object, logger_only: bool = False, log_level: Literal["DEBUG", "INFO", "ERROR"] = "DEBUG", ): @@ -598,7 +598,12 @@ def print_verbose( elif log_level == "ERROR": verbose_logger.error(print_statement) if litellm.set_verbose is True and logger_only is False: - print(print_statement) # noqa: T201 + stdout_statement: Final = ( + redact_credentials_in_payload(print_statement) + if isinstance(print_statement, Mapping) + else print_statement + ) + print(stdout_statement) # noqa: T201 except Exception: pass diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index b0c55d3f0f2..cf6c5f0df9e 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -116,6 +116,7 @@ from litellm.utils import ( get_valid_models, is_cached_message, is_prompt_caching_valid_prompt, + print_verbose, trim_messages, validate_environment, validate_chat_completion_tool_choice, @@ -7195,6 +7196,39 @@ class TestFinalOptionalParamsLineRedaction: assert "'temperature': 0.25" in printed +class TestPrintVerboseRedaction: + """print_verbose() must not leak secrets to stdout when callers pass dicts like + model_call_details that contain api_key fields.""" + + SECRET_VALUE: Final = "sk-ant-test-DO-NOT-LEAK-12345" + + def _payload(self) -> Mapping[str, object]: + return { + "model": "claude-3-5-sonnet-20241022", + "api_key": self.SECRET_VALUE, + "messages": [{"role": "user", "content": "hi"}], + } + + def test_utils_print_verbose_redacts_stdout_when_set_verbose( + self, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.setattr(litellm, "set_verbose", True) + print_verbose(self._payload()) + out: Final = capsys.readouterr().out + assert self.SECRET_VALUE not in out, f"api_key leaked to stdout: {out!r}" + assert "'api_key': 'REDACTED'" in out + assert "'model': 'claude-3-5-sonnet-20241022'" in out + + def test_utils_print_verbose_routes_through_logger_redacted( + self, caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.setattr(litellm, "set_verbose", False) + caplog.set_level(logging.DEBUG, logger="LiteLLM") + print_verbose(self._payload()) + leaked: Final = [r.getMessage() for r in caplog.records if self.SECRET_VALUE in r.getMessage()] + assert not leaked, f"api_key leaked through verbose_logger: {leaked!r}" + + class TestDropParamsStringCoercion: @pytest.mark.parametrize("drop_params", ["true", "True", True]) def test_truthy_drop_params_drops_unsupported_temperature(self, drop_params, monkeypatch):