fix(proxy): close the last three surfaces that bypassed config ownership

The two dashboard toggles under litellm_settings wrote through save_config,
so the refusal applied, but they mutated the litellm module global first: a
refused write still took effect in the running process until the next reload.
Both now check before they mutate.

/config/field/delete drops the stored key without touching the store, so a
deleted key kept reading back from the process. It now refreshes the store
like the other write paths.

/config/list reported source and editable for the general_settings rows but
not for the litellm_settings ones, so the dashboard would have shown a
config-declared toggle as editable.
This commit is contained in:
Yuneng Jiang 2026-09-18 01:53:54 -07:00
parent 8e67a33fc3
commit 23ee8ad1c3
No known key found for this signature in database
2 changed files with 84 additions and 1 deletions

View file

@ -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

View file

@ -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