diff --git a/litellm/proxy/spend_tracking/spend_tracking_utils.py b/litellm/proxy/spend_tracking/spend_tracking_utils.py index ba5f873c4e9..6065e8c3bc8 100644 --- a/litellm/proxy/spend_tracking/spend_tracking_utils.py +++ b/litellm/proxy/spend_tracking/spend_tracking_utils.py @@ -1294,11 +1294,14 @@ def _truncated_for_db(value: str, max_length: int) -> str: return value start_chars: Final = int(max_length * 0.35) end_chars: Final = int(max_length * 0.65) + # The tail is indexed from the front: `value[-0:]` is the whole string, and both shares + # floor to 0 at a limit of 1 or less, so the tightest limits stored the entire prompt. + tail_chars: Final = min(end_chars, max_length - start_chars) return ( f"{value[:start_chars]}" f"... ({LITELLM_TRUNCATED_PAYLOAD_FIELD} skipped {len(value) - start_chars - end_chars} chars. " f"{LITELLM_TRUNCATION_DB_SAFEGUARD_NOTE}) ..." - f"{value[-min(end_chars, max_length - start_chars) :]}" + f"{value[len(value) - tail_chars :]}" ) diff --git a/tests/unit/proxy/spend_tracking/test_spend_tracking_utils.py b/tests/unit/proxy/spend_tracking/test_spend_tracking_utils.py index f52f40ad55d..48ee4674288 100644 --- a/tests/unit/proxy/spend_tracking/test_spend_tracking_utils.py +++ b/tests/unit/proxy/spend_tracking/test_spend_tracking_utils.py @@ -816,17 +816,14 @@ def test_large_request_no_truncation_threshold(): assert "skipped" in truncated_content -@pytest.mark.parametrize( - ("max_string_length_prompt_in_db", "stored_tail"), - [(0, "claude-sonnet-5-5"), (1, "claude-sonnet-5-5"), (-1, "laude-sonnet-5-5"), (-2, "aude-sonnet-5-5")], -) -def test_a_limit_under_two_keeps_mains_tail_after_the_marker(max_string_length_prompt_in_db: int, stored_tail: str): +@pytest.mark.parametrize("max_string_length_prompt_in_db", [0, 1, -1, -2]) +def test_a_limit_under_two_keeps_no_tail_after_the_marker(max_string_length_prompt_in_db: int): sanitized: Final = _sanitize_request_body_for_spend_logs_payload( {"model": "claude-sonnet-5-5"}, max_string_length_prompt_in_db=max_string_length_prompt_in_db ) assert sanitized["model"].startswith("...") - assert sanitized["model"].endswith(f") ...{stored_tail}") + assert sanitized["model"].endswith(") ...") def test_small_request_no_truncation(): @@ -970,6 +967,22 @@ def test_sanitize_request_body_for_spend_logs_payload_long_string(): assert sanitized["normal_text"] == "short text" +@pytest.mark.parametrize("limit", [0, 1, -1]) +def test_sanitize_request_body_never_stores_more_than_it_was_given(monkeypatch, limit): + """The truncation is a database-size safeguard, so the tightest limits must be + the ones that store least. Both shares floor to zero at a limit of 1 or less, + and `value[-0:]` is the whole string, so the entire prompt was written out + behind a marker saying it had been skipped.""" + monkeypatch.setenv("MAX_STRING_LENGTH_PROMPT_IN_DB", str(limit)) + long_string: Final = "a" * 5000 + + sanitized = _sanitize_request_body_for_spend_logs_payload({"text": long_string}) + + assert long_string not in sanitized["text"] + assert len(sanitized["text"]) < len(long_string) + assert LITELLM_TRUNCATED_PAYLOAD_FIELD in sanitized["text"] + + def test_sanitize_request_body_for_spend_logs_payload_nested_dict(): from litellm.constants import MAX_STRING_LENGTH_PROMPT_IN_DB