fix(spend_logs): persist prompts in SpendLogs.messages for all call types

This commit is contained in:
Devin AI 2026-07-28 15:50:45 +00:00
parent daf22ec871
commit f08a8b7927
3 changed files with 104 additions and 18 deletions

View file

@ -441,7 +441,7 @@ def get_logging_payload(kwargs, response_obj, start_time, end_time) -> SpendLogs
requester_ip_address=clean_metadata.get("requester_ip_address", None),
custom_llm_provider=kwargs.get("custom_llm_provider", ""),
messages=_get_messages_for_spend_logs_payload(
standard_logging_payload=standard_logging_payload, metadata=metadata
standard_logging_payload=standard_logging_payload, kwargs=kwargs
),
response=_get_response_for_spend_logs_payload(payload=standard_logging_payload, kwargs=kwargs),
proxy_server_request=_get_proxy_server_request_for_spend_logs_payload(
@ -655,19 +655,45 @@ async def get_spend_by_team_and_customer(
def _get_messages_for_spend_logs_payload(
standard_logging_payload: Optional[StandardLoggingPayload],
metadata: Optional[dict] = None,
kwargs: Optional[dict] = None,
) -> str:
if _should_store_prompts_and_responses_in_spend_logs():
if standard_logging_payload is not None:
call_type = standard_logging_payload.get("call_type", "")
if call_type == "_arealtime":
messages = standard_logging_payload.get("messages")
if messages is not None:
try:
return safe_dumps(messages)
except Exception:
return "{}"
return "{}"
"""
Serialize the request prompt for ``LiteLLM_SpendLogs.messages``.
Stored for every call type when ``store_prompts_in_spend_logs`` is on; the
input lives under ``messages`` in the standard logging payload regardless of
the API surface (chat completions, Responses API, realtime, ...).
"""
if standard_logging_payload is None or not _should_store_prompts_and_responses_in_spend_logs():
return "{}"
messages: Any = standard_logging_payload.get("messages")
if messages is None:
return "{}"
if kwargs is not None:
from litellm.litellm_core_utils.redact_messages import (
should_redact_message_logging,
)
model_call_details = {
"litellm_params": kwargs.get("litellm_params", {}),
"standard_callback_dynamic_params": kwargs.get("standard_callback_dynamic_params"),
}
if should_redact_message_logging(model_call_details=model_call_details):
return safe_dumps([{"role": "user", "content": REDACTED_BY_LITELM_STRING}])
sanitized_messages = _sanitize_request_body_for_spend_logs_payload({"messages": messages}).get("messages", messages)
try:
messages_json_str = safe_dumps(sanitized_messages)
except Exception:
return "{}"
if LITELLM_TRUNCATED_PAYLOAD_FIELD in messages_json_str:
verbose_proxy_logger.info(
"Spend Log: messages were truncated before storing in DB. %s",
LITELLM_TRUNCATION_DB_SAFEGUARD_NOTE,
)
return messages_json_str
_SENSITIVE_REQUEST_BODY_KEYS = frozenset({"secret_fields"})

View file

@ -385,6 +385,9 @@ def test_spend_logs_payload_with_prompts_enabled(monkeypatch):
print("json payload: ", json.dumps(payload, indent=4, default=str))
# Verify messages and response are included in payload
assert json.loads(payload["messages"] or "{}") == [
{"role": "user", "content": "Hello!"}
]
assert payload["response"] == json.dumps(
{"role": "assistant", "content": "Hi there!"}
)

View file

@ -544,21 +544,78 @@ def test_get_messages_for_spend_logs_realtime_empty_when_disabled(mock_should_st
@patch(
"litellm.proxy.spend_tracking.spend_tracking_utils._should_store_prompts_and_responses_in_spend_logs"
)
def test_get_messages_for_spend_logs_non_realtime_returns_empty(mock_should_store):
@pytest.mark.parametrize("call_type", ["acompletion", "aresponses"])
def test_get_messages_for_spend_logs_stores_prompts_for_all_call_types(
mock_should_store, call_type
):
"""
Test that _get_messages_for_spend_logs_payload returns '{}' for non-realtime
calls even when store_prompts_in_spend_logs is True.
Regression for #34747: prompts were only persisted for realtime calls, so
LiteLLM_SpendLogs.messages stayed '{}' for /chat/completions and /responses.
"""
mock_should_store.return_value = True
payload = cast(
StandardLoggingPayload,
{
"call_type": "acompletion",
"call_type": call_type,
"messages": [{"role": "user", "content": "Hello"}],
},
)
result = _get_messages_for_spend_logs_payload(payload)
assert result == "{}"
assert json.loads(result) == [{"role": "user", "content": "Hello"}]
@patch(
"litellm.proxy.spend_tracking.spend_tracking_utils._should_store_prompts_and_responses_in_spend_logs"
)
def test_get_messages_for_spend_logs_truncates_large_strings(mock_should_store):
from litellm.constants import MAX_STRING_LENGTH_PROMPT_IN_DB
mock_should_store.return_value = True
payload = cast(
StandardLoggingPayload,
{
"call_type": "acompletion",
"messages": [
{
"role": "user",
"content": "A" * (MAX_STRING_LENGTH_PROMPT_IN_DB + 500),
}
],
},
)
result = _get_messages_for_spend_logs_payload(payload)
assert LITELLM_TRUNCATED_PAYLOAD_FIELD in result
assert (
len(json.loads(result)[0]["content"]) < MAX_STRING_LENGTH_PROMPT_IN_DB + 500
)
@patch(
"litellm.proxy.spend_tracking.spend_tracking_utils._should_store_prompts_and_responses_in_spend_logs"
)
def test_get_messages_for_spend_logs_redacts_when_message_logging_off(
mock_should_store,
):
"""turn_off_message_logging must win over store_prompts_in_spend_logs."""
mock_should_store.return_value = True
payload = cast(
StandardLoggingPayload,
{
"call_type": "acompletion",
"messages": [{"role": "user", "content": "my secret prompt"}],
},
)
result = _get_messages_for_spend_logs_payload(
payload,
kwargs={
"litellm_params": {"metadata": {}},
"standard_callback_dynamic_params": {"turn_off_message_logging": True},
},
)
assert "my secret prompt" not in result
assert json.loads(result) == [
{"role": "user", "content": REDACTED_BY_LITELM_STRING}
]
@patch(