diff --git a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py index c5a104d54d8..025d789b883 100644 --- a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py @@ -1679,6 +1679,20 @@ def create_pass_through_route( user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), subpath: str = "", # captures sub-paths when include_subpath=True ): + from litellm.proxy.auth.auth_utils import ( # noqa: PLC0415 + get_request_route, + ) + from litellm.proxy.pass_through_endpoints.pass_through_endpoints import ( # noqa: PLC0415 + InitPassThroughEndpointHelpers, + ) + + path = get_request_route(request) + if not InitPassThroughEndpointHelpers.is_registered_pass_through_route(route=path): + raise HTTPException( + status_code=404, + detail=f"Pass-through endpoint {endpoint} not found. This could have been deleted or not yet added to the proxy.", + ) + return await chat_completion_pass_through_endpoint( fastapi_response=fastapi_response, request=request, diff --git a/tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py b/tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py index aa937980831..4d0164c90a7 100644 --- a/tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py +++ b/tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py @@ -1134,6 +1134,65 @@ def test_create_pass_through_route_reregistering_same_adapter_reuses_id(): litellm.adapters = original_adapters +@pytest.mark.asyncio +async def test_create_pass_through_route_adapter_dispatch_rejects_deregistered_route(): + """ + Regression test: an adapter-backed pass-through route must stop + dispatching once it's no longer registered (e.g. after + delete_pass_through_endpoints removes it from the registry), the same + way the URL-backed pass-through route already does. Without this check, + a deleted adapter route stays reachable indefinitely because FastAPI has + no built-in route removal and the adapter closure otherwise dispatches + unconditionally on its captured adapter_id. + """ + original_adapters = litellm.adapters + + class _DeregisteredAdapter(CustomLogger): + pass + + from fastapi import HTTPException + + adapter = _DeregisteredAdapter() + unique_path = "/test/path/unique/adapter_deregistered" + + try: + litellm.adapters = [] + + endpoint_func = create_pass_through_route( + endpoint=unique_path, + target=adapter, + ) + + with ( + patch( + "litellm.proxy.pass_through_endpoints.pass_through_endpoints.InitPassThroughEndpointHelpers.is_registered_pass_through_route" + ) as mock_is_registered, + patch( + "litellm.proxy.pass_through_endpoints.pass_through_endpoints.chat_completion_pass_through_endpoint" + ) as mock_chat_completion, + ): + mock_is_registered.return_value = False + + mock_request = MagicMock(spec=Request) + mock_request.scope = {"path": unique_path} + mock_request.url = MagicMock() + mock_request.url.path = unique_path + mock_user_api_key_dict = MagicMock() + mock_user_api_key_dict.api_key = "test-key" + + with pytest.raises(HTTPException) as exc_info: + await endpoint_func( + request=mock_request, + user_api_key_dict=mock_user_api_key_dict, + fastapi_response=MagicMock(), + ) + + assert exc_info.value.status_code == 404 + mock_chat_completion.assert_not_called() + finally: + litellm.adapters = original_adapters + + def test_resolve_pass_through_request_timeout_precedence(): assert resolve_pass_through_request_timeout(endpoint_timeout=900) == 900.0