From fa73c52f9fba99215a76863960533ec08679d772 Mon Sep 17 00:00:00 2001 From: Yash Raj Pandey Date: Tue, 1 Sep 2026 22:57:21 -0400 Subject: [PATCH 1/9] fix(proxy): hydrate non-LLM objects when the model reconcile fails _add_deployment_locked ran the model reconcile and the non-LLM object hydration inside one try/except. An exception in prefetch_config_params, _get_models_from_db, _update_llm_router or _update_general_settings therefore skipped _init_non_llm_objects_in_db, so guardrails, policies, vector stores, MCP servers, agents and pass-through endpoints were not hydrated for that pass. The handler only logs, so nothing surfaced. add_deployment also runs on a timer, so a transient DB error dropped every non-LLM object until a later pass happened to succeed. Give the non-LLM hydration its own try/except. It reads its own tables and does not depend on the model reconcile. --- litellm/proxy/proxy_server.py | 13 +++- tests/test_litellm/proxy/test_proxy_server.py | 74 +++++++++++++++++++ 2 files changed, 84 insertions(+), 3 deletions(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 0915b8dd1b9..30658f3529b 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -7123,12 +7123,19 @@ class ProxyConfig: db_general_settings=db_general_settings.param_value, ) - # initialize vector stores, guardrails, etc. table in db - await self._init_non_llm_objects_in_db(prisma_client=prisma_client) - except Exception as e: verbose_proxy_logger.exception("litellm.proxy.proxy_server.py::ProxyConfig:add_deployment - %s", e) + # Own try: the model reconcile above must not decide whether guardrails, vector + # stores, MCP servers and agents get hydrated. They read their own tables. + try: + # initialize vector stores, guardrails, etc. table in db + await self._init_non_llm_objects_in_db(prisma_client=prisma_client) + except Exception as e: + verbose_proxy_logger.exception( + "litellm.proxy.proxy_server.py::ProxyConfig:add_deployment non-LLM objects - %s", e + ) + # Read while the lock is still held: once it is released the next reconcile can # begin, and clear_cache's leading wipe would make this look like a mass drop. return ReconcileOutcome( diff --git a/tests/test_litellm/proxy/test_proxy_server.py b/tests/test_litellm/proxy/test_proxy_server.py index b7bb58378d4..06f47a7715b 100644 --- a/tests/test_litellm/proxy/test_proxy_server.py +++ b/tests/test_litellm/proxy/test_proxy_server.py @@ -1052,6 +1052,80 @@ async def test_initialize_scheduled_jobs_hydrates_mcp_when_store_model_in_db_fal mock_proxy_config.init_mcp_servers_from_db.assert_awaited_once() +@pytest.mark.asyncio +async def test_add_deployment_hydrates_non_llm_objects_when_model_reconcile_fails(): + """ + Regression: _add_deployment_locked ran the model reconcile and the non-LLM object + hydration inside one try/except. A failure in the reconcile skipped guardrails, + vector stores, MCP servers, agents and pass-through endpoints for that pass, and + the swallowed exception left only a log line behind. add_deployment also runs on a + timer, so a transient DB error dropped every non-LLM object until a later pass. + """ + from litellm.proxy.proxy_server import ProxyConfig + from litellm.proxy.utils import ProxyLogging + + class FailingReconcileProxyConfig(ProxyConfig): + """Reconcile fails the way a DB outage makes it fail; hydration records itself.""" + + def __init__(self): + super().__init__() + self.non_llm_hydrations = 0 + + async def _get_models_from_db(self, prisma_client): + raise RuntimeError("db unavailable") + + async def _init_non_llm_objects_in_db(self, prisma_client): + self.non_llm_hydrations += 1 + + proxy_config = FailingReconcileProxyConfig() + + outcome = await proxy_config._add_deployment_locked( + prisma_client=MagicMock(), + proxy_logging_obj=MagicMock(spec=ProxyLogging), + ) + + assert proxy_config.non_llm_hydrations == 1 + assert outcome.still_desired is None + + +@pytest.mark.asyncio +async def test_add_deployment_survives_a_non_llm_hydration_failure(): + """ + The new hydration boundary must behave like the reconcile one: log the error and + carry on, so a failing guardrail or vector-store table cannot take down the whole + reconcile pass or escape into the scheduler job that calls it. + """ + from litellm.proxy.proxy_server import ProxyConfig + from litellm.proxy.utils import ProxyLogging + + class FailingHydrationProxyConfig(ProxyConfig): + """Reconcile succeeds, hydration raises the way an unavailable table does.""" + + def __init__(self): + super().__init__() + self.reconcile_ran = False + + async def _get_models_from_db(self, prisma_client): + return [] + + async def _update_llm_router(self, new_models, proxy_logging_obj): + self.reconcile_ran = True + return frozenset() + + async def _init_non_llm_objects_in_db(self, prisma_client): + raise RuntimeError("guardrail table unavailable") + + proxy_config = FailingHydrationProxyConfig() + + outcome = await proxy_config._add_deployment_locked( + prisma_client=MagicMock(), + proxy_logging_obj=MagicMock(spec=ProxyLogging), + ) + + assert proxy_config.reconcile_ran is True + assert outcome.still_desired == frozenset() + + @pytest.mark.asyncio async def test_init_mcp_servers_from_db_respects_supported_db_objects(monkeypatch): """ From 03e4ab7b1c22e3254e4c5855f9facb9388b4cf34 Mon Sep 17 00:00:00 2001 From: Yash Raj Pandey Date: Mon, 7 Sep 2026 16:46:28 -0400 Subject: [PATCH 2/9] test(proxy): assert the hydration ran in the failure-path test test_add_deployment_survives_a_non_llm_hydration_failure passed with the fix reverted. Reverting stops _init_non_llm_objects_in_db from being called at all, so nothing raised and the test proved nothing. Record that the override ran and assert it. Both added tests now fail with litellm/proxy/proxy_server.py reverted to the base. Co-Authored-By: Claude Opus 5 --- tests/test_litellm/proxy/test_proxy_server.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_litellm/proxy/test_proxy_server.py b/tests/test_litellm/proxy/test_proxy_server.py index 06f47a7715b..dfa89468fa0 100644 --- a/tests/test_litellm/proxy/test_proxy_server.py +++ b/tests/test_litellm/proxy/test_proxy_server.py @@ -1104,6 +1104,7 @@ async def test_add_deployment_survives_a_non_llm_hydration_failure(): def __init__(self): super().__init__() self.reconcile_ran = False + self.hydration_attempted = False async def _get_models_from_db(self, prisma_client): return [] @@ -1113,6 +1114,7 @@ async def test_add_deployment_survives_a_non_llm_hydration_failure(): return frozenset() async def _init_non_llm_objects_in_db(self, prisma_client): + self.hydration_attempted = True raise RuntimeError("guardrail table unavailable") proxy_config = FailingHydrationProxyConfig() @@ -1123,6 +1125,7 @@ async def test_add_deployment_survives_a_non_llm_hydration_failure(): ) assert proxy_config.reconcile_ran is True + assert proxy_config.hydration_attempted is True assert outcome.still_desired == frozenset() From 2ddb7f055389493819a7c1282b7b537bd0a3808b Mon Sep 17 00:00:00 2001 From: Yash Raj Pandey Date: Mon, 7 Sep 2026 16:59:41 -0400 Subject: [PATCH 3/9] style(proxy): mark the new blind-except with the repo's noqa The lint gate failed with BLE001 at 2917 over the 2916 limit. Splitting the one try/except into two adds one blind except by construction. The new handler swallows the same way the single handler did before the split, so the noqa carries that reason. Local count on proxy_server.py drops from 124 to 123, which puts the repo total back at 2916. Co-Authored-By: Claude Opus 5 --- litellm/proxy/proxy_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 30658f3529b..efe1a3ab474 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -7131,7 +7131,7 @@ class ProxyConfig: try: # initialize vector stores, guardrails, etc. table in db await self._init_non_llm_objects_in_db(prisma_client=prisma_client) - except Exception as e: + except Exception as e: # noqa: BLE001 - same swallow the single handler above already had; a hydration failure must not stop the reconcile verbose_proxy_logger.exception( "litellm.proxy.proxy_server.py::ProxyConfig:add_deployment non-LLM objects - %s", e ) From 8ca24b2b88c4090722ec446ad936b6261bb2a891 Mon Sep 17 00:00:00 2001 From: Yash Raj Pandey Date: Wed, 14 Oct 2026 11:05:33 -0400 Subject: [PATCH 4/9] test(proxy): override the public reconcile helpers in the hydration tests Upstream #45170 renamed _get_models_from_db to get_models_from_db, and add_deployment_locked now calls the public name. The private override in the regression test no longer ran. The reconcile then failed only on a MagicMock await. Override the public names so the test fails the reconcile the way it describes. --- .../proxy/test_proxy_server_endpoints_and_startup.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/unit/proxy/test_proxy_server_endpoints_and_startup.py b/tests/unit/proxy/test_proxy_server_endpoints_and_startup.py index ac6f75adb85..2be6df23a54 100644 --- a/tests/unit/proxy/test_proxy_server_endpoints_and_startup.py +++ b/tests/unit/proxy/test_proxy_server_endpoints_and_startup.py @@ -1162,7 +1162,7 @@ async def test_initialize_scheduled_jobs_hydrates_mcp_when_store_model_in_db_fal @pytest.mark.asyncio async def test_add_deployment_hydrates_non_llm_objects_when_model_reconcile_fails(): """ - Regression: _add_deployment_locked ran the model reconcile and the non-LLM object + Regression: add_deployment_locked ran the model reconcile and the non-LLM object hydration inside one try/except. A failure in the reconcile skipped guardrails, vector stores, MCP servers, agents and pass-through endpoints for that pass, and the swallowed exception left only a log line behind. add_deployment also runs on a @@ -1178,7 +1178,7 @@ async def test_add_deployment_hydrates_non_llm_objects_when_model_reconcile_fail super().__init__() self.non_llm_hydrations = 0 - async def _get_models_from_db(self, prisma_client): + async def get_models_from_db(self, prisma_client): raise RuntimeError("db unavailable") async def _init_non_llm_objects_in_db(self, prisma_client): @@ -1186,7 +1186,7 @@ async def test_add_deployment_hydrates_non_llm_objects_when_model_reconcile_fail proxy_config = FailingReconcileProxyConfig() - outcome = await proxy_config._add_deployment_locked( + outcome = await proxy_config.add_deployment_locked( prisma_client=MagicMock(), proxy_logging_obj=MagicMock(spec=ProxyLogging), ) @@ -1213,7 +1213,7 @@ async def test_add_deployment_survives_a_non_llm_hydration_failure(): self.reconcile_ran = False self.hydration_attempted = False - async def _get_models_from_db(self, prisma_client): + async def get_models_from_db(self, prisma_client): return [] async def _update_llm_router(self, new_models, proxy_logging_obj): @@ -1226,7 +1226,7 @@ async def test_add_deployment_survives_a_non_llm_hydration_failure(): proxy_config = FailingHydrationProxyConfig() - outcome = await proxy_config._add_deployment_locked( + outcome = await proxy_config.add_deployment_locked( prisma_client=MagicMock(), proxy_logging_obj=MagicMock(spec=ProxyLogging), ) From 335f144f864d2e36aeaf2ee7a0290dac0ac31170 Mon Sep 17 00:00:00 2001 From: Yash Raj Pandey Date: Wed, 14 Oct 2026 11:49:25 -0400 Subject: [PATCH 5/9] test(proxy): let the reconcile succeed in the hydration-failure test The test said the reconcile succeeds, but get_config_param awaited the MagicMock prisma client and raised after the router update. With the fix reverted, the test failed only because of that error. Return no general_settings row, so the reconcile completes and the test checks only the new hydration handler. The regression test raises from get_models_from_db. A real outage does not raise there, so say what the test does instead. A skipped pass also leaves loaded objects in place, so do not say that it drops them. --- .../unit/proxy/test_proxy_server_endpoints_and_startup.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/unit/proxy/test_proxy_server_endpoints_and_startup.py b/tests/unit/proxy/test_proxy_server_endpoints_and_startup.py index 2be6df23a54..bd3cecfa973 100644 --- a/tests/unit/proxy/test_proxy_server_endpoints_and_startup.py +++ b/tests/unit/proxy/test_proxy_server_endpoints_and_startup.py @@ -1166,13 +1166,14 @@ async def test_add_deployment_hydrates_non_llm_objects_when_model_reconcile_fail hydration inside one try/except. A failure in the reconcile skipped guardrails, vector stores, MCP servers, agents and pass-through endpoints for that pass, and the swallowed exception left only a log line behind. add_deployment also runs on a - timer, so a transient DB error dropped every non-LLM object until a later pass. + timer, so while the reconcile kept failing, no pass loaded new or changed non-LLM + objects. """ from litellm.proxy.proxy_server import ProxyConfig from litellm.proxy.utils import ProxyLogging class FailingReconcileProxyConfig(ProxyConfig): - """Reconcile fails the way a DB outage makes it fail; hydration records itself.""" + """Reconcile raises inside the shared try, before the hydration; hydration records itself.""" def __init__(self): super().__init__() @@ -1227,7 +1228,7 @@ async def test_add_deployment_survives_a_non_llm_hydration_failure(): proxy_config = FailingHydrationProxyConfig() outcome = await proxy_config.add_deployment_locked( - prisma_client=MagicMock(), + prisma_client=MagicMock(get_generic_data=AsyncMock(return_value=None)), proxy_logging_obj=MagicMock(spec=ProxyLogging), ) From 5de826861f48910ed713da91cb8172195596267c Mon Sep 17 00:00:00 2001 From: Yash Raj Pandey Date: Wed, 14 Oct 2026 13:24:21 -0400 Subject: [PATCH 6/9] fix(proxy): keep DB pass-through routes when the reload cannot read them The reload job reads the pass-through endpoints from LiteLLM_Config on every pass. A failed read returned an empty list, and the empty list removed every DB-backed pass-through route. A DB error or a revoked SELECT grant made those routes return 404 until a later pass read the table again The reload path now re-raises a read error, logs it and keeps the current routes. A successful read with no endpoints, or with no pass_through_endpoints key, still removes the routes. The list and info API keep the lenient read --- .../pass_through_endpoints.py | 12 ++++- .../test_pass_through_endpoints.py | 50 +++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py index 1663907b556..45620179347 100644 --- a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py @@ -3645,6 +3645,8 @@ def _request_app(request: Request) -> FastAPI: async def _get_pass_through_endpoints_from_db( endpoint_id: str | None = None, user_api_key_dict: UserAPIKeyAuth | None = None, + *, + raise_on_read_error: bool = False, ) -> list[PassThroughGenericEndpoint]: from litellm.proxy._types import LitellmUserRoles from litellm.proxy.proxy_server import get_config_general_settings @@ -3655,7 +3657,11 @@ async def _get_pass_through_endpoints_from_db( response: Final[ConfigFieldInfo] = await get_config_general_settings( field_name="pass_through_endpoints", user_api_key_dict=user_api_key_dict ) + except HTTPException: + return [] except Exception: + if raise_on_read_error: + raise return [] pass_through_endpoint_data: Final = _config_field_endpoints(response) @@ -4141,5 +4147,9 @@ async def initialize_pass_through_endpoints_in_db(): """ Gets all pass-through endpoints from db and initializes them in the proxy server. """ - pass_through_endpoints: Final = await _get_pass_through_endpoints_from_db() + try: + pass_through_endpoints: Final = await _get_pass_through_endpoints_from_db(raise_on_read_error=True) + except Exception: # noqa: BLE001 # a failed read keeps the routes from the last good read until the next pass + verbose_proxy_logger.exception("Could not read pass-through endpoints from the DB, keeping the current routes") + return await initialize_pass_through_endpoints(pass_through_endpoints=pass_through_endpoints) diff --git a/tests/unit/proxy/pass_through_endpoints/test_pass_through_endpoints.py b/tests/unit/proxy/pass_through_endpoints/test_pass_through_endpoints.py index eb31797d460..086f91d7447 100644 --- a/tests/unit/proxy/pass_through_endpoints/test_pass_through_endpoints.py +++ b/tests/unit/proxy/pass_through_endpoints/test_pass_through_endpoints.py @@ -8985,6 +8985,56 @@ async def test_a_deleted_db_pass_through_stops_serving_on_the_next_db_sync(tmp_p assert [str(request.url) for request in config_upstream] == ["http://config-upstream.test/api"] +class _UnreadableConfigTable(_InMemoryConfigTable): + async def get_generic_data(self, key: str, value: str, table_name: str) -> _StoredConfigRow | None: + raise RuntimeError("permission denied for table LiteLLM_Config") + + async def find_first(self, where: Mapping[str, str]) -> _StoredConfigRow | None: + raise RuntimeError("permission denied for table LiteLLM_Config") + + async def find_unique(self, where: Mapping[str, str]) -> _StoredConfigRow | None: + raise RuntimeError("permission denied for table LiteLLM_Config") + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("config_table_after", "expected_status", "expected_upstream"), + [ + (_UnreadableConfigTable({}), 200, ["http://db-upstream.test/api"]), + (_InMemoryConfigTable({"general_settings": {"pass_through_endpoints": []}}), 404, []), + (_InMemoryConfigTable({"general_settings": {}}), 404, []), + ], + ids=["read-fails", "emptied-list", "dropped-key"], +) +async def test_the_reload_job_keeps_db_pass_throughs_only_when_the_db_read_fails( + tmp_path, monkeypatch, config_table_after, expected_status, expected_upstream +): + from litellm.proxy import proxy_server + from litellm.proxy.utils import ProxyLogging + + proxy: Final = await _boot_db_backed_proxy( + tmp_path, + monkeypatch, + config_pass_through_endpoints=[], + db_pass_through_endpoints=[ + {"id": "db-kept", "path": "/db-kept", "target": "http://db-upstream.test/api", "auth": False} + ], + master_key="sk-pass-through-master", + ) + await _run_db_sync_cycle(proxy) + served_before, _ = await _send_through_proxy("/db-kept", {}) + + monkeypatch.setitem(proxy_server.general_settings, "supported_db_objects", ["pass_through_endpoints"]) + monkeypatch.setattr(proxy_server, "prisma_client", config_table_after) + await proxy.proxy_config.add_deployment( + prisma_client=config_table_after, proxy_logging_obj=MagicMock(spec=ProxyLogging) + ) + served_after, db_upstream = await _send_through_proxy("/db-kept", {}) + + assert (served_before.status_code, served_after.status_code) == (200, expected_status) + assert [str(request.url) for request in db_upstream] == expected_upstream + + @pytest.mark.asyncio async def test_config_pass_through_reads_its_custom_key_header_when_the_db_holds_pass_throughs(tmp_path, monkeypatch): proxy: Final = await _boot_db_backed_proxy( From 75acf8b8b1edd2cc715f52ec8beb7a37ee1b4f09 Mon Sep 17 00:00:00 2001 From: Yash Raj Pandey Date: Wed, 14 Oct 2026 13:24:21 -0400 Subject: [PATCH 7/9] test(proxy): fail the router update in the reconcile-failure test get_models_from_db catches a DB error and returns None, so a real outage does not raise there. Raise from _update_llm_router instead, inside the same try as before --- .../unit/proxy/test_proxy_server_endpoints_and_startup.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/unit/proxy/test_proxy_server_endpoints_and_startup.py b/tests/unit/proxy/test_proxy_server_endpoints_and_startup.py index bd3cecfa973..54285663064 100644 --- a/tests/unit/proxy/test_proxy_server_endpoints_and_startup.py +++ b/tests/unit/proxy/test_proxy_server_endpoints_and_startup.py @@ -1173,14 +1173,17 @@ async def test_add_deployment_hydrates_non_llm_objects_when_model_reconcile_fail from litellm.proxy.utils import ProxyLogging class FailingReconcileProxyConfig(ProxyConfig): - """Reconcile raises inside the shared try, before the hydration; hydration records itself.""" + """The router update raises inside the shared try, before the hydration; hydration records itself.""" def __init__(self): super().__init__() self.non_llm_hydrations = 0 async def get_models_from_db(self, prisma_client): - raise RuntimeError("db unavailable") + return [] + + async def _update_llm_router(self, new_models, proxy_logging_obj): + raise RuntimeError("router update failed") async def _init_non_llm_objects_in_db(self, prisma_client): self.non_llm_hydrations += 1 From f6f2d08359eb2e405d09e86229d87ef9a39dcb32 Mon Sep 17 00:00:00 2001 From: Yash Raj Pandey Date: Wed, 14 Oct 2026 13:24:22 -0400 Subject: [PATCH 8/9] style(proxy): use the noqa reason form for the hydration handler AGENTS.md wants "# noqa: RULE # reason". Remove the comment above the second try, because the handler reason already says why it exists --- litellm/proxy/proxy_server.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 240d5851777..f2e858bc712 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -8444,12 +8444,10 @@ class ProxyConfig: except Exception as e: verbose_proxy_logger.exception("litellm.proxy.proxy_server.py::ProxyConfig:add_deployment - %s", e) - # Own try: the model reconcile above must not decide whether guardrails, vector - # stores, MCP servers and agents get hydrated. They read their own tables. try: # initialize vector stores, guardrails, etc. table in db await self._init_non_llm_objects_in_db(prisma_client=prisma_client) - except Exception as e: # noqa: BLE001 - same swallow the single handler above already had; a hydration failure must not stop the reconcile + except Exception as e: # noqa: BLE001 # log and carry on, like the reconcile handler above verbose_proxy_logger.exception( "litellm.proxy.proxy_server.py::ProxyConfig:add_deployment non-LLM objects - %s", e ) From a7c9f1c4c01b3f1872f591b522452da9efa0e373 Mon Sep 17 00:00:00 2001 From: Yash Raj Pandey Date: Wed, 14 Oct 2026 15:50:28 -0400 Subject: [PATCH 9/9] fix(proxy): reload DB pass-through routes only with their auth flags A pass-through route has two halves. The route registry holds the target and the headers, and general_settings holds the auth flag. The hydration try reloaded the registry even when a failed model reconcile skipped _update_general_settings. A route changed from auth false to auth true then sent its new target and headers with no key Reload the pass-through routes in the reconcile try, right after _update_general_settings. A failed reconcile now keeps the old route and its old auth flag together. The other non-LLM objects still load in their own try --- litellm/proxy/proxy_server.py | 6 +-- .../test_pass_through_endpoints.py | 50 +++++++++++++++++++ 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index f2e858bc712..16c83a46942 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -8441,6 +8441,9 @@ class ProxyConfig: db_general_settings=db_general_settings.param_value, ) + if self._should_load_db_object(object_type="pass_through_endpoints"): + await self._init_pass_through_endpoints_in_db() + except Exception as e: verbose_proxy_logger.exception("litellm.proxy.proxy_server.py::ProxyConfig:add_deployment - %s", e) @@ -8539,9 +8542,6 @@ class ProxyConfig: if self._should_load_db_object(object_type="agents"): await self._init_agents_in_db(prisma_client=prisma_client) - if self._should_load_db_object(object_type="pass_through_endpoints"): - await self._init_pass_through_endpoints_in_db() - if self._should_load_db_object(object_type="prompts"): await self._init_prompts_in_db(prisma_client=prisma_client) diff --git a/tests/unit/proxy/pass_through_endpoints/test_pass_through_endpoints.py b/tests/unit/proxy/pass_through_endpoints/test_pass_through_endpoints.py index 086f91d7447..c7c2e07d5e0 100644 --- a/tests/unit/proxy/pass_through_endpoints/test_pass_through_endpoints.py +++ b/tests/unit/proxy/pass_through_endpoints/test_pass_through_endpoints.py @@ -12,6 +12,7 @@ from contextlib import ExitStack, contextmanager from dataclasses import dataclass from datetime import datetime from io import BytesIO +from pathlib import Path from types import MappingProxyType, ModuleType, SimpleNamespace from typing import Final, Optional, cast from unittest.mock import AsyncMock, MagicMock, patch @@ -9035,6 +9036,55 @@ async def test_the_reload_job_keeps_db_pass_throughs_only_when_the_db_read_fails assert [str(request.url) for request in db_upstream] == expected_upstream +@pytest.mark.asyncio +async def test_a_failed_model_reconcile_never_serves_a_new_db_pass_through_with_its_old_auth_flag( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + from litellm.proxy import proxy_server + from litellm.proxy.utils import ProxyLogging, invalidate_config_param + + proxy: Final = await _boot_db_backed_proxy( + tmp_path, + monkeypatch, + config_pass_through_endpoints=[], + db_pass_through_endpoints=[ + {"id": "db-guarded", "path": "/db-guarded", "target": "http://public-upstream.test/api", "auth": False} + ], + master_key="sk-pass-through-master", + ) + await _run_db_sync_cycle(proxy) + proxy.config_table.rows["general_settings"] = { + "pass_through_endpoints": [ + { + "id": "db-guarded", + "path": "/db-guarded", + "target": "http://protected-upstream.test/api", + "headers": {"x-upstream-key": "upstream-secret"}, + "auth": True, + } + ] + } + await invalidate_config_param("general_settings") + + async def failing_router_update(new_models: object, proxy_logging_obj: object) -> frozenset[str]: + raise RuntimeError("router update failed") + + monkeypatch.setitem( + proxy_server.proxy_config.settings, "supported_db_objects", ["models", "pass_through_endpoints"] + ) + monkeypatch.setattr(proxy_server.proxy_config, "_update_llm_router", failing_router_update) + assert proxy_server.prisma_client is not None + await proxy_server.proxy_config.add_deployment( + prisma_client=proxy_server.prisma_client, proxy_logging_obj=MagicMock(spec=ProxyLogging) + ) + anonymous, anonymous_upstream = await _send_through_proxy("/db-guarded", {}) + + assert anonymous.status_code == 200 + assert [(str(request.url), request.headers.get("x-upstream-key")) for request in anonymous_upstream] == [ + ("http://public-upstream.test/api", None) + ] + + @pytest.mark.asyncio async def test_config_pass_through_reads_its_custom_key_header_when_the_db_holds_pass_throughs(tmp_path, monkeypatch): proxy: Final = await _boot_db_backed_proxy(