mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-05 02:41:56 +00:00
Merge pull request #41868 from BerriAI/litellm_config_update_rejects_config_owned_keys
fix(proxy): refuse config-owned keys on POST /config/update
This commit is contained in:
commit
9b342cdd40
10 changed files with 234 additions and 70 deletions
|
|
@ -1785,6 +1785,12 @@ jobs:
|
|||
- wait_for_service:
|
||||
url: http://localhost:4000
|
||||
timeout: "300"
|
||||
- run:
|
||||
name: Seed the routing strategy through /config/update
|
||||
command: |
|
||||
curl --noproxy '*' -sSf -X POST http://localhost:4000/config/update \
|
||||
-H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' \
|
||||
-d '{"router_settings": {"routing_strategy": "usage-based-routing-v2"}}'
|
||||
- run:
|
||||
name: Run tests
|
||||
command: |
|
||||
|
|
|
|||
|
|
@ -125,6 +125,9 @@ start_proxy() {
|
|||
start_proxy 4000 proxy.log
|
||||
proxy_pid="$launched_pid"
|
||||
.venv/bin/python .circleci/scripts/wait_integration_services.py
|
||||
curl --noproxy '*' -sSf -X POST "$INTEGRATION_PROXY_URL/config/update" \
|
||||
-H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' \
|
||||
-d '{"router_settings": {"num_retries": 0}}' > "$results/seed-router-settings.json"
|
||||
if [ "$suite" = management ]; then
|
||||
export INTEGRATION_PEER_URL=http://127.0.0.1:4001
|
||||
start_proxy 4001 peer.log
|
||||
|
|
|
|||
|
|
@ -6919,9 +6919,7 @@ class ProxyConfig:
|
|||
self._add_callbacks_from_db_config(config_data)
|
||||
|
||||
# router settings
|
||||
await self._add_router_settings_from_db_config(
|
||||
config_data=config_data, llm_router=llm_router, prisma_client=prisma_client
|
||||
)
|
||||
await self._add_router_settings_from_db_config(llm_router=llm_router, prisma_client=prisma_client)
|
||||
|
||||
return still_desired_ids
|
||||
|
||||
|
|
@ -7129,13 +7127,11 @@ class ProxyConfig:
|
|||
|
||||
async def _add_router_settings_from_db_config(
|
||||
self,
|
||||
config_data: Mapping[str, object],
|
||||
llm_router: Router | None,
|
||||
prisma_client: PrismaClient | None,
|
||||
) -> None:
|
||||
if llm_router is None or prisma_client is None:
|
||||
return
|
||||
self.router_settings.load_yaml(_as_settings_mapping(config_data.get("router_settings")))
|
||||
db_router_settings: Final[_ConfigParamRow | None] = await _config_param_table(prisma_client).find_first(
|
||||
where={"param_name": "router_settings"}
|
||||
)
|
||||
|
|
@ -17030,6 +17026,48 @@ async def update_config(
|
|||
if prisma_client is None:
|
||||
raise Exception("No DB Connected")
|
||||
|
||||
requested_general_settings: Final[Mapping[str, JsonValue]] = (
|
||||
config_info.general_settings.model_dump(exclude_none=True, exclude_unset=True)
|
||||
if config_info.general_settings is not None
|
||||
else {}
|
||||
)
|
||||
raw_litellm_settings: Final[Mapping[str, JsonValue]] = _CONFIG_SECTION_VALUES.validate_python(
|
||||
config_info.litellm_settings if config_info.litellm_settings is not None else {}
|
||||
)
|
||||
incoming_success_callback: Final = raw_litellm_settings.get("success_callback")
|
||||
updated_litellm_settings: Final[Mapping[str, JsonValue]] = _CONFIG_SECTION_VALUES.validate_python(
|
||||
{
|
||||
**raw_litellm_settings,
|
||||
**(
|
||||
{"success_callback": normalize_callback_names(incoming_success_callback)}
|
||||
if isinstance(incoming_success_callback, list)
|
||||
else {}
|
||||
),
|
||||
}
|
||||
)
|
||||
typed_router_settings: Final[Mapping[str, JsonValue]] = (
|
||||
config_info.router_settings.model_dump(exclude_none=True, exclude_unset=True)
|
||||
if config_info.router_settings is not None
|
||||
else {}
|
||||
)
|
||||
router_settings_updates: Final[Mapping[str, JsonValue]] = {
|
||||
**typed_router_settings,
|
||||
**(
|
||||
{
|
||||
key: value
|
||||
for key, value in raw_router_settings.items()
|
||||
if key not in typed_router_settings and value is not None
|
||||
}
|
||||
if isinstance(raw_router_settings, dict)
|
||||
else {}
|
||||
),
|
||||
}
|
||||
proxy_config.reject_config_owned_writes(
|
||||
section_name="general_settings", changed_keys=requested_general_settings
|
||||
)
|
||||
proxy_config.reject_config_owned_writes(section_name="litellm_settings", changed_keys=raw_litellm_settings)
|
||||
proxy_config.reject_config_owned_writes(section_name="router_settings", changed_keys=router_settings_updates)
|
||||
|
||||
async def _read_section(param_name: str) -> dict:
|
||||
row: Final[_ConfigParamRow | None] = await _config_param_table(prisma_client).find_first(
|
||||
where={"param_name": param_name}
|
||||
|
|
@ -17056,8 +17094,7 @@ async def update_config(
|
|||
if config_info.general_settings is not None:
|
||||
existing = await _read_section("general_settings")
|
||||
before_general_settings: Final = copy.deepcopy(existing)
|
||||
updates: Mapping[str, JsonValue] = config_info.general_settings.dict(exclude_none=True)
|
||||
for k, v in updates.items():
|
||||
for k, v in requested_general_settings.items():
|
||||
if k == "alert_to_webhook_url":
|
||||
if "alerting" not in existing:
|
||||
existing["alerting"] = ["slack"]
|
||||
|
|
@ -17100,15 +17137,9 @@ async def update_config(
|
|||
if config_info.litellm_settings is not None:
|
||||
existing = await _read_section("litellm_settings")
|
||||
before_litellm_settings: Final = copy.deepcopy(existing)
|
||||
updated_litellm_settings: Final = dict(config_info.litellm_settings)
|
||||
|
||||
incoming_cb = updated_litellm_settings.get("success_callback")
|
||||
if isinstance(incoming_cb, list):
|
||||
updated_litellm_settings["success_callback"] = normalize_callback_names(incoming_cb)
|
||||
|
||||
merged: Final = {**existing, **updated_litellm_settings}
|
||||
|
||||
incoming_cb = updated_litellm_settings.get("success_callback")
|
||||
incoming_cb: Final = updated_litellm_settings.get("success_callback")
|
||||
existing_cb: Final = existing.get("success_callback")
|
||||
if isinstance(incoming_cb, list):
|
||||
if isinstance(existing_cb, list):
|
||||
|
|
@ -17131,15 +17162,6 @@ async def update_config(
|
|||
if isinstance(raw_router_settings, dict):
|
||||
existing = await _read_section("router_settings")
|
||||
before_router_settings: Final = copy.deepcopy(existing)
|
||||
typed_router_settings: Final = (
|
||||
config_info.router_settings.dict(exclude_none=True) if config_info.router_settings is not None else {}
|
||||
)
|
||||
raw_router_settings_without_none: Final = {
|
||||
key: value
|
||||
for key, value in raw_router_settings.items()
|
||||
if key not in typed_router_settings and value is not None
|
||||
}
|
||||
router_settings_updates: Final = {**typed_router_settings, **raw_router_settings_without_none}
|
||||
new_router_settings: Final = {**existing, **router_settings_updates}
|
||||
await _upsert_section("router_settings", new_router_settings)
|
||||
asyncio.create_task(
|
||||
|
|
|
|||
|
|
@ -213,7 +213,6 @@ files_settings:
|
|||
api_key: os.environ/OPENAI_API_KEY
|
||||
|
||||
router_settings:
|
||||
routing_strategy: usage-based-routing-v2
|
||||
redis_host: os.environ/REDIS_HOST
|
||||
redis_password: os.environ/REDIS_PASSWORD
|
||||
redis_port: os.environ/REDIS_PORT
|
||||
|
|
|
|||
|
|
@ -13,5 +13,4 @@ litellm_settings:
|
|||
host: os.environ/REDIS_HOST
|
||||
port: os.environ/REDIS_PORT
|
||||
router_settings:
|
||||
num_retries: 0
|
||||
disable_cooldowns: true
|
||||
|
|
|
|||
|
|
@ -3079,6 +3079,9 @@ async def test_update_config_success_callback_normalization():
|
|||
async def add_deployment(self, prisma_client=None, proxy_logging_obj=None): # noqa: F811 # pytest fixture, not a redefinition
|
||||
return None
|
||||
|
||||
def reject_config_owned_writes(self, *, section_name, changed_keys):
|
||||
return None
|
||||
|
||||
setattr(proxy_server, "proxy_config", MockProxyConfig())
|
||||
|
||||
config_update = ConfigYAML(litellm_settings={"success_callback": ["SQS", "sQs"]})
|
||||
|
|
|
|||
|
|
@ -3399,9 +3399,8 @@ async def test_ProxyConfig__add_router_settings_from_db_config_updates_router():
|
|||
fake_prisma.db.litellm_config.find_first = AsyncMock(
|
||||
return_value=SimpleNamespace(param_value={"timeout": 30, "retries": 2, "fallbacks": []})
|
||||
)
|
||||
config_data = {"router_settings": {"timeout": 10}}
|
||||
pc.router_settings.load_yaml({"timeout": 10})
|
||||
await pc._add_router_settings_from_db_config(
|
||||
config_data=config_data,
|
||||
llm_router=fake_router,
|
||||
prisma_client=fake_prisma,
|
||||
)
|
||||
|
|
@ -3421,7 +3420,7 @@ async def test_ProxyConfig__add_router_settings_from_db_config_updates_router():
|
|||
async def test_ProxyConfig__add_router_settings_from_db_config_none_router_noop():
|
||||
pc = ProxyConfig()
|
||||
# No router and no prisma — should silently return.
|
||||
await pc._add_router_settings_from_db_config(config_data={}, llm_router=None, prisma_client=None)
|
||||
await pc._add_router_settings_from_db_config(llm_router=None, prisma_client=None)
|
||||
# Error-style: bad call signature raises.
|
||||
with pytest.raises(TypeError):
|
||||
await pc._add_router_settings_from_db_config() # type: ignore[call-arg]
|
||||
|
|
|
|||
|
|
@ -139,6 +139,108 @@ def test_config_update_persists_disable_cooldowns(client, auth_as, mock_prisma,
|
|||
assert persisted["disable_cooldowns"] is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("section", "store_attr", "yaml_values", "changed_values"),
|
||||
[
|
||||
("general_settings", "settings", {"alerting": ["slack"]}, {"alerting": ["email"]}),
|
||||
("litellm_settings", "litellm_settings", {"success_callback": ["langfuse"]}, {"success_callback": ["otel"]}),
|
||||
("router_settings", "router_settings", {"num_retries": 0}, {"num_retries": 2}),
|
||||
],
|
||||
)
|
||||
def test_config_update_rejects_config_owned_keys_and_accepts_the_same_value(
|
||||
client, auth_as, mock_prisma, monkeypatch, section, store_attr, yaml_values, changed_values
|
||||
):
|
||||
from litellm.proxy import proxy_server as ps
|
||||
from litellm.proxy._types import LitellmUserRoles
|
||||
|
||||
table = _install_litellm_config(mock_prisma)
|
||||
monkeypatch.setattr(ps, "prisma_client", mock_prisma)
|
||||
monkeypatch.setattr(ps.proxy_config, "add_deployment", AsyncMock())
|
||||
store = getattr(ps.proxy_config, store_attr)
|
||||
store.load_yaml(yaml_values)
|
||||
try:
|
||||
with auth_as(LitellmUserRoles.PROXY_ADMIN):
|
||||
rejected = client.post("/config/update", json={section: changed_values})
|
||||
rejected_message = rejected.json()["error"]["message"]
|
||||
table.upsert.assert_not_called()
|
||||
accepted = client.post("/config/update", json={section: yaml_values})
|
||||
finally:
|
||||
store.load_yaml({})
|
||||
|
||||
assert rejected.status_code == 400
|
||||
assert f"{section} key '{next(iter(yaml_values))}' is set in the config file and cannot be changed here" in (
|
||||
rejected_message
|
||||
)
|
||||
assert accepted.status_code == 200
|
||||
persisted = json.loads(table.upsert.call_args.kwargs["data"]["create"]["param_value"])
|
||||
assert persisted[next(iter(yaml_values))] == yaml_values[next(iter(yaml_values))]
|
||||
|
||||
|
||||
def test_config_update_persists_only_the_general_settings_keys_the_request_set(
|
||||
client, auth_as, mock_prisma, monkeypatch
|
||||
):
|
||||
from litellm.proxy import proxy_server as ps
|
||||
from litellm.proxy._types import LitellmUserRoles
|
||||
|
||||
table = _install_litellm_config(mock_prisma)
|
||||
monkeypatch.setattr(ps, "prisma_client", mock_prisma)
|
||||
monkeypatch.setattr(ps.proxy_config, "add_deployment", AsyncMock())
|
||||
ps.proxy_config.settings.load_yaml({"health_check_interval": 60})
|
||||
try:
|
||||
with auth_as(LitellmUserRoles.PROXY_ADMIN):
|
||||
response = client.post("/config/update", json={"general_settings": {"alerting_threshold": 600}})
|
||||
finally:
|
||||
ps.proxy_config.settings.load_yaml({})
|
||||
|
||||
assert response.status_code == 200
|
||||
persisted = json.loads(table.upsert.call_args.kwargs["data"]["create"]["param_value"])
|
||||
assert persisted == {"alerting_threshold": 600}
|
||||
|
||||
|
||||
def test_config_update_persists_only_the_router_settings_keys_the_request_set(
|
||||
client, auth_as, mock_prisma, monkeypatch
|
||||
):
|
||||
from litellm.proxy import proxy_server as ps
|
||||
from litellm.proxy._types import LitellmUserRoles
|
||||
|
||||
table = _install_litellm_config(mock_prisma)
|
||||
monkeypatch.setattr(ps, "prisma_client", mock_prisma)
|
||||
monkeypatch.setattr(ps.proxy_config, "add_deployment", AsyncMock())
|
||||
ps.proxy_config.router_settings.load_yaml({"model_group_alias": {"opus": "claude-opus-5"}})
|
||||
try:
|
||||
with auth_as(LitellmUserRoles.PROXY_ADMIN):
|
||||
response = client.post(
|
||||
"/config/update", json={"router_settings": {"retry_policy": {"TimeoutErrorRetries": 3}}}
|
||||
)
|
||||
finally:
|
||||
ps.proxy_config.router_settings.load_yaml({})
|
||||
|
||||
assert response.status_code == 200, response.text
|
||||
persisted = json.loads(table.upsert.call_args.kwargs["data"]["create"]["param_value"])
|
||||
assert persisted == {"retry_policy": {"TimeoutErrorRetries": 3}}
|
||||
|
||||
|
||||
def test_config_update_accepts_a_config_owned_success_callback_the_file_spells_in_mixed_case(
|
||||
client, auth_as, mock_prisma, monkeypatch
|
||||
):
|
||||
from litellm.proxy import proxy_server as ps
|
||||
from litellm.proxy._types import LitellmUserRoles
|
||||
|
||||
table = _install_litellm_config(mock_prisma)
|
||||
monkeypatch.setattr(ps, "prisma_client", mock_prisma)
|
||||
monkeypatch.setattr(ps.proxy_config, "add_deployment", AsyncMock())
|
||||
ps.proxy_config.litellm_settings.load_yaml({"success_callback": ["Langfuse"]})
|
||||
try:
|
||||
with auth_as(LitellmUserRoles.PROXY_ADMIN):
|
||||
response = client.post("/config/update", json={"litellm_settings": {"success_callback": ["Langfuse"]}})
|
||||
finally:
|
||||
ps.proxy_config.litellm_settings.load_yaml({})
|
||||
|
||||
assert response.status_code == 200
|
||||
persisted = json.loads(table.upsert.call_args.kwargs["data"]["create"]["param_value"])
|
||||
assert persisted["success_callback"] == ["langfuse"]
|
||||
|
||||
|
||||
def test_config_update_rejects_assistants_config(client, auth_as, mock_prisma, monkeypatch):
|
||||
from litellm.proxy import proxy_server as ps
|
||||
from litellm.proxy._types import LitellmUserRoles
|
||||
|
|
|
|||
|
|
@ -4974,8 +4974,8 @@ async def test_add_router_settings_from_db_config_merge_logic():
|
|||
mock_prisma_client.db.litellm_config.find_first = AsyncMock(return_value=mock_db_config)
|
||||
|
||||
# Call the method under test
|
||||
proxy_config.router_settings.load_yaml(config_data["router_settings"])
|
||||
await proxy_config._add_router_settings_from_db_config(
|
||||
config_data=config_data,
|
||||
llm_router=mock_router,
|
||||
prisma_client=mock_prisma_client,
|
||||
)
|
||||
|
|
@ -5029,9 +5029,7 @@ async def test_invalid_db_routing_groups_do_not_abort_other_router_settings():
|
|||
mock_prisma_client = MagicMock()
|
||||
mock_prisma_client.db.litellm_config.find_first = AsyncMock(return_value=mock_db_config)
|
||||
|
||||
await ProxyConfig()._add_router_settings_from_db_config(
|
||||
config_data={}, llm_router=router, prisma_client=mock_prisma_client
|
||||
)
|
||||
await ProxyConfig()._add_router_settings_from_db_config(llm_router=router, prisma_client=mock_prisma_client)
|
||||
|
||||
assert router.num_retries == 7
|
||||
assert router._model_to_group == {"m1": "g1"}
|
||||
|
|
@ -5053,9 +5051,7 @@ async def test_valid_db_routing_groups_still_replace_router_groups():
|
|||
mock_prisma_client = MagicMock()
|
||||
mock_prisma_client.db.litellm_config.find_first = AsyncMock(return_value=mock_db_config)
|
||||
|
||||
await ProxyConfig()._add_router_settings_from_db_config(
|
||||
config_data={}, llm_router=router, prisma_client=mock_prisma_client
|
||||
)
|
||||
await ProxyConfig()._add_router_settings_from_db_config(llm_router=router, prisma_client=mock_prisma_client)
|
||||
|
||||
assert router.num_retries == 7
|
||||
assert router._model_to_group == {"m2": "g2"}
|
||||
|
|
@ -5098,8 +5094,8 @@ async def test_add_router_settings_from_db_config_empty_db_lists_do_not_clobber_
|
|||
mock_prisma_client = MagicMock()
|
||||
mock_prisma_client.db.litellm_config.find_first = AsyncMock(return_value=mock_db_config)
|
||||
|
||||
proxy_config.router_settings.load_yaml(config_data["router_settings"])
|
||||
await proxy_config._add_router_settings_from_db_config(
|
||||
config_data=config_data,
|
||||
llm_router=mock_router,
|
||||
prisma_client=mock_prisma_client,
|
||||
)
|
||||
|
|
@ -5135,8 +5131,8 @@ async def test_add_router_settings_from_db_config_empty_db_list_still_clears_unc
|
|||
mock_prisma_client = MagicMock()
|
||||
mock_prisma_client.db.litellm_config.find_first = AsyncMock(return_value=mock_db_config)
|
||||
|
||||
proxy_config.router_settings.load_yaml(config_data["router_settings"])
|
||||
await proxy_config._add_router_settings_from_db_config(
|
||||
config_data=config_data,
|
||||
llm_router=mock_router,
|
||||
prisma_client=mock_prisma_client,
|
||||
)
|
||||
|
|
@ -5160,8 +5156,8 @@ async def test_add_router_settings_from_db_config_edge_cases():
|
|||
mock_router.update_settings = MagicMock()
|
||||
|
||||
# Test Case 1: No router provided
|
||||
proxy_config.router_settings.load_yaml({"test": "value"})
|
||||
await proxy_config._add_router_settings_from_db_config(
|
||||
config_data={"router_settings": {"test": "value"}},
|
||||
llm_router=None,
|
||||
prisma_client=MagicMock(),
|
||||
)
|
||||
|
|
@ -5169,8 +5165,8 @@ async def test_add_router_settings_from_db_config_edge_cases():
|
|||
mock_router.update_settings.assert_not_called()
|
||||
|
||||
# Test Case 2: No prisma client provided
|
||||
proxy_config.router_settings.load_yaml({"test": "value"})
|
||||
await proxy_config._add_router_settings_from_db_config(
|
||||
config_data={"router_settings": {"test": "value"}},
|
||||
llm_router=mock_router,
|
||||
prisma_client=None,
|
||||
)
|
||||
|
|
@ -5183,8 +5179,8 @@ async def test_add_router_settings_from_db_config_edge_cases():
|
|||
|
||||
config_data = {"router_settings": {"routing_strategy": "usage-based"}}
|
||||
|
||||
proxy_config.router_settings.load_yaml(config_data["router_settings"])
|
||||
await proxy_config._add_router_settings_from_db_config(
|
||||
config_data=config_data,
|
||||
llm_router=mock_router,
|
||||
prisma_client=mock_prisma_client,
|
||||
)
|
||||
|
|
@ -5198,8 +5194,8 @@ async def test_add_router_settings_from_db_config_edge_cases():
|
|||
mock_db_config.param_value = {"db_setting": "db_value"}
|
||||
mock_prisma_client.db.litellm_config.find_first = AsyncMock(return_value=mock_db_config)
|
||||
|
||||
proxy_config.router_settings.load_yaml({})
|
||||
await proxy_config._add_router_settings_from_db_config(
|
||||
config_data={}, # No router_settings in config
|
||||
llm_router=mock_router,
|
||||
prisma_client=mock_prisma_client,
|
||||
)
|
||||
|
|
@ -5211,9 +5207,8 @@ async def test_add_router_settings_from_db_config_edge_cases():
|
|||
# Test Case 5: Both config and DB router_settings are None/empty
|
||||
mock_prisma_client.db.litellm_config.find_first = AsyncMock(return_value=None)
|
||||
|
||||
await proxy_config._add_router_settings_from_db_config(
|
||||
config_data={}, llm_router=mock_router, prisma_client=mock_prisma_client
|
||||
)
|
||||
proxy_config.router_settings.load_yaml({})
|
||||
await proxy_config._add_router_settings_from_db_config(llm_router=mock_router, prisma_client=mock_prisma_client)
|
||||
|
||||
# Should not call update_settings when no settings exist
|
||||
mock_router.update_settings.assert_not_called()
|
||||
|
|
@ -5225,8 +5220,8 @@ async def test_add_router_settings_from_db_config_edge_cases():
|
|||
|
||||
config_data = {"router_settings": {"config_setting": "config_value"}}
|
||||
|
||||
proxy_config.router_settings.load_yaml(config_data["router_settings"])
|
||||
await proxy_config._add_router_settings_from_db_config(
|
||||
config_data=config_data,
|
||||
llm_router=mock_router,
|
||||
prisma_client=mock_prisma_client,
|
||||
)
|
||||
|
|
@ -5275,8 +5270,8 @@ async def test_add_router_settings_shallow_merge_behavior():
|
|||
mock_prisma_client = MagicMock()
|
||||
mock_prisma_client.db.litellm_config.find_first = AsyncMock(return_value=mock_db_config)
|
||||
|
||||
proxy_config.router_settings.load_yaml(config_data["router_settings"])
|
||||
await proxy_config._add_router_settings_from_db_config(
|
||||
config_data=config_data,
|
||||
llm_router=mock_router,
|
||||
prisma_client=mock_prisma_client,
|
||||
)
|
||||
|
|
@ -5298,6 +5293,36 @@ async def test_add_router_settings_shallow_merge_behavior():
|
|||
assert merged_settings["top_level"] == "config_top"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_router_settings_reload_keeps_db_values_writable(tmp_path, monkeypatch):
|
||||
from litellm.proxy.proxy_server import ProxyConfig
|
||||
|
||||
config_path: Final = tmp_path / "config.yaml"
|
||||
config_path.write_text(yaml.safe_dump({"model_list": [], "router_settings": {"disable_cooldowns": True}}))
|
||||
db_row: Final = types.SimpleNamespace(param_value={"num_retries": 0})
|
||||
|
||||
async def read_config_row(_prisma_client, param_name):
|
||||
return db_row if param_name == "router_settings" else None
|
||||
|
||||
mock_prisma_client: Final = MagicMock()
|
||||
mock_prisma_client.db.litellm_config.find_first = AsyncMock(return_value=db_row)
|
||||
mock_router: Final = MagicMock()
|
||||
monkeypatch.setattr(proxy_server_module, "get_config_param", read_config_row)
|
||||
monkeypatch.setattr(proxy_server_module, "prisma_client", mock_prisma_client)
|
||||
monkeypatch.setattr(proxy_server_module, "store_model_in_db", True)
|
||||
monkeypatch.setattr(proxy_server_module, "user_config_file_path", None)
|
||||
proxy_config: Final = ProxyConfig()
|
||||
|
||||
for _ in range(2):
|
||||
await proxy_config.get_config(config_file_path=str(config_path))
|
||||
await proxy_config._add_router_settings_from_db_config(llm_router=mock_router, prisma_client=mock_prisma_client)
|
||||
|
||||
assert mock_router.update_settings.call_args.kwargs == {"disable_cooldowns": True, "num_retries": 0}
|
||||
assert proxy_config.router_settings.source("num_retries") == "db"
|
||||
assert proxy_config.router_settings.rejected_writes({"num_retries": 3}) == ()
|
||||
assert proxy_config.router_settings.rejected_writes({"disable_cooldowns": False}) == ("disable_cooldowns",)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_model_info_v1_oci_secrets_not_leaked():
|
||||
"""
|
||||
|
|
@ -7585,7 +7610,15 @@ async def test_update_general_settings_db_pass_through_endpoint_cannot_override_
|
|||
assert still_open.api_key is None
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app_routes_restored():
|
||||
routes_before: Final = tuple(app.router.routes)
|
||||
yield
|
||||
app.router.routes[:] = routes_before
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.usefixtures("app_routes_restored")
|
||||
async def test_deleting_the_stored_pass_through_row_takes_the_route_out_of_service():
|
||||
"""A pass-through route the database declared has to stop serving when that row is
|
||||
deleted. The proxy's own registry of live pass-through routes is what decides whether
|
||||
|
|
@ -7624,6 +7657,7 @@ async def test_deleting_the_stored_pass_through_row_takes_the_route_out_of_servi
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.usefixtures("app_routes_restored")
|
||||
async def test_a_stored_pass_through_row_never_disturbs_the_config_declared_routes():
|
||||
"""``pass_through_endpoints`` is config-owned once the file declares it, so writing and then
|
||||
deleting a stored row resolves to the same list both times and the config file's routes keep
|
||||
|
|
@ -14028,32 +14062,29 @@ async def test_authoritative_floor_spend_keeps_a_reset_marker_written_during_the
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_throttle_settings_are_not_hot_applied_from_the_database():
|
||||
"""LIT-5285: a stored sign-in limit does not take effect on a live worker.
|
||||
|
||||
_update_general_settings copies an allowlist of keys out of the DB row on every config
|
||||
poll. Adding these to it would let a stored value outrank config.yaml without a restart,
|
||||
so an operator locked out by a bad value could not fix it by editing YAML and restarting.
|
||||
"""
|
||||
async def test_login_throttle_limits_from_the_config_file_outrank_the_database(monkeypatch):
|
||||
import litellm.proxy.proxy_server as ps
|
||||
from litellm.proxy.proxy_server import ProxyConfig
|
||||
|
||||
original = dict(ps.general_settings)
|
||||
try:
|
||||
ps.general_settings.clear()
|
||||
await ProxyConfig()._update_general_settings(
|
||||
db_general_settings={
|
||||
"max_failed_login_attempts_per_source": 999,
|
||||
"failed_login_window_seconds": 1,
|
||||
"failed_login_block_seconds": 1,
|
||||
}
|
||||
)
|
||||
assert "max_failed_login_attempts_per_source" not in ps.general_settings
|
||||
assert "failed_login_window_seconds" not in ps.general_settings
|
||||
assert "failed_login_block_seconds" not in ps.general_settings
|
||||
finally:
|
||||
ps.general_settings.clear()
|
||||
ps.general_settings.update(original)
|
||||
monkeypatch.setattr(
|
||||
ps,
|
||||
"general_settings",
|
||||
{
|
||||
"max_failed_login_attempts_per_source": 10,
|
||||
"failed_login_window_seconds": 60,
|
||||
"failed_login_block_seconds": 300,
|
||||
},
|
||||
)
|
||||
await ProxyConfig()._update_general_settings(
|
||||
db_general_settings={
|
||||
"max_failed_login_attempts_per_source": 999,
|
||||
"failed_login_window_seconds": 1,
|
||||
"failed_login_block_seconds": 1,
|
||||
}
|
||||
)
|
||||
assert ps.general_settings.get("max_failed_login_attempts_per_source") == 10
|
||||
assert ps.general_settings.get("failed_login_window_seconds") == 60
|
||||
assert ps.general_settings.get("failed_login_block_seconds") == 300
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
|
|
@ -360,7 +360,7 @@ async def test_config_update_persists_and_reads_back_retry_policy(monkeypatch):
|
|||
|
||||
async def _apply_router_settings(*args, **kwargs):
|
||||
await proxy_server.proxy_config._add_router_settings_from_db_config(
|
||||
config_data={}, llm_router=router, prisma_client=prisma_client
|
||||
llm_router=router, prisma_client=prisma_client
|
||||
)
|
||||
|
||||
monkeypatch.setattr(proxy_server, "prisma_client", prisma_client)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue