fix(logging): keep whole-word matching to one-word request text, and count a neighbouring echo, escape, label or marker as a break
Some checks failed
LiteLLM Rust / rust-lint (push) Has been cancelled
LiteLLM Rust / rust-test (push) Has been cancelled
Cost map pins recheck / rerun-cost-map-guard (push) Has been cancelled
LiteLLM Rust DB / db-queries (push) Has been cancelled
LiteLLM Rust / rust-wheel (push) Has been cancelled
Terraform Modules / fmt, validate, test (aws) (push) Has been cancelled
Terraform Modules / fmt, validate, test (gcp) (push) Has been cancelled
Terraform Provider / gofmt, vet, build, test (push) Has been cancelled
Terraform Provider / Provider endpoints vs proxy OpenAPI schema (push) Has been cancelled

This commit is contained in:
yucheng 2026-11-04 19:39:56 -08:00
parent 1c67f492c3
commit 5a47a6b499
2 changed files with 67 additions and 16 deletions

View file

@ -527,8 +527,9 @@ def quoted_request_texts(text: str, texts: Collection[str]) -> frozenset[str]:
def _with_echoes_replaced(text: str, echoes: Collection[str]) -> str | None:
"""
``text`` with each quote of ``echoes`` replaced by ``REDACTED_BY_LITELLM`` in one left-to-right pass, the
longest where several start at one place. ``None`` when ``echoes`` turn up more often than a failure path
should look through.
longest where several start at one place. An echo that is a single word only counts where it stands as its own
word, so a prompt such as ``Error`` leaves ``RateLimitError`` alone. ``None`` when ``echoes`` turn up more often
than a failure path should look through.
"""
found: Final = tuple(
itertools.islice(
@ -537,7 +538,17 @@ def _with_echoes_replaced(text: str, echoes: Collection[str]) -> str | None:
)
if len(found) > _MAX_ECHO_OCCURRENCES:
return None
ends: Final = dict(sorted((span for span in found if _stands_alone(text, *span)), key=lambda span: span[1]))
echo_starts: Final = frozenset(start for start, _, _ in found)
echo_ends: Final = frozenset(end for _, end, _ in found)
counted: Final = (
(start, end)
for start, end, single_word in found
if not single_word
or (
(start in echo_ends or _is_break_before(text, start)) and (end in echo_starts or _is_break_after(text, end))
)
)
ends: Final = dict(sorted(counted, key=lambda span: span[1]))
starts: Final = sorted(ends)
frontiers: Final = itertools.accumulate(starts, functools.partial(_frontier_after, ends), initial=0)
chosen: Final = tuple(start for start, frontier in zip(starts, frontiers) if start >= frontier)
@ -554,15 +565,16 @@ def _frontier_after(ends: Mapping[int, int], frontier: int, start: int) -> int:
return ends[start] if start >= frontier else frontier
def _quote_spans(text: str, form: str) -> Iterator[tuple[int, int]]:
"""Where each quote of ``form`` in ``text`` starts and ends, overlaps included. A form short enough to occur in
ordinary wording only counts between quotes."""
def _quote_spans(text: str, form: str) -> Iterator[tuple[int, int, bool]]:
"""Where each quote of ``form`` in ``text`` starts and ends, overlaps included, and whether ``form`` is a single
word. A form short enough to occur in ordinary wording only counts between quotes."""
if len(form) >= _MIN_UNQUOTED_TEXT_LENGTH:
return ((start, start + len(form)) for start in _found(text, form))
single_word: Final = form.isascii() and form.replace("_", "").isalnum()
return ((start, start + len(form), single_word) for start in _found(text, form))
quoted_starts: Final = itertools.chain.from_iterable(
_found(text, f"{opening}{form}{closing}") for opening, closing in itertools.product(_QUOTES, repeat=2)
)
return ((start + 1, start + 1 + len(form)) for start in quoted_starts)
return ((start + 1, start + 1 + len(form), False) for start in quoted_starts)
def _found(text: str, piece: str) -> Iterator[int]:
@ -575,21 +587,29 @@ def _found(text: str, piece: str) -> Iterator[int]:
)
def _stands_alone(text: str, start: int, end: int) -> bool:
"""Whether ``text[start:end]`` is not part of a longer word. An escape such as ``\\n`` before it counts as a break."""
return (not _is_word_character(text[start]) or _is_word_break(text, start)) and (
not _is_word_character(text[end - 1]) or end == len(text) or not _is_word_character(text[end])
)
def _is_word_break(text: str, index: int) -> bool:
def _is_break_before(text: str, index: int) -> bool:
"""
Whether a word starts at ``index`` of ``text``. An escape such as ``\\n`` before it and a ``REDACTED_BY_LITELLM``
an earlier pass put there count as breaks.
"""
return (
index == 0
or not _is_word_character(text[index - 1])
or text.endswith(REDACTED_BY_LITELLM, 0, index)
or _ESCAPE_TAIL.search(text, max(0, index - 10), index) is not None
)
def _is_break_after(text: str, index: int) -> bool:
"""Whether a word ends at ``index`` of ``text``. A pydantic ``input_value=`` or a ``REDACTED_BY_LITELLM`` right
after it counts as a break."""
return (
index == len(text)
or not _is_word_character(text[index])
or text.startswith((_INPUT_VALUE_LABEL, REDACTED_BY_LITELLM), index)
)
def _is_word_character(character: str) -> bool:
return character.isascii() and (character.isalnum() or character == "_")

View file

@ -2477,6 +2477,37 @@ def test_scrub_request_text_replaces_a_prompt_that_stands_as_its_own_word(error:
assert scrub_request_text(error, request_texts("Error")) == scrubbed
@pytest.mark.parametrize(
("texts", "error", "scrubbed"),
[
(
("my ssn is 1234",),
"litellm.BadRequestErrormy ssn is 1234Error",
"litellm.BadRequestErrorredacted-by-litellmError",
),
(("Hello", "world"), "content: Helloworld!", "content: redacted-by-litellmredacted-by-litellm!"),
(
("secretinput_value=",),
"bad: secretinput_value=x",
"bad: redacted-by-litellminput_value=redacted-by-litellm",
),
(("Firstinput_value=1", "Second"), "x: Firstinput_value=1Second", "x: redacted-by-litellmredacted-by-litellm"),
(("Firstinput_value=1", "Second"), "x: SecondFirstinput_value=1", "x: redacted-by-litellmredacted-by-litellm"),
],
ids=[
"several_words_inside_a_word",
"next_to_another_text",
"before_its_own_input_value_label",
"after_an_earlier_replacement",
"before_an_earlier_replacement",
],
)
def test_scrub_request_text_replaces_a_prompt_glued_to_other_text_when_it_is_not_one_word(
texts: tuple[str, ...], error: str, scrubbed: str
) -> None:
assert scrub_request_text(error, request_texts(*texts)) == scrubbed
def test_scrub_request_text_replaces_the_longest_prompt_that_starts_at_one_place() -> None:
scrubbed: Final = scrub_request_text("bad: secret code list", request_texts("secret", "secret code", "code list"))