diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 83d6fcc0bee..4d264ee901d 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -69,7 +69,10 @@ from litellm.litellm_core_utils.get_litellm_params import get_litellm_params from litellm.litellm_core_utils.llm_cost_calc.tool_call_cost_tracking import ( StandardBuiltInToolCostTracking, ) -from litellm.litellm_core_utils.logging_utils import truncate_base64_in_messages +from litellm.litellm_core_utils.logging_utils import ( + truncate_base64_data_uris, + truncate_base64_in_messages, +) from litellm.litellm_core_utils.model_param_helper import ModelParamHelper from litellm.litellm_core_utils.redact_messages import ( redact_message_input_output_from_custom_logger, @@ -5014,8 +5017,8 @@ class StandardLoggingPayloadSetup: error_code=error_status, error_class=error_class, llm_provider=_llm_provider_in_exception, - traceback=traceback_info, - error_message=error_message, + traceback=truncate_base64_data_uris(traceback_info), + error_message=truncate_base64_data_uris(error_message), error_rate_limit_category=rate_limit_category, error_rate_limit_type=rate_limit_type, error_budget_entity_type=budget_error.entity_type if budget_error else None, diff --git a/litellm/litellm_core_utils/logging_utils.py b/litellm/litellm_core_utils/logging_utils.py index 720a850b47f..c590c18597f 100644 --- a/litellm/litellm_core_utils/logging_utils.py +++ b/litellm/litellm_core_utils/logging_utils.py @@ -68,7 +68,7 @@ def _base64_data_uri_replacer(match: re.Match) -> str: return f"data:{mime_type};base64,[base64_data truncated: {size_str}]" -def _truncate_base64_in_string(value: str) -> str: +def truncate_base64_data_uris(value: str) -> str: """Replace long base64 data-URI payloads in a string with a size placeholder.""" if MAX_BASE64_LENGTH_FOR_LOGGING <= 0: return value @@ -84,7 +84,7 @@ def _truncate_base64_in_value(value: Any) -> Any: # Stack entries: (source_value, depth, parent_container, key_or_index) # We mutate *copies* of dicts/lists in-place via parent references. if isinstance(value, str): - return _truncate_base64_in_string(value) + return truncate_base64_data_uris(value) if not isinstance(value, (dict, list)): return value @@ -99,7 +99,7 @@ def _truncate_base64_in_value(value: Any) -> Any: if isinstance(container, dict): for k, v in container.items(): if isinstance(v, str): - container[k] = _truncate_base64_in_string(v) + container[k] = truncate_base64_data_uris(v) elif isinstance(v, dict): copy: Union[dict, list] = {ck: cv for ck, cv in v.items()} container[k] = copy @@ -111,7 +111,7 @@ def _truncate_base64_in_value(value: Any) -> Any: elif isinstance(container, list): for i, v in enumerate(container): if isinstance(v, str): - container[i] = _truncate_base64_in_string(v) + container[i] = truncate_base64_data_uris(v) elif isinstance(v, dict): copy = {ck: cv for ck, cv in v.items()} container[i] = copy diff --git a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py index edc257f4c3f..71010befe75 100644 --- a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py +++ b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py @@ -2594,6 +2594,39 @@ def test_get_error_information_falls_back_to_str_when_no_message_attr(): assert result["error_class"] == "ValueError" +def test_get_error_information_truncates_base64_data_uris(): + """ + Regression for #34753: a failed request carrying a file upload echoed the + whole base64 payload back through the provider error string, so + error_information.error_message (and its traceback) grew to megabytes and + blew past the DB query size once written to LiteLLM_SpendLogs.metadata. + + MAX_BASE64_LENGTH_FOR_LOGGING must bound base64 data URIs here too, while + keeping the surrounding human-readable error text intact. + """ + from litellm.litellm_core_utils.litellm_logging import StandardLoggingPayloadSetup + + payload = "A" * 3_000_000 + message = ( + "litellm.ContextWindowExceededError: OpenAIException - This model's maximum " + "context length is 128000 tokens. Request body: {'messages': [{'role': 'user', " + f"'content': [{{'type': 'file', 'file_data': 'data:application/pdf;base64,{payload}'}}]}}]}}" + ) + + exc = ValueError(message) + result = StandardLoggingPayloadSetup.get_error_information( + exc, traceback_str=f"Traceback: data:application/pdf;base64,{payload}" + ) + + assert payload not in result["error_message"] + assert payload not in (result["traceback"] or "") + assert len(result["error_message"]) < 1000 + assert "base64_data truncated" in result["error_message"] + assert "base64_data truncated" in (result["traceback"] or "") + assert "maximum context length is 128000 tokens" in result["error_message"] + assert "data:application/pdf;base64," in result["error_message"] + + # ────────────────────────────────────────────────────────────────────── # Tests for _get_assembled_streaming_response non-streaming early return # ────────────────────────────────────────────────────────────────────── diff --git a/tests/test_litellm/litellm_core_utils/test_logging_utils.py b/tests/test_litellm/litellm_core_utils/test_logging_utils.py index b0dad0bf228..5a7adbb8a9d 100644 --- a/tests/test_litellm/litellm_core_utils/test_logging_utils.py +++ b/tests/test_litellm/litellm_core_utils/test_logging_utils.py @@ -6,7 +6,7 @@ import pytest from litellm.litellm_core_utils.logging_utils import ( _format_base64_size, - _truncate_base64_in_string, + truncate_base64_data_uris, truncate_base64_in_messages, ) @@ -30,19 +30,19 @@ class TestFormatBase64Size: # --------------------------------------------------------------------------- -# _truncate_base64_in_string +# truncate_base64_data_uris # --------------------------------------------------------------------------- class TestTruncateBase64InString: def test_short_data_uri_not_truncated(self): uri = "data:image/png;base64,AAAA" - assert _truncate_base64_in_string(uri) == uri + assert truncate_base64_data_uris(uri) == uri def test_long_data_uri_truncated(self): payload = "A" * 200 uri = f"data:application/pdf;base64,{payload}" - result = _truncate_base64_in_string(uri) + result = truncate_base64_data_uris(uri) assert "base64_data truncated" in result assert "application/pdf" in result assert payload not in result @@ -50,12 +50,12 @@ class TestTruncateBase64InString: def test_multiple_data_uris(self): payload = "B" * 200 text = f"first: data:image/png;base64,{payload} second: data:image/jpeg;base64,{payload}" - result = _truncate_base64_in_string(text) + result = truncate_base64_data_uris(text) assert result.count("base64_data truncated") == 2 def test_no_data_uri(self): text = "hello world, no base64 here" - assert _truncate_base64_in_string(text) == text + assert truncate_base64_data_uris(text) == text # ---------------------------------------------------------------------------