mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-16 23:41:43 +00:00
fix(spend_logs): persist prompts in SpendLogs.messages for all call types
This commit is contained in:
parent
daf22ec871
commit
f08a8b7927
3 changed files with 104 additions and 18 deletions
|
|
@ -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"})
|
||||
|
|
|
|||
|
|
@ -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!"}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue