From 912572bfa5c0807f0bd60f94509a47a5014ffd2f Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Thu, 3 Sep 2026 02:53:50 -0700 Subject: [PATCH] fix(utils): redact credentials nested in extra_body on the verbose optional-params line The "Final returned optional params" line printed whatever the caller nested inside extra_body, so a credential tucked in there reached stdout in plaintext one line after the request line that already redacts it. The call site now runs redact_credentials_in_payload behind a guard reading both of print_verbose's consumers, litellm.set_verbose and the LiteLLM logger's DEBUG level, so the line prints in exactly the cases it did before and the walk costs nothing when nothing would read it. --- litellm/utils.py | 11 ++++- tests/test_litellm/test_utils.py | 70 ++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/litellm/utils.py b/litellm/utils.py index f5a4f8a38f8..dde21c53c24 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -544,6 +544,14 @@ def print_verbose( pass +def _print_verbose_is_active() -> bool: + """Whether print_verbose would reach either of its two consumers, so a call site can skip + building a payload nothing would read. _is_debugging_on() is not the same predicate: it reads + litellm._logging.set_verbose, while print_verbose's print reads litellm.set_verbose, and + assigning the documented litellm.set_verbose = True rebinds only the latter.""" + return litellm.set_verbose is True or verbose_logger.isEnabledFor(logging.DEBUG) + + ####### CLIENT ################### # make it easy to log if completion/embedding runs succeeded or failed + see what happened | Non-Blocking def custom_llm_setup(): @@ -4705,7 +4713,8 @@ def get_optional_params( openai_params=list(DEFAULT_CHAT_COMPLETION_PARAM_VALUES.keys()), additional_drop_params=additional_drop_params, ) - print_verbose(f"Final returned optional params: {optional_params}") + if _print_verbose_is_active(): + print_verbose(f"Final returned optional params: {redact_credentials_in_payload(optional_params)}") optional_params = _apply_openai_param_overrides( optional_params=optional_params, non_default_params=non_default_params, diff --git a/tests/test_litellm/test_utils.py b/tests/test_litellm/test_utils.py index 59d5902e138..4ec7bfe2786 100644 --- a/tests/test_litellm/test_utils.py +++ b/tests/test_litellm/test_utils.py @@ -5867,3 +5867,73 @@ class TestVerboseRequestLineRedaction: assert "model='gpt-3.5-turbo'" in printed assert "max_tokens=17" in printed assert "temperature=0.25" in printed + + +class TestFinalOptionalParamsLineRedaction: + """A verbose run echoes the fully built optional params too, and `extra_body` carries whatever the + caller nested inside it straight onto that line, so a credential tucked in there lands in a terminal + or a log drain in plaintext. It has to be redacted on both surfaces `print_verbose` writes to, and the + line has to keep printing on both, because `litellm.set_verbose` and the DEBUG logger are independent + switches and neither implies the other.""" + + FAKE_NESTED_KEY: Final = "sk-fake-lit6835-nested-0000000000" + + def _complete(self, **kwargs) -> None: + litellm.completion( + model="gpt-3.5-turbo", + messages=[{"role": "user", "content": "hello"}], + mock_response="hi", + **kwargs, + ) + + def _printed_line(self, capsys) -> str: + captured: Final = capsys.readouterr() + return "\n".join( + line for line in (captured.out + captured.err).splitlines() if "Final returned optional params" in line + ) + + def test_nested_credential_is_redacted_when_only_set_verbose_is_on(self, capsys, caplog, monkeypatch): + monkeypatch.setattr(litellm, "set_verbose", True) + with caplog.at_level(logging.WARNING, logger=verbose_logger.name): + capsys.readouterr() + self._complete(extra_body={"providers": [{"name": "openai", "api_key": self.FAKE_NESTED_KEY}]}) + printed: Final = self._printed_line(capsys) + + assert printed + assert self.FAKE_NESTED_KEY not in printed + assert "'api_key': 'REDACTED'" in printed + assert "'name': 'openai'" in printed + + def test_line_still_reaches_the_logger_when_only_the_debug_logger_is_on(self, capsys, caplog, monkeypatch): + monkeypatch.setattr(litellm, "set_verbose", False) + with caplog.at_level(logging.DEBUG, logger=verbose_logger.name): + self._complete(extra_body={"providers": [{"name": "openai", "api_key": self.FAKE_NESTED_KEY}]}) + logged: Final = "\n".join( + record.getMessage() + for record in caplog.records + if "Final returned optional params" in record.getMessage() + ) + + assert logged + assert self.FAKE_NESTED_KEY not in logged + assert "'name': 'openai'" in logged + + def test_nothing_is_emitted_when_neither_verbose_switch_is_on(self, capsys, caplog, monkeypatch): + monkeypatch.setattr(litellm, "set_verbose", False) + with caplog.at_level(logging.WARNING, logger=verbose_logger.name): + capsys.readouterr() + self._complete(extra_body={"providers": [{"name": "openai", "api_key": self.FAKE_NESTED_KEY}]}) + captured: Final = capsys.readouterr() + + assert "Final returned optional params" not in captured.out + captured.err + assert self.FAKE_NESTED_KEY not in captured.out + captured.err + + def test_ordinary_optional_params_still_reach_the_line(self, capsys, caplog, monkeypatch): + monkeypatch.setattr(litellm, "set_verbose", True) + with caplog.at_level(logging.WARNING, logger=verbose_logger.name): + capsys.readouterr() + self._complete(max_tokens=17, temperature=0.25) + printed: Final = self._printed_line(capsys) + + assert "'max_tokens': 17" in printed + assert "'temperature': 0.25" in printed