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.
This commit is contained in:
yucheng 2026-10-30 17:58:29 -07:00
parent 3de0381ca1
commit c7c02bbdc5
2 changed files with 25 additions and 4 deletions

View file

@ -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]})

View file

@ -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: <redacted-by-litellm> rejected"
@pytest.mark.parametrize(
"value",
[PROMPT, [PROMPT], {"content": PROMPT}, [{"content": PROMPT}], [{"content": LONG_PROMPT}], [LONG_PROMPT]],