From 0cecc9ea9469ce1b1bbb26292bc4a054c4a799a6 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Wed, 28 Oct 2026 22:38:17 -0700 Subject: [PATCH] perf(logging): skip the base64 data URI scan and thread hop for text without a data URI --- litellm/litellm_core_utils/logging_utils.py | 17 ++++-- .../litellm_core_utils/test_logging_utils.py | 55 +++++++++++++++++++ 2 files changed, 67 insertions(+), 5 deletions(-) diff --git a/litellm/litellm_core_utils/logging_utils.py b/litellm/litellm_core_utils/logging_utils.py index 8f58e8b7051..8e9e23c5cb7 100644 --- a/litellm/litellm_core_utils/logging_utils.py +++ b/litellm/litellm_core_utils/logging_utils.py @@ -44,6 +44,7 @@ Helper utils used for logging callbacks # Regex matching data-URI base64 content: "data:;base64," # Captures: group(1)=mime_type, group(2)=base64_payload _DATA_URI_RE: Final = re.compile(r"data:([^;,\s]{1,255});base64,([A-Za-z0-9+/=]+)") +_DATA_URI_BASE64_MARKER: Final = ";base64," # Maximum nesting depth for _truncate_base64_in_value to guard against # pathological payloads. OpenAI message format is typically 3-4 levels deep. @@ -60,9 +61,13 @@ def _base64_data_uri_replacer(match: re.Match[str]) -> str: return f"data:{mime_type};base64,[base64_data truncated: {size_str}]" +def _may_contain_base64_data_uri(value: str) -> bool: + return ";" in value and _DATA_URI_BASE64_MARKER in value + + def _truncate_base64_in_string(value: str) -> str: """Replace long base64 data-URI payloads in a string with a size placeholder.""" - if MAX_BASE64_LENGTH_FOR_LOGGING <= 0: + if MAX_BASE64_LENGTH_FOR_LOGGING <= 0 or not _may_contain_base64_data_uri(value): return value return _DATA_URI_RE.sub(_base64_data_uri_replacer, value) @@ -155,14 +160,16 @@ async def truncate_base64_in_messages_async( messages: str | list | dict | None, # mutable-ok: same contract as truncate_base64_in_messages ) -> str | list | dict | None: # mutable-ok: same contract as truncate_base64_in_messages """ - Same result as truncate_base64_in_messages, but payloads whose string content - reaches BASE64_TRUNCATION_OFFLOAD_THRESHOLD_CHARS are scanned in a worker + Same result as truncate_base64_in_messages, but payloads whose data-URI-bearing + strings reach BASE64_TRUNCATION_OFFLOAD_THRESHOLD_CHARS are scanned in a worker thread so the regex pass over multi-MB base64 images does not block the event loop. """ if messages is None or MAX_BASE64_LENGTH_FOR_LOGGING <= 0: return messages - total_chars: Final = sum(len(leaf) for leaf in _iter_string_leaves(messages)) - if total_chars < BASE64_TRUNCATION_OFFLOAD_THRESHOLD_CHARS: + data_uri_chars: Final = sum( + len(leaf) for leaf in _iter_string_leaves(messages) if _may_contain_base64_data_uri(leaf) + ) + if data_uri_chars < BASE64_TRUNCATION_OFFLOAD_THRESHOLD_CHARS: return truncate_base64_in_messages(messages) return await asyncio.to_thread(truncate_base64_in_messages, messages) diff --git a/tests/unit/litellm_core_utils/test_logging_utils.py b/tests/unit/litellm_core_utils/test_logging_utils.py index edf0dc7960b..5cfd470cb9a 100644 --- a/tests/unit/litellm_core_utils/test_logging_utils.py +++ b/tests/unit/litellm_core_utils/test_logging_utils.py @@ -241,6 +241,38 @@ def scan_threads(monkeypatch): return threads +@pytest.fixture +def regex_scans(monkeypatch): + """Record every string the data URI regex runs over.""" + scanned: list[str] = [] + original = logging_utils._DATA_URI_RE + + class RecordingPattern: + def sub(self, repl, string: str) -> str: + scanned.append(string) + return original.sub(repl, string) + + monkeypatch.setattr(logging_utils, "_DATA_URI_RE", RecordingPattern()) + return scanned + + +def _tool_result_messages(tool_output: str) -> list: + return [ + {"role": "user", "content": "Summarize the rows the tool returned."}, + { + "role": "assistant", + "content": None, + "tool_calls": [ + {"id": "call_1", "type": "function", "function": {"name": "dump_rows", "arguments": "{}"}} + ], + }, + {"role": "tool", "tool_call_id": "call_1", "content": tool_output}, + ] + + +_PLAIN_TOOL_OUTPUT_CHARS = 400 * 1024 + + class TestTruncateBase64InMessagesAsync: @pytest.mark.asyncio async def test_large_payload_is_scanned_off_the_event_loop(self, monkeypatch, scan_threads): @@ -268,6 +300,29 @@ class TestTruncateBase64InMessagesAsync: assert scan_threads assert set(scan_threads) == {threading.get_ident()} + @pytest.mark.asyncio + @pytest.mark.parametrize( + "row", + [ + '{"id": 7, "name": "widget", "status": "ok"}\n', + "data: {\"choices\": [{\"delta\": {\"content\": \"hi\"}}]}\n\n", + "const total = rows.map((r) => r.price).reduce((a, b) => a + b, 0);\n", + ], + ids=["json_rows", "sse_lines", "code_with_semicolons"], + ) + async def test_tool_message_without_data_uri_skips_the_regex_and_the_thread_hop( + self, scan_threads, regex_scans, row + ): + tool_output = (row * (_PLAIN_TOOL_OUTPUT_CHARS // len(row) + 1))[:_PLAIN_TOOL_OUTPUT_CHARS] + messages = _tool_result_messages(tool_output) + + result = await truncate_base64_in_messages_async(messages) + + assert result == messages + assert regex_scans == [] + assert scan_threads + assert set(scan_threads) == {threading.get_ident()} + @pytest.mark.asyncio async def test_none_and_disabled_truncation_short_circuit(self, monkeypatch, scan_threads): assert await truncate_base64_in_messages_async(None) is None