mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-02 07:13:02 +00:00
perf(logging): skip the base64 data URI scan and thread hop for text without a data URI
This commit is contained in:
parent
764a7a6536
commit
0cecc9ea94
2 changed files with 67 additions and 5 deletions
|
|
@ -44,6 +44,7 @@ Helper utils used for logging callbacks
|
|||
# Regex matching data-URI base64 content: "data:<mime>;base64,<payload>"
|
||||
# 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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue