Skip to content

Commit ff36dfd

Browse files
Merge pull request #14637 from akraines/feature/middle-truncate-spend-logs
feat: implement middle-truncation for spend log payloads
2 parents e7bc700 + 115a3e9 commit ff36dfd

File tree

3 files changed

+105
-13
lines changed

3 files changed

+105
-13
lines changed

litellm/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@
179179
os.getenv("NON_LLM_CONNECTION_TIMEOUT", 15)
180180
) # timeout for adjacent services (e.g. jwt auth)
181181
MAX_EXCEPTION_MESSAGE_LENGTH = int(os.getenv("MAX_EXCEPTION_MESSAGE_LENGTH", 2000))
182-
MAX_STRING_LENGTH_PROMPT_IN_DB = int(os.getenv("MAX_STRING_LENGTH_PROMPT_IN_DB", 1000))
182+
MAX_STRING_LENGTH_PROMPT_IN_DB = int(os.getenv("MAX_STRING_LENGTH_PROMPT_IN_DB", 2048))
183183
BEDROCK_MAX_POLICY_SIZE = int(os.getenv("BEDROCK_MAX_POLICY_SIZE", 75))
184184
REPLICATE_POLLING_DELAY_SECONDS = float(
185185
os.getenv("REPLICATE_POLLING_DELAY_SECONDS", 0.5)

litellm/proxy/spend_tracking/spend_tracking_utils.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,34 @@ def _sanitize_value(value: Any) -> Any:
501501
return [_sanitize_value(item) for item in value]
502502
elif isinstance(value, str):
503503
if len(value) > MAX_STRING_LENGTH_PROMPT_IN_DB:
504-
return f"{value[:MAX_STRING_LENGTH_PROMPT_IN_DB]}... ({LITELLM_TRUNCATED_PAYLOAD_FIELD} {len(value) - MAX_STRING_LENGTH_PROMPT_IN_DB} chars)"
504+
# Keep 35% from beginning and 65% from end (end is usually more important)
505+
# This split ensures we keep more context from the end of conversations
506+
start_ratio = 0.35
507+
end_ratio = 0.65
508+
509+
# Calculate character distribution
510+
start_chars = int(MAX_STRING_LENGTH_PROMPT_IN_DB * start_ratio)
511+
end_chars = int(MAX_STRING_LENGTH_PROMPT_IN_DB * end_ratio)
512+
513+
# Ensure we don't exceed the total limit
514+
total_keep = start_chars + end_chars
515+
if total_keep > MAX_STRING_LENGTH_PROMPT_IN_DB:
516+
end_chars = MAX_STRING_LENGTH_PROMPT_IN_DB - start_chars
517+
518+
# If the string length is less than what we want to keep, just truncate normally
519+
if len(value) <= MAX_STRING_LENGTH_PROMPT_IN_DB:
520+
return value
521+
522+
# Calculate how many characters are being skipped
523+
skipped_chars = len(value) - total_keep
524+
525+
# Build the truncated string: beginning + truncation marker + end
526+
truncated_value = (
527+
f"{value[:start_chars]}"
528+
f"... ({LITELLM_TRUNCATED_PAYLOAD_FIELD} skipped {skipped_chars} chars) ..."
529+
f"{value[-end_chars:]}"
530+
)
531+
return truncated_value
505532
return value
506533
return value
507534

tests/logging_callback_tests/test_spend_logs.py

Lines changed: 76 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -401,11 +401,16 @@ def test_spend_logs_payload_with_prompts_enabled(monkeypatch):
401401
def test_large_request_no_truncation_threshold():
402402
"""
403403
Test that MAX_STRING_LENGTH_PROMPT_IN_DB constant is used for request body sanitization
404+
and that the new truncation logic keeps beginning (35%) and end (65%) of the string
404405
"""
405406
from litellm.constants import MAX_STRING_LENGTH_PROMPT_IN_DB, LITELLM_TRUNCATED_PAYLOAD_FIELD
406407

407408
# Create a large string that exceeds the threshold
408-
large_content = "x" * (MAX_STRING_LENGTH_PROMPT_IN_DB + 500)
409+
# Use a pattern that allows us to verify beginning and end are preserved
410+
start_pattern = "START" * 250 # 1250 chars
411+
middle_pattern = "MIDDLE" * 200 # 1200 chars
412+
end_pattern = "END" * 250 # 750 chars
413+
large_content = start_pattern + middle_pattern + end_pattern
409414

410415
request_body = {
411416
"messages": [
@@ -418,10 +423,20 @@ def test_large_request_no_truncation_threshold():
418423

419424
# Verify the content was truncated
420425
truncated_content = sanitized["messages"][0]["content"]
421-
assert len(truncated_content) > MAX_STRING_LENGTH_PROMPT_IN_DB # includes truncation message
422-
assert truncated_content.startswith("x" * MAX_STRING_LENGTH_PROMPT_IN_DB)
426+
427+
# Calculate expected character counts (35% start, 65% end)
428+
expected_start_chars = int(MAX_STRING_LENGTH_PROMPT_IN_DB * 0.35)
429+
expected_end_chars = int(MAX_STRING_LENGTH_PROMPT_IN_DB * 0.65)
430+
431+
# Should keep first 35% of MAX_STRING_LENGTH_PROMPT_IN_DB chars
432+
assert truncated_content.startswith(large_content[:expected_start_chars])
433+
434+
# Should keep last 65% of MAX_STRING_LENGTH_PROMPT_IN_DB chars
435+
assert truncated_content.endswith(large_content[-expected_end_chars:])
436+
437+
# Should have truncation marker
423438
assert LITELLM_TRUNCATED_PAYLOAD_FIELD in truncated_content
424-
assert "500 chars" in truncated_content
439+
assert "skipped" in truncated_content
425440

426441

427442
def test_small_request_no_truncation():
@@ -452,7 +467,7 @@ def test_configurable_string_length_env_var(monkeypatch):
452467
Test that MAX_STRING_LENGTH_PROMPT_IN_DB can be configured via environment variable
453468
"""
454469
# Set environment variable to a custom value
455-
monkeypatch.setenv("MAX_STRING_LENGTH_PROMPT_IN_DB", "500")
470+
monkeypatch.setenv("MAX_STRING_LENGTH_PROMPT_IN_DB", "1000")
456471

457472
# Import after setting env var to ensure it picks up the new value
458473
import importlib
@@ -465,10 +480,43 @@ def test_configurable_string_length_env_var(monkeypatch):
465480
from litellm.proxy.spend_tracking.spend_tracking_utils import _sanitize_request_body_for_spend_logs_payload
466481

467482
# Verify the constant was set to the env var value
468-
assert MAX_STRING_LENGTH_PROMPT_IN_DB == 500
483+
assert MAX_STRING_LENGTH_PROMPT_IN_DB == 1000
469484

470485
# Test truncation with the custom value
471-
large_content = "y" * 750 # 250 chars over the custom limit
486+
large_content = "A" * 500 + "B" * 800 + "C" * 500 # 1800 chars total
487+
488+
request_body = {
489+
"messages": [
490+
{"role": "user", "content": large_content}
491+
],
492+
"model": "gpt-4"
493+
}
494+
495+
sanitized = _sanitize_request_body_for_spend_logs_payload(request_body)
496+
497+
# Verify truncation occurred with 35% beginning and 65% end preserved
498+
truncated_content = sanitized["messages"][0]["content"]
499+
expected_start = int(1000 * 0.35) # 350 chars from beginning
500+
expected_end = int(1000 * 0.65) # 650 chars from end
501+
502+
assert truncated_content.startswith(large_content[:expected_start])
503+
assert truncated_content.endswith(large_content[-expected_end:])
504+
assert LITELLM_TRUNCATED_PAYLOAD_FIELD in truncated_content
505+
assert "skipped" in truncated_content
506+
assert "800" in truncated_content # Should mention skipped 800 chars
507+
508+
509+
def test_truncation_preserves_beginning_and_end():
510+
"""
511+
Test that truncation preserves the beginning (35%) and end (65%) of content for better debugging
512+
"""
513+
from litellm.constants import MAX_STRING_LENGTH_PROMPT_IN_DB, LITELLM_TRUNCATED_PAYLOAD_FIELD
514+
515+
# Create content with distinct beginning, middle, and end
516+
beginning = "BEGIN_" * 200 # 1200 chars
517+
middle = "MIDDLE_" * 300 # 2100 chars
518+
end = "_END" * 300 # 1200 chars
519+
large_content = beginning + middle + end
472520

473521
request_body = {
474522
"messages": [
@@ -478,9 +526,26 @@ def test_configurable_string_length_env_var(monkeypatch):
478526
}
479527

480528
sanitized = _sanitize_request_body_for_spend_logs_payload(request_body)
529+
truncated_content = sanitized["messages"][0]["content"]
530+
531+
# Calculate expected splits (35% beginning, 65% end)
532+
expected_start_chars = int(MAX_STRING_LENGTH_PROMPT_IN_DB * 0.35)
533+
expected_end_chars = int(MAX_STRING_LENGTH_PROMPT_IN_DB * 0.65)
481534

482-
# Verify truncation occurred at the custom threshold
483-
truncated_content = sanitized["messages"][0]["content"]
484-
assert truncated_content.startswith("y" * 500)
535+
# Check that beginning is preserved
536+
expected_beginning = large_content[:expected_start_chars]
537+
assert truncated_content.startswith(expected_beginning)
538+
539+
# Check that end is preserved
540+
expected_end = large_content[-expected_end_chars:]
541+
assert truncated_content.endswith(expected_end)
542+
543+
# Check truncation marker is present
485544
assert LITELLM_TRUNCATED_PAYLOAD_FIELD in truncated_content
486-
assert "250 chars" in truncated_content
545+
assert "skipped" in truncated_content
546+
547+
# Calculate expected skipped chars
548+
total_chars = len(large_content)
549+
kept_chars = expected_start_chars + expected_end_chars
550+
expected_skipped = total_chars - kept_chars
551+
assert str(expected_skipped) in truncated_content

0 commit comments

Comments
 (0)