From 415032118ffc96adaa6966fbb92a87ad207ad53c Mon Sep 17 00:00:00 2001 From: mateo Date: Mon, 21 Sep 2026 23:11:57 +0000 Subject: [PATCH] refactor(proxy): read and write config sections through ProxyRuntimeConfig attributes Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../_experimental/mcp_server/tool_registry.py | 6 +- litellm/proxy/agent_endpoints/endpoints.py | 25 ++-- .../proxy/guardrails/guardrail_endpoints.py | 8 +- .../coordination_redis_endpoints.py | 2 +- .../cost_tracking_settings.py | 48 ++++---- .../fallback_management_endpoints.py | 14 ++- .../mcp_management_endpoints.py | 13 +- .../model_management_endpoints.py | 26 ++-- .../router_settings_endpoints.py | 2 +- .../management_endpoints/scim/scim_v2.py | 4 +- litellm/proxy/proxy_cli.py | 11 +- litellm/proxy/read_model_list.py | 2 +- .../proxy_setting_endpoints.py | 112 ++++++++---------- 13 files changed, 130 insertions(+), 143 deletions(-) diff --git a/litellm/proxy/_experimental/mcp_server/tool_registry.py b/litellm/proxy/_experimental/mcp_server/tool_registry.py index e9e28c8a782..57f3a57417a 100644 --- a/litellm/proxy/_experimental/mcp_server/tool_registry.py +++ b/litellm/proxy/_experimental/mcp_server/tool_registry.py @@ -1,5 +1,5 @@ import json -from collections.abc import Callable +from collections.abc import Callable, Iterable, Mapping from typing import TYPE_CHECKING, Any, Final from litellm._logging import verbose_logger @@ -86,7 +86,7 @@ class MCPToolRegistry: def load_tools_from_config( self, - mcp_tools_config: dict[str, Any] | None = None, + mcp_tools_config: Iterable[Mapping[str, Any]] | None = None, config_file_path: str | None = None, ) -> None: """ @@ -105,7 +105,7 @@ class MCPToolRegistry: for tool_config in mcp_tools_config: if not isinstance(tool_config, dict): - raise ValueError("mcp_tools_config must be a list of dictionaries") + raise ValueError("mcp_tools_config must be a list of dictionaries") # pyright: ignore[reportUnnecessaryIsInstance] # entries arrive as raw YAML dicts at runtime name = tool_config.get("name") description = tool_config.get("description") diff --git a/litellm/proxy/agent_endpoints/endpoints.py b/litellm/proxy/agent_endpoints/endpoints.py index aa8979a73c6..31cdf882c52 100644 --- a/litellm/proxy/agent_endpoints/endpoints.py +++ b/litellm/proxy/agent_endpoints/endpoints.py @@ -950,12 +950,12 @@ async def make_agent_public( ) updated_public_agent_groups: Final = [*current_public_agent_groups, agent.agent_id] - if "litellm_settings" not in config or config["litellm_settings"] is None: - config["litellm_settings"] = {} - - config["litellm_settings"]["public_agent_groups"] = updated_public_agent_groups - - await proxy_config.save_config(new_config=config) + await proxy_config.save_config( + new_config=config.with_section( + "litellm_settings", + {**config.litellm_settings, "public_agent_groups": updated_public_agent_groups}, # mutable-ok: replacement section for save_config + ) + ) litellm.public_agent_groups = updated_public_agent_groups @@ -1054,14 +1054,13 @@ async def make_agents_public( litellm.public_agent_groups = request.agent_ids - # Update config with new settings - if "litellm_settings" not in config or config["litellm_settings"] is None: - config["litellm_settings"] = {} - - config["litellm_settings"]["public_agent_groups"] = litellm.public_agent_groups - # Save the updated config - await proxy_config.save_config(new_config=config) + await proxy_config.save_config( + new_config=config.with_section( + "litellm_settings", + {**config.litellm_settings, "public_agent_groups": litellm.public_agent_groups}, # mutable-ok: replacement section for save_config + ) + ) verbose_proxy_logger.debug( "Updated public agent groups to: %s by user: %s", litellm.public_agent_groups, user_api_key_dict.user_id diff --git a/litellm/proxy/guardrails/guardrail_endpoints.py b/litellm/proxy/guardrails/guardrail_endpoints.py index 6874d7aa73e..17d7b40c2fd 100644 --- a/litellm/proxy/guardrails/guardrail_endpoints.py +++ b/litellm/proxy/guardrails/guardrail_endpoints.py @@ -167,11 +167,11 @@ async def list_guardrails(): """ from litellm.proxy.proxy_server import proxy_config - config: Final = proxy_config.config + _guardrails_config: Final = [ # mutable-ok: dict copies for the response layer + dict(guardrail) for guardrail in proxy_config.config.guardrails + ] - _guardrails_config: Final = cast(list[dict] | None, config.get("guardrails")) - - if _guardrails_config is None: + if not _guardrails_config: return _get_guardrails_list_response([]) return _get_guardrails_list_response(_guardrails_config) diff --git a/litellm/proxy/management_endpoints/coordination_redis_endpoints.py b/litellm/proxy/management_endpoints/coordination_redis_endpoints.py index c59ee92f073..021849446e3 100644 --- a/litellm/proxy/management_endpoints/coordination_redis_endpoints.py +++ b/litellm/proxy/management_endpoints/coordination_redis_endpoints.py @@ -196,7 +196,7 @@ async def _current_coordination_redis_settings() -> dict[str, object] | None: if persisted is not None: return persisted - config_state: Final = _SETTINGS_ADAPTER.validate_python(proxy_config.get_config_state()) + config_state: Final = _SETTINGS_ADAPTER.validate_python(proxy_config.get_config_state().to_mapping()) general_settings: Final = config_state.get(_GENERAL_SETTINGS_PARAM_NAME) if not isinstance(general_settings, Mapping): return None diff --git a/litellm/proxy/management_endpoints/cost_tracking_settings.py b/litellm/proxy/management_endpoints/cost_tracking_settings.py index cb376f286ec..ee3762bc5d3 100644 --- a/litellm/proxy/management_endpoints/cost_tracking_settings.py +++ b/litellm/proxy/management_endpoints/cost_tracking_settings.py @@ -215,7 +215,7 @@ async def get_cost_discount_config( config: Final = await proxy_config.get_config() # Get cost_discount_config from litellm_settings - litellm_settings: Final = config.get("litellm_settings", {}) + litellm_settings: Final = config.litellm_settings cost_discount_config: Final = litellm_settings.get("cost_discount_config", {}) return {"values": cost_discount_config} @@ -294,15 +294,13 @@ async def update_cost_discount_config( # Load existing config config: Final = await proxy_config.get_config() - # Ensure litellm_settings exists - if "litellm_settings" not in config: - config["litellm_settings"] = {} - - # Update cost_discount_config - config["litellm_settings"]["cost_discount_config"] = cost_discount_config - - # Save the updated config to DB - await proxy_config.save_config(new_config=config) + # Update cost_discount_config and save + await proxy_config.save_config( + new_config=config.with_section( + "litellm_settings", + {**config.litellm_settings, "cost_discount_config": cost_discount_config}, # mutable-ok: replacement section for save_config + ) + ) # Update in-memory litellm.cost_discount_config litellm.cost_discount_config = cost_discount_config @@ -348,7 +346,7 @@ async def get_cost_margin_config( config: Final = await proxy_config.get_config() # Get cost_margin_config from litellm_settings - litellm_settings: Final = config.get("litellm_settings", {}) + litellm_settings: Final = config.litellm_settings cost_margin_config: Final = litellm_settings.get("cost_margin_config", {}) return {"values": cost_margin_config} @@ -468,15 +466,13 @@ async def update_cost_margin_config( # Load existing config config: Final = await proxy_config.get_config() - # Ensure litellm_settings exists - if "litellm_settings" not in config: - config["litellm_settings"] = {} - - # Update cost_margin_config - config["litellm_settings"]["cost_margin_config"] = cost_margin_config - - # Save the updated config to DB - await proxy_config.save_config(new_config=config) + # Update cost_margin_config and save + await proxy_config.save_config( + new_config=config.with_section( + "litellm_settings", + {**config.litellm_settings, "cost_margin_config": cost_margin_config}, # mutable-ok: replacement section for save_config + ) + ) # Update in-memory litellm.cost_margin_config litellm.cost_margin_config = cost_margin_config @@ -546,11 +542,13 @@ async def update_block_requests_for_models_without_pricing( ) try: - config = await proxy_config.get_config() - if "litellm_settings" not in config: - config["litellm_settings"] = {} # mutable-ok: config is a plain-dict payload for save_config - config["litellm_settings"]["block_requests_for_models_without_pricing"] = request.enabled - await proxy_config.save_config(new_config=config) + config: Final = await proxy_config.get_config() + await proxy_config.save_config( + new_config=config.with_section( + "litellm_settings", + {**config.litellm_settings, "block_requests_for_models_without_pricing": request.enabled}, # mutable-ok: replacement section for save_config + ) + ) litellm.block_requests_for_models_without_pricing = request.enabled verbose_proxy_logger.info("Updated block_requests_for_models_without_pricing: %s", request.enabled) diff --git a/litellm/proxy/management_endpoints/fallback_management_endpoints.py b/litellm/proxy/management_endpoints/fallback_management_endpoints.py index 543538f5edd..ea42d58893b 100644 --- a/litellm/proxy/management_endpoints/fallback_management_endpoints.py +++ b/litellm/proxy/management_endpoints/fallback_management_endpoints.py @@ -11,7 +11,7 @@ DELETE /fallback/{model} - Delete fallbacks for a specific model # pyright: reportMissingImports=false import json -from typing import TYPE_CHECKING, Final, Literal +from typing import TYPE_CHECKING, Final, Literal, cast from litellm._logging import verbose_proxy_logger from litellm.proxy._types import UserAPIKeyAuth @@ -124,7 +124,7 @@ async def create_fallback( # Load existing config config: Final = await proxy_config.get_config() - router_settings: Final = config.get("router_settings", {}) + router_settings: Final = dict(config.router_settings) # mutable-ok: fallback list is rewritten below before the DB upsert # Get the appropriate fallback list based on type fallback_key = "fallbacks" @@ -134,7 +134,9 @@ async def create_fallback( fallback_key = "content_policy_fallbacks" # Get existing fallbacks - existing_fallbacks: Final[list[dict[str, list[str]]]] = router_settings.get(fallback_key, []) + existing_fallbacks: Final[list[dict[str, list[str]]]] = list( # mutable-ok: copy so edits stay off the frozen config + cast("list[dict[str, list[str]]]", router_settings.get(fallback_key)) # cast-ok: entries are model -> fallback models maps + ) # Update or add the fallback configuration fallback_updated = False @@ -293,7 +295,7 @@ async def delete_fallback( # Load existing config config: Final = await proxy_config.get_config() - router_settings: Final = config.get("router_settings", {}) + router_settings: Final = dict(config.router_settings) # mutable-ok: fallback list is rewritten below before the DB upsert # Get the appropriate fallback list based on type fallback_key = "fallbacks" @@ -303,7 +305,9 @@ async def delete_fallback( fallback_key = "content_policy_fallbacks" # Get existing fallbacks - existing_fallbacks: Final[list[dict[str, list[str]]]] = router_settings.get(fallback_key, []) + existing_fallbacks: Final[list[dict[str, list[str]]]] = list( # mutable-ok: copy so edits stay off the frozen config + cast("list[dict[str, list[str]]]", router_settings.get(fallback_key)) # cast-ok: entries are model -> fallback models maps + ) # Find and remove the fallback configuration fallback_found = False diff --git a/litellm/proxy/management_endpoints/mcp_management_endpoints.py b/litellm/proxy/management_endpoints/mcp_management_endpoints.py index c1388e8bb81..26e7a2cfc43 100644 --- a/litellm/proxy/management_endpoints/mcp_management_endpoints.py +++ b/litellm/proxy/management_endpoints/mcp_management_endpoints.py @@ -3012,14 +3012,13 @@ if MCP_AVAILABLE: litellm.public_mcp_servers = request.mcp_server_ids - # Update config with new settings - if "litellm_settings" not in config or config["litellm_settings"] is None: - config["litellm_settings"] = {} - - config["litellm_settings"]["public_mcp_servers"] = litellm.public_mcp_servers - # Save the updated config - await proxy_config.save_config(new_config=config) + await proxy_config.save_config( + new_config=config.with_section( + "litellm_settings", + {**config.litellm_settings, "public_mcp_servers": litellm.public_mcp_servers}, # mutable-ok: replacement section for save_config + ) + ) verbose_proxy_logger.debug( "Updated public mcp servers to: %s by user: %s", litellm.public_mcp_servers, user_api_key_dict.user_id diff --git a/litellm/proxy/management_endpoints/model_management_endpoints.py b/litellm/proxy/management_endpoints/model_management_endpoints.py index 10a0a2f3104..a0326cd9cae 100644 --- a/litellm/proxy/management_endpoints/model_management_endpoints.py +++ b/litellm/proxy/management_endpoints/model_management_endpoints.py @@ -2777,14 +2777,13 @@ async def update_public_model_groups( # from DB values via _update_config_from_db), so set the in-memory value AFTER config: Final = await proxy_config.get_config() - # Update config with new settings - if "litellm_settings" not in config: - config["litellm_settings"] = {} - - config["litellm_settings"]["public_model_groups"] = request.model_groups - # Save the updated config - await proxy_config.save_config(new_config=config) + await proxy_config.save_config( + new_config=config.with_section( + "litellm_settings", + {**config.litellm_settings, "public_model_groups": request.model_groups}, # mutable-ok: replacement section for save_config + ) + ) # Set in-memory value AFTER get_config() and save_config() to avoid # get_config() overwriting with stale DB value @@ -2844,14 +2843,13 @@ async def update_useful_links( # from DB values via _update_config_from_db), so set the in-memory value AFTER config: Final = await proxy_config.get_config() - # Update config with new settings - if "litellm_settings" not in config: - config["litellm_settings"] = {} - - config["litellm_settings"]["public_model_groups_links"] = request.useful_links - # Save the updated config - await proxy_config.save_config(new_config=config) + await proxy_config.save_config( + new_config=config.with_section( + "litellm_settings", + {**config.litellm_settings, "public_model_groups_links": request.useful_links}, # mutable-ok: replacement section for save_config + ) + ) # Set in-memory value AFTER get_config() and save_config() to avoid # get_config() overwriting with stale DB value diff --git a/litellm/proxy/management_endpoints/router_settings_endpoints.py b/litellm/proxy/management_endpoints/router_settings_endpoints.py index fc000b1638b..fa5eca7ef32 100644 --- a/litellm/proxy/management_endpoints/router_settings_endpoints.py +++ b/litellm/proxy/management_endpoints/router_settings_endpoints.py @@ -92,7 +92,7 @@ async def get_router_settings( # Try to get router settings from config config: Final = await proxy_config.get_config() - router_settings_from_config: Final = config.get("router_settings", {}) + router_settings_from_config: Final = config.router_settings current_values: Final[dict[str, Any]] = {} if llm_router is not None: diff --git a/litellm/proxy/management_endpoints/scim/scim_v2.py b/litellm/proxy/management_endpoints/scim/scim_v2.py index 3292a0141d1..de4480b146a 100644 --- a/litellm/proxy/management_endpoints/scim/scim_v2.py +++ b/litellm/proxy/management_endpoints/scim/scim_v2.py @@ -373,7 +373,7 @@ async def _get_scim_upsert_user_setting() -> bool: from litellm.proxy.proxy_server import proxy_config config: Final = await proxy_config.get_config() - litellm_settings: Final = config.get("litellm_settings", {}) or {} + litellm_settings: Final = config.litellm_settings scim_upsert_user: Final = litellm_settings.get("scim_upsert_user", True) # Default to True if not set (backward compatibility) @@ -412,7 +412,7 @@ async def _get_scim_admin_group() -> str | None: from litellm.proxy.proxy_server import proxy_config config: Final = await proxy_config.get_config() - litellm_settings: Final = config.get("litellm_settings", {}) or {} + litellm_settings: Final = config.litellm_settings return litellm_settings.get("scim_admin_group") or None except Exception as e: verbose_proxy_logger.warning("Error reading scim_admin_group setting, defaulting to None: %s", e) diff --git a/litellm/proxy/proxy_cli.py b/litellm/proxy/proxy_cli.py index 78885461724..8dbcc773197 100644 --- a/litellm/proxy/proxy_cli.py +++ b/litellm/proxy/proxy_cli.py @@ -1195,10 +1195,9 @@ def run_server( _config: Final = asyncio.run(proxy_config.get_config(config_file_path=config)) ### LITELLM SETTINGS ### - litellm_settings = _config.get("litellm_settings", None) + litellm_settings = _config.litellm_settings if ( - litellm_settings is not None - and "json_logs" in litellm_settings + "json_logs" in litellm_settings and litellm_settings["json_logs"] is True ): import litellm @@ -1207,12 +1206,10 @@ def run_server( litellm._turn_on_json() ### GENERAL SETTINGS ### - general_settings = _config.get("general_settings", {}) - if general_settings is None: - general_settings = {} + general_settings = _config.general_settings ### LOAD KEY MANAGEMENT SETTINGS FIRST (needed for custom secret manager) ### key_management_settings: Final = general_settings.get("key_management_settings", None) - if key_management_settings is not None: + if isinstance(key_management_settings, Mapping): import litellm litellm._key_management_settings = KeyManagementSettings(**key_management_settings) diff --git a/litellm/proxy/read_model_list.py b/litellm/proxy/read_model_list.py index a1830e7f2bc..8c71ea689f0 100644 --- a/litellm/proxy/read_model_list.py +++ b/litellm/proxy/read_model_list.py @@ -26,4 +26,4 @@ def read_model_list(config_path: str) -> list[dict[str, Any]]: from litellm.proxy.proxy_server import ProxyConfig config: Final = asyncio.run(ProxyConfig().get_config(config_file_path=config_path)) - return config.get("model_list") or [] + return [dict(model) for model in config.model_list] # mutable-ok: callers consume plain dicts diff --git a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py index 7bdadeadf86..b94e079e642 100644 --- a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py +++ b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py @@ -517,7 +517,7 @@ class WebSearchInterceptionSettingsResponse(SettingsResponse): ) -def _with_websearch_enabled_resolved(config: Mapping[str, object]) -> dict[str, object]: +def _with_websearch_enabled_resolved(config: ProxyRuntimeConfig) -> ProxyRuntimeConfig: """ Answer with the stored flag when there is one, and only otherwise with what this process is running. @@ -535,19 +535,19 @@ def _with_websearch_enabled_resolved(config: Mapping[str, object]) -> dict[str, WebSearchInterceptionLogger, ) - litellm_settings: Final[Mapping[str, object]] = _as_settings_section(config.get("litellm_settings")) + litellm_settings: Final[Mapping[str, object]] = config.litellm_settings stored: Final[Mapping[str, object]] = _as_settings_section(litellm_settings.get("websearch_interception_params")) if "enabled" in stored: - return dict(config) + return config - resolved: Final = { + resolved: Final = { # mutable-ok: replacement value for the stored section **stored, "enabled": bool(litellm.logging_callback_manager.get_custom_loggers_for_type(WebSearchInterceptionLogger)), } - return { - **config, - "litellm_settings": {**litellm_settings, "websearch_interception_params": resolved}, - } + return config.with_section( + "litellm_settings", + {**litellm_settings, "websearch_interception_params": resolved}, # mutable-ok: replacement section for save_config + ) def _as_settings_section(value: object) -> Mapping[str, object]: @@ -616,24 +616,24 @@ async def add_allowed_ip( # Load existing config config: Final = await proxy_config.get_config() verbose_proxy_logger.debug("Loaded config: %s", config) - if "general_settings" not in config: - config["general_settings"] = {} - if "allowed_ips" not in config["general_settings"]: - config["general_settings"]["allowed_ips"] = [] + before_allowed_ips: Final = tuple(config.general_settings.get("allowed_ips") or ()) + updated_allowed_ips: Final = ( + (*before_allowed_ips, ip_address.ip) if ip_address.ip not in before_allowed_ips else before_allowed_ips + ) + updated: Final = config.with_section( + "general_settings", + {**config.general_settings, "allowed_ips": list(updated_allowed_ips)}, # mutable-ok: replacement section for save_config + ) - before_allowed_ips: Final = list(config["general_settings"]["allowed_ips"]) - if ip_address.ip not in config["general_settings"]["allowed_ips"]: - config["general_settings"]["allowed_ips"].append(ip_address.ip) - - await proxy_config.save_config(new_config=config) + await proxy_config.save_config(new_config=updated) asyncio.create_task( create_config_audit_log( param_name="general_settings", action="updated", - before_value={"allowed_ips": before_allowed_ips}, - after_value={"allowed_ips": config["general_settings"]["allowed_ips"]}, + before_value={"allowed_ips": list(before_allowed_ips)}, # mutable-ok: audit log wants a JSON list + after_value={"allowed_ips": list(updated_allowed_ips)}, # mutable-ok: audit log wants a JSON list user_api_key_dict=user_api_key_dict, ) ) @@ -667,24 +667,22 @@ async def delete_allowed_ip( # Load existing config config: Final = await proxy_config.get_config() verbose_proxy_logger.debug("Loaded config: %s", config) - if "general_settings" not in config: - config["general_settings"] = {} - if "allowed_ips" not in config["general_settings"]: - config["general_settings"]["allowed_ips"] = [] + before_allowed_ips: Final = tuple(config.general_settings.get("allowed_ips") or ()) + updated_allowed_ips: Final = tuple(ip for ip in before_allowed_ips if ip != ip_address.ip) + updated: Final = config.with_section( + "general_settings", + {**config.general_settings, "allowed_ips": list(updated_allowed_ips)}, # mutable-ok: replacement section for save_config + ) - before_allowed_ips: Final = list(config["general_settings"]["allowed_ips"]) - if ip_address.ip in config["general_settings"]["allowed_ips"]: - config["general_settings"]["allowed_ips"].remove(ip_address.ip) - - await proxy_config.save_config(new_config=config) + await proxy_config.save_config(new_config=updated) asyncio.create_task( create_config_audit_log( param_name="general_settings", action="deleted", - before_value={"allowed_ips": before_allowed_ips}, - after_value={"allowed_ips": config["general_settings"]["allowed_ips"]}, + before_value={"allowed_ips": list(before_allowed_ips)}, # mutable-ok: audit log wants a JSON list + after_value={"allowed_ips": list(updated_allowed_ips)}, # mutable-ok: audit log wants a JSON list user_api_key_dict=user_api_key_dict, ) ) @@ -751,7 +749,7 @@ def _root_schema(settings_class: type[BaseModel]) -> _RootSchema: async def _get_settings_with_schema( settings_key: str, settings_class: type[BaseModel], - config: dict, + config: ProxyRuntimeConfig | Mapping[str, object], ) -> dict: """ Common utility function to get settings with schema information. @@ -759,10 +757,15 @@ async def _get_settings_with_schema( Args: settings_key: The key in litellm_settings to get settings_class: The Pydantic class to use for schema - config: The config dictionary + config: The resolved proxy config, or a config-shaped mapping """ - litellm_settings: Final = config.get("litellm_settings", {}) or {} - settings_data: Final = litellm_settings.get(settings_key, {}) or {} + litellm_settings: Final[Mapping[str, object]] = ( + config.litellm_settings + if isinstance(config, ProxyRuntimeConfig) + else _as_settings_section(config.get("litellm_settings")) + ) + raw_settings_data: Final = litellm_settings.get(settings_key, {}) or {} + settings_data: Final = raw_settings_data if isinstance(raw_settings_data, Mapping) else MappingProxyType({}) # Create the settings object settings: Final = settings_class(**(settings_data)) @@ -990,19 +993,18 @@ async def _update_litellm_setting( # because get_config() may overwrite litellm. with stale DB values # via LITELLM_SETTINGS_SAFE_DB_OVERRIDES. config: Final = await proxy_config.get_config() - before_value: Final = config.get("litellm_settings", {}).get(settings_key) + before_value: Final = config.litellm_settings.get(settings_key) # Update the in-memory settings (after get_config to avoid stale override) setattr(litellm, settings_key, in_memory_var) - # Update config with new settings - if "litellm_settings" not in config: - config["litellm_settings"] = {} - - config["litellm_settings"][settings_key] = in_memory_var - # Save the updated config - await proxy_config.save_config(new_config=config) + await proxy_config.save_config( + new_config=config.with_section( + "litellm_settings", + {**config.litellm_settings, settings_key: in_memory_var}, # mutable-ok: replacement section for save_config + ) + ) # Fire-and-forget so an audit-log failure (transient DB blip, etc.) # never surfaces as a 500 after save_config has already committed, @@ -1185,18 +1187,7 @@ async def update_sso_settings( if isinstance(stored, dict): before_sso_data = proxy_config._decrypt_db_variables(stored) - # Load existing config - config: Final = await proxy_config.get_config() - - # Update config with new environment variables - if "environment_variables" not in config: - config["environment_variables"] = {} - - # Update general_settings for user_email (admin email) - if "general_settings" not in config: - config["general_settings"] = {} - - # Update environment variables in config and in memory + # Update environment variables in memory sso_data: Final = sso_config.model_dump() for field_name, value in sso_data.items(): if field_name in SSO_FIELD_ENV_VARS: @@ -1363,15 +1354,16 @@ async def update_ui_theme_settings( # Load existing config config: Final = await proxy_config.get_config() - before_theme: Final = config.get("litellm_settings", {}).get("ui_theme_config") + before_theme: Final = config.litellm_settings.get("ui_theme_config") # Convert theme config to dict theme_data: Final = theme_config.model_dump(exclude_none=True) # Store UI theme config in litellm_settings (where it's retrieved from) - if "litellm_settings" not in config: - config["litellm_settings"] = {} - config["litellm_settings"]["ui_theme_config"] = theme_data + updated: Final = config.with_section( + "litellm_settings", + {**config.litellm_settings, "ui_theme_config": theme_data}, # mutable-ok: replacement section for save_config + ) # The vars below are the only environment variables this endpoint owns, and # they must stay in step with _UI_THEME_FIELD_ENV_VARS. A non-empty value @@ -1395,7 +1387,7 @@ async def update_ui_theme_settings( # Persist the theme config (litellm_settings). save_config defaults to # include_env_vars=False, so it does not snapshot environment_variables. - await proxy_config.save_config(new_config=config) + await proxy_config.save_config(new_config=updated) # Persist only the two owned env vars, merged against the existing DB row. await proxy_config.save_environment_variables(env_updates) @@ -1731,7 +1723,7 @@ async def get_ui_settings(): await user_api_key_cache.async_set_cache(key=UI_SETTINGS_CACHE_KEY, value=ui_settings, ttl=UI_SETTINGS_CACHE_TTL) # Build config-like object for schema helper - config: Final[dict[str, object]] = {"litellm_settings": {"ui_settings": ui_settings}} + config: Final[Mapping[str, object]] = {"litellm_settings": {"ui_settings": ui_settings}} # mutable-ok: schema helper only reads it settings: Final = await _get_settings_with_schema( settings_key="ui_settings",