fix(proxy): let /config/field/delete remove ignored stored general_settings keys

This commit is contained in:
mateo-berri 2026-10-28 21:24:40 -07:00
parent 02367d7449
commit 8fd9c71c46
3 changed files with 67 additions and 15 deletions

View file

@ -8501,7 +8501,7 @@ class ProxyConfig:
for key in sorted(unmanaged - self._warned_unmanaged_keys):
verbose_proxy_logger.warning(
"general_settings.%s is stored in the database, but no settings API manages it, so the stored value "
"is ignored. Set it in the config file to use it.",
"is ignored. Set it in the config file to use it, or remove it with POST /config/field/delete.",
key,
)
self._warned_unmanaged_keys = unmanaged
@ -20646,7 +20646,16 @@ async def delete_config_general_settings(
if data.field_name in _GENERAL_SETTINGS_UI_LITELLM_FIELDS:
return await _reset_general_settings_ui_litellm_field(data.field_name, user_api_key_dict)
if data.field_name not in ConfigGeneralSettings.model_fields:
db_general_settings: Final[_ConfigParamRow | None] = await _config_param_table(prisma_client).find_first(
where={"param_name": "general_settings"}
)
stored_keys: Final[frozenset[str]] = (
frozenset(dict(db_general_settings.param_value))
if db_general_settings is not None and db_general_settings.param_value is not None
else frozenset()
)
if data.field_name not in ConfigGeneralSettings.model_fields and data.field_name not in stored_keys:
raise HTTPException(
status_code=400,
detail={"error": f"Invalid field={data.field_name} passed in."},
@ -20654,12 +20663,6 @@ async def delete_config_general_settings(
proxy_config.reject_config_owned_deletes(section_name="general_settings", keys=(data.field_name,))
## get general settings from db
db_general_settings: Final[_ConfigParamRow | None] = await _config_param_table(prisma_client).find_first(
where={"param_name": "general_settings"}
)
### pop the value
if db_general_settings is None or db_general_settings.param_value is None:
raise HTTPException(
status_code=400,

View file

@ -4355,14 +4355,13 @@ async def test_ProxyConfig__update_general_settings_updates_health_check_retenti
reschedule.assert_awaited_once()
@pytest.mark.asyncio
async def test_ProxyConfig__update_general_settings_ignores_a_stored_key_no_settings_api_manages_and_warns_once(
monkeypatch, caplog
):
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
) -> None:
monkeypatch.setattr("litellm.proxy.proxy_server.general_settings", {})
pc = ProxyConfig()
stored_row = {"enforce_user_param": True, "store_prompts_in_spend_logs": True}
pc: Final = ProxyConfig()
stored_row: Final = {"enforce_user_param": True, "store_prompts_in_spend_logs": True}
with caplog.at_level(logging.WARNING, logger="LiteLLM Proxy"):
await pc._update_general_settings(stored_row)
@ -4371,10 +4370,10 @@ async def test_ProxyConfig__update_general_settings_ignores_a_stored_key_no_sett
assert proxy_server.general_settings["store_prompts_in_spend_logs"] is True
assert "enforce_user_param" not in proxy_server.general_settings
warnings = [record.getMessage() for record in caplog.records if "enforce_user_param" in record.getMessage()]
warnings: Final = [record.getMessage() for record in caplog.records if "enforce_user_param" in record.getMessage()]
assert warnings == [
"general_settings.enforce_user_param is stored in the database, but no settings API manages it, so the "
"stored value is ignored. Set it in the config file to use it."
"stored value is ignored. Set it in the config file to use it, or remove it with POST /config/field/delete."
]

View file

@ -15417,6 +15417,56 @@ async def test_delete_config_general_settings_still_removes_a_key_the_database_o
assert "max_request_size_mb" not in pc.settings
@pytest.mark.asyncio
async def test_delete_config_general_settings_removes_a_stored_key_no_settings_api_manages(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from litellm.proxy._types import ConfigFieldDelete
from litellm.proxy.proxy_server import ProxyConfig, delete_config_general_settings
pc: Final = ProxyConfig()
pc._load_yaml_settings_stores({"general_settings": {}})
stored_row: Final = {"enforce_user_param": True, "store_prompts_in_spend_logs": True}
pc.settings.apply_db_row("general_settings", stored_row)
fake_prisma: Final = _fake_prisma_with_config(stored_row)
monkeypatch.setattr(proxy_server_module, "proxy_config", pc)
monkeypatch.setattr(proxy_server_module, "prisma_client", fake_prisma)
admin: Final = UserAPIKeyAuth(api_key="hashed-admin", user_id="admin-1", user_role=LitellmUserRoles.PROXY_ADMIN)
await delete_config_general_settings(
data=ConfigFieldDelete(field_name="enforce_user_param", config_type="general_settings"),
user_api_key_dict=admin,
)
written: Final = json.loads(fake_prisma.db.litellm_config.upsert.await_args.kwargs["data"]["update"]["param_value"])
assert written == {"store_prompts_in_spend_logs": True}
assert pc.settings.unmanaged_db_keys() == ()
@pytest.mark.asyncio
async def test_delete_config_general_settings_refuses_a_key_neither_managed_nor_stored(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from litellm.proxy._types import ConfigFieldDelete
from litellm.proxy.proxy_server import ProxyConfig, delete_config_general_settings
pc: Final = ProxyConfig()
pc._load_yaml_settings_stores({"general_settings": {}})
fake_prisma: Final = _fake_prisma_with_config({"store_prompts_in_spend_logs": True})
monkeypatch.setattr(proxy_server_module, "proxy_config", pc)
monkeypatch.setattr(proxy_server_module, "prisma_client", fake_prisma)
admin: Final = UserAPIKeyAuth(api_key="hashed-admin", user_id="admin-1", user_role=LitellmUserRoles.PROXY_ADMIN)
with pytest.raises(HTTPException) as refused:
await delete_config_general_settings(
data=ConfigFieldDelete(field_name="enforce_user_param", config_type="general_settings"),
user_api_key_dict=admin,
)
assert refused.value.status_code == 400
fake_prisma.db.litellm_config.upsert.assert_not_awaited()
@pytest.mark.asyncio
async def test_config_field_info_reports_the_declared_value_of_a_config_owned_secret(monkeypatch):
from litellm.proxy.proxy_server import ProxyConfig, get_config_general_settings