From 58cf647f2cf8fe788cd43a073679c92550405fa2 Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Thu, 19 Feb 2026 11:28:32 -0800 Subject: [PATCH] add base64 truncation for logging payloads --- litellm/litellm_core_utils/logging_utils.py | 74 ++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/litellm/litellm_core_utils/logging_utils.py b/litellm/litellm_core_utils/logging_utils.py index 8cde8ccef1c..06330d2ca13 100644 --- a/litellm/litellm_core_utils/logging_utils.py +++ b/litellm/litellm_core_utils/logging_utils.py @@ -1,11 +1,14 @@ import asyncio +import copy import functools import inspect +import re import time from datetime import datetime -from typing import TYPE_CHECKING, Any, List, Optional, Union +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union from litellm._logging import verbose_logger +from litellm.constants import MAX_BASE64_LENGTH_FOR_LOGGING from litellm.types.utils import ( ModelResponse, ModelResponseStream, @@ -34,6 +37,75 @@ import litellm 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 = re.compile(r"data:([^;]+);base64,([A-Za-z0-9+/=\s]+)") + + +def _format_base64_size(num_chars: int) -> str: + """Return a human-readable byte-size estimate from a base64 character count.""" + num_bytes = num_chars * 3 / 4 + if num_bytes >= 1_048_576: + return f"{num_bytes / 1_048_576:.2f}MB" + if num_bytes >= 1024: + return f"{num_bytes / 1024:.1f}KB" + return f"{int(num_bytes)}B" + + +def _truncate_base64_in_string(value: str) -> str: + """ + Replace long base64 data-URI payloads in a string with a size placeholder. + + Example: + "data:application/pdf;base64,AAAA...AAAA" + -> "data:application/pdf;base64,[base64_data truncated: 1.75MB]" + """ + if MAX_BASE64_LENGTH_FOR_LOGGING <= 0: + return value + + def _replacer(match: re.Match) -> str: + mime_type = match.group(1) + payload = match.group(2) + if len(payload) <= MAX_BASE64_LENGTH_FOR_LOGGING: + return match.group(0) + size_str = _format_base64_size(len(payload)) + return f"data:{mime_type};base64,[base64_data truncated: {size_str}]" + + return _DATA_URI_RE.sub(_replacer, value) + + +def _truncate_base64_in_value(value: Any) -> Any: + """Recursively truncate base64 data URIs in a JSON-like value (str/list/dict).""" + if isinstance(value, str): + return _truncate_base64_in_string(value) + if isinstance(value, dict): + return {k: _truncate_base64_in_value(v) for k, v in value.items()} + if isinstance(value, list): + return [_truncate_base64_in_value(item) for item in value] + return value + + +def truncate_base64_in_messages( + messages: Optional[Union[str, list, dict]], +) -> Optional[Union[str, list, dict]]: + """ + Return a copy of *messages* with long base64 data-URI payloads replaced + by human-readable size placeholders. + + This is intended for logging payloads (e.g. StandardLoggingPayload.messages) + where the full base64 content is not useful and wastes memory/bandwidth. + + The original messages object is never mutated; a new structure is returned + only when truncation actually occurs. + """ + if messages is None or MAX_BASE64_LENGTH_FOR_LOGGING <= 0: + return messages + try: + return _truncate_base64_in_value(messages) + except Exception: + return messages + + # Global service logger instance to avoid recreating it _service_logger = None