mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-15 23:31:29 +00:00
fix(logging): honor MAX_BASE64_LENGTH_FOR_LOGGING in error_information
Base64 data URIs echoed back by provider errors made error_information.error_message and its traceback grow to the size of the uploaded file, which then blew past the DB query size once written to LiteLLM_SpendLogs.metadata
This commit is contained in:
parent
daf22ec871
commit
e7cf3d5113
4 changed files with 49 additions and 13 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue