fix(logging): redact raw_request when turn_off_message_logging is set in the proxy config (#43219)

* fix(logging): redact raw_request when turn_off_message_logging is set in the proxy config

The raw request branch bound turn_off_message_logging by name at import, before the proxy config set it, so loggers kept receiving the prompt in metadata.raw_request and raw_request_typed_dict. It now runs the same per-request redaction check messages use, and json_logs is read at call time for the same reason

* fix(logging): keep raw_request_typed_dict for the explicit readers and tolerate missing headers in the json debug log

* refactor(logging): drop the stale comment above the raw request typed dict

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
This commit is contained in:
devin-ai-integration[bot] 2026-09-25 16:06:44 -07:00 • committed by GitHub
parent e065a2575b
commit d86c2e1f42
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 106 additions and 39 deletions

View file

@ -20,11 +20,7 @@ from httpx import Response
from pydantic import BaseModel, JsonValue
import litellm
from litellm import (
_custom_logger_compatible_callbacks_literal,
json_logs,
turn_off_message_logging,
)
from litellm import _custom_logger_compatible_callbacks_literal
from litellm._logging import (
_is_debugging_on,
_redact_string,
@ -43,6 +39,7 @@ from litellm.constants import (
DEFAULT_MOCK_RESPONSE_PROMPT_TOKEN_COUNT,
EMPTY_MAPPING,
PROVIDER_REQUEST_ID_HEADERS,
REDACTED_BY_LITELLM,
)
from litellm.cost_calculator import (
RealtimeAPITokenUsageProcessor,
@ -1358,10 +1355,19 @@ class Logging(LiteLLMLoggingBaseClass):
_litellm_params: Final = self.model_call_details.get("litellm_params", {})
_metadata: Final = _litellm_params.get("metadata", {}) or {}
try:
# [Non-blocking Extra Debug Information in metadata]
if turn_off_message_logging is True:
_metadata["raw_request"] = "redacted by litellm. \
'litellm.turn_off_message_logging=True'"
self.model_call_details["raw_request_typed_dict"] = RawRequestTypedDict(
raw_request_api_base=self._get_masked_api_base(str(additional_args.get("api_base") or "")),
raw_request_body=self._get_raw_request_body(additional_args.get("complete_input_dict", {})),
# NOTE: setting ignore_sensitive_headers to True will cause
# the Authorization header to be leaked when calls to the health
# endpoint are made and fail.
raw_request_headers=self._get_masked_headers(
additional_args.get("headers", {}) or {},
),
error=None,
)
if should_redact_message_logging(self.model_call_details):
_metadata["raw_request"] = REDACTED_BY_LITELLM
else:
curl_command: Final = self._get_request_curl_command(
api_base=additional_args.get("api_base", ""),
@ -1369,20 +1375,7 @@ class Logging(LiteLLMLoggingBaseClass):
additional_args=additional_args,
data=additional_args.get("complete_input_dict", {}),
)
_metadata["raw_request"] = _redact_string(str(curl_command))
# split up, so it's easier to parse in the UI
self.model_call_details["raw_request_typed_dict"] = RawRequestTypedDict(
raw_request_api_base=self._get_masked_api_base(str(additional_args.get("api_base") or "")),
raw_request_body=self._get_raw_request_body(additional_args.get("complete_input_dict", {})),
# NOTE: setting ignore_sensitive_headers to True will cause
# the Authorization header to be leaked when calls to the health
# endpoint are made and fail.
raw_request_headers=self._get_masked_headers(
additional_args.get("headers", {}) or {},
),
error=None,
)
except Exception as e:
self.model_call_details["raw_request_typed_dict"] = RawRequestTypedDict(
error=str(e),
@ -1476,7 +1469,7 @@ class Logging(LiteLLMLoggingBaseClass):
def _print_llm_call_debugging_log(
self,
api_base: str,
headers: dict,
headers: dict | None,
additional_args: dict,
):
"""
@ -1485,8 +1478,8 @@ class Logging(LiteLLMLoggingBaseClass):
Prints the RAW curl command sent from LiteLLM
"""
if _is_debugging_on() or self.litellm_request_debug:
if json_logs:
masked_headers: Final = self._get_masked_headers(headers)
if litellm.json_logs:
masked_headers: Final = self._get_masked_headers(headers or {})
masked_api_base: Final = self._get_masked_api_base(str(api_base or ""))
if self.litellm_request_debug:
verbose_logger.warning( # .warning ensures this shows up in all environments
@ -1563,20 +1556,12 @@ class Logging(LiteLLMLoggingBaseClass):
else:
attr = "debug"
if json_logs:
callattr = verbose_logger.warning if attr == "warning" else verbose_logger.debug
callattr(
"RAW RESPONSE:\n{}\n\n".format(
self.model_call_details.get("original_response", self.model_call_details)
),
)
else:
callattr = verbose_logger.warning if attr == "warning" else verbose_logger.debug
callattr(
"RAW RESPONSE:\n{}\n\n".format(
self.model_call_details.get("original_response", self.model_call_details)
)
callattr: Final = verbose_logger.warning if attr == "warning" else verbose_logger.debug
callattr(
"RAW RESPONSE:\n{}\n\n".format(
self.model_call_details.get("original_response", self.model_call_details)
)
)
if getattr(self, "logger_fn", None) and callable(self.logger_fn):
try:
self.logger_fn(

View file

@ -20,7 +20,7 @@ from openai._legacy_response import HttpxBinaryResponseContent
import litellm
from litellm._logging import session_id_var, trace_id_var
from litellm.constants import SENTRY_PII_DENYLIST
from litellm.constants import REDACTED_BY_LITELLM, SENTRY_PII_DENYLIST
from litellm.cost_calculator import ocr_batch_cost
from litellm.integrations.custom_logger import CustomLogger
from litellm.litellm_core_utils.litellm_logging import Logging as LitellmLogging
@ -6615,6 +6615,65 @@ def test_pre_call_redacts_and_masks_raw_request(logging_obj):
assert "key=*****" in raw_api_base
_PRIVATE_RAW_REQUEST_ARGS: Final = {
"api_base": "https://api.openai.com/v1/chat/completions",
"headers": {},
"complete_input_dict": {"messages": [{"role": "user", "content": "PRIVATE-PHRASE"}]},
}
def _pre_call_with_raw_request_logging(logging_obj) -> dict:
metadata: Final = {"user_api_key_alias": "qa-key"}
logging_obj.model_call_details["litellm_params"] = {"metadata": metadata}
logging_obj.log_raw_request_response = True
logging_obj.pre_call(input="hi", api_key="", additional_args=_PRIVATE_RAW_REQUEST_ARGS)
return metadata
def _assert_raw_request_redacted_for_callbacks_only(logging_obj, metadata: dict) -> None:
assert metadata["raw_request"] == REDACTED_BY_LITELLM
typed_dict: Final = logging_obj.model_call_details["raw_request_typed_dict"]
assert typed_dict["raw_request_body"] == _PRIVATE_RAW_REQUEST_ARGS["complete_input_dict"]
assert typed_dict["error"] is None
def test_pre_call_raw_request_honors_turn_off_message_logging_set_after_import(logging_obj, monkeypatch):
monkeypatch.setattr(litellm, "turn_off_message_logging", True)
metadata = _pre_call_with_raw_request_logging(logging_obj)
_assert_raw_request_redacted_for_callbacks_only(logging_obj, metadata)
def test_pre_call_raw_request_honors_per_request_turn_off_message_logging(logging_obj, monkeypatch):
monkeypatch.setattr(litellm, "turn_off_message_logging", False)
logging_obj.model_call_details["standard_callback_dynamic_params"] = {"turn_off_message_logging": True}
metadata = _pre_call_with_raw_request_logging(logging_obj)
_assert_raw_request_redacted_for_callbacks_only(logging_obj, metadata)
def test_debugging_log_honors_json_logs_set_after_import(logging_obj, monkeypatch):
monkeypatch.setattr(litellm, "json_logs", True)
logging_obj.litellm_request_debug = True
with patch("litellm.litellm_core_utils.litellm_logging.verbose_logger.warning") as warning:
logging_obj._print_llm_call_debugging_log(api_base="https://api.openai.com/v1", headers={}, additional_args={})
assert "https://api.openai.com/v1" in warning.call_args.kwargs["extra"]["api_base"]
def test_debugging_log_with_json_logs_tolerates_missing_headers(logging_obj, monkeypatch):
monkeypatch.setattr(litellm, "json_logs", True)
logging_obj.litellm_request_debug = True
with patch("litellm.litellm_core_utils.litellm_logging.verbose_logger.warning") as warning:
logging_obj._print_llm_call_debugging_log(api_base="https://api.openai.com/v1", headers=None, additional_args={})
assert "https://api.openai.com/v1" in warning.call_args.kwargs["extra"]["api_base"]
def _streaming_logging_obj_with_callbacks(callbacks: list[CustomLogger]):
import datetime

View file

@ -626,6 +626,29 @@ def test_return_raw_request_does_not_call_provider(respx_mock: respx.MockRouter)
]
def test_return_raw_request_ignores_turn_off_message_logging(
respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch
):
from litellm.types.utils import CallTypes
from litellm.utils import return_raw_request
model: Final = "gpt-4o"
messages: Final = [{"role": "user", "content": "PRIVATE-PHRASE"}]
route: Final = respx_mock.post("https://api.openai.com/v1/chat/completions").mock(
return_value=_mocked_openai_chat_response(model)
)
monkeypatch.setattr(litellm, "turn_off_message_logging", True)
request: Final = return_raw_request(
endpoint=CallTypes.completion,
kwargs={"model": model, "messages": messages},
)
assert route.call_count == 0
assert request.get("error") is None
assert request["raw_request_body"]["messages"] == messages
def test_completion_forwards_verbosity_in_raw_request(respx_mock: respx.MockRouter):
"""Regression test: completion() must forward the verbosity param to the provider request body."""
from litellm.types.utils import CallTypes