test(proxy): cover frozen runtime config and section-based save_config

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
mateo 2026-09-21 23:11:58 +00:00
parent 415032118f
commit 108f699294
20 changed files with 428 additions and 288 deletions

View file

@ -7,7 +7,7 @@ from fastapi import FastAPI
from fastapi.testclient import TestClient
from litellm.constants import REDACTED_BY_LITELM_STRING
from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth
from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth, ProxyRuntimeConfig
from litellm.proxy.agent_endpoints import endpoints as agent_endpoints
from litellm.proxy.agent_endpoints.auth.agent_permission_handler import (
RestrictedAgentAccess,
@ -1075,20 +1075,19 @@ class _DbBackedProxyConfig:
def __init__(self, stored_litellm_settings: dict[str, object] | None = None) -> None:
self.stored_litellm_settings_json: str = json.dumps(stored_litellm_settings or {})
async def get_config(self) -> dict[str, dict[str, object]]:
async def get_config(self):
from litellm.proxy.proxy_server import ProxyConfig
config: Final[dict[str, dict[str, object]]] = {"litellm_settings": {}}
db_param_value: Final[dict[str, object]] = json.loads(self.stored_litellm_settings_json)
if not db_param_value:
return config
return ProxyRuntimeConfig()
proxy_config: Final = ProxyConfig()
db_values: Final = proxy_config._prepared_db_settings_values("litellm_settings", db_param_value)
proxy_config._apply_litellm_settings_db_values(db_values)
return {"litellm_settings": dict(proxy_config.litellm_settings.resolved())}
return ProxyRuntimeConfig.from_resolved({"litellm_settings": dict(proxy_config.litellm_settings.resolved())})
async def save_config(self, new_config: dict[str, dict[str, object]]) -> None:
self.stored_litellm_settings_json = json.dumps(new_config.get("litellm_settings") or {})
async def save_config(self, new_config: ProxyRuntimeConfig) -> None:
self.stored_litellm_settings_json = json.dumps(new_config.litellm_settings)
def test_make_agent_public_twice_keeps_both_agents_public(monkeypatch: pytest.MonkeyPatch) -> None:

View file

@ -298,8 +298,8 @@ async def test_load_config_returns_and_binds_the_general_settings_store(tmp_path
assert returned_store is proxy_config.settings
assert proxy_server.general_settings is proxy_config.settings
assert isinstance(config_state["general_settings"], dict)
assert config_state["general_settings"]["max_file_size_mb"] == 5
assert isinstance(config_state.general_settings, dict)
assert config_state.general_settings["max_file_size_mb"] == 5
def test_settings_store_starts_with_an_unset_source() -> None:

View file

@ -21,6 +21,7 @@ from litellm.proxy._types import (
NewUserResponse,
ProxyErrorTypes,
ProxyException,
ProxyRuntimeConfig,
UserAPIKeyAuth,
)
from litellm.proxy.management_endpoints.scim.scim_v2 import (
@ -417,7 +418,7 @@ async def test_scim_create_user_respects_default_role_set_via_ui(mocker, monkeyp
# Step 2: Simulate the UI saving "Internal User (Create/Delete/View)" as default role
# Mock the proxy_config and store_model_in_db that _update_litellm_setting needs
mock_proxy_config = mocker.MagicMock()
mock_proxy_config.get_config = AsyncMock(return_value={"litellm_settings": {}})
mock_proxy_config.get_config = AsyncMock(return_value=ProxyRuntimeConfig.from_resolved({"litellm_settings": {}}))
mock_proxy_config.save_config = AsyncMock()
mocker.patch(
@ -1380,7 +1381,7 @@ async def test_update_user_without_groups_preserves_memberships_and_role(mocker,
from litellm.proxy.proxy_server import proxy_config
async def mock_get_config():
return {"litellm_settings": {"scim_admin_group": "litellm-admins"}}
return ProxyRuntimeConfig.from_resolved({"litellm_settings": {"scim_admin_group": "litellm-admins"}})
monkeypatch.setattr(proxy_config, "get_config", mock_get_config)
monkeypatch.setattr("litellm.default_internal_user_params", None, raising=False)
@ -1939,7 +1940,7 @@ async def test_create_group_with_nonexistent_users_rejects(mocker, monkeypatch):
# Mock the feature flag to False (SCIM 2.0 strict mode)
async def mock_get_config():
return {"litellm_settings": {"scim_upsert_user": False}}
return ProxyRuntimeConfig.from_resolved({"litellm_settings": {"scim_upsert_user": False}})
from litellm.proxy.proxy_server import proxy_config
@ -2008,7 +2009,7 @@ async def test_update_group_with_nonexistent_users_rejects(mocker, monkeypatch):
# Mock the feature flag to False (SCIM 2.0 strict mode)
async def mock_get_config():
return {"litellm_settings": {"scim_upsert_user": False}}
return ProxyRuntimeConfig.from_resolved({"litellm_settings": {"scim_upsert_user": False}})
from litellm.proxy.proxy_server import proxy_config
@ -2098,7 +2099,7 @@ async def test_create_group_with_nonexistent_users_creates_when_flag_true(mocker
# Mock the feature flag to True (backward compatible mode)
async def mock_get_config():
return {"litellm_settings": {"scim_upsert_user": True}}
return ProxyRuntimeConfig.from_resolved({"litellm_settings": {"scim_upsert_user": True}})
from litellm.proxy.proxy_server import proxy_config
@ -2192,7 +2193,7 @@ async def test_extract_group_member_ids_with_flag_true_creates_users(mocker, mon
# Mock the feature flag to True (backward compatible mode)
async def mock_get_config():
return {"litellm_settings": {"scim_upsert_user": True}}
return ProxyRuntimeConfig.from_resolved({"litellm_settings": {"scim_upsert_user": True}})
from litellm.proxy.proxy_server import proxy_config
@ -2261,7 +2262,7 @@ async def test_extract_group_member_ids_with_flag_false_rejects(mocker, monkeypa
# Mock the feature flag to False (SCIM 2.0 strict mode)
async def mock_get_config():
return {"litellm_settings": {"scim_upsert_user": False}}
return ProxyRuntimeConfig.from_resolved({"litellm_settings": {"scim_upsert_user": False}})
from litellm.proxy.proxy_server import proxy_config
@ -2320,7 +2321,7 @@ async def test_process_group_patch_operations_with_flag_true_creates_users(mocke
# Mock the feature flag to True (backward compatible mode)
async def mock_get_config():
return {"litellm_settings": {"scim_upsert_user": True}}
return ProxyRuntimeConfig.from_resolved({"litellm_settings": {"scim_upsert_user": True}})
from litellm.proxy.proxy_server import proxy_config
@ -2377,7 +2378,7 @@ async def test_process_group_patch_operations_with_flag_false_rejects(mocker, mo
# Mock the feature flag to False (SCIM 2.0 strict mode)
async def mock_get_config():
return {"litellm_settings": {"scim_upsert_user": False}}
return ProxyRuntimeConfig.from_resolved({"litellm_settings": {"scim_upsert_user": False}})
from litellm.proxy.proxy_server import proxy_config
@ -2426,7 +2427,7 @@ async def test_create_user_grants_admin_when_in_scim_admin_group(mocker, monkeyp
from litellm.proxy.proxy_server import proxy_config
async def mock_get_config():
return {"litellm_settings": {"scim_admin_group": "litellm-admins"}}
return ProxyRuntimeConfig.from_resolved({"litellm_settings": {"scim_admin_group": "litellm-admins"}})
monkeypatch.setattr(proxy_config, "get_config", mock_get_config)
monkeypatch.setattr("litellm.default_internal_user_params", None, raising=False)
@ -2471,7 +2472,7 @@ async def test_create_user_keeps_default_when_not_in_scim_admin_group(mocker, mo
from litellm.proxy.proxy_server import proxy_config
async def mock_get_config():
return {"litellm_settings": {"scim_admin_group": "litellm-admins"}}
return ProxyRuntimeConfig.from_resolved({"litellm_settings": {"scim_admin_group": "litellm-admins"}})
monkeypatch.setattr(proxy_config, "get_config", mock_get_config)
monkeypatch.setattr("litellm.default_internal_user_params", None, raising=False)
@ -2517,7 +2518,7 @@ async def test_update_user_demotes_admin_when_removed_from_scim_admin_group(mock
from litellm.proxy.proxy_server import proxy_config
async def mock_get_config():
return {"litellm_settings": {"scim_admin_group": "litellm-admins"}}
return ProxyRuntimeConfig.from_resolved({"litellm_settings": {"scim_admin_group": "litellm-admins"}})
monkeypatch.setattr(proxy_config, "get_config", mock_get_config)
monkeypatch.setattr("litellm.default_internal_user_params", None, raising=False)
@ -2576,7 +2577,7 @@ async def test_update_user_does_not_force_role_when_scim_admin_group_unset(mocke
from litellm.proxy.proxy_server import proxy_config
async def mock_get_config():
return {"litellm_settings": {}}
return ProxyRuntimeConfig.from_resolved({"litellm_settings": {}})
monkeypatch.setattr(proxy_config, "get_config", mock_get_config)
monkeypatch.setattr("litellm.default_internal_user_params", None, raising=False)
@ -2636,7 +2637,7 @@ async def test_update_user_demotes_when_default_params_lack_user_role(mocker, mo
from litellm.proxy.proxy_server import proxy_config
async def mock_get_config():
return {"litellm_settings": {"scim_admin_group": "litellm-admins"}}
return ProxyRuntimeConfig.from_resolved({"litellm_settings": {"scim_admin_group": "litellm-admins"}})
monkeypatch.setattr(proxy_config, "get_config", mock_get_config)
monkeypatch.setattr("litellm.default_internal_user_params", {"max_budget": 10}, raising=False)
@ -2695,7 +2696,7 @@ async def test_patch_user_demotes_admin_when_removed_from_scim_admin_group(mocke
from litellm.proxy.proxy_server import proxy_config
async def mock_get_config():
return {"litellm_settings": {"scim_admin_group": "litellm-admins"}}
return ProxyRuntimeConfig.from_resolved({"litellm_settings": {"scim_admin_group": "litellm-admins"}})
monkeypatch.setattr(proxy_config, "get_config", mock_get_config)
monkeypatch.setattr("litellm.default_internal_user_params", None, raising=False)
@ -2762,7 +2763,7 @@ async def test_patch_user_grants_admin_by_team_display_name(mocker, monkeypatch)
from litellm.proxy.proxy_server import proxy_config
async def mock_get_config():
return {"litellm_settings": {"scim_admin_group": "LiteLLM Admins"}}
return ProxyRuntimeConfig.from_resolved({"litellm_settings": {"scim_admin_group": "LiteLLM Admins"}})
monkeypatch.setattr(proxy_config, "get_config", mock_get_config)
monkeypatch.setattr("litellm.default_internal_user_params", None, raising=False)
@ -2851,7 +2852,7 @@ async def test_recompute_scim_member_roles_demotes_when_not_in_admin_group(mocke
from litellm.proxy.proxy_server import proxy_config
async def mock_get_config():
return {"litellm_settings": {"scim_admin_group": "litellm-admins"}}
return ProxyRuntimeConfig.from_resolved({"litellm_settings": {"scim_admin_group": "litellm-admins"}})
monkeypatch.setattr(proxy_config, "get_config", mock_get_config)
monkeypatch.setattr("litellm.default_internal_user_params", None, raising=False)
@ -2871,7 +2872,7 @@ async def test_recompute_scim_member_roles_grants_when_in_admin_group(mocker, mo
from litellm.proxy.proxy_server import proxy_config
async def mock_get_config():
return {"litellm_settings": {"scim_admin_group": "litellm-admins"}}
return ProxyRuntimeConfig.from_resolved({"litellm_settings": {"scim_admin_group": "litellm-admins"}})
monkeypatch.setattr(proxy_config, "get_config", mock_get_config)
monkeypatch.setattr("litellm.default_internal_user_params", None, raising=False)
@ -2891,7 +2892,7 @@ async def test_recompute_scim_member_roles_noop_when_admin_group_unset(mocker, m
from litellm.proxy.proxy_server import proxy_config
async def mock_get_config():
return {"litellm_settings": {}}
return ProxyRuntimeConfig.from_resolved({"litellm_settings": {}})
monkeypatch.setattr(proxy_config, "get_config", mock_get_config)
@ -3153,7 +3154,7 @@ async def test_create_user_existing_email_upsert_demotes_when_admin_group_set(mo
from litellm.proxy.proxy_server import proxy_config
async def mock_get_config():
return {"litellm_settings": {"scim_admin_group": "litellm-admins"}}
return ProxyRuntimeConfig.from_resolved({"litellm_settings": {"scim_admin_group": "litellm-admins"}})
monkeypatch.setattr(proxy_config, "get_config", mock_get_config)
monkeypatch.setattr("litellm.default_internal_user_params", None, raising=False)
@ -3379,7 +3380,7 @@ async def test_process_group_patch_operations_add_retains_existing_members(mocke
"""
async def mock_get_config():
return {"litellm_settings": {"scim_upsert_user": True}}
return ProxyRuntimeConfig.from_resolved({"litellm_settings": {"scim_upsert_user": True}})
from litellm.proxy.proxy_server import proxy_config
@ -3417,7 +3418,7 @@ async def test_process_group_patch_operations_remove_uses_members_with_roles(moc
member leaves the rest of the team intact rather than emptying it."""
async def mock_get_config():
return {"litellm_settings": {"scim_upsert_user": True}}
return ProxyRuntimeConfig.from_resolved({"litellm_settings": {"scim_upsert_user": True}})
from litellm.proxy.proxy_server import proxy_config
@ -4072,7 +4073,7 @@ def scim_upsert_user_enabled(monkeypatch):
from litellm.proxy.proxy_server import proxy_config
async def mock_get_config():
return {"litellm_settings": {"scim_upsert_user": True}}
return ProxyRuntimeConfig.from_resolved({"litellm_settings": {"scim_upsert_user": True}})
monkeypatch.setattr(proxy_config, "get_config", mock_get_config)
@ -4082,7 +4083,7 @@ def scim_upsert_user_disabled(monkeypatch):
from litellm.proxy.proxy_server import proxy_config
async def mock_get_config():
return {"litellm_settings": {"scim_upsert_user": False}}
return ProxyRuntimeConfig.from_resolved({"litellm_settings": {"scim_upsert_user": False}})
monkeypatch.setattr(proxy_config, "get_config", mock_get_config)

View file

@ -11,6 +11,7 @@ from litellm.proxy._types import (
LiteLLM_ObjectPermissionTable,
LiteLLM_TeamTable,
LitellmUserRoles,
ProxyRuntimeConfig,
UserAPIKeyAuth,
)
@ -51,7 +52,7 @@ async def test_list_search_tools_db_only(monkeypatch):
with patch("litellm.proxy.proxy_server.prisma_client", mock_prisma):
# Mock proxy_config
mock_proxy_config = MagicMock()
mock_proxy_config.get_config = AsyncMock(return_value={})
mock_proxy_config.get_config = AsyncMock(return_value=ProxyRuntimeConfig.from_resolved({}))
mock_proxy_config.parse_search_tools = MagicMock(return_value=None)
with patch("litellm.proxy.proxy_server.proxy_config", mock_proxy_config):
# Mock auth
@ -115,7 +116,7 @@ async def test_list_search_tools_config_only(monkeypatch):
# Mock proxy_config
mock_proxy_config = MagicMock()
mock_proxy_config.get_config = AsyncMock(
return_value={"search_tools": config_tools}
return_value=ProxyRuntimeConfig.from_resolved({"search_tools": config_tools})
)
mock_proxy_config.parse_search_tools = MagicMock(return_value=config_tools)
with patch("litellm.proxy.proxy_server.proxy_config", mock_proxy_config):
@ -191,7 +192,7 @@ async def test_list_search_tools_filters_duplicate_config_tools(monkeypatch):
# Mock proxy_config
mock_proxy_config = MagicMock()
mock_proxy_config.get_config = AsyncMock(
return_value={"search_tools": config_tools}
return_value=ProxyRuntimeConfig.from_resolved({"search_tools": config_tools})
)
mock_proxy_config.parse_search_tools = MagicMock(return_value=config_tools)
with patch("litellm.proxy.proxy_server.proxy_config", mock_proxy_config):
@ -301,7 +302,7 @@ async def test_list_search_tools_datetime_conversion(monkeypatch):
with patch("litellm.proxy.proxy_server.prisma_client", mock_prisma):
# Mock proxy_config
mock_proxy_config = MagicMock()
mock_proxy_config.get_config = AsyncMock(return_value={})
mock_proxy_config.get_config = AsyncMock(return_value=ProxyRuntimeConfig.from_resolved({}))
mock_proxy_config.parse_search_tools = MagicMock(return_value=None)
with patch("litellm.proxy.proxy_server.proxy_config", mock_proxy_config):
# Mock auth
@ -521,7 +522,7 @@ async def test_list_search_tools_db_masking_sensitive_values(monkeypatch):
with patch("litellm.proxy.proxy_server.prisma_client", mock_prisma):
# Mock proxy_config
mock_proxy_config = MagicMock()
mock_proxy_config.get_config = AsyncMock(return_value={})
mock_proxy_config.get_config = AsyncMock(return_value=ProxyRuntimeConfig.from_resolved({}))
mock_proxy_config.parse_search_tools = MagicMock(return_value=None)
with patch("litellm.proxy.proxy_server.proxy_config", mock_proxy_config):
# Mock auth
@ -654,7 +655,7 @@ def _mock_search_tool_backend(db_tools):
mock_registry = MagicMock()
mock_registry.get_all_search_tools_from_db = AsyncMock(return_value=db_tools)
mock_proxy_config = MagicMock()
mock_proxy_config.get_config = AsyncMock(return_value={})
mock_proxy_config.get_config = AsyncMock(return_value=ProxyRuntimeConfig.from_resolved({}))
mock_proxy_config.parse_search_tools = MagicMock(return_value=None)
with (
patch(

View file

@ -13,7 +13,7 @@ from fastapi import HTTPException
import litellm
from litellm.caching.caching import RedisCache
from litellm.caching.redis_cluster_cache import RedisClusterCache
from litellm.proxy._types import LitellmTableNames, LitellmUserRoles
from litellm.proxy._types import LitellmTableNames, LitellmUserRoles, ProxyRuntimeConfig
from litellm.proxy.auth.user_api_key_auth import UserAPIKeyAuth
from litellm.proxy.management_endpoints.coordination_redis_endpoints import (
_REDACTED_VALUE,
@ -59,7 +59,7 @@ def _prisma_with_general_settings(general_settings: dict | None) -> MagicMock:
def _proxy_config(file_general_settings: dict | None = None) -> MagicMock:
proxy_config = MagicMock()
proxy_config.get_config_state = MagicMock(
return_value={"general_settings": file_general_settings or {}},
return_value=ProxyRuntimeConfig.from_resolved({"general_settings": file_general_settings or {}}),
)
return proxy_config
@ -624,7 +624,7 @@ def _real_proxy_config(file_general_settings: dict) -> "object":
proxy_config = ProxyConfig()
proxy_config._load_yaml_settings_stores({"general_settings": file_general_settings})
proxy_config.get_config_state = MagicMock(
return_value={"general_settings": file_general_settings}
return_value=ProxyRuntimeConfig.from_resolved({"general_settings": file_general_settings})
)
return proxy_config

View file

@ -14,7 +14,7 @@ from pydantic import ValidationError
import litellm
from litellm._internal_context import pinned_billing_time
from litellm.proxy._types import CostEstimateRequest
from litellm.proxy._types import CostEstimateRequest, ProxyRuntimeConfig
from litellm.proxy.management_endpoints.cost_tracking_settings import router
from litellm.proxy.proxy_server import app
@ -32,7 +32,7 @@ class TestCostTrackingSettings:
# Mock the proxy_config to return a config with cost_discount_config
mock_proxy_config = AsyncMock()
mock_proxy_config.get_config = AsyncMock(
return_value={
return_value=ProxyRuntimeConfig.from_resolved({
"litellm_settings": {
"cost_discount_config": {
"vertex_ai": 0.05,
@ -40,7 +40,7 @@ class TestCostTrackingSettings:
"openai": 0.01,
}
}
}
})
)
mock_prisma_client = MagicMock()
@ -80,7 +80,7 @@ class TestCostTrackingSettings:
"""
# Mock the proxy_config to return a config without cost_discount_config
mock_proxy_config = AsyncMock()
mock_proxy_config.get_config = AsyncMock(return_value={"litellm_settings": {}})
mock_proxy_config.get_config = AsyncMock(return_value=ProxyRuntimeConfig.from_resolved({"litellm_settings": {}}))
mock_prisma_client = MagicMock()
@ -114,7 +114,7 @@ class TestCostTrackingSettings:
"""
# Mock the proxy_config
mock_proxy_config = AsyncMock()
mock_proxy_config.get_config = AsyncMock(return_value={"litellm_settings": {}})
mock_proxy_config.get_config = AsyncMock(return_value=ProxyRuntimeConfig.from_resolved({"litellm_settings": {}}))
mock_proxy_config.save_config = AsyncMock()
mock_prisma_client = MagicMock()
@ -705,7 +705,7 @@ class TestBlockRequestsForModelsWithoutPricing:
@pytest.mark.asyncio
async def test_patch_persists_and_updates_flag(self):
mock_proxy_config = AsyncMock()
mock_proxy_config.get_config = AsyncMock(return_value={"litellm_settings": {}})
mock_proxy_config.get_config = AsyncMock(return_value=ProxyRuntimeConfig.from_resolved({"litellm_settings": {}}))
mock_proxy_config.save_config = AsyncMock()
with (
@ -725,7 +725,7 @@ class TestBlockRequestsForModelsWithoutPricing:
assert litellm.block_requests_for_models_without_pricing is True
saved_config = mock_proxy_config.save_config.call_args.kwargs["new_config"]
assert saved_config["litellm_settings"]["block_requests_for_models_without_pricing"] is True
assert saved_config.litellm_settings["block_requests_for_models_without_pricing"] is True
def test_peer_workers_pick_up_persisted_flag_on_config_reload(self):
"""A PATCH only mutates the flag on the worker that served it; peer workers must pick the

View file

@ -19,6 +19,7 @@ from litellm.proxy._types import (
LitellmUserRoles,
Member,
ProxyException,
ProxyRuntimeConfig,
ReconcileOutcome,
UserAPIKeyAuth,
)
@ -1385,7 +1386,7 @@ class TestUpdatePublicModelGroups:
async def mock_get_config(*args, **kwargs):
# This simulates _update_config_from_db calling setattr(litellm, "public_model_groups", old_value)
litellm.public_model_groups = old_db_models
return {"litellm_settings": {"public_model_groups": old_db_models}}
return ProxyRuntimeConfig.from_resolved({"litellm_settings": {"public_model_groups": old_db_models}})
mock_proxy_config = MagicMock()
mock_proxy_config.get_config = mock_get_config
@ -1443,7 +1444,7 @@ class TestUpdatePublicModelGroups:
async def mock_get_config(*args, **kwargs):
litellm.public_model_groups_links = old_links
return {"litellm_settings": {"public_model_groups_links": old_links}}
return ProxyRuntimeConfig.from_resolved({"litellm_settings": {"public_model_groups_links": old_links}})
mock_proxy_config = MagicMock()
mock_proxy_config.get_config = mock_get_config

View file

@ -11,7 +11,7 @@ from fastapi.testclient import TestClient
from litellm.proxy import proxy_server
from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth
from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth, ProxyRuntimeConfig
from litellm.proxy.management_endpoints.router_settings_endpoints import (
get_router_settings,
)
@ -104,7 +104,7 @@ class TestRouterSettingsEndpoints:
monkeypatch.setattr(proxy_server, "llm_router", llm_router)
async def fake_get_config(self, config_file_path=None):
return {}
return ProxyRuntimeConfig.from_resolved({})
monkeypatch.setattr(
proxy_server.ProxyConfig, "get_config", fake_get_config, raising=True

View file

@ -13,10 +13,11 @@ import litellm
from litellm.proxy._types import (
LiteLLM_BudgetTable,
LiteLLM_OrganizationTable,
LitellmUserRoles,
NewTeamRequest,
ProxyException,
ProxyRuntimeConfig,
UserAPIKeyAuth,
LitellmUserRoles,
)
from litellm.proxy.management_endpoints.team_endpoints import (
_get_default_team_param,
@ -543,13 +544,11 @@ class TestUpdateLitellmSettingOrdering:
async def mock_get_config():
# Simulate what _update_config_from_db does for safe overrides
litellm.default_team_params = stale_value
return {
return ProxyRuntimeConfig.from_resolved({
"litellm_settings": {
"default_team_params": stale_value,
}
}
saved_configs = []
})
async def mock_save_config(new_config=None):
saved_configs.append(new_config)

View file

@ -23,7 +23,7 @@ from unittest.mock import AsyncMock, MagicMock
import pytest
import litellm
from litellm.proxy._types import CommonProxyErrors
from litellm.proxy._types import CommonProxyErrors, ProxyRuntimeConfig
from litellm.proxy.common_utils.encrypt_decrypt_utils import encrypt_value_helper
from litellm.proxy.proxy_server import (
ProxyConfig,
@ -641,7 +641,7 @@ def test_ProxyConfig___init___sets_defaults():
"worker_registry": pc.worker_registry,
}
assert snapshot == {
"config": {},
"config": ProxyRuntimeConfig(),
"last_semantic_filter_config": None,
"worker_registry": [],
}
@ -943,11 +943,10 @@ async def test_ProxyConfig_save_config_merges_changed_keys_without_copying_file_
"router_settings": {"num_retries": 1},
"litellm_settings": {"drop_params": True},
}
proxy_config.update_config_state(config=baseline)
changed: Final = {
**baseline,
"general_settings": {**baseline["general_settings"], "allowed_ips": ["127.0.0.1"]},
}
proxy_config.update_config_state(config=ProxyRuntimeConfig.from_resolved(baseline))
changed: Final = proxy_config.get_config_state().with_section(
"general_settings", {**baseline["general_settings"], "allowed_ips": ["127.0.0.1"]}
)
await proxy_config.save_config(changed)
@ -964,9 +963,9 @@ async def test_ProxyConfig_save_config_skips_unchanged_config(monkeypatch):
"router_settings": {"num_retries": 1},
"litellm_settings": {"drop_params": True},
}
proxy_config.update_config_state(config=baseline)
proxy_config.update_config_state(config=ProxyRuntimeConfig.from_resolved(baseline))
await proxy_config.save_config(baseline)
await proxy_config.save_config(proxy_config.get_config_state())
assert table.rows == {"general_settings": {"db_only": "stored"}}
assert table.upserted_param_names == []
@ -975,10 +974,10 @@ async def test_ProxyConfig_save_config_skips_unchanged_config(monkeypatch):
@pytest.mark.asyncio
async def test_ProxyConfig_save_config_skips_unchanged_unmanaged_values(monkeypatch):
proxy_config, table = _db_backed_proxy_config(monkeypatch, {})
baseline: Final = {"general_settings": {}, "guardrails": {"enabled": True}}
proxy_config.update_config_state(config=baseline)
baseline: Final = {"general_settings": {}, "guardrails": [{"enabled": True}]}
proxy_config.update_config_state(config=ProxyRuntimeConfig.from_resolved(baseline))
await proxy_config.save_config(baseline)
await proxy_config.save_config(proxy_config.get_config_state())
assert table.rows == {}
assert table.upserted_param_names == []
@ -991,10 +990,14 @@ async def test_ProxyConfig_save_config_leaves_omitted_sections_unchanged(monkeyp
{"general_settings": {"allowed_ips": ["10.0.0.1"], "db_only": "stored"}},
)
proxy_config.update_config_state(
config={"general_settings": {"allowed_ips": ["10.0.0.1"]}, "router_settings": {"num_retries": 1}}
config=ProxyRuntimeConfig.from_resolved(
{"general_settings": {"allowed_ips": ["10.0.0.1"]}, "router_settings": {"num_retries": 1}}
)
)
await proxy_config.save_config({"router_settings": {"num_retries": 2}})
await proxy_config.save_config(
proxy_config.get_config_state().with_section("router_settings", {"num_retries": 2})
)
assert table.rows == {
"general_settings": {"allowed_ips": ["10.0.0.1"], "db_only": "stored"},
@ -1006,9 +1009,13 @@ async def test_ProxyConfig_save_config_leaves_omitted_sections_unchanged(monkeyp
@pytest.mark.asyncio
async def test_ProxyConfig_save_config_decodes_a_serialized_config_row(monkeypatch):
proxy_config, table = _db_backed_proxy_config(monkeypatch, {"general_settings": '{"db_only":"stored"}'})
proxy_config.update_config_state(config={"general_settings": {"allowed_ips": []}})
proxy_config.update_config_state(
config=ProxyRuntimeConfig.from_resolved({"general_settings": {"allowed_ips": []}})
)
await proxy_config.save_config({"general_settings": {"allowed_ips": ["127.0.0.1"]}})
await proxy_config.save_config(
proxy_config.get_config_state().with_section("general_settings", {"allowed_ips": ["127.0.0.1"]})
)
assert table.rows == {"general_settings": {"db_only": "stored", "allowed_ips": ["127.0.0.1"]}}
assert table.upserted_param_names == ["general_settings"]
@ -1018,13 +1025,13 @@ async def test_ProxyConfig_save_config_decodes_a_serialized_config_row(monkeypat
async def test_ProxyConfig_save_config_serializes_concurrent_changes_to_one_section(monkeypatch):
first, table = _db_backed_proxy_config(monkeypatch, {"general_settings": {"a": 0, "b": 0}})
second: Final = ProxyConfig()
baseline: Final = {"general_settings": {"a": 0, "b": 0}}
first.update_config_state(config=baseline)
second.update_config_state(config=baseline)
baseline: Final = ProxyRuntimeConfig.from_resolved({"general_settings": {"a": 0, "b": 0}})
first.update_config_state(config=ProxyRuntimeConfig.from_resolved(baseline))
second.update_config_state(config=ProxyRuntimeConfig.from_resolved(baseline))
await asyncio.gather(
first.save_config({"general_settings": {"a": 1, "b": 0}}),
second.save_config({"general_settings": {"a": 0, "b": 1}}),
first.save_config(first.get_config_state().with_section("general_settings", {"a": 1, "b": 0})),
second.save_config(second.get_config_state().with_section("general_settings", {"a": 0, "b": 1})),
)
assert table.rows == {"general_settings": {"a": 1, "b": 1}}
@ -1033,10 +1040,14 @@ async def test_ProxyConfig_save_config_serializes_concurrent_changes_to_one_sect
@pytest.mark.asyncio
async def test_ProxyConfig_save_config_updates_the_baseline_after_a_save(monkeypatch):
proxy_config, table = _db_backed_proxy_config(monkeypatch, {})
proxy_config.update_config_state(config={"general_settings": {}})
proxy_config.update_config_state(
config=ProxyRuntimeConfig.from_resolved({"general_settings": {}})
)
await proxy_config.save_config({"general_settings": {"removed_key": True}})
await proxy_config.save_config({"general_settings": {}})
await proxy_config.save_config(
proxy_config.get_config_state().with_section("general_settings", {"removed_key": True})
)
await proxy_config.save_config(proxy_config.get_config_state().with_section("general_settings", {}))
assert table.rows == {"general_settings": {}}
@ -1045,11 +1056,15 @@ async def test_ProxyConfig_save_config_updates_the_baseline_after_a_save(monkeyp
async def test_ProxyConfig_save_config_keeps_omitted_sections_in_its_next_baseline(monkeypatch):
proxy_config, table = _db_backed_proxy_config(monkeypatch, {"general_settings": {"allowed_ips": ["10.0.0.1"]}})
proxy_config.update_config_state(
config={"general_settings": {"allowed_ips": ["10.0.0.1"]}, "router_settings": {"num_retries": 1}}
config=ProxyRuntimeConfig.from_resolved(
{"general_settings": {"allowed_ips": ["10.0.0.1"]}, "router_settings": {"num_retries": 1}}
)
)
await proxy_config.save_config({"router_settings": {"num_retries": 2}})
await proxy_config.save_config({"general_settings": {}})
await proxy_config.save_config(
proxy_config.get_config_state().with_section("router_settings", {"num_retries": 2})
)
await proxy_config.save_config(proxy_config.get_config_state().with_section("general_settings", {}))
assert table.rows == {"general_settings": {}, "router_settings": {"num_retries": 2}}
@ -1068,8 +1083,8 @@ async def test_ProxyConfig_save_config_uses_the_baseline_from_the_loaded_config(
monkeypatch.setattr("litellm.proxy.proxy_server.store_model_in_db", True)
monkeypatch.setattr("litellm.proxy.proxy_server.general_settings", {"store_model_in_db": True})
monkeypatch.setattr("litellm.proxy.proxy_server.invalidate_config_param", AsyncMock())
first["general_settings"]["first"] = True
second["general_settings"]["second"] = True
first = first.with_section("general_settings", {**first.general_settings, "first": True})
second = second.with_section("general_settings", {**second.general_settings, "second": True})
await proxy_config.save_config(second)
await proxy_config.save_config(first)
@ -1080,11 +1095,17 @@ async def test_ProxyConfig_save_config_uses_the_baseline_from_the_loaded_config(
@pytest.mark.asyncio
async def test_ProxyConfig_save_config_accepts_non_json_model_metadata(monkeypatch):
proxy_config, table = _db_backed_proxy_config(monkeypatch, {})
proxy_config.update_config_state(config={"general_settings": {"allowed_ips": []}})
config: Final = {
"model_list": [{"model_name": "date-model", "model_info": {"created_at": datetime(2026, 1, 1)}}],
"general_settings": {"allowed_ips": ["127.0.0.1"]},
}
proxy_config.update_config_state(
config=ProxyRuntimeConfig.from_resolved({"general_settings": {"allowed_ips": []}})
)
config: Final = (
proxy_config.get_config_state()
.with_section(
"model_list",
({"model_name": "date-model", "model_info": {"created_at": datetime(2026, 1, 1)}},),
)
.with_section("general_settings", {"allowed_ips": ["127.0.0.1"]})
)
await proxy_config.save_config(config)
@ -1100,8 +1121,8 @@ async def test_ProxyConfig_save_config_writes_only_changed_router_settings(monke
"router_settings": {"num_retries": 1},
"litellm_settings": {"drop_params": True},
}
proxy_config.update_config_state(config=baseline)
changed: Final = {**baseline, "router_settings": {"num_retries": 2}}
proxy_config.update_config_state(config=ProxyRuntimeConfig.from_resolved(baseline))
changed: Final = proxy_config.get_config_state().with_section("router_settings", {"num_retries": 2})
await proxy_config.save_config(changed)
@ -1115,8 +1136,10 @@ async def test_ProxyConfig_save_config_removes_a_key_only_when_the_db_has_it(mon
monkeypatch, {"general_settings": {"removed_key": "db", "db_only": "stored"}}
)
baseline: Final = {"general_settings": {"removed_key": "yaml", "file_only": "yaml"}}
proxy_config.update_config_state(config=baseline)
changed: Final = {"general_settings": {"file_only": "yaml"}}
proxy_config.update_config_state(config=ProxyRuntimeConfig.from_resolved(baseline))
changed: Final = proxy_config.get_config_state().with_section(
"general_settings", {"file_only": "yaml"}
)
await proxy_config.save_config(changed)
@ -1128,9 +1151,9 @@ async def test_ProxyConfig_save_config_removes_a_key_only_when_the_db_has_it(mon
async def test_ProxyConfig_save_config_keeps_an_unstored_removed_key_as_a_noop(monkeypatch):
proxy_config, table = _db_backed_proxy_config(monkeypatch, {"general_settings": {"db_only": "stored"}})
baseline: Final = {"general_settings": {"file_only": "yaml"}}
proxy_config.update_config_state(config=baseline)
proxy_config.update_config_state(config=ProxyRuntimeConfig.from_resolved(baseline))
await proxy_config.save_config({"general_settings": {}})
await proxy_config.save_config(proxy_config.get_config_state().with_section("general_settings", {}))
assert table.rows == {"general_settings": {"db_only": "stored"}}
assert table.upserted_param_names == []
@ -1146,18 +1169,19 @@ async def test_ProxyConfig_get_config_keeps_state_separate_from_returned_config(
proxy_config: Final = ProxyConfig()
loaded: Final = await proxy_config.get_config(config_file_path=str(config_file))
loaded["general_settings"]["max_parallel_requests"] = 6
mutated: Final = loaded.with_section("general_settings", {"max_parallel_requests": 6})
assert proxy_config.get_config_state()["general_settings"]["max_parallel_requests"] == 5
assert proxy_config.get_config_state().general_settings["max_parallel_requests"] == 5
assert mutated.general_settings["max_parallel_requests"] == 6
def test_ProxyConfig_update_config_state_keeps_a_copy_of_its_input():
source: Final = {"general_settings": {"max_parallel_requests": 5}}
proxy_config: Final = ProxyConfig()
proxy_config.update_config_state(config=source)
proxy_config.update_config_state(config=ProxyRuntimeConfig.from_resolved(source))
source["general_settings"]["max_parallel_requests"] = 6
assert proxy_config.get_config_state()["general_settings"]["max_parallel_requests"] == 5
assert proxy_config.get_config_state().general_settings["max_parallel_requests"] == 5
@pytest.mark.asyncio
@ -1169,7 +1193,7 @@ async def test_ProxyConfig_save_config_writes_yaml_when_no_db(tmp_path, monkeypa
monkeypatch.setattr("litellm.proxy.proxy_server.general_settings", {})
pc = ProxyConfig()
cfg = {"model_list": [], "general_settings": {"a": 1}, "litellm_settings": {}}
await pc.save_config(cfg)
await pc.save_config(ProxyRuntimeConfig.from_resolved(cfg))
import yaml as _yaml
loaded = _yaml.safe_load(target.read_text())
@ -1186,7 +1210,7 @@ async def test_ProxyConfig_save_config_writes_a_loadable_yaml_for_a_loaded_confi
monkeypatch.setattr("litellm.proxy.proxy_server.general_settings", {})
proxy_config: Final = ProxyConfig()
loaded_config: Final = await proxy_config.get_config(config_file_path=str(config_file))
loaded_config["general_settings"]["max_parallel_requests"] = 6
loaded_config = loaded_config.with_section("general_settings", {"max_parallel_requests": 6})
await proxy_config.save_config(loaded_config)
@ -1206,33 +1230,38 @@ async def test_ProxyConfig_save_config_invalid_path_raises(monkeypatch):
monkeypatch.setattr("litellm.proxy.proxy_server.general_settings", {})
pc = ProxyConfig()
with pytest.raises(FileNotFoundError):
await pc.save_config({"x": 1})
await pc.save_config(ProxyRuntimeConfig.from_resolved({"x": 1}))
@pytest.mark.asyncio
async def test_ProxyConfig_save_config_db_omits_environment_variables_by_default(monkeypatch):
proxy_config, table = _db_backed_proxy_config(monkeypatch, {})
baseline: Final = {"model_list": [], "litellm_settings": {}}
proxy_config.update_config_state(config=baseline)
config: Final = {
"model_list": [{"model_name": "gpt-4o"}],
"litellm_settings": {"success_callback": ["langfuse"]},
"environment_variables": {"OPENAI_API_KEY": "sk-from-yaml"},
}
proxy_config.update_config_state(config=ProxyRuntimeConfig.from_resolved(baseline))
config: Final = (
proxy_config.get_config_state()
.with_section("model_list", ({"model_name": "gpt-4o"},))
.with_section("litellm_settings", {"success_callback": ["langfuse"]})
.with_section("environment_variables", {"OPENAI_API_KEY": "sk-from-yaml"})
)
await proxy_config.save_config(config)
assert table.rows == {"litellm_settings": {"success_callback": ["langfuse"]}}
assert table.upserted_param_names == ["litellm_settings"]
assert config["environment_variables"] == {"OPENAI_API_KEY": "sk-from-yaml"}
assert config.environment_variables == {"OPENAI_API_KEY": "sk-from-yaml"}
@pytest.mark.asyncio
async def test_ProxyConfig_save_config_db_persists_environment_variables_when_opted_in(monkeypatch):
proxy_config, table = _db_backed_proxy_config(monkeypatch, {})
proxy_config.update_config_state(config={"litellm_settings": {}})
proxy_config.update_config_state(
config=ProxyRuntimeConfig.from_resolved({"litellm_settings": {}})
)
monkeypatch.setattr("litellm.proxy.proxy_server.master_key", "sk-test-salt-key")
config: Final = {"litellm_settings": {}, "environment_variables": {"OPENAI_API_KEY": "sk-explicit"}}
config: Final = proxy_config.get_config_state().with_section(
"environment_variables", {"OPENAI_API_KEY": "sk-explicit"}
)
await proxy_config.save_config(config, include_env_vars=True)
@ -1244,10 +1273,12 @@ async def test_ProxyConfig_save_config_db_persists_environment_variables_when_op
@pytest.mark.asyncio
async def test_ProxyConfig_save_config_persists_unchanged_environment_variables_when_opted_in(monkeypatch):
proxy_config, table = _db_backed_proxy_config(monkeypatch, {})
config: Final = {
"litellm_settings": {},
"environment_variables": {"OPENAI_API_KEY": "sk-explicit"},
}
proxy_config.update_config_state(
config=ProxyRuntimeConfig.from_resolved(
{"litellm_settings": {}, "environment_variables": {"OPENAI_API_KEY": "sk-explicit"}}
)
)
config: Final = proxy_config.get_config_state()
monkeypatch.setattr("litellm.proxy.proxy_server.master_key", "sk-test-salt-key")
await proxy_config.save_config(config)
@ -1377,25 +1408,25 @@ def test_ProxyConfig__get_team_config_missing_team_id_raises():
def test_ProxyConfig_load_team_config_returns_team_dict():
pc = ProxyConfig()
pc.config = {
pc.config = ProxyRuntimeConfig.from_resolved({
"litellm_settings": {
"default_team_settings": [
{"team_id": "ta", "max_budget": 99, "drop_params": True},
]
}
}
})
out = pc.load_team_config(team_id="ta")
assert out == {"team_id": "ta", "max_budget": 99, "drop_params": True}
def test_ProxyConfig_load_team_config_no_settings_returns_empty():
pc = ProxyConfig()
pc.config = {"litellm_settings": {}}
pc.config = ProxyRuntimeConfig.from_resolved({"litellm_settings": {}})
# Missing entry — happy path returns {} (no default_team_settings).
out = pc.load_team_config(team_id="missing")
assert out == {}
# Error-style: a misconfigured team list without team_id raises.
pc.config = {"litellm_settings": {"default_team_settings": [{"no_id": True}]}}
pc.config = ProxyRuntimeConfig.from_resolved({"litellm_settings": {"default_team_settings": [{"no_id": True}]}})
with pytest.raises(Exception, match="team_id missing from team"):
pc.load_team_config(team_id="anything")
@ -1477,11 +1508,9 @@ async def test_ProxyConfig_get_config_loads_from_file(tmp_path, monkeypatch):
monkeypatch.delenv("LITELLM_CONFIG_BUCKET_NAME", raising=False)
pc = ProxyConfig()
cfg = await pc.get_config(config_file_path=str(f))
assert cfg == {
"model_list": [],
"general_settings": {},
"litellm_settings": {},
}
assert cfg.model_list == ()
assert cfg.general_settings == {}
assert cfg.litellm_settings == {}
@pytest.mark.asyncio
@ -1505,8 +1534,8 @@ async def test_ProxyConfig_get_config_from_a_bucket_merges_includes(monkeypatch)
cfg = await ProxyConfig().get_config()
assert cfg["model_list"] == [{"model_name": "included-model"}]
assert "include" not in cfg
assert [dict(model) for model in cfg.model_list] == [{"model_name": "included-model"}]
assert "include" not in (cfg.model_extra or {})
@pytest.mark.asyncio
@ -1597,8 +1626,8 @@ async def test_ProxyConfig_get_config_resolves_keys_held_only_by_the_secret_mana
cfg = await ProxyConfig().get_config(config_file_path=config_file_path)
assert {
"master_key": cfg["general_settings"]["master_key"],
"api_key": cfg["model_list"][0]["litellm_params"]["api_key"],
"master_key": cfg.general_settings["master_key"],
"api_key": cfg.model_list[0]["litellm_params"]["api_key"],
"hosted_keys": litellm._key_management_settings.hosted_keys,
} == {
"master_key": "master-from-vault",
@ -1640,7 +1669,7 @@ async def test_ProxyConfig_get_config_reuses_an_already_initialized_secret_manag
assert {
"client_reused": litellm.secret_manager_client is first_client,
"master_key": second["general_settings"]["master_key"],
"master_key": second.general_settings["master_key"],
} == {"client_reused": True, "master_key": "master-from-vault"}
@ -1656,8 +1685,8 @@ async def test_ProxyConfig_get_config_without_key_management_system_leaves_secre
cfg = await ProxyConfig().get_config(config_file_path=config_file_path)
assert {
"master_key": cfg["general_settings"]["master_key"],
"api_key": cfg["model_list"][0]["litellm_params"]["api_key"],
"master_key": cfg.general_settings["master_key"],
"api_key": cfg.model_list[0]["litellm_params"]["api_key"],
"client": litellm.secret_manager_client,
"warned_about": [call.args[1] for call in warn.call_args_list],
} == {"master_key": None, "api_key": None, "client": None, "warned_about": []}
@ -1674,7 +1703,7 @@ async def test_ProxyConfig_get_config_warns_when_a_reference_is_missing_from_the
cfg = await ProxyConfig().get_config(config_file_path=config_file_path)
assert {
"api_key": cfg["model_list"][0]["litellm_params"]["api_key"],
"api_key": cfg.model_list[0]["litellm_params"]["api_key"],
"warned_about": [call.args[1] for call in warn.call_args_list],
} == {"api_key": None, "warned_about": ["os.environ/NOT_IN_VAULT"]}
@ -1694,7 +1723,7 @@ async def test_ProxyConfig_get_config_does_not_warn_for_a_name_outside_hosted_ke
cfg = await ProxyConfig().get_config(config_file_path=config_file_path)
assert {
"api_key": cfg["model_list"][0]["litellm_params"]["api_key"],
"api_key": cfg.model_list[0]["litellm_params"]["api_key"],
"client_is_up": litellm.secret_manager_client is not None,
"warned_about": [call.args[1] for call in warn.call_args_list],
} == {"api_key": None, "client_is_up": True, "warned_about": []}
@ -1717,7 +1746,7 @@ async def test_ProxyConfig_get_config_does_not_warn_under_write_only_access_mode
cfg = await ProxyConfig().get_config(config_file_path=config_file_path)
assert {
"master_key": cfg["general_settings"]["master_key"],
"master_key": cfg.general_settings["master_key"],
"client_is_up": litellm.secret_manager_client is not None,
"warned_about": [call.args[1] for call in warn.call_args_list],
} == {"master_key": None, "client_is_up": True, "warned_about": []}
@ -1730,13 +1759,13 @@ async def test_ProxyConfig_get_config_does_not_warn_under_write_only_access_mode
def test_ProxyConfig_update_config_state_and_get_config_state_roundtrip():
pc = ProxyConfig()
cfg = {"model_list": [], "general_settings": {"x": 1}, "litellm_settings": {}}
pc.update_config_state(config=cfg)
cfg = ProxyRuntimeConfig.from_resolved({"model_list": [], "general_settings": {"x": 1}, "litellm_settings": {}})
pc.update_config_state(config=ProxyRuntimeConfig.from_resolved(cfg))
out = pc.get_config_state()
assert out == cfg
# Mutating the returned dict must not affect internal state.
out["model_list"].append({"new": True})
assert pc.get_config_state() == cfg
# The returned model is frozen, so it cannot be mutated to corrupt internal state.
with pytest.raises(ValidationError):
out.model_list = ()
def test_ProxyConfig_update_config_state_with_bad_arg_raises():
@ -1745,17 +1774,12 @@ def test_ProxyConfig_update_config_state_with_bad_arg_raises():
pc.update_config_state() # type: ignore[call-arg]
def test_ProxyConfig_get_config_state_handles_undeepcopyable(monkeypatch):
# Pins ProxyConfig.get_config_state — see source for behavior.
def test_ProxyConfig_get_config_state_returns_the_stored_model(monkeypatch):
# get_config_state returns the stored model as is (no copy needed: it is frozen).
pc = ProxyConfig()
class NoCopy:
def __deepcopy__(self, memo):
raise RuntimeError("nope")
pc.config = {"x": NoCopy()} # type: ignore[assignment]
# Exception is caught internally and an empty dict returned.
assert pc.get_config_state() == {}
cfg = ProxyRuntimeConfig.from_resolved({"x": 1})
pc.config = cfg
assert pc.get_config_state() is cfg
# ---------------------------------------------------------------------------
@ -1766,15 +1790,17 @@ def test_ProxyConfig_get_config_state_handles_undeepcopyable(monkeypatch):
def test_ProxyConfig_load_credential_list_returns_items():
pc = ProxyConfig()
creds = pc.load_credential_list(
{
"credential_list": [
ProxyRuntimeConfig.from_resolved(
{
"credential_list": [
{
"credential_name": "openai-key",
"credential_info": {"provider": "openai"},
"credential_values": {"api_key": "sk-x"},
}
]
}
}
)
)
assert len(creds) == 1
dumped = creds[0].model_dump()
@ -1788,7 +1814,9 @@ def test_ProxyConfig_load_credential_list_returns_items():
def test_ProxyConfig_load_credential_list_invalid_entry_raises():
pc = ProxyConfig()
with pytest.raises(ValidationError):
pc.load_credential_list({"credential_list": [{"missing_required": True}]})
pc.load_credential_list(
ProxyRuntimeConfig.from_resolved({"credential_list": [{"missing_required": True}]})
)
# ---------------------------------------------------------------------------
@ -1798,14 +1826,16 @@ def test_ProxyConfig_load_credential_list_invalid_entry_raises():
def test_ProxyConfig_parse_search_tools_returns_parsed():
pc = ProxyConfig()
cfg = {
"search_tools": [
{
"search_tool_name": "web",
"litellm_params": {"search_provider": "google"},
}
]
}
cfg = ProxyRuntimeConfig.from_resolved(
{
"search_tools": [
{
"search_tool_name": "web",
"litellm_params": {"search_provider": "google"},
}
]
}
)
out = pc.parse_search_tools(cfg)
assert out is not None
assert len(out) == 1
@ -1817,7 +1847,7 @@ def test_ProxyConfig_parse_search_tools_returns_parsed():
def test_ProxyConfig_parse_search_tools_missing_returns_none():
pc = ProxyConfig()
assert pc.parse_search_tools({}) is None
assert pc.parse_search_tools(ProxyRuntimeConfig()) is None
def test_ProxyConfig_merge_config_and_db_search_tools_returns_superset():
@ -1884,7 +1914,7 @@ async def test_ProxyConfig__init_search_tools_in_db_loads_merged_tools(monkeypat
pc = ProxyConfig()
pc.update_config_state(
{
ProxyRuntimeConfig.from_resolved({
"search_tools": [
{
"search_tool_name": "shared-search",
@ -1895,7 +1925,7 @@ async def test_ProxyConfig__init_search_tools_in_db_loads_merged_tools(monkeypat
"litellm_params": {"search_provider": "perplexity"},
},
]
}
})
)
db_tools = [
{
@ -1936,7 +1966,7 @@ async def test_ProxyConfig__init_search_tools_in_db_clears_router_when_last_tool
from litellm.proxy import proxy_server
pc = ProxyConfig()
pc.update_config_state({})
pc.update_config_state(ProxyRuntimeConfig.from_resolved({}))
fake_router = MagicMock()
fake_router.search_tools = [{"search_tool_name": "deleted-search", "litellm_params": {}}]
mock_get_db_tools = AsyncMock(return_value=[])
@ -1990,7 +2020,7 @@ async def test_ProxyConfig_reload_search_tools_from_db_serializes_overlapping_re
from litellm.proxy import proxy_server
pc = ProxyConfig()
pc.update_config_state({})
pc.update_config_state(ProxyRuntimeConfig.from_resolved({}))
fake_router = MagicMock()
fake_router.search_tools = []
@ -2049,7 +2079,7 @@ async def test_ProxyConfig_reload_search_tools_from_db_noops_without_prisma(monk
def test_ProxyConfig__load_environment_variables_sets_env(monkeypatch):
monkeypatch.delenv("TEST_LOAD_ENV_X", raising=False)
pc = ProxyConfig()
pc._load_environment_variables({"environment_variables": {"TEST_LOAD_ENV_X": "hello"}})
pc._load_environment_variables(ProxyRuntimeConfig.from_resolved({"environment_variables": {"TEST_LOAD_ENV_X": "hello"}}))
result = {
"TEST_LOAD_ENV_X": os.environ.get("TEST_LOAD_ENV_X"),
"set": True,
@ -2061,7 +2091,7 @@ def test_ProxyConfig__load_environment_variables_sets_env(monkeypatch):
def test_ProxyConfig__load_environment_variables_blocks_dangerous_keys(monkeypatch):
original_path = os.environ.get("PATH", "")
pc = ProxyConfig()
pc._load_environment_variables({"environment_variables": {"PATH": "/evil/bin"}})
pc._load_environment_variables(ProxyRuntimeConfig.from_resolved({"environment_variables": {"PATH": "/evil/bin"}}))
# PATH must be unchanged — it's a blocked key.
assert os.environ.get("PATH", "") == original_path
@ -2087,7 +2117,7 @@ async def test_ProxyConfig_load_config_minimal_yaml(tmp_path, monkeypatch):
snapshot = {
"raised": raised,
"config_loaded": pc.config is not None,
"model_list_key_present": "model_list" in pc.config,
"model_list_key_present": "model_list" in pc.config.model_fields_set,
}
assert snapshot == {
"raised": False,
@ -2321,7 +2351,7 @@ async def test_ProxyConfig_load_config_blank_callback_settings_does_not_crash(tm
async def test_ProxyConfig__init_non_llm_configs_empty_config():
pc = ProxyConfig()
try:
await pc._init_non_llm_configs(config={}, config_file_path=None)
await pc._init_non_llm_configs(config=ProxyRuntimeConfig.from_resolved({}), config_file_path=None)
raised = False
except Exception:
raised = True
@ -2339,7 +2369,7 @@ async def test_ProxyConfig__init_non_llm_configs_premium_invalid_worker_registry
pc = ProxyConfig()
with pytest.raises(ValidationError):
await pc._init_non_llm_configs(
config={"worker_registry": [{"totally": "invalid"}]},
config=ProxyRuntimeConfig.from_resolved({"worker_registry": [{"totally": "invalid"}]}),
config_file_path=None,
)
@ -2350,7 +2380,7 @@ async def test_ProxyConfig__init_non_llm_configs_worker_registry_requires_premiu
pc = ProxyConfig()
with pytest.raises(ValueError, match="Trying to use `worker_registry`You must be a LiteLLM") as exc_info:
await pc._init_non_llm_configs(
config={"worker_registry": [{"worker_id": "worker-a", "name": "Worker A", "url": "http://localhost:4001"}]},
config=ProxyRuntimeConfig.from_resolved({"worker_registry": [{"worker_id": "worker-a", "name": "Worker A", "url": "http://localhost:4001"}]}),
config_file_path=None,
)
message = str(exc_info.value)
@ -2364,12 +2394,12 @@ async def test_ProxyConfig__init_non_llm_configs_worker_registry_loads_for_premi
monkeypatch.setattr("litellm.proxy.proxy_server.premium_user", True)
pc = ProxyConfig()
await pc._init_non_llm_configs(
config={
config=ProxyRuntimeConfig.from_resolved({
"worker_registry": [
{"worker_id": "worker-a", "name": "Worker A", "url": "http://localhost:4001"},
{"worker_id": "worker-b", "name": "Worker B", "url": "https://worker-b.example.com"},
]
},
}),
config_file_path=None,
)
assert [(w.worker_id, w.name, w.url) for w in pc.worker_registry] == [
@ -2383,7 +2413,7 @@ async def test_ProxyConfig__init_non_llm_configs_worker_registry_loads_for_premi
async def test_ProxyConfig__init_non_llm_configs_no_worker_registry_is_never_gated(monkeypatch, premium):
monkeypatch.setattr("litellm.proxy.proxy_server.premium_user", premium)
pc = ProxyConfig()
await pc._init_non_llm_configs(config={}, config_file_path=None)
await pc._init_non_llm_configs(config=ProxyRuntimeConfig.from_resolved({}), config_file_path=None)
assert pc.worker_registry == []
@ -2396,7 +2426,7 @@ async def test_ProxyConfig__init_non_llm_configs_no_worker_registry_is_never_gat
async def test_ProxyConfig__init_policy_engine_no_policies_noop():
pc = ProxyConfig()
try:
await pc._init_policy_engine(config={}, prisma_client=None, llm_router=None)
await pc._init_policy_engine(config=ProxyRuntimeConfig.from_resolved({}), prisma_client=None, llm_router=None)
raised = False
except Exception:
raised = True
@ -2412,13 +2442,9 @@ async def test_ProxyConfig__init_policy_engine_none_config_noop():
pc = ProxyConfig()
# None config returns early without raising.
await pc._init_policy_engine(config=None, prisma_client=None, llm_router=None)
# Error-style: invalid policies value should raise.
with pytest.raises(AttributeError):
await pc._init_policy_engine(
config={"policies": "not-a-list"},
prisma_client=None,
llm_router=None,
)
# Error-style: invalid policies value is rejected at model construction.
with pytest.raises(ValidationError):
ProxyRuntimeConfig.from_resolved({"policies": "not-a-list"})
# ---------------------------------------------------------------------------
@ -2540,7 +2566,7 @@ def _capture_proxy_warnings(config: dict) -> tuple[tuple[str, ...], list[str]]:
verbose_proxy_logger.setLevel(logging.WARNING)
verbose_proxy_logger.addHandler(handler)
try:
result = ProxyConfig()._warn_on_misplaced_jwt_keys(config=config)
result = ProxyConfig()._warn_on_misplaced_jwt_keys(config=ProxyRuntimeConfig.from_resolved(config))
finally:
verbose_proxy_logger.removeHandler(handler)
verbose_proxy_logger.setLevel(original_level)
@ -3203,7 +3229,7 @@ async def test_ProxyConfig__update_llm_router_no_models_smoke(monkeypatch):
pc = ProxyConfig()
async def fake_get_config(*args, **kwargs):
return {}
return ProxyRuntimeConfig.from_resolved({})
monkeypatch.setattr(pc, "get_config", fake_get_config)
monkeypatch.setattr(
@ -3273,7 +3299,7 @@ def test_ProxyConfig__add_callbacks_from_db_config_processes_lists(monkeypatch):
"failure_callback": ["f_a"],
}
}
pc._add_callbacks_from_db_config(cfg)
pc._add_callbacks_from_db_config(ProxyRuntimeConfig.from_resolved(cfg))
snapshot = {
"cb_added": "cb_a" in litellm.callbacks,
"success_added": "s_a" in litellm.success_callback,
@ -3551,7 +3577,7 @@ async def test_ProxyConfig_add_deployment_applies_db_router_settings(monkeypatch
)
async def fake_get_config(*args, **kwargs):
return {}
return ProxyRuntimeConfig.from_resolved({})
monkeypatch.setattr(pc, "get_config", fake_get_config)
monkeypatch.setattr(pc, "_get_models_from_db", AsyncMock(return_value=[]))
@ -3578,7 +3604,7 @@ def _stub_add_deployment_collaborators(
fake_router.get_model_list = MagicMock(return_value=[])
async def fake_get_config(*args: object, **kwargs: object) -> dict[str, object]:
return {}
return ProxyRuntimeConfig.from_resolved({})
monkeypatch.setattr(litellm, "credential_list", [])
monkeypatch.setattr(pc, "get_config", fake_get_config)
@ -4286,7 +4312,7 @@ class _FakeAgentRow:
async def test_ProxyConfig__init_non_llm_configs_registers_agents_from_config(clean_agent_registry, config_key):
"""The documented ``agents:`` key must register agents, as must the legacy ``agent_list:``."""
await ProxyConfig()._init_non_llm_configs(
config={config_key: [_config_agent("config-agent")]},
config=ProxyRuntimeConfig.from_resolved({config_key: [_config_agent("config-agent")]}),
config_file_path=None,
)
@ -4297,7 +4323,7 @@ async def test_ProxyConfig__init_non_llm_configs_registers_agents_from_config(cl
async def test_ProxyConfig__init_agents_in_db_keeps_config_defined_agents(clean_agent_registry):
"""A DB reload rebuilds the registry; config-defined agents must survive it alongside DB rows."""
await ProxyConfig()._init_non_llm_configs(
config={"agents": [_config_agent("config-agent")]},
config=ProxyRuntimeConfig.from_resolved({"agents": [_config_agent("config-agent")]}),
config_file_path=None,
)
@ -4333,7 +4359,7 @@ async def test_ProxyStartupEvent_jwt_auth_resolves_agent_claims_against_live_reg
)
if agents_source == "config":
await ProxyConfig()._init_non_llm_configs(
config={"agents": [_config_agent("loaded-agent")]},
config=ProxyRuntimeConfig.from_resolved({"agents": [_config_agent("loaded-agent")]}),
config_file_path=None,
)
elif agents_source == "db":
@ -4386,7 +4412,7 @@ async def test_ProxyConfig__init_non_llm_configs_prefers_agents_key_by_presence(
Selecting on truthiness instead would silently register the legacy entries
for a config that spells out ``agents: []``.
"""
await ProxyConfig()._init_non_llm_configs(config=config, config_file_path=None)
await ProxyConfig()._init_non_llm_configs(config=ProxyRuntimeConfig.from_resolved(config), config_file_path=None)
assert [agent.agent_name for agent in clean_agent_registry.get_agent_list()] == expected_agent_names
@ -4402,7 +4428,7 @@ async def test_ProxyConfig__init_non_llm_configs_empty_agents_key_clears_remembe
clean_agent_registry.load_agents_from_config([_config_agent("stale-agent")])
assert clean_agent_registry.config_agents != ()
await ProxyConfig()._init_non_llm_configs(config={"agents": []}, config_file_path=None)
await ProxyConfig()._init_non_llm_configs(config=ProxyRuntimeConfig.from_resolved({"agents": []}), config_file_path=None)
assert clean_agent_registry.config_agents == ()
clean_agent_registry.load_agents_from_db_and_config(db_agents=None)
@ -4714,3 +4740,52 @@ def test_websearch_interception_settings_can_be_named_in_supported_db_objects(mo
monkeypatch.setattr(proxy_server, "general_settings", {"supported_db_objects": ["models"]})
assert proxy_server.should_load_db_object(object_type="websearch_interception_settings") is False
@pytest.mark.asyncio
async def test_ProxyConfig_save_config_writes_yaml_with_only_the_section_that_changed(tmp_path, monkeypatch):
"""A file-backed save replaces the file with the updated model's content; the
untouched section round-trips verbatim while the edited one holds the new value."""
config_file: Final = tmp_path / "config.yaml"
config_file.write_text("general_settings:\n max_parallel_requests: 5\nrouter_settings:\n num_retries: 3\n")
monkeypatch.setattr("litellm.proxy.proxy_server.user_config_file_path", str(config_file))
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", None)
monkeypatch.setattr("litellm.proxy.proxy_server.store_model_in_db", False)
monkeypatch.setattr("litellm.proxy.proxy_server.general_settings", {})
proxy_config: Final = ProxyConfig()
loaded: Final = await proxy_config.get_config(config_file_path=str(config_file))
updated: Final = loaded.with_section(
"general_settings", {**loaded.general_settings, "allowed_ips": ["127.0.0.1"]}
)
await proxy_config.save_config(updated)
import yaml as _yaml
assert _yaml.safe_load(config_file.read_text()) == {
"general_settings": {"max_parallel_requests": 5, "allowed_ips": ["127.0.0.1"]},
"router_settings": {"num_retries": 3},
}
@pytest.mark.asyncio
async def test_ProxyConfig_save_config_db_writes_exactly_the_differing_section(monkeypatch):
proxy_config, table = _db_backed_proxy_config(
monkeypatch,
{"general_settings": {"db_only": "stored"}, "router_settings": {"db_only": "stored"}},
)
proxy_config.update_config_state(
config=ProxyRuntimeConfig.from_resolved(
{"general_settings": {"a": 1}, "router_settings": {"num_retries": 1}}
)
)
await proxy_config.save_config(
proxy_config.get_config_state().with_section("general_settings", {"a": 2})
)
assert table.rows == {
"general_settings": {"db_only": "stored", "a": 2},
"router_settings": {"db_only": "stored"},
}
assert table.upserted_param_names == ["general_settings"]

View file

@ -20,6 +20,7 @@ from unittest.mock import AsyncMock, MagicMock
import pytest
from .conftest import VOLATILE_KEYS, normalize
from litellm.proxy._types import ProxyRuntimeConfig
def _seed_settings_store(monkeypatch, db_row: dict, yaml_values: dict | None = None) -> None:
@ -888,7 +889,7 @@ def test_config_callback_delete_happy_admin(client, auth_as, mock_prisma, monkey
fake_proxy_config = MagicMock()
fake_proxy_config.get_config = AsyncMock(
return_value={"litellm_settings": {"success_callback": ["langfuse", "slack"]}}
return_value=ProxyRuntimeConfig.from_resolved({"litellm_settings": {"success_callback": ["langfuse", "slack"]}})
)
fake_proxy_config.save_config = AsyncMock()
fake_proxy_config.add_deployment = AsyncMock()
@ -933,7 +934,7 @@ def test_config_callback_delete_not_found(client, auth_as, mock_prisma, monkeypa
monkeypatch.setattr(ps, "store_model_in_db", True)
fake_proxy_config = MagicMock()
fake_proxy_config.get_config = AsyncMock(return_value={"litellm_settings": {"success_callback": ["slack"]}})
fake_proxy_config.get_config = AsyncMock(return_value=ProxyRuntimeConfig.from_resolved({"litellm_settings": {"success_callback": ["slack"]}}))
monkeypatch.setattr(ps, "proxy_config", fake_proxy_config)
with auth_as(LitellmUserRoles.PROXY_ADMIN):
@ -961,11 +962,11 @@ def test_get_config_callbacks_happy(client, auth_as, mock_prisma, monkeypatch):
fake_proxy_config = MagicMock()
fake_proxy_config.get_config = AsyncMock(
return_value={
return_value=ProxyRuntimeConfig.from_resolved({
"litellm_settings": {"success_callback": []},
"general_settings": {},
"environment_variables": {},
}
})
)
monkeypatch.setattr(ps, "proxy_config", fake_proxy_config)
@ -1029,11 +1030,11 @@ def _install_callbacks_config(monkeypatch, mock_prisma):
fake_proxy_config = MagicMock()
fake_proxy_config.get_config = AsyncMock(
return_value={
return_value=ProxyRuntimeConfig.from_resolved({
"litellm_settings": {"success_callback": ["langfuse", "datadog", "otel"]},
"general_settings": {"alerting": ["slack"]},
"environment_variables": dict(_CALLBACK_ENV_FIXTURE),
}
})
)
monkeypatch.setattr(ps, "proxy_config", fake_proxy_config)
@ -1175,7 +1176,7 @@ def test_get_config_callbacks_redacts_email_alerting_vars_for_view_only_admin(
fake_proxy_config = MagicMock()
fake_proxy_config.get_config = AsyncMock(
return_value={
return_value=ProxyRuntimeConfig.from_resolved({
"litellm_settings": {"success_callback": []},
"general_settings": {"alerting": ["email"]},
"environment_variables": {
@ -1188,7 +1189,7 @@ def test_get_config_callbacks_redacts_email_alerting_vars_for_view_only_admin(
"EMAIL_LOGO_URL": "https://example.com/logo.png",
"EMAIL_SUPPORT_CONTACT": "support@example.com",
},
}
})
)
monkeypatch.setattr(ps, "proxy_config", fake_proxy_config)
@ -1228,11 +1229,11 @@ def test_get_config_callbacks_appends_runtime_only_callbacks(client, auth_as, mo
fake_proxy_config = MagicMock()
fake_proxy_config.get_config = AsyncMock(
return_value={
return_value=ProxyRuntimeConfig.from_resolved({
"litellm_settings": {"success_callback": ["langfuse"]},
"general_settings": {},
"environment_variables": dict(_CALLBACK_ENV_FIXTURE),
}
})
)
monkeypatch.setattr(ps, "proxy_config", fake_proxy_config)
@ -1269,11 +1270,11 @@ def test_get_config_callbacks_accepts_scalar_and_null_yaml_callbacks(client, aut
fake_proxy_config = MagicMock()
fake_proxy_config.get_config = AsyncMock(
return_value={
return_value=ProxyRuntimeConfig.from_resolved({
"litellm_settings": {"success_callback": "langfuse", "failure_callback": None, "callbacks": None},
"general_settings": {},
"environment_variables": dict(_CALLBACK_ENV_FIXTURE),
}
})
)
monkeypatch.setattr(ps, "proxy_config", fake_proxy_config)
@ -1308,11 +1309,11 @@ def test_get_config_callbacks_deduplicates_configured_and_runtime(client, auth_a
fake_proxy_config = MagicMock()
fake_proxy_config.get_config = AsyncMock(
return_value={
return_value=ProxyRuntimeConfig.from_resolved({
"litellm_settings": {"success_callback": ["langfuse", "arize", "logfire"]},
"general_settings": {},
"environment_variables": dict(_CALLBACK_ENV_FIXTURE),
}
})
)
monkeypatch.setattr(ps, "proxy_config", fake_proxy_config)
@ -1353,11 +1354,11 @@ def test_get_config_callbacks_keeps_yaml_otel_family_callbacks_next_to_configure
fake_proxy_config = MagicMock()
fake_proxy_config.get_config = AsyncMock(
return_value={
return_value=ProxyRuntimeConfig.from_resolved({
"litellm_settings": {"callbacks": ["langfuse_otel"]},
"general_settings": {},
"environment_variables": dict(_CALLBACK_ENV_FIXTURE),
}
})
)
monkeypatch.setattr(ps, "proxy_config", fake_proxy_config)
@ -1430,11 +1431,11 @@ def test_get_config_callbacks_deduplicates_dotted_path_callback(
fake_proxy_config = MagicMock()
fake_proxy_config.get_config = AsyncMock(
return_value={
return_value=ProxyRuntimeConfig.from_resolved({
"litellm_settings": {config_key: [dotted_path]},
"general_settings": {},
"environment_variables": dict(_CALLBACK_ENV_FIXTURE),
}
})
)
monkeypatch.setattr(ps, "proxy_config", fake_proxy_config)
@ -1465,11 +1466,11 @@ def test_get_config_callbacks_lists_dict_shaped_config_callbacks(client, auth_as
fake_proxy_config = MagicMock()
fake_proxy_config.get_config = AsyncMock(
return_value={
return_value=ProxyRuntimeConfig.from_resolved({
"litellm_settings": {"success_callback": {"langsmith": {"batch_size": 1}}},
"general_settings": {},
"environment_variables": dict(_CALLBACK_ENV_FIXTURE),
}
})
)
monkeypatch.setattr(ps, "proxy_config", fake_proxy_config)
@ -1500,11 +1501,11 @@ def test_get_config_callbacks_excludes_internal_runtime_callbacks(client, auth_a
fake_proxy_config = MagicMock()
fake_proxy_config.get_config = AsyncMock(
return_value={
return_value=ProxyRuntimeConfig.from_resolved({
"litellm_settings": {"success_callback": []},
"general_settings": {},
"environment_variables": dict(_CALLBACK_ENV_FIXTURE),
}
})
)
monkeypatch.setattr(ps, "proxy_config", fake_proxy_config)
@ -1585,11 +1586,11 @@ def test_get_config_callbacks_redacts_runtime_only_row_secrets_for_view_only_adm
fake_proxy_config = MagicMock()
fake_proxy_config.get_config = AsyncMock(
return_value={
return_value=ProxyRuntimeConfig.from_resolved({
"litellm_settings": {"success_callback": []},
"general_settings": {},
"environment_variables": dict(_CALLBACK_ENV_FIXTURE),
}
})
)
monkeypatch.setattr(ps, "proxy_config", fake_proxy_config)

View file

@ -26,6 +26,7 @@ from litellm.proxy import proxy_server
from litellm.utils import _invalidate_model_cost_lowercase_map
from .conftest import normalize # type: ignore[import-not-found]
from litellm.proxy._types import ProxyRuntimeConfig
@pytest.mark.parametrize(
@ -350,7 +351,7 @@ def test_v2_model_info_reports_pricing_overrides_to_the_admin_ui(client, auth_as
monkeypatch.setattr(proxy_server, "llm_model_list", model_list)
monkeypatch.setattr(proxy_server, "prisma_client", MagicMock())
monkeypatch.setattr(proxy_server, "user_model", None)
monkeypatch.setattr(proxy_server.proxy_config, "get_config", AsyncMock(return_value={}))
monkeypatch.setattr(proxy_server.proxy_config, "get_config", AsyncMock(return_value=ProxyRuntimeConfig.from_resolved({})))
monkeypatch.setattr(
proxy_server,
"_apply_search_filter_to_models",
@ -739,7 +740,7 @@ def mixed_auto_router_router(monkeypatch):
monkeypatch.setattr(proxy_server, "llm_model_list", model_list)
monkeypatch.setattr(proxy_server, "prisma_client", MagicMock())
monkeypatch.setattr(proxy_server, "user_model", None)
monkeypatch.setattr(proxy_server.proxy_config, "get_config", AsyncMock(return_value={}))
monkeypatch.setattr(proxy_server.proxy_config, "get_config", AsyncMock(return_value=ProxyRuntimeConfig.from_resolved({})))
monkeypatch.setattr(
proxy_server,
"_apply_search_filter_to_models",
@ -806,7 +807,7 @@ async def test_model_info_v2_query_sentinel_does_not_filter(monkeypatch, mixed_a
from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth
monkeypatch.setattr(proxy_server, "prisma_client", MagicMock())
monkeypatch.setattr(proxy_server.proxy_config, "get_config", AsyncMock(return_value={}))
monkeypatch.setattr(proxy_server.proxy_config, "get_config", AsyncMock(return_value=ProxyRuntimeConfig.from_resolved({})))
monkeypatch.setattr(
proxy_server,
"_apply_search_filter_to_models",
@ -871,7 +872,7 @@ def access_group_router(monkeypatch):
monkeypatch.setattr(proxy_server, "llm_model_list", model_list)
monkeypatch.setattr(proxy_server, "prisma_client", MagicMock())
monkeypatch.setattr(proxy_server, "user_model", None)
monkeypatch.setattr(proxy_server.proxy_config, "get_config", AsyncMock(return_value={}))
monkeypatch.setattr(proxy_server.proxy_config, "get_config", AsyncMock(return_value=ProxyRuntimeConfig.from_resolved({})))
monkeypatch.setattr(
proxy_server,
"_apply_search_filter_to_models",

View file

@ -18,6 +18,7 @@ import litellm.proxy.proxy_server as ps
from litellm.proxy._types import (
LiteLLM_UserTable,
LitellmUserRoles,
ProxyRuntimeConfig,
UserAPIKeyAuth,
)
from litellm.proxy.common_utils.model_listing_utils import (
@ -119,7 +120,7 @@ async def test_model_info_v2_translates_team_model_name(monkeypatch):
monkeypatch.setattr(ps, "llm_router", router)
monkeypatch.setattr(ps, "prisma_client", MagicMock())
monkeypatch.setattr(ps.proxy_config, "get_config", AsyncMock(return_value={}))
monkeypatch.setattr(ps.proxy_config, "get_config", AsyncMock(return_value=ProxyRuntimeConfig.from_resolved({})))
monkeypatch.setattr(
ps,
"_apply_search_filter_to_models",
@ -175,7 +176,7 @@ async def test_model_info_v2_exact_model_filter_matches_team_public_name(monkeyp
monkeypatch.setattr(ps, "llm_router", router)
monkeypatch.setattr(ps, "user_model", None)
monkeypatch.setattr(ps, "prisma_client", MagicMock())
monkeypatch.setattr(ps.proxy_config, "get_config", AsyncMock(return_value={}))
monkeypatch.setattr(ps.proxy_config, "get_config", AsyncMock(return_value=ProxyRuntimeConfig.from_resolved({})))
monkeypatch.setattr(
ps,
"_apply_search_filter_to_models",

View file

@ -335,3 +335,52 @@ def test_virtual_key_mapping_counts_as_configured_when_any_issuer_sets_the_claim
)
assert jwt_auth.is_virtual_key_mapping_configured() is is_configured
def test_proxy_runtime_config_rejects_field_assignment():
from litellm.proxy._types import ProxyRuntimeConfig
config = ProxyRuntimeConfig.from_resolved({"litellm_settings": {"drop_params": True}})
with pytest.raises(ValidationError):
config.litellm_settings = {}
def test_proxy_runtime_config_loads_a_null_section_as_empty():
from litellm.proxy._types import ProxyRuntimeConfig
config = ProxyRuntimeConfig.from_resolved({"litellm_settings": None, "general_settings": {"a": 1}})
assert config.litellm_settings == {}
assert config.general_settings == {"a": 1}
def test_proxy_runtime_config_to_mapping_keeps_unknown_sections():
from litellm.proxy._types import ProxyRuntimeConfig
config = ProxyRuntimeConfig.from_resolved({"my_custom_section": {"k": "v"}, "general_settings": {"a": 1}})
assert config.to_mapping()["my_custom_section"] == {"k": "v"}
def test_proxy_runtime_config_with_section_keeps_the_loaded_baseline():
from litellm.proxy._types import ProxyRuntimeConfig
config = ProxyRuntimeConfig.from_resolved({"general_settings": {"a": 1}})
updated = config.with_section("general_settings", {"a": 2})
assert updated.baseline["general_settings"] == {"a": 1}
assert updated.to_mapping()["general_settings"] == {"a": 2}
assert config.general_settings == {"a": 1}
def test_proxy_runtime_config_to_mapping_yaml_dumps_lists_not_tuples():
import yaml
from litellm.proxy._types import ProxyRuntimeConfig
config = ProxyRuntimeConfig.from_resolved({"model_list": [{"model_name": "m"}], "guardrails": [{"g": 1}]})
dumped = yaml.safe_dump(dict(config.to_mapping()))
assert "!!python/tuple" not in dumped
assert yaml.safe_load(dumped) == {"model_list": [{"model_name": "m"}], "guardrails": [{"g": 1}]}

View file

@ -19,6 +19,7 @@ from litellm.proxy.management_endpoints.fallback_management_endpoints import (
delete_fallback,
get_fallback,
)
from litellm.proxy._types import ProxyRuntimeConfig
class TestFallbackCreateRequest:
@ -130,7 +131,7 @@ class TestCreateFallback:
def mock_proxy_config(self):
"""Create a mock proxy config"""
config = MagicMock()
config.get_config = AsyncMock(return_value={"router_settings": {}})
config.get_config = AsyncMock(return_value=ProxyRuntimeConfig.from_resolved({"router_settings": {}}))
return config
@pytest.fixture
@ -438,11 +439,11 @@ class TestDeleteFallback:
"""Create a mock proxy config"""
config = MagicMock()
config.get_config = AsyncMock(
return_value={
return_value=ProxyRuntimeConfig.from_resolved({
"router_settings": {
"fallbacks": [{"gpt-3.5-turbo": ["gpt-4", "claude-3-haiku"]}]
}
}
})
)
return config

View file

@ -19,6 +19,7 @@ from uvicorn.config import LOOP_FACTORIES
from uvicorn.importer import import_from_string
from litellm.proxy.proxy_cli import ProxyInitializationHelpers, run_server
from litellm.proxy._types import ProxyRuntimeConfig
@pytest.fixture(autouse=True)
@ -870,13 +871,13 @@ class TestProxyInitializationHelpers:
save_worker_config=MagicMock(),
)
mock_proxy_module.ProxyConfig.return_value.get_config = AsyncMock(
return_value={
return_value=ProxyRuntimeConfig.from_resolved({
"general_settings": {
"database_url": "postgresql://test:test@localhost:5432/test",
"database_connection_pool_limit": 5,
**timeout_config,
}
}
})
)
clean_env = {
@ -996,7 +997,7 @@ class TestProxyInitializationHelpers:
save_worker_config=MagicMock(),
)
mock_proxy_module.ProxyConfig.return_value.get_config = AsyncMock(
return_value={
return_value=ProxyRuntimeConfig.from_resolved({
"general_settings": {
"database_url": "postgresql://test:test@localhost:5432/test",
"database_connect_timeout": 15,
@ -1006,7 +1007,7 @@ class TestProxyInitializationHelpers:
"statement_cache_size": 0,
},
}
}
})
)
clean_env = {
@ -1122,12 +1123,12 @@ class TestProxyInitializationHelpers:
save_worker_config=MagicMock(),
)
mock_proxy_module.ProxyConfig.return_value.get_config = AsyncMock(
return_value={
return_value=ProxyRuntimeConfig.from_resolved({
"general_settings": {
"database_url": "postgresql://test:test@localhost:5432/test",
"database_disable_prepared_statements": config_value,
}
}
})
)
clean_env = {
@ -1773,7 +1774,7 @@ class TestProxyInitializationHelpers:
# Mock the ProxyConfig.get_config method to return a proper async config
async def mock_get_config(config_file_path=None):
return {"general_settings": {}, "litellm_settings": {}}
return ProxyRuntimeConfig.from_resolved({"general_settings": {}, "litellm_settings": {}})
mock_proxy_config_instance = MagicMock()
mock_proxy_config_instance.get_config = mock_get_config
@ -2664,7 +2665,7 @@ def _run_server_and_capture_urls(
) -> dict:
loaded_config = yaml.safe_load(Path(config_path).read_text())
mock_proxy_config = MagicMock()
mock_proxy_config.return_value.get_config = AsyncMock(return_value=loaded_config)
mock_proxy_config.return_value.get_config = AsyncMock(return_value=ProxyRuntimeConfig.from_resolved(loaded_config))
mock_proxy_module = MagicMock(
app=MagicMock(),
ProxyConfig=mock_proxy_config,

View file

@ -37,6 +37,7 @@ from litellm.proxy._types import (
ModelAccessDeniedProxyException,
ProxyErrorTypes,
ProxyException,
ProxyRuntimeConfig,
TokenCountRequest,
UserAPIKeyAuth,
)
@ -1163,7 +1164,7 @@ def test_get_config_custom_callback_api_env_vars(monkeypatch):
mock_router = MagicMock()
mock_router.get_settings.return_value = {}
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", mock_router)
monkeypatch.setattr(proxy_config, "get_config", AsyncMock(return_value=config_data))
monkeypatch.setattr(proxy_config, "get_config", AsyncMock(return_value=ProxyRuntimeConfig.from_resolved(config_data)))
# Bypass auth dependency
original_overrides = app.dependency_overrides.copy()
@ -1213,7 +1214,7 @@ def test_get_config_callbacks_fall_back_to_process_env(mock_env_vars, monkeypatc
mock_router = MagicMock()
mock_router.get_settings.return_value = {}
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", mock_router)
monkeypatch.setattr(proxy_config, "get_config", AsyncMock(return_value=config_data))
monkeypatch.setattr(proxy_config, "get_config", AsyncMock(return_value=ProxyRuntimeConfig.from_resolved(config_data)))
original_overrides = app.dependency_overrides.copy()
app.dependency_overrides[user_api_key_auth] = lambda: UserAPIKeyAuth(
@ -1260,7 +1261,7 @@ def test_get_config_callback_env_secrets_redacted_for_non_admin(mock_env_vars, m
mock_router = MagicMock()
mock_router.get_settings.return_value = {}
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", mock_router)
monkeypatch.setattr(proxy_config, "get_config", AsyncMock(return_value=config_data))
monkeypatch.setattr(proxy_config, "get_config", AsyncMock(return_value=ProxyRuntimeConfig.from_resolved(config_data)))
original_overrides = app.dependency_overrides.copy()
app.dependency_overrides[user_api_key_auth] = lambda: UserAPIKeyAuth(
@ -1310,7 +1311,7 @@ def test_get_config_returns_email_settings(monkeypatch):
mock_router = MagicMock()
mock_router.get_settings.return_value = {}
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", mock_router)
monkeypatch.setattr(proxy_config, "get_config", AsyncMock(return_value=config_data))
monkeypatch.setattr(proxy_config, "get_config", AsyncMock(return_value=ProxyRuntimeConfig.from_resolved(config_data)))
original_overrides = app.dependency_overrides.copy()
app.dependency_overrides[user_api_key_auth] = lambda: UserAPIKeyAuth(
@ -1347,7 +1348,7 @@ def _get_email_alert_variables(monkeypatch, config_data):
mock_router = MagicMock()
mock_router.get_settings.return_value = {}
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", mock_router)
monkeypatch.setattr(proxy_config, "get_config", AsyncMock(return_value=config_data))
monkeypatch.setattr(proxy_config, "get_config", AsyncMock(return_value=ProxyRuntimeConfig.from_resolved(config_data)))
original_overrides = app.dependency_overrides.copy()
app.dependency_overrides[user_api_key_auth] = lambda: UserAPIKeyAuth(
@ -1472,7 +1473,7 @@ def test_get_config_returns_slack_webhook(monkeypatch):
mock_logging.slack_alerting_instance._all_possible_alert_types.return_value = ["budget_alerts"]
mock_logging.slack_alerting_instance.alert_to_webhook_url = {}
monkeypatch.setattr("litellm.proxy.proxy_server.proxy_logging_obj", mock_logging)
monkeypatch.setattr(proxy_config, "get_config", AsyncMock(return_value=config_data))
monkeypatch.setattr(proxy_config, "get_config", AsyncMock(return_value=ProxyRuntimeConfig.from_resolved(config_data)))
original_overrides = app.dependency_overrides.copy()
app.dependency_overrides[user_api_key_auth] = lambda: UserAPIKeyAuth(
@ -1522,7 +1523,7 @@ def test_get_config_cleared_slack_webhook_not_overridden_by_os_env(monkeypatch):
mock_logging.slack_alerting_instance._all_possible_alert_types.return_value = ["budget_alerts"]
mock_logging.slack_alerting_instance.alert_to_webhook_url = {}
monkeypatch.setattr("litellm.proxy.proxy_server.proxy_logging_obj", mock_logging)
monkeypatch.setattr(proxy_config, "get_config", AsyncMock(return_value=config_data))
monkeypatch.setattr(proxy_config, "get_config", AsyncMock(return_value=ProxyRuntimeConfig.from_resolved(config_data)))
original_overrides = app.dependency_overrides.copy()
app.dependency_overrides[user_api_key_auth] = lambda: UserAPIKeyAuth(
@ -3008,7 +3009,7 @@ async def test_delete_deployment_type_mismatch():
mock_llm_router.delete_deployment = MagicMock(side_effect=mock_delete_deployment)
async def mock_get_config(config_file_path):
return {
return ProxyRuntimeConfig.from_resolved({
"model_list": [
{
"model_name": "openai-gpt-4o",
@ -3021,7 +3022,7 @@ async def test_delete_deployment_type_mismatch():
"model_info": {"id": 12345679},
},
]
}
})
pc.get_config = AsyncMock(side_effect=mock_get_config)
@ -4088,8 +4089,10 @@ async def test_write_config_to_file(monkeypatch):
with patch("builtins.open", mock_file_open), patch("yaml.dump") as mock_yaml_dump:
# Call save_config with test data
test_config = {"key": "value", "model_list": ["model1", "model2"]}
await proxy_config.save_config(new_config=test_config)
new_config = ProxyRuntimeConfig().model_copy(
update={"key": "value", "model_list": [{"model_name": "model1"}, {"model_name": "model2"}]}
)
await proxy_config.save_config(new_config=new_config)
# Verify that file was NOT opened for writing (since store_model_in_db=True)
mock_file_open.assert_not_called()
@ -4135,7 +4138,7 @@ async def test_write_config_to_file_when_store_model_in_db_false(monkeypatch):
with patch("builtins.open", mock_file_open), patch("yaml.dump") as mock_yaml_dump:
# Call save_config with test data
test_config = {"key": "value", "other_key": "other_value"}
await proxy_config.save_config(new_config=test_config)
await proxy_config.save_config(new_config=ProxyRuntimeConfig.from_resolved(test_config))
# Verify that file WAS opened for writing (since store_model_in_db=False)
mock_file_open.assert_called_once_with(f"{test_config_path}", "w")
@ -6993,7 +6996,7 @@ def test_get_config_normalizes_string_callbacks(monkeypatch):
mock_router = MagicMock()
mock_router.get_settings.return_value = {}
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", mock_router)
monkeypatch.setattr(proxy_config, "get_config", AsyncMock(return_value=config_data))
monkeypatch.setattr(proxy_config, "get_config", AsyncMock(return_value=ProxyRuntimeConfig.from_resolved(config_data)))
original_overrides = app.dependency_overrides.copy()
app.dependency_overrides[user_api_key_auth] = lambda: UserAPIKeyAuth(
@ -11237,7 +11240,7 @@ class TestDeleteDeploymentSync:
mock_router.delete_deployment.return_value = MagicMock()
with patch("litellm.proxy.proxy_server.llm_router", mock_router):
with patch.object(proxy_config, "get_config", AsyncMock(return_value={"model_list": []})):
with patch.object(proxy_config, "get_config", AsyncMock(return_value=ProxyRuntimeConfig.from_resolved({"model_list": []}))):
still_desired = await proxy_config._delete_deployment(db_models=[])
mock_router.delete_deployment.assert_called_once_with(id="model-id-to-evict")
@ -11260,7 +11263,7 @@ class TestDeleteDeploymentSync:
mock_router = MagicMock()
with patch("litellm.proxy.proxy_server.llm_router", mock_router):
with patch.object(proxy_config, "get_config", AsyncMock(return_value={})):
with patch.object(proxy_config, "get_config", AsyncMock(return_value=ProxyRuntimeConfig.from_resolved({}))):
await proxy_config._update_llm_router(new_models=None, proxy_logging_obj=MagicMock())
mock_router.delete_deployment.assert_not_called()
@ -11492,7 +11495,7 @@ async def test_update_config_field_throttle_persists_to_litellm_settings(monkeyp
saved: dict = {}
async def fake_get_config():
return {"litellm_settings": {}}
return ProxyRuntimeConfig.from_resolved({"litellm_settings": {}})
async def fake_save_config(new_config=None):
saved.update(new_config or {})
@ -11604,7 +11607,7 @@ async def test_update_config_field_max_ui_session_budget_sets_live_value(monkeyp
saved: dict = {}
async def fake_get_config():
return {"litellm_settings": {}}
return ProxyRuntimeConfig.from_resolved({"litellm_settings": {}})
async def fake_save_config(new_config=None):
saved.update(new_config or {})
@ -11824,7 +11827,7 @@ async def test_update_config_field_prompt_caching_persists_to_litellm_settings(m
saved: dict = {}
async def fake_get_config():
return {"litellm_settings": {}}
return ProxyRuntimeConfig.from_resolved({"litellm_settings": {}})
async def fake_save_config(new_config=None):
saved.update(new_config or {})
@ -11873,7 +11876,7 @@ async def test_update_config_field_prompt_caching_rejects_invalid(monkeypatch, f
from litellm.proxy.proxy_server import update_config_general_settings
async def fake_get_config():
return {"litellm_settings": {}}
return ProxyRuntimeConfig.from_resolved({"litellm_settings": {}})
monkeypatch.setattr(ps.proxy_config, "get_config", fake_get_config)
monkeypatch.setattr(ps, "prisma_client", MagicMock())
@ -11915,7 +11918,7 @@ async def test_reset_config_field_restores_type_default(monkeypatch, field_name,
saved: dict = {}
async def fake_get_config():
return {"litellm_settings": {field_name: "stale"}}
return ProxyRuntimeConfig.from_resolved({"litellm_settings": {field_name: "stale"}})
async def fake_save_config(new_config=None):
saved.update(new_config or {})
@ -11952,7 +11955,7 @@ async def test_update_config_field_throttle_rejects_invalid(monkeypatch, bad_val
from litellm.proxy.proxy_server import update_config_general_settings
async def fake_get_config():
return {"litellm_settings": {}}
return ProxyRuntimeConfig.from_resolved({"litellm_settings": {}})
monkeypatch.setattr(ps.proxy_config, "get_config", fake_get_config)
monkeypatch.setattr(ps, "prisma_client", MagicMock())
@ -12703,7 +12706,7 @@ def test_delete_callback_audits_litellm_settings_deletion(_update_config_setup,
monkeypatch.setattr(
real_proxy_config,
"get_config",
AsyncMock(return_value={"litellm_settings": {"success_callback": ["langfuse", "datadog"]}}),
AsyncMock(return_value=ProxyRuntimeConfig.from_resolved({"litellm_settings": {"success_callback": ["langfuse", "datadog"]}})),
)
monkeypatch.setattr(real_proxy_config, "save_config", AsyncMock(return_value=None))
try:
@ -12736,7 +12739,7 @@ def test_delete_callback_audits_before_reload_failure(_update_config_setup, monk
monkeypatch.setattr(
real_proxy_config,
"get_config",
AsyncMock(return_value={"litellm_settings": {"success_callback": ["langfuse", "datadog"]}}),
AsyncMock(return_value=ProxyRuntimeConfig.from_resolved({"litellm_settings": {"success_callback": ["langfuse", "datadog"]}})),
)
monkeypatch.setattr(real_proxy_config, "save_config", AsyncMock(return_value=None))
monkeypatch.setattr(

View file

@ -11,6 +11,7 @@ catch-all handler in _update_llm_router.
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from litellm.proxy._types import ProxyRuntimeConfig
from litellm.proxy.proxy_server import ProxyConfig
@ -89,7 +90,7 @@ class TestUpdateLlmRouterResilience:
proxy_config,
"get_config",
new_callable=AsyncMock,
return_value={"model_list": []},
return_value=ProxyRuntimeConfig.from_resolved({"model_list": []}),
),
patch.object(proxy_config, "_add_deployment", return_value=1) as mock_add,
patch.object(
@ -161,7 +162,7 @@ class TestDeleteDeploymentResilience:
proxy_config,
"get_config",
new_callable=AsyncMock,
return_value={
return_value=ProxyRuntimeConfig.from_resolved({
"model_list": [
{
"model_name": "gpt-4",
@ -169,7 +170,7 @@ class TestDeleteDeploymentResilience:
"model_info": {"id": "config-id-1"},
}
]
},
}),
),
patch("litellm.proxy.proxy_server.llm_router", mock_router),
patch("litellm.proxy.proxy_server.premium_user", False),
@ -265,7 +266,7 @@ class TestDeleteDeploymentKeepsPluginConfigModels:
}
proxy_config = ProxyConfig()
with (
patch.object(proxy_config, "get_config", new_callable=AsyncMock, return_value=raw_config),
patch.object(proxy_config, "get_config", new_callable=AsyncMock, return_value=ProxyRuntimeConfig.from_resolved(raw_config)),
patch("litellm.proxy.proxy_server.llm_router", router),
patch("litellm.proxy.proxy_server.user_config_file_path", config_file_path),
patch("litellm.proxy.proxy_server.premium_user", False),

View file

@ -5,7 +5,7 @@ import pytest
from fastapi.testclient import TestClient
from litellm.proxy._types import DefaultInternalUserParams, LitellmUserRoles
from litellm.proxy._types import DefaultInternalUserParams, LitellmUserRoles, ProxyRuntimeConfig
from litellm.proxy.proxy_server import app
from litellm.types.proxy.management_endpoints.ui_sso import (
DefaultTeamSSOParams,
@ -18,7 +18,7 @@ client = TestClient(app)
@pytest.fixture
def mock_proxy_config(monkeypatch):
"""Mock the proxy_config to avoid actual file operations during tests"""
mock_config = {
mock_config = ProxyRuntimeConfig.from_resolved({
"litellm_settings": {
"default_internal_user_params": {
"user_role": LitellmUserRoles.INTERNAL_USER,
@ -42,7 +42,7 @@ def mock_proxy_config(monkeypatch):
"MICROSOFT_CLIENT_SECRET": "test_microsoft_client_secret",
"PROXY_BASE_URL": "https://example.com",
},
}
})
async def mock_get_config():
return mock_config
@ -70,7 +70,7 @@ def mock_proxy_config(monkeypatch):
# Return the config, the save_config call counter, and any env-var updates
# the endpoint routed through the dedicated save_environment_variables path
return {
"config": mock_config,
"config": lambda: mock_config,
"save_call_count": lambda: save_config_call_count,
"env_updates": lambda: saved_env_updates,
}
@ -104,7 +104,7 @@ class TestProxySettingEndpoints:
# Check values match our mock config
values = data["values"]
mock_params = mock_proxy_config["config"]["litellm_settings"][
mock_params = mock_proxy_config["config"]().litellm_settings[
"default_internal_user_params"
]
assert values["user_role"] == mock_params["user_role"]
@ -185,7 +185,7 @@ class TestProxySettingEndpoints:
assert settings["models"] == new_settings["models"]
# Verify the config was updated
updated_config = mock_proxy_config["config"]["litellm_settings"][
updated_config = mock_proxy_config["config"]().litellm_settings[
"default_internal_user_params"
]
assert updated_config["user_role"] == new_settings["user_role"]
@ -207,7 +207,7 @@ class TestProxySettingEndpoints:
# Check values match our mock config
values = data["values"]
mock_params = mock_proxy_config["config"]["litellm_settings"][
mock_params = mock_proxy_config["config"]().litellm_settings[
"default_team_params"
]
assert values["models"] == mock_params["models"]
@ -258,7 +258,7 @@ class TestProxySettingEndpoints:
assert settings["rpm_limit"] == new_settings["rpm_limit"]
# Verify the config was updated
updated_config = mock_proxy_config["config"]["litellm_settings"][
updated_config = mock_proxy_config["config"]().litellm_settings[
"default_team_params"
]
assert updated_config["models"] == new_settings["models"]
@ -1216,7 +1216,7 @@ class TestProxySettingEndpoints:
def test_get_ui_theme_settings_with_favicon_configured(self, mock_proxy_config):
"""Test getting UI theme settings when favicon is configured"""
mock_proxy_config["config"]["litellm_settings"]["ui_theme_config"] = {
mock_proxy_config["config"]().litellm_settings["ui_theme_config"] = {
"logo_url": "https://example.com/logo.png",
"favicon_url": "https://example.com/favicon.ico",
}
@ -2634,7 +2634,7 @@ def test_add_allowed_ip_hands_save_config_only_the_changed_general_setting(monke
save_config: Final = AsyncMock(side_effect=lambda new_config: new_config)
async def _get_config():
return {"general_settings": dict(file_settings)}
return ProxyRuntimeConfig.from_resolved({"general_settings": dict(file_settings)})
monkeypatch.setattr(proxy_server_module, "prisma_client", fake_prisma)
monkeypatch.setattr(proxy_server_module, "store_model_in_db", True)
@ -2656,10 +2656,13 @@ def test_add_allowed_ip_hands_save_config_only_the_changed_general_setting(monke
assert resp.status_code == 200, resp.text
save_config.assert_awaited_once()
persisted: Final = save_config.await_args.kwargs["new_config"]["general_settings"]
changed, removed = changed_section_keys(file_settings, persisted)
persisted: Final = save_config.await_args.kwargs["new_config"]
changed, removed = changed_section_keys(file_settings, persisted.general_settings)
assert dict(changed) == {"allowed_ips": ["203.0.113.77"]}
assert removed == frozenset()
# the frozen model save_config was handed still carries the loaded
# baseline, so the write is provably an add of just the new ip
assert "allowed_ips" not in persisted.baseline["general_settings"]
assert store["allowed_ips"] == ["203.0.113.77"]
finally:
app.dependency_overrides.pop(user_api_key_auth, None)
@ -2679,7 +2682,9 @@ def test_delete_allowed_ip_writes_deleted_audit_log(monkeypatch):
fake_prisma = MagicMock()
fake_prisma.db.litellm_auditlog.create = audit_create
config = {"general_settings": {"allowed_ips": ["203.0.113.77", "198.51.100.1"]}}
config = ProxyRuntimeConfig.from_resolved(
{"general_settings": {"allowed_ips": ["203.0.113.77", "198.51.100.1"]}}
)
async def _get_config():
return config
@ -2740,7 +2745,7 @@ def test_allowed_ip_routes_refuse_a_config_owned_list_with_a_clear_400(route, mo
fake_prisma.db.litellm_auditlog.create = AsyncMock()
async def _get_config():
return {"general_settings": {"allowed_ips": ["203.0.113.77"]}}
return ProxyRuntimeConfig.from_resolved({"general_settings": {"allowed_ips": ["203.0.113.77"]}})
async def _save_config(new_config=None):
saved.append(new_config)
@ -3130,7 +3135,7 @@ class TestMcpToolSearchSettingsEndpoints:
def test_get_returns_stored_values_and_field_schema(self, mock_proxy_config, mock_auth, monkeypatch):
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", object())
mock_proxy_config["config"]["litellm_settings"]["mcp_tool_search"] = {
mock_proxy_config["config"]().litellm_settings["mcp_tool_search"] = {
"embedding_model": "text-embedding-3-small",
"core_tools": ["treasury-get_rates"],
}
@ -3175,7 +3180,7 @@ class TestMcpToolSearchSettingsEndpoints:
assert resp.status_code == 200, resp.text
assert mock_proxy_config["save_call_count"]() == 1
assert litellm.mcp_tool_search == payload
assert mock_proxy_config["config"]["litellm_settings"]["mcp_tool_search"] == payload
assert mock_proxy_config["config"]().litellm_settings["mcp_tool_search"] == payload
def test_update_rejects_out_of_range_top_k(self, mock_proxy_config, monkeypatch):
monkeypatch.setattr("litellm.proxy.proxy_server.store_model_in_db", True)
@ -3206,7 +3211,7 @@ class TestWebSearchInterceptionSettingsEndpoints:
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", object())
monkeypatch.setattr(litellm, "callbacks", [WebSearchInterceptionLogger(search_tool_name="running")])
mock_proxy_config["config"]["litellm_settings"]["websearch_interception_params"] = {
mock_proxy_config["config"]().litellm_settings["websearch_interception_params"] = {
"enabled": True,
"enabled_providers": ["bedrock", "vertex_ai"],
"search_tool_name": "my-perplexity-search",
@ -3249,7 +3254,7 @@ class TestWebSearchInterceptionSettingsEndpoints:
assert resp.status_code == 200, resp.text
assert mock_proxy_config["save_call_count"]() == 1
assert mock_proxy_config["config"]["litellm_settings"]["websearch_interception_params"] == payload
assert mock_proxy_config["config"]().litellm_settings["websearch_interception_params"] == payload
def test_get_reports_enabled_while_the_callback_is_running_without_a_stored_flag(
self, mock_proxy_config, mock_auth, monkeypatch
@ -3261,7 +3266,7 @@ class TestWebSearchInterceptionSettingsEndpoints:
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", object())
monkeypatch.setattr(litellm, "callbacks", [WebSearchInterceptionLogger(search_tool_name="from-config")])
mock_proxy_config["config"]["litellm_settings"]["websearch_interception_params"] = {
mock_proxy_config["config"]().litellm_settings["websearch_interception_params"] = {
"enabled_providers": ["bedrock"],
"search_tool_name": "my-perplexity-search",
}
@ -3278,7 +3283,7 @@ class TestWebSearchInterceptionSettingsEndpoints:
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", object())
monkeypatch.setattr(litellm, "callbacks", [])
mock_proxy_config["config"]["litellm_settings"]["websearch_interception_params"] = {
mock_proxy_config["config"]().litellm_settings["websearch_interception_params"] = {
"enabled_providers": ["bedrock"],
}
@ -3314,7 +3319,7 @@ class TestWebSearchInterceptionSettingsEndpoints:
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", object())
monkeypatch.setattr(litellm, "callbacks", [])
mock_proxy_config["config"]["litellm_settings"]["websearch_interception_params"] = {
mock_proxy_config["config"]().litellm_settings["websearch_interception_params"] = {
"enabled": True,
"search_tool_name": "cluster-search",
}
@ -3331,7 +3336,7 @@ class TestWebSearchInterceptionSettingsEndpoints:
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", object())
monkeypatch.setattr(litellm, "callbacks", [])
mock_proxy_config["config"]["litellm_settings"]["websearch_interception_params"] = {"enabled": True}
mock_proxy_config["config"]().litellm_settings["websearch_interception_params"] = {"enabled": True}
resp = client.get("/get/websearch_interception_settings")
@ -3349,7 +3354,7 @@ class TestWebSearchInterceptionSettingsEndpoints:
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", object())
monkeypatch.setattr(litellm, "callbacks", [WebSearchInterceptionLogger(search_tool_name="running")])
mock_proxy_config["config"]["litellm_settings"]["websearch_interception_params"] = {"enabled": True}
mock_proxy_config["config"]().litellm_settings["websearch_interception_params"] = {"enabled": True}
resp = client.get("/get/websearch_interception_settings")

View file

@ -33,6 +33,7 @@ from litellm.router_strategy.budget_limiter import RouterBudgetLimiting
from litellm.router_utils.pre_call_checks.model_rate_limit_check import ModelRateLimitingCheck
from litellm.router_utils.pre_call_checks.prompt_caching_deployment_check import PromptCachingDeploymentCheck
from litellm.types.router import RetryPolicy, UpdateRouterConfig
from litellm.proxy._types import ProxyRuntimeConfig
@pytest.fixture(autouse=True)
@ -366,7 +367,7 @@ async def test_config_update_persists_and_reads_back_retry_policy(monkeypatch):
monkeypatch.setattr(proxy_server, "prisma_client", prisma_client)
monkeypatch.setattr(proxy_server, "llm_router", router)
monkeypatch.setattr(proxy_server.proxy_config, "add_deployment", _apply_router_settings)
monkeypatch.setattr(proxy_server.proxy_config, "get_config", AsyncMock(return_value={}))
monkeypatch.setattr(proxy_server.proxy_config, "get_config", AsyncMock(return_value=ProxyRuntimeConfig.from_resolved({})))
posted = UpdateRouterConfig(
retry_policy=RetryPolicy(