diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 52a19eb6aa0..105287b28e2 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -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, diff --git a/tests/unit/proxy/proxy_server/test_proxy_config.py b/tests/unit/proxy/proxy_server/test_proxy_config.py index 36afbc5ad78..629d39efd3f 100644 --- a/tests/unit/proxy/proxy_server/test_proxy_config.py +++ b/tests/unit/proxy/proxy_server/test_proxy_config.py @@ -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." ] diff --git a/tests/unit/proxy/test_proxy_server_endpoints_and_startup.py b/tests/unit/proxy/test_proxy_server_endpoints_and_startup.py index 11c9bca6ca0..812fb020e21 100644 --- a/tests/unit/proxy/test_proxy_server_endpoints_and_startup.py +++ b/tests/unit/proxy/test_proxy_server_endpoints_and_startup.py @@ -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