feat(proxy): control response storage in spend logs

This commit is contained in:
spawrks 2026-08-18 14:37:21 -05:00
parent c2c2a623c0
commit d0f0180cac
5 changed files with 126 additions and 1 deletions

View file

@ -2754,6 +2754,10 @@ class ConfigGeneralSettings(LiteLLMPydanticObjectBase):
None,
description="If True, stores request messages and responses in spend logs. Default is False.",
)
store_responses_in_spend_logs: bool | None = Field(
None,
description="Controls response content storage in spend logs independently. When unset, follows store_prompts_in_spend_logs for backward compatibility.",
)
disable_auto_add_proxy_admin_to_teams: bool | None = Field(
None,
description="By default, the user calling /team/new is automatically added to the new team as a team admin. If True, proxy admins are no longer auto-added; members explicitly listed in members_with_roles are unaffected. Default is False.",

View file

@ -7107,6 +7107,20 @@ class ProxyConfig:
# For other types, convert to bool
general_settings["store_prompts_in_spend_logs"] = bool(value)
if "store_responses_in_spend_logs" in _general_settings:
if "store_responses_in_spend_logs" in self._yaml_general_settings_keys:
value = general_settings.get("store_responses_in_spend_logs")
else:
value = _general_settings["store_responses_in_spend_logs"]
if value is None:
general_settings["store_responses_in_spend_logs"] = None
elif isinstance(value, bool):
general_settings["store_responses_in_spend_logs"] = value
elif isinstance(value, str):
general_settings["store_responses_in_spend_logs"] = value.lower() == "true"
else:
general_settings["store_responses_in_spend_logs"] = bool(value)
if "disable_auto_add_proxy_admin_to_teams" in _general_settings:
value = _general_settings["disable_auto_add_proxy_admin_to_teams"]
if isinstance(value, str):
@ -17042,6 +17056,7 @@ _GENERAL_SETTINGS_CONFIG_LIST_FIELD_TYPES: Final[Mapping[str, str]] = MappingPro
"pass_through_endpoints": "PydanticModel",
"store_model_in_db": "Boolean",
"store_prompts_in_spend_logs": "Boolean",
"store_responses_in_spend_logs": "Boolean",
"maximum_spend_logs_retention_period": "String",
"maximum_health_check_retention_period": "String",
"maximum_spend_logs_cleanup_batch_size": "Integer",

View file

@ -1352,7 +1352,7 @@ def _get_response_for_spend_logs_payload(
) -> str:
if payload is None:
return "{}"
if should_store_prompts_and_responses_in_spend_logs():
if _should_store_responses_in_spend_logs():
response_obj: object = payload.get("response")
if response_obj is None:
return "{}"
@ -1419,6 +1419,22 @@ def should_store_prompts_and_responses_in_spend_logs() -> bool:
return get_secret_bool("STORE_PROMPTS_IN_SPEND_LOGS") is True
def _should_store_responses_in_spend_logs() -> bool:
from litellm.proxy.proxy_server import general_settings
from litellm.secret_managers.main import get_secret_bool
store_responses_value: Final = general_settings.get("store_responses_in_spend_logs")
if store_responses_value is not None:
if isinstance(store_responses_value, str):
return store_responses_value.lower() == "true"
return store_responses_value is True
store_responses_env: Final = get_secret_bool("STORE_RESPONSES_IN_SPEND_LOGS")
if store_responses_env is not None:
return store_responses_env
return should_store_prompts_and_responses_in_spend_logs()
def _get_status_for_spend_log(
metadata: dict,
) -> Literal["success", "failure"]:

View file

@ -353,3 +353,15 @@ class TestYamlStorePromptsDbOverride:
"""_yaml_general_settings_keys should be empty on init."""
proxy_config = ProxyConfig()
assert proxy_config._yaml_general_settings_keys == set()
@pytest.mark.asyncio
async def test_response_storage_yaml_value_takes_precedence_over_db(self):
proxy_config = self._make_proxy_config_with_yaml_keys({"store_responses_in_spend_logs"})
test_general_settings = {"store_responses_in_spend_logs": False}
with mock.patch("litellm.proxy.proxy_server.general_settings", test_general_settings):
await proxy_config._update_general_settings(
db_general_settings={"store_responses_in_spend_logs": True},
)
assert test_general_settings["store_responses_in_spend_logs"] is False

View file

@ -39,6 +39,7 @@ from litellm.proxy.spend_tracking.spend_tracking_utils import (
_sanitize_error_information_for_spend_logs,
_sanitize_guardrail_information_for_spend_logs,
_sanitize_request_body_for_spend_logs_payload,
_should_store_responses_in_spend_logs,
get_logging_payload,
get_spend_logs_id,
should_store_prompts_and_responses_in_spend_logs,
@ -1708,6 +1709,83 @@ def test_should_store_prompts_and_responses_in_spend_logs_case_insensitive_strin
assert result is False, "Expected False (from env var) when key missing, got True"
@pytest.mark.parametrize(
("settings", "environment", "expected"),
[
({"store_prompts_in_spend_logs": True}, {}, True),
({"store_prompts_in_spend_logs": False}, {}, False),
(
{
"store_prompts_in_spend_logs": True,
"store_responses_in_spend_logs": False,
},
{},
False,
),
(
{
"store_prompts_in_spend_logs": False,
"store_responses_in_spend_logs": True,
},
{},
True,
),
({"store_prompts_in_spend_logs": True}, {"STORE_RESPONSES_IN_SPEND_LOGS": False}, False),
({"store_prompts_in_spend_logs": False}, {"STORE_RESPONSES_IN_SPEND_LOGS": True}, True),
(
{
"store_prompts_in_spend_logs": True,
"store_responses_in_spend_logs": "FALSE",
},
{},
False,
),
],
)
@patch("litellm.secret_managers.main.get_secret_bool")
def test_should_store_responses_in_spend_logs(
mock_get_secret_bool,
settings,
environment,
expected,
):
mock_get_secret_bool.side_effect = environment.get
with patch("litellm.proxy.proxy_server.general_settings", settings):
assert _should_store_responses_in_spend_logs() is expected
def test_spend_logs_can_store_request_without_response():
settings = {
"store_prompts_in_spend_logs": True,
"store_responses_in_spend_logs": False,
}
kwargs = {
"litellm_params": {
"proxy_server_request": {
"body": {
"model": "gpt-5-mini",
"messages": [{"role": "user", "content": "Hello!"}],
}
}
}
}
payload = cast(
StandardLoggingPayload,
{"response": {"role": "assistant", "content": "Hi there!"}},
)
with patch("litellm.proxy.proxy_server.general_settings", settings):
request_result = _get_proxy_server_request_for_spend_logs_payload(
metadata={},
litellm_params=kwargs["litellm_params"],
kwargs=kwargs,
)
response_result = _get_response_for_spend_logs_payload(payload=payload, kwargs=kwargs)
assert json.loads(request_result)["messages"] == [{"role": "user", "content": "Hello!"}]
assert response_result == "{}"
def test_get_spend_logs_metadata_guardrail_info_fallback_from_metadata():
"""
When standard_logging_payload is None (e.g. guardrail blocks before LLM call),