Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 31 additions & 8 deletions litellm/proxy/spend_tracking/spend_tracking_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import hashlib
import json
import os
import secrets
from datetime import datetime
from datetime import datetime as dt
Expand All @@ -10,7 +11,10 @@

import litellm
from litellm._logging import verbose_proxy_logger
from litellm.constants import MAX_STRING_LENGTH_PROMPT_IN_DB, REDACTED_BY_LITELM_STRING
from litellm.constants import (
MAX_STRING_LENGTH_PROMPT_IN_DB as DEFAULT_MAX_STRING_LENGTH_PROMPT_IN_DB,
)
from litellm.constants import REDACTED_BY_LITELM_STRING
from litellm.litellm_core_utils.core_helpers import (
get_litellm_metadata_from_kwargs,
reconstruct_model_name,
Expand All @@ -30,6 +34,20 @@
from litellm.utils import get_end_user_id_for_cost_tracking


def _get_max_string_length_prompt_in_db() -> int:
"""
Resolve prompt truncation threshold at runtime so values loaded later via
proxy config environment_variables are honored.
"""
max_length_str = os.getenv("MAX_STRING_LENGTH_PROMPT_IN_DB")
if max_length_str is None:
return DEFAULT_MAX_STRING_LENGTH_PROMPT_IN_DB
try:
return int(max_length_str)
except (TypeError, ValueError):
return DEFAULT_MAX_STRING_LENGTH_PROMPT_IN_DB


def _is_master_key(api_key: str, _master_key: Optional[str]) -> bool:
if _master_key is None:
return False
Expand Down Expand Up @@ -599,6 +617,7 @@ def _get_messages_for_spend_logs_payload(
def _sanitize_request_body_for_spend_logs_payload(
request_body: dict,
visited: Optional[set] = None,
max_string_length_prompt_in_db: Optional[int] = None,
) -> dict:
"""
Recursively sanitize request body to prevent logging large base64 strings or other large values.
Expand All @@ -608,6 +627,8 @@ def _sanitize_request_body_for_spend_logs_payload(

if visited is None:
visited = set()
if max_string_length_prompt_in_db is None:
max_string_length_prompt_in_db = _get_max_string_length_prompt_in_db()

# Get the object's memory address to track visited objects
obj_id = id(request_body)
Expand All @@ -617,27 +638,29 @@ def _sanitize_request_body_for_spend_logs_payload(

def _sanitize_value(value: Any) -> Any:
if isinstance(value, dict):
return _sanitize_request_body_for_spend_logs_payload(value, visited)
return _sanitize_request_body_for_spend_logs_payload(
value, visited, max_string_length_prompt_in_db
)
elif isinstance(value, list):
return [_sanitize_value(item) for item in value]
elif isinstance(value, str):
if len(value) > MAX_STRING_LENGTH_PROMPT_IN_DB:
if len(value) > max_string_length_prompt_in_db:
# Keep 35% from beginning and 65% from end (end is usually more important)
# This split ensures we keep more context from the end of conversations
start_ratio = 0.35
end_ratio = 0.65

# Calculate character distribution
start_chars = int(MAX_STRING_LENGTH_PROMPT_IN_DB * start_ratio)
end_chars = int(MAX_STRING_LENGTH_PROMPT_IN_DB * end_ratio)
start_chars = int(max_string_length_prompt_in_db * start_ratio)
end_chars = int(max_string_length_prompt_in_db * end_ratio)

# Ensure we don't exceed the total limit
total_keep = start_chars + end_chars
if total_keep > MAX_STRING_LENGTH_PROMPT_IN_DB:
end_chars = MAX_STRING_LENGTH_PROMPT_IN_DB - start_chars
if total_keep > max_string_length_prompt_in_db:
end_chars = max_string_length_prompt_in_db - start_chars

# If the string length is less than what we want to keep, just truncate normally
if len(value) <= MAX_STRING_LENGTH_PROMPT_IN_DB:
if len(value) <= max_string_length_prompt_in_db:
return value

# Calculate how many characters are being skipped
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,21 @@ def test_sanitize_request_body_for_spend_logs_payload_mixed_types():
assert len(sanitized["nested"]["dict"]["key"]) == expected_length


def test_sanitize_request_body_for_spend_logs_payload_uses_runtime_env_override(
monkeypatch: pytest.MonkeyPatch,
):
from litellm.constants import MAX_STRING_LENGTH_PROMPT_IN_DB

override_max = max(MAX_STRING_LENGTH_PROMPT_IN_DB + 1000, 6000)
test_string = "a" * (MAX_STRING_LENGTH_PROMPT_IN_DB + 500)

# Simulate config-loaded env var being set after module import.
monkeypatch.setenv("MAX_STRING_LENGTH_PROMPT_IN_DB", str(override_max))

sanitized = _sanitize_request_body_for_spend_logs_payload({"text": test_string})
assert sanitized["text"] == test_string


def test_sanitize_request_body_for_spend_logs_payload_circular_reference():
# Create a circular reference
a: dict[str, Any] = {}
Expand Down Expand Up @@ -1279,4 +1294,3 @@ def test_get_logging_payload_includes_request_duration_ms():
)

assert payload["request_duration_ms"] == 3000

Loading