From a5c0874a756db19d3829c57586d23df843053f82 Mon Sep 17 00:00:00 2001 From: Vansh Date: Tue, 3 Nov 2026 09:19:00 +0530 Subject: [PATCH] fix(logging): omit oversized bodies from raw request log to prevent event loop freeze (#49673) --- litellm/__init__.py | 1 + litellm/constants.py | 1 + litellm/litellm_core_utils/litellm_logging.py | 17 ++++- .../test_litellm_logging.py | 71 +++++++++++++++++++ 4 files changed, 88 insertions(+), 2 deletions(-) diff --git a/litellm/__init__.py b/litellm/__init__.py index baee4e9f7ba..113a55c3920 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -192,6 +192,7 @@ standard_logging_payload_excluded_fields: Optional[List[str]] = ( None # Fields to exclude from StandardLoggingPayload before callbacks receive it ) log_raw_request_response: bool = False +max_raw_request_body_log_chars: Optional[int] = None log_client_error_tracebacks: bool = False log_auth_failure_key_identity: bool = False request_correlation_in_logs: bool = False diff --git a/litellm/constants.py b/litellm/constants.py index 14a2a9f9eca..b269655d4ce 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -2187,6 +2187,7 @@ MICROSOFT_USER_LAST_NAME_ATTRIBUTE: Final = str(os.getenv("MICROSOFT_USER_LAST_N # Maximum payload size (in bytes) to fully serialize for DEBUG logging. # Payloads larger than this are truncated to avoid multi-second json.dumps blocking the response. MAX_PAYLOAD_SIZE_FOR_DEBUG_LOG: Final = get_env_int("MAX_PAYLOAD_SIZE_FOR_DEBUG_LOG", 102400) # 100 KB +MAX_RAW_REQUEST_BODY_LOG_CHARS: Final = get_env_int("MAX_RAW_REQUEST_BODY_LOG_CHARS", 102400) # 100 KB # Policy template enrichment MAX_COMPETITOR_NAMES: Final = get_env_int("MAX_COMPETITOR_NAMES", 100) diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 37657a94f16..46eaa901c73 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -47,6 +47,7 @@ from litellm.constants import ( INTERNAL_CALLBACK_NAMES, KUBERNETES_POD_ROUTING_KEY, LITELLM_LOGGING_NO_UPSTREAM_LLM_CALL, + MAX_RAW_REQUEST_BODY_LOG_CHARS, PROVIDER_REQUEST_ID_HEADERS, REALTIME_METERED_USAGE_KEY, REDACTED_BY_LITELLM, @@ -1831,8 +1832,17 @@ class Logging(LiteLLMLoggingBaseClass): else: verbose_logger.debug("\x1b[92m%s\x1b[0m\n", curl_command) + def _get_max_raw_request_body_log_chars(self) -> int: + if getattr(litellm, "max_raw_request_body_log_chars", None) is not None: + return cast(int, litellm.max_raw_request_body_log_chars) + return MAX_RAW_REQUEST_BODY_LOG_CHARS + def _get_request_body(self, data: dict) -> str: - return str(data) + body: Final = str(data) + max_chars: Final = self._get_max_raw_request_body_log_chars() + if len(body) > max_chars: + return f"<{len(body)} chars omitted, over MAX_RAW_REQUEST_BODY_LOG_CHARS={max_chars}>" + return body def _get_request_curl_command(self, api_base: str, headers: dict | None, additional_args: dict, data: dict) -> str: masked_api_base: Final = self._get_masked_api_base(api_base) @@ -1848,7 +1858,10 @@ class Logging(LiteLLMLoggingBaseClass): if additional_args.get("request_str", None) is not None: # print the sagemaker / bedrock client request curl_command = "\nRequest Sent from LiteLLM:\n" - request_str: Final = additional_args.get("request_str", "") + request_str: str = str(additional_args.get("request_str", "")) + max_chars: Final = self._get_max_raw_request_body_log_chars() + if len(request_str) > max_chars: + request_str = f"<{len(request_str)} chars omitted, over MAX_RAW_REQUEST_BODY_LOG_CHARS={max_chars}>" curl_command += request_str return curl_command diff --git a/tests/unit/litellm_core_utils/test_litellm_logging.py b/tests/unit/litellm_core_utils/test_litellm_logging.py index 13e181b2b92..0755ed1a07e 100644 --- a/tests/unit/litellm_core_utils/test_litellm_logging.py +++ b/tests/unit/litellm_core_utils/test_litellm_logging.py @@ -8662,6 +8662,77 @@ def test_pre_call_redacts_and_masks_raw_request(logging_obj): assert "key=*****" in raw_api_base +def test_pre_call_omits_raw_request_body_over_size_limit(logging_obj, monkeypatch): + """Redacting an oversized raw request blocks the event loop, so the body is omitted from the curl command.""" + monkeypatch.setattr("litellm.litellm_core_utils.litellm_logging.MAX_RAW_REQUEST_BODY_LOG_CHARS", 100) + metadata = {"user_api_key_alias": "qa-key"} + logging_obj.model_call_details["litellm_params"] = {"metadata": metadata} + logging_obj.log_raw_request_response = True + body = {"messages": [{"role": "user", "content": "LONG-PROMPT-CONTENT " * 20}]} + + logging_obj.pre_call( + input="hi", + api_key="", + additional_args={ + "api_base": "https://api.openai.com/v1/chat/completions", + "headers": {}, + "complete_input_dict": body, + }, + ) + + raw_request = metadata["raw_request"] + assert "LONG-PROMPT-CONTENT" not in raw_request + assert "chars omitted, over MAX_RAW_REQUEST_BODY_LOG_CHARS=100" in raw_request + assert "https://api.openai.com/v1/chat/completions" in raw_request + assert logging_obj.model_call_details["raw_request_typed_dict"]["raw_request_body"] == body + + +def test_pre_call_omits_request_str_over_size_limit(logging_obj, monkeypatch): + """Oversized request_str (e.g. from Bedrock or SageMaker) is also omitted from raw request logging.""" + monkeypatch.setattr("litellm.litellm_core_utils.litellm_logging.MAX_RAW_REQUEST_BODY_LOG_CHARS", 100) + metadata = {"user_api_key_alias": "qa-key"} + logging_obj.model_call_details["litellm_params"] = {"metadata": metadata} + logging_obj.log_raw_request_response = True + long_request_str = "BEDROCK-RAW-PAYLOAD-" * 20 + + logging_obj.pre_call( + input="hi", + api_key="", + additional_args={ + "api_base": "https://bedrock-runtime.us-east-1.amazonaws.com", + "headers": {}, + "request_str": long_request_str, + }, + ) + + raw_request = metadata["raw_request"] + assert "BEDROCK-RAW-PAYLOAD-" not in raw_request + assert "chars omitted, over MAX_RAW_REQUEST_BODY_LOG_CHARS=100" in raw_request + + +def test_pre_call_includes_request_body_under_size_limit(logging_obj, monkeypatch): + """Normal sized payloads under the limit are fully included and redacted.""" + monkeypatch.setattr("litellm.litellm_core_utils.litellm_logging.MAX_RAW_REQUEST_BODY_LOG_CHARS", 1000) + metadata = {"user_api_key_alias": "qa-key"} + logging_obj.model_call_details["litellm_params"] = {"metadata": metadata} + logging_obj.log_raw_request_response = True + body = {"messages": [{"role": "user", "content": "short prompt"}]} + + logging_obj.pre_call( + input="hi", + api_key="", + additional_args={ + "api_base": "https://api.openai.com/v1/chat/completions", + "headers": {}, + "complete_input_dict": body, + }, + ) + + raw_request = metadata["raw_request"] + assert "short prompt" in raw_request + assert "chars omitted" not in raw_request + + _PRIVATE_RAW_REQUEST_ARGS: Final = { "api_base": "https://api.openai.com/v1/chat/completions", "headers": {},