mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-26 01:12:21 +00:00
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:
parent
415032118f
commit
108f699294
20 changed files with 428 additions and 288 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}]}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue