diff --git a/litellm/litellm_core_utils/redact_messages.py b/litellm/litellm_core_utils/redact_messages.py index ace55b53dfc..d8473468465 100644 --- a/litellm/litellm_core_utils/redact_messages.py +++ b/litellm/litellm_core_utils/redact_messages.py @@ -514,6 +514,16 @@ def scrub_request_text(text: str, texts: Collection[str]) -> str: return REDACTED_BY_LITELLM if scrubbed is None else scrubbed +def quoted_request_texts(text: str, texts: Collection[str]) -> frozenset[str]: + """ + The request ``texts`` that ``scrub_request_text`` acts on in ``text``: those it quotes, or all of them when + ``text`` holds a pydantic ``input_value`` or is too large to search one text at a time. + """ + if len(text) * len(texts) > _MAX_SCRUB_COMPARISONS: + return frozenset(texts) + return frozenset(each for each in texts if scrub_request_text(text, (each,)) != text) + + 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 diff --git a/tests/unit/litellm_core_utils/test_redact_messages.py b/tests/unit/litellm_core_utils/test_redact_messages.py index 07b300d94ea..8ccf73e1066 100644 --- a/tests/unit/litellm_core_utils/test_redact_messages.py +++ b/tests/unit/litellm_core_utils/test_redact_messages.py @@ -32,6 +32,7 @@ from litellm.litellm_core_utils.redact_messages import ( _redact_responses_api_output, _redacted_responses_api_response, perform_redaction, + quoted_request_texts, redacted_model_call_details_for_custom_logger, redacted_response_obj_for_custom_logger, redacted_standard_logging_payload, @@ -2493,6 +2494,24 @@ def test_scrub_request_text_replaces_the_whole_text_when_the_prompt_repeats_past assert scrub_request_text("bad: secret " * echoes, request_texts("secret")) == scrubbed +@pytest.mark.parametrize( + ("error", "quoted"), + [ + ("Invalid content: 'secret one' and \"tab\\tafter\"", {"secret one", "tab\tafter"}), + ("litellm.RateLimitError: Rate limit reached", set()), + ("1 validation error\n Input should be a valid string [input_value='cut...', input_type=str]", None), + ("x" * 5_000, None), + ], + ids=["quoted_verbatim_or_escaped", "quoted_only_inside_a_word", "pydantic_input_value", "too_large_to_search"], +) +def test_quoted_request_texts_names_the_texts_a_scrub_of_the_error_acts_on(error: str, quoted: set[str] | None) -> None: + texts: Final = request_texts( + "secret one", "tab\tafter", "Error", [f"message number {index}" for index in range(2_000)] + ) + + assert quoted_request_texts(error, texts) == (texts if quoted is None else quoted) + + def test_scrub_request_text_keeps_no_copy_of_the_prompt_after_it_returns() -> None: prompt: Final = "".join(f"secret {index} " for index in range(20_000)) error: Final = f"Invalid content: '{prompt}'"