diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 91be0ba7c39..02695f7f1fb 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -17589,6 +17589,7 @@ async def _persist_general_settings_ui_litellm_field( field_name: str, value: object, user_api_key_dict: UserAPIKeyAuth ) -> dict: validated: Final = _validate_general_settings_ui_litellm_value(field_name, value) + proxy_config.reject_config_owned_writes(section_name="litellm_settings", changed_keys={field_name: validated}) config: Final = await proxy_config.get_config() before_value: Final = config.get("litellm_settings", {}).get(field_name) setattr(litellm, field_name, validated) @@ -17601,9 +17602,10 @@ async def _persist_general_settings_ui_litellm_field( async def _reset_general_settings_ui_litellm_field(field_name: str, user_api_key_dict: UserAPIKeyAuth) -> dict: + default_value: Final = _general_settings_ui_litellm_default(_GENERAL_SETTINGS_UI_LITELLM_FIELDS[field_name]) + proxy_config.reject_config_owned_writes(section_name="litellm_settings", changed_keys={field_name: default_value}) config: Final = await proxy_config.get_config() before_value: Final = config.get("litellm_settings", {}).get(field_name) - default_value: Final = _general_settings_ui_litellm_default(_GENERAL_SETTINGS_UI_LITELLM_FIELDS[field_name]) setattr(litellm, field_name, default_value) if "litellm_settings" in config: config["litellm_settings"].pop(field_name, None) @@ -17768,6 +17770,7 @@ async def get_config_list( stored_in_db_litellm = False else: stored_in_db_litellm = None + _litellm_source = proxy_config.litellm_settings.source(litellm_field_name) return_val.append( ConfigList( field_name=litellm_field_name, @@ -17779,6 +17782,8 @@ async def get_config_list( field_options=list(spec.get("options", ())) or None, field_tab=spec.get("tab"), nested_fields=None, + source=_litellm_source, + editable=_litellm_source != "config", ) ) @@ -17857,6 +17862,7 @@ async def delete_config_general_settings( }, ) await invalidate_config_param("general_settings") + proxy_config.settings.apply_db_row("general_settings", general_settings) asyncio.create_task( create_config_audit_log( "general_settings", "deleted", before_general_settings, general_settings, user_api_key_dict diff --git a/tests/test_litellm/proxy/test_proxy_server.py b/tests/test_litellm/proxy/test_proxy_server.py index c69e8104f09..18eb739180d 100644 --- a/tests/test_litellm/proxy/test_proxy_server.py +++ b/tests/test_litellm/proxy/test_proxy_server.py @@ -11542,6 +11542,83 @@ async def test_update_config_general_settings_emits_audit_log(monkeypatch): assert before["some_api_key"] != "sk-stored-secret" +@pytest.mark.asyncio +async def test_delete_config_general_settings_is_visible_to_the_next_read(monkeypatch): + import litellm.proxy.proxy_server as proxy_server_module + from litellm.proxy._types import ConfigFieldDelete + from litellm.proxy.config_resolvers import SettingsStore + from litellm.proxy.proxy_server import delete_config_general_settings, get_config_general_settings + + fake = _fake_prisma_with_config({"max_request_size_mb": 42}) + monkeypatch.setattr(proxy_server_module, "prisma_client", fake) + + settings = SettingsStore("general_settings") + settings.load_yaml({}) + settings.apply_db_row("general_settings", {"max_request_size_mb": 42}) + monkeypatch.setattr(proxy_server_module.proxy_config, "settings", settings) + + admin = UserAPIKeyAuth(api_key="hashed-admin", user_id="admin-1", user_role=LitellmUserRoles.PROXY_ADMIN) + await delete_config_general_settings( + data=ConfigFieldDelete(field_name="max_request_size_mb", config_type="general_settings"), + user_api_key_dict=admin, + ) + + with pytest.raises(HTTPException) as excinfo: + await get_config_general_settings(field_name="max_request_size_mb", user_api_key_dict=admin) + assert excinfo.value.status_code == 400 + assert "is not set" in excinfo.value.detail["error"] + + +@pytest.mark.asyncio +async def test_ui_litellm_field_write_refuses_a_key_the_config_file_declares(monkeypatch): + import litellm.proxy.proxy_server as proxy_server_module + from litellm.proxy._types import ConfigFieldUpdate + from litellm.proxy.proxy_server import ProxyConfig, update_config_general_settings + + pc = ProxyConfig() + pc._load_yaml_settings_stores({"litellm_settings": {"enable_anthropic_prompt_caching": True}}) + monkeypatch.setattr(proxy_server_module, "proxy_config", pc) + monkeypatch.setattr(litellm, "enable_anthropic_prompt_caching", True) + + fake = _fake_prisma_with_config({}) + monkeypatch.setattr(proxy_server_module, "prisma_client", fake) + + admin = UserAPIKeyAuth(api_key="hashed-admin", user_id="admin-1", user_role=LitellmUserRoles.PROXY_ADMIN) + with pytest.raises(HTTPException) as excinfo: + await update_config_general_settings( + data=ConfigFieldUpdate( + field_name="enable_anthropic_prompt_caching", field_value=False, config_type="general_settings" + ), + user_api_key_dict=admin, + ) + + assert excinfo.value.status_code == 400 + assert excinfo.value.detail["keys"] == ["enable_anthropic_prompt_caching"] + assert litellm.enable_anthropic_prompt_caching is True + fake.db.litellm_config.upsert.assert_not_awaited() + + +@pytest.mark.asyncio +async def test_ui_litellm_field_reset_refuses_a_key_the_config_file_declares(monkeypatch): + import litellm.proxy.proxy_server as proxy_server_module + from litellm.proxy.proxy_server import ProxyConfig, _reset_general_settings_ui_litellm_field + + pc = ProxyConfig() + pc._load_yaml_settings_stores({"litellm_settings": {"enable_anthropic_prompt_caching": True}}) + monkeypatch.setattr(proxy_server_module, "proxy_config", pc) + monkeypatch.setattr(litellm, "enable_anthropic_prompt_caching", True) + + fake = _fake_prisma_with_config({}) + monkeypatch.setattr(proxy_server_module, "prisma_client", fake) + + admin = UserAPIKeyAuth(api_key="hashed-admin", user_id="admin-1", user_role=LitellmUserRoles.PROXY_ADMIN) + with pytest.raises(HTTPException) as excinfo: + await _reset_general_settings_ui_litellm_field("enable_anthropic_prompt_caching", admin) + + assert excinfo.value.status_code == 400 + assert litellm.enable_anthropic_prompt_caching is True + + @pytest.mark.asyncio async def test_update_config_general_settings_refuses_a_key_the_config_file_declares(monkeypatch): import litellm.proxy.proxy_server as proxy_server_module