From c7c02bbdc596c9a16fed2bff5b9faf9d49b7269b Mon Sep 17 00:00:00 2001 From: yucheng Date: Fri, 30 Oct 2026 17:58:29 -0700 Subject: [PATCH] perf(logging): skip escaping plain request text and walk each request value once A redacted failure built JSON and repr copies of every request text for each error field, though printable ASCII without a double quote or backslash escapes to itself, and walked the original messages a second time. The scrubbed output is unchanged. --- litellm/litellm_core_utils/redact_messages.py | 13 ++++++++++--- .../litellm_core_utils/test_redact_messages.py | 16 +++++++++++++++- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/litellm/litellm_core_utils/redact_messages.py b/litellm/litellm_core_utils/redact_messages.py index 681f1080022..e0d4d9d8661 100644 --- a/litellm/litellm_core_utils/redact_messages.py +++ b/litellm/litellm_core_utils/redact_messages.py @@ -448,6 +448,7 @@ def _redacted_responses_api_response( _REQUEST_TEXT_KEYS: Final = ("messages", "input", "prompt", "system", "instructions") _STRUCTURAL_KEYS: Final = frozenset({"role", "type", "id", "tool_call_id", "name"}) _MIN_UNQUOTED_TEXT_LENGTH: Final = 4 +_ESCAPED_CHARACTERS: Final = ('"', "\\") # pydantic shows a long input value as its repr's first 25 and last 24 characters around "...", quotes and # container keys included, so each side of the cut shows at most 24 characters of a quoted text _CUT_SIDE_LENGTH: Final = 24 @@ -473,9 +474,12 @@ def _next_level(level: tuple[object, ...], _: None) -> tuple[object, ...]: def request_texts(*values: object) -> frozenset[str]: """ The non-empty strings a request carries under ``values``, skipping structural fields such as ``role`` - and ``type``. + and ``type``. A value passed more than once is walked once. """ - levels: Final = itertools.takewhile(bool, itertools.accumulate(itertools.repeat(None), _next_level, initial=values)) + distinct_values: Final = tuple({id(value): value for value in values}.values()) + levels: Final = itertools.takewhile( + bool, itertools.accumulate(itertools.repeat(None), _next_level, initial=distinct_values) + ) return frozenset(value for value in itertools.chain.from_iterable(levels) if isinstance(value, str) and value) @@ -490,7 +494,10 @@ def failure_request_texts(model_call_details: Mapping[str, object], *originals: def _echo_forms(text: str) -> frozenset[str]: - """How an error can quote ``text``: verbatim, or JSON or repr escaped.""" + """How an error can quote ``text``: verbatim, or JSON or repr escaped. Printable ASCII without a double quote + or a backslash escapes to itself.""" + if text.isascii() and text.isprintable() and not any(character in text for character in _ESCAPED_CHARACTERS): + return frozenset((text,)) return frozenset({text, json.dumps(text)[1:-1], json.dumps(text, ensure_ascii=False)[1:-1], repr(text)[1:-1]}) diff --git a/tests/unit/litellm_core_utils/test_redact_messages.py b/tests/unit/litellm_core_utils/test_redact_messages.py index 78739ab6f25..c12006ddafe 100644 --- a/tests/unit/litellm_core_utils/test_redact_messages.py +++ b/tests/unit/litellm_core_utils/test_redact_messages.py @@ -12,7 +12,7 @@ import json import os import threading import tracemalloc -from collections.abc import AsyncIterator, Mapping +from collections.abc import AsyncIterator, Callable, Mapping from datetime import datetime from types import MappingProxyType, SimpleNamespace from typing import Final, Optional, Union @@ -2101,6 +2101,20 @@ def test_scrub_request_text_replaces_each_way_an_error_quotes_the_prompt(quoted: assert scrubbed.startswith("Invalid content: <") and scrubbed.endswith("> rejected") +@pytest.mark.parametrize( + "prompt", + ['say "hi" to Bob', "C:\\Users\\bob", "café for Bob", "tab\tfor Bob"], + ids=["double_quote", "backslash", "non_ascii", "control_character"], +) +@pytest.mark.parametrize("escaped", [json.dumps, repr], ids=["json", "repr"]) +def test_scrub_request_text_replaces_an_escaped_echo_of_a_prompt_that_escaping_changes( + prompt: str, escaped: Callable[[str], str] +) -> None: + error: Final = f"Invalid content: <{escaped(prompt)[1:-1]}> rejected" + + assert scrub_request_text(error, request_texts(prompt)) == "Invalid content: rejected" + + @pytest.mark.parametrize( "value", [PROMPT, [PROMPT], {"content": PROMPT}, [{"content": PROMPT}], [{"content": LONG_PROMPT}], [LONG_PROMPT]],