fix(proxy): propagate UI-editable general_settings from DB to runtime

This commit is contained in:
Devin AI 2026-07-29 10:32:19 +00:00
parent c274cf321c
commit e447692ee9
2 changed files with 87 additions and 28 deletions

View file

@ -17,6 +17,7 @@ import traceback
import warnings
from collections.abc import Mapping
from datetime import datetime, timedelta, timezone
from types import MappingProxyType
from typing import (
TYPE_CHECKING,
Any,
@ -702,6 +703,58 @@ _EXTRA_SECRET_GENERAL_SETTINGS_FIELDS = frozenset(
)
# general_settings fields the Admin UI can read and write (GET /config/list,
# POST /config/field/update). Kept in one place so the DB->runtime propagation
# in ProxyConfig._update_general_settings stays in sync with what the UI offers
_GENERAL_SETTINGS_UI_EDITABLE_FIELDS: Mapping[str, str] = MappingProxyType(
{
"max_parallel_requests": "Integer",
"global_max_parallel_requests": "Integer",
"max_request_size_mb": "Integer",
"max_response_size_mb": "Integer",
"proxy_config_reload_interval_seconds": "Integer",
"pass_through_endpoints": "PydanticModel",
"store_model_in_db": "Boolean",
"store_prompts_in_spend_logs": "Boolean",
"maximum_spend_logs_retention_period": "String",
"mcp_internal_ip_ranges": "List",
"mcp_trusted_proxy_ranges": "List",
"mcp_xff_num_trusted_hops": "Integer",
"always_include_stream_usage": "Boolean",
"forward_client_headers_to_llm_api": "Boolean",
"mcp_required_fields": "List",
"cancel_on_disconnect": "Boolean",
"skip_user_budget_on_team_key": "Boolean",
"disable_auto_add_proxy_admin_to_teams": "Boolean",
}
)
# UI-editable fields whose DB value needs custom handling (normalization, a
# global other than general_settings, or a side effect such as rescheduling a
# job); _update_general_settings applies these itself
_GENERAL_SETTINGS_DB_SIDE_EFFECT_FIELDS = frozenset(
{
"pass_through_endpoints",
"store_model_in_db",
"store_prompts_in_spend_logs",
"maximum_spend_logs_retention_period",
"disable_auto_add_proxy_admin_to_teams",
}
)
# Every other UI-editable field is read straight out of general_settings at
# request time, so copying the DB value across is all the propagation needed
_GENERAL_SETTINGS_DB_PASSTHROUGH_FIELDS = (
frozenset(_GENERAL_SETTINGS_UI_EDITABLE_FIELDS) - _GENERAL_SETTINGS_DB_SIDE_EFFECT_FIELDS
) | frozenset(
{
"user_url_allowed_hosts",
"user_url_validation",
"provider_url_destination_allowed_hosts",
}
)
def _redact_worker_config_for_logging(worker_config: str | dict[str, JsonValue] | None) -> JsonValue:
"""Mask sensitive fields in the worker config before it enters a log record.
@ -5970,11 +6023,7 @@ class ProxyConfig:
if old_value != new_value:
await self._reschedule_spend_log_cleanup_job()
for key in (
"user_url_allowed_hosts",
"user_url_validation",
"provider_url_destination_allowed_hosts",
):
for key in _GENERAL_SETTINGS_DB_PASSTHROUGH_FIELDS:
if key in _general_settings:
general_settings[key] = _general_settings[key]
_apply_ssrf_general_settings(_general_settings)
@ -15153,26 +15202,7 @@ async def get_config_list(
else:
db_general_settings_dict = {}
allowed_args = {
"max_parallel_requests": {"type": "Integer"},
"global_max_parallel_requests": {"type": "Integer"},
"max_request_size_mb": {"type": "Integer"},
"max_response_size_mb": {"type": "Integer"},
"proxy_config_reload_interval_seconds": {"type": "Integer"},
"pass_through_endpoints": {"type": "PydanticModel"},
"store_model_in_db": {"type": "Boolean"},
"store_prompts_in_spend_logs": {"type": "Boolean"},
"maximum_spend_logs_retention_period": {"type": "String"},
"mcp_internal_ip_ranges": {"type": "List"},
"mcp_trusted_proxy_ranges": {"type": "List"},
"mcp_xff_num_trusted_hops": {"type": "Integer"},
"always_include_stream_usage": {"type": "Boolean"},
"forward_client_headers_to_llm_api": {"type": "Boolean"},
"mcp_required_fields": {"type": "List"},
"cancel_on_disconnect": {"type": "Boolean"},
"skip_user_budget_on_team_key": {"type": "Boolean"},
"disable_auto_add_proxy_admin_to_teams": {"type": "Boolean"},
}
allowed_args = _GENERAL_SETTINGS_UI_EDITABLE_FIELDS
return_val = []
@ -15180,7 +15210,7 @@ async def get_config_list(
if field_name in allowed_args:
## HANDLE TYPED DICT
typed_dict_type = allowed_args[field_name]["type"]
typed_dict_type = allowed_args[field_name]
if typed_dict_type == "PydanticModel":
if field_name == "pass_through_endpoints":
@ -15222,7 +15252,7 @@ async def get_config_list(
_response_obj = ConfigList(
field_name=field_name,
field_type=allowed_args[field_name]["type"],
field_type=allowed_args[field_name],
field_description=field_info.description or "",
field_value=_redact_general_setting_value(
field_name,
@ -15250,7 +15280,7 @@ async def get_config_list(
_response_obj = ConfigList(
field_name=field_name,
field_type=allowed_args[field_name]["type"],
field_type=allowed_args[field_name],
field_description=field_info.description or "",
field_value=_redact_general_setting_value(field_name, _field_value, is_full_admin),
stored_in_db=_stored_in_db,

View file

@ -6739,6 +6739,35 @@ async def test_update_general_settings_disable_auto_add_proxy_admin_to_teams(db_
assert ps.general_settings["disable_auto_add_proxy_admin_to_teams"] is expected
@pytest.mark.asyncio
async def test_update_general_settings_propagates_ui_editable_fields():
"""
Every general setting the Admin UI can write must reach the live
general_settings dict on the next config poll. Regression for
skip_user_budget_on_team_key (issue #35076): the UI wrote it to the DB and
/config/list rendered it as enabled while auth kept enforcing the user
budget, because DB->runtime propagation had its own hardcoded allowlist.
"""
from litellm.proxy.proxy_server import (
_GENERAL_SETTINGS_DB_SIDE_EFFECT_FIELDS,
_GENERAL_SETTINGS_UI_EDITABLE_FIELDS,
ProxyConfig,
)
proxy_config = ProxyConfig()
db_general_settings = {
field: True for field in _GENERAL_SETTINGS_UI_EDITABLE_FIELDS if field not in _GENERAL_SETTINGS_DB_SIDE_EFFECT_FIELDS
}
with patch("litellm.proxy.proxy_server.general_settings", {}):
await proxy_config._update_general_settings(db_general_settings=db_general_settings)
import litellm.proxy.proxy_server as ps
assert ps.general_settings["skip_user_budget_on_team_key"] is True
assert {field: ps.general_settings.get(field) for field in db_general_settings} == db_general_settings
@pytest.mark.asyncio
async def test_update_general_settings_store_model_in_db_string_normalization():
"""