diff --git a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py index 8e43b0f52d3..0b0733be4a7 100644 --- a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py @@ -26,6 +26,7 @@ from fastapi import ( ) from fastapi.responses import StreamingResponse from starlette.datastructures import UploadFile as StarletteUploadFile +from starlette.routing import Match from starlette.websockets import WebSocketState from websockets.asyncio.client import connect from websockets.exceptions import ( @@ -2772,24 +2773,44 @@ class SafeRouteAdder: https://github.com/BerriAI/litellm/issues/37925). Move the just-appended route (the last item in app.routes) to sit immediately - before the first such generic route, so it is matched first instead. If no - generic provider route is registered (e.g. a minimal deployment), leave the - route appended -- current behavior is preserved as a safe fallback. + before the first generic route whose template would actually capture this + route's own path, so it is matched first instead. A route is moved only when + that genuine overlap exists: reordering unconditionally on any "{provider}" + sighting would also promote unrelated wildcard pass-throughs (e.g. a + "/key/{subpath:path}" entry) ahead of every route registered afterward, + including unrelated, authenticated built-in routes like "/key/generate" -- + a real route.matches() check on this route's own path rules that out, since + a route path containing its own "{...}" placeholder never structurally + matches a "/{provider}/..." template. If no generic provider route captures + this path (including when none is registered at all, e.g. a minimal + deployment), leave the route appended -- current behavior is preserved as a + safe fallback. Builds the reordered list in one expression and reassigns app.router.routes wholesale, rather than mutating the existing list in place with pop()/insert(). """ routes: Final = app.routes new_route: Final = routes[-1] + scope: Final = { + "type": "http", + "method": "GET", + "path": new_route.path, + "headers": [], + "query_string": b"", + "root_path": "", + } for index, route in enumerate(routes[:-1]): route_path = getattr(route, "path", None) - if route_path and SafeRouteAdder._GENERIC_PROVIDER_PATH_MARKER in route_path: - app.router.routes = [ # mutable-ok: framework's list # rebind-ok: reordering is the fix - *routes[:index], - new_route, - *routes[index:-1], - ] - return + if not (route_path and SafeRouteAdder._GENERIC_PROVIDER_PATH_MARKER in route_path): + continue + if route.matches(scope)[0] == Match.NONE: + continue + app.router.routes = [ # mutable-ok: framework's list # rebind-ok: reordering is the fix + *routes[:index], + new_route, + *routes[index:-1], + ] + return @staticmethod def add_api_route_if_not_exists( diff --git a/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py b/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py index 4296314fdf9..c650a93d42a 100644 --- a/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py +++ b/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py @@ -3451,6 +3451,44 @@ def test_custom_pass_through_endpoint_prefix_wins_over_native_provider_routes(): ] +def test_wildcard_pass_through_does_not_shadow_unrelated_built_in_routes(): + """ + A pass_through_endpoints entry with a wildcard subpath (e.g. "/key/{subpath:path}") + must not be promoted ahead of unrelated, authenticated built-in routes like + "/key/generate" just because it lands after the first "/{provider}/..." route in + app.routes once appended. Its own path never structurally matches a + "/{provider}/..." template, so it must be left exactly where it was appended. + """ + from litellm.proxy.pass_through_endpoints.pass_through_endpoints import ( + InitPassThroughEndpointHelpers, + ) + from litellm.proxy.proxy_server import app + + key_generate_route_name_before = _resolve_route_name("POST", "/key/generate") + assert key_generate_route_name_before is not None + + added_paths = {"/key/{subpath:path}"} + try: + InitPassThroughEndpointHelpers.add_exact_path_route( + app=app, + path="/key/{subpath:path}", + target="https://example.com/", + custom_headers=None, + forward_headers=False, + merge_query_params=False, + dependencies=None, + cost_per_request=None, + endpoint_id="test-key-wildcard", + ) + + assert _resolve_route_name("POST", "/key/generate") == key_generate_route_name_before + finally: + app.router.routes = [ + route for route in app.router.routes + if getattr(route, "path", None) not in added_paths + ] + + def test_move_before_generic_provider_routes_is_a_no_op_without_a_generic_route(): """ If no generic "/{provider}/..." route is registered on the app (e.g. a minimal