From e491cace81024cd361eaaeb7326e463978ff21b0 Mon Sep 17 00:00:00 2001 From: joereyna Date: Wed, 11 Mar 2026 19:14:35 -0700 Subject: [PATCH 1/5] fix: strip SERVER_ROOT_PATH prefix before checking mapped pass-through routes --- .../proxy/pass_through_endpoints/pass_through_endpoints.py | 7 ++++++- 1 file changed, 6 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 a06a3aa3da..1033ba7921 100644 --- a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py @@ -2058,8 +2058,13 @@ class InitPassThroughEndpointHelpers: bool: True if route is a registered pass-through endpoint, False otherwise """ ## CHECK IF MAPPED PASS THROUGH ENDPOINT + # Strip server root path prefix so mapped routes match when SERVER_ROOT_PATH is set + root_path = get_server_root_path() + normalized_route = route + if root_path and root_path != "/" and route.startswith(root_path): + normalized_route = route[len(root_path):] for mapped_route in LiteLLMRoutes.mapped_pass_through_routes.value: - if route.startswith(mapped_route): + if normalized_route.startswith(mapped_route): return True # Fast path: check if any registered route key contains this path From 47e41deab60027c26c6b6b8a6ba4d494f43f3994 Mon Sep 17 00:00:00 2001 From: joereyna Date: Wed, 11 Mar 2026 20:00:25 -0700 Subject: [PATCH 2/5] fix: enforce SERVER_ROOT_PATH prefix guard in mapped pass-through route checks Routes lacking the root prefix can no longer spuriously match mapped pass-through routes (vertex_ai, bedrock, etc.) when SERVER_ROOT_PATH is set. Also applies the same fix to the identical check in user_api_key_auth.py (litellm_user_api_key header extraction). --- litellm/proxy/auth/user_api_key_auth.py | 16 ++++++++++---- .../pass_through_endpoints.py | 21 ++++++++++++------- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/litellm/proxy/auth/user_api_key_auth.py b/litellm/proxy/auth/user_api_key_auth.py index c992cfb53e..40a1e25068 100644 --- a/litellm/proxy/auth/user_api_key_auth.py +++ b/litellm/proxy/auth/user_api_key_auth.py @@ -50,7 +50,7 @@ from litellm.proxy.common_utils.http_parsing_utils import ( _read_request_body, _safe_get_request_headers, populate_request_with_path_params) from litellm.proxy.common_utils.realtime_utils import _realtime_request_body -from litellm.proxy.utils import PrismaClient, ProxyLogging +from litellm.proxy.utils import PrismaClient, ProxyLogging, get_server_root_path from litellm.secret_managers.main import get_secret_bool from litellm.types.services import ServiceTypes @@ -386,9 +386,17 @@ async def check_api_key_for_custom_headers_or_pass_through_endpoints( api_key: str, ) -> Union[UserAPIKeyAuth, str]: is_mapped_pass_through_route: bool = False - for mapped_route in LiteLLMRoutes.mapped_pass_through_routes.value: # type: ignore - if route.startswith(mapped_route): - is_mapped_pass_through_route = True + root_path = get_server_root_path() + if root_path and root_path != "/": + if route.startswith(root_path): + normalized_route = route[len(root_path):] + for mapped_route in LiteLLMRoutes.mapped_pass_through_routes.value: # type: ignore + if normalized_route.startswith(mapped_route): + is_mapped_pass_through_route = True + else: + for mapped_route in LiteLLMRoutes.mapped_pass_through_routes.value: # type: ignore + if route.startswith(mapped_route): + is_mapped_pass_through_route = True if is_mapped_pass_through_route: if request.headers.get("litellm_user_api_key") is not None: api_key = request.headers.get("litellm_user_api_key") or "" diff --git a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py index 1033ba7921..8d6a4c00b7 100644 --- a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py @@ -2058,14 +2058,21 @@ class InitPassThroughEndpointHelpers: bool: True if route is a registered pass-through endpoint, False otherwise """ ## CHECK IF MAPPED PASS THROUGH ENDPOINT - # Strip server root path prefix so mapped routes match when SERVER_ROOT_PATH is set + # When SERVER_ROOT_PATH is set, all valid routes carry that prefix. + # Strip it before comparing against mapped routes; if the route does not + # carry the prefix, it cannot be a mapped pass-through route. root_path = get_server_root_path() - normalized_route = route - if root_path and root_path != "/" and route.startswith(root_path): - normalized_route = route[len(root_path):] - for mapped_route in LiteLLMRoutes.mapped_pass_through_routes.value: - if normalized_route.startswith(mapped_route): - return True + if root_path and root_path != "/": + if route.startswith(root_path): + normalized_route = route[len(root_path):] + for mapped_route in LiteLLMRoutes.mapped_pass_through_routes.value: + if normalized_route.startswith(mapped_route): + return True + # Route lacks expected prefix — not a mapped pass-through route + else: + for mapped_route in LiteLLMRoutes.mapped_pass_through_routes.value: + if route.startswith(mapped_route): + return True # Fast path: check if any registered route key contains this path # Keys are in format: "{endpoint_id}:exact:{path}:{methods}" or "{endpoint_id}:subpath:{path}:{methods}" From 791e598ad546639bb7757f176568adb3dabe3bbb Mon Sep 17 00:00:00 2001 From: joereyna Date: Wed, 11 Mar 2026 23:50:34 -0700 Subject: [PATCH 3/5] fix: add break on match and guard empty normalized_route in mapped route checks - Add break after match in user_api_key_auth.py loop to avoid unnecessary iterations over remaining mapped routes - Guard against normalized_route being empty when route == root_path exactly, which would otherwise match every mapped route via startswith("") - Apply same empty-string guard in pass_through_endpoints.py for consistency --- litellm/proxy/auth/user_api_key_auth.py | 9 ++++++--- .../pass_through_endpoints/pass_through_endpoints.py | 9 +++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/litellm/proxy/auth/user_api_key_auth.py b/litellm/proxy/auth/user_api_key_auth.py index 40a1e25068..e57adfda05 100644 --- a/litellm/proxy/auth/user_api_key_auth.py +++ b/litellm/proxy/auth/user_api_key_auth.py @@ -390,13 +390,16 @@ async def check_api_key_for_custom_headers_or_pass_through_endpoints( if root_path and root_path != "/": if route.startswith(root_path): normalized_route = route[len(root_path):] - for mapped_route in LiteLLMRoutes.mapped_pass_through_routes.value: # type: ignore - if normalized_route.startswith(mapped_route): - is_mapped_pass_through_route = True + if normalized_route: # guard against route == root_path exactly + for mapped_route in LiteLLMRoutes.mapped_pass_through_routes.value: # type: ignore + if normalized_route.startswith(mapped_route): + is_mapped_pass_through_route = True + break else: for mapped_route in LiteLLMRoutes.mapped_pass_through_routes.value: # type: ignore if route.startswith(mapped_route): is_mapped_pass_through_route = True + break if is_mapped_pass_through_route: if request.headers.get("litellm_user_api_key") is not None: api_key = request.headers.get("litellm_user_api_key") or "" diff --git a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py index 8d6a4c00b7..3c208fc8d3 100644 --- a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py @@ -2065,10 +2065,11 @@ class InitPassThroughEndpointHelpers: if root_path and root_path != "/": if route.startswith(root_path): normalized_route = route[len(root_path):] - for mapped_route in LiteLLMRoutes.mapped_pass_through_routes.value: - if normalized_route.startswith(mapped_route): - return True - # Route lacks expected prefix — not a mapped pass-through route + if normalized_route: # guard against route == root_path exactly + for mapped_route in LiteLLMRoutes.mapped_pass_through_routes.value: + if normalized_route.startswith(mapped_route): + return True + # Route lacks expected prefix (or is exactly root_path) — not a mapped pass-through route else: for mapped_route in LiteLLMRoutes.mapped_pass_through_routes.value: if route.startswith(mapped_route): From 938452cc59e13c0978088a2befff143e0562ef57 Mon Sep 17 00:00:00 2001 From: joereyna Date: Thu, 12 Mar 2026 07:55:22 -0700 Subject: [PATCH 4/5] fix: extract normalize_route_for_root_path to deduplicate root-path stripping --- litellm/proxy/auth/user_api_key_auth.py | 16 ++++------------ .../pass_through_endpoints.py | 19 ++++--------------- litellm/proxy/utils.py | 10 ++++++++++ 3 files changed, 18 insertions(+), 27 deletions(-) diff --git a/litellm/proxy/auth/user_api_key_auth.py b/litellm/proxy/auth/user_api_key_auth.py index e57adfda05..70ed7ad3c8 100644 --- a/litellm/proxy/auth/user_api_key_auth.py +++ b/litellm/proxy/auth/user_api_key_auth.py @@ -50,7 +50,7 @@ from litellm.proxy.common_utils.http_parsing_utils import ( _read_request_body, _safe_get_request_headers, populate_request_with_path_params) from litellm.proxy.common_utils.realtime_utils import _realtime_request_body -from litellm.proxy.utils import PrismaClient, ProxyLogging, get_server_root_path +from litellm.proxy.utils import PrismaClient, ProxyLogging, normalize_route_for_root_path from litellm.secret_managers.main import get_secret_bool from litellm.types.services import ServiceTypes @@ -386,18 +386,10 @@ async def check_api_key_for_custom_headers_or_pass_through_endpoints( api_key: str, ) -> Union[UserAPIKeyAuth, str]: is_mapped_pass_through_route: bool = False - root_path = get_server_root_path() - if root_path and root_path != "/": - if route.startswith(root_path): - normalized_route = route[len(root_path):] - if normalized_route: # guard against route == root_path exactly - for mapped_route in LiteLLMRoutes.mapped_pass_through_routes.value: # type: ignore - if normalized_route.startswith(mapped_route): - is_mapped_pass_through_route = True - break - else: + normalized_route = normalize_route_for_root_path(route) + if normalized_route is not None: for mapped_route in LiteLLMRoutes.mapped_pass_through_routes.value: # type: ignore - if route.startswith(mapped_route): + if normalized_route.startswith(mapped_route): is_mapped_pass_through_route = True break if is_mapped_pass_through_route: diff --git a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py index 3c208fc8d3..d475be5a06 100644 --- a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py @@ -54,7 +54,7 @@ from litellm.proxy.common_utils.http_parsing_utils import ( _read_request_body, _safe_get_request_headers, ) -from litellm.proxy.utils import get_server_root_path +from litellm.proxy.utils import get_server_root_path, normalize_route_for_root_path from litellm.secret_managers.main import get_secret_str from litellm.types.llms.custom_http import httpxSpecialProvider from litellm.types.passthrough_endpoints.pass_through_endpoints import ( @@ -2058,21 +2058,10 @@ class InitPassThroughEndpointHelpers: bool: True if route is a registered pass-through endpoint, False otherwise """ ## CHECK IF MAPPED PASS THROUGH ENDPOINT - # When SERVER_ROOT_PATH is set, all valid routes carry that prefix. - # Strip it before comparing against mapped routes; if the route does not - # carry the prefix, it cannot be a mapped pass-through route. - root_path = get_server_root_path() - if root_path and root_path != "/": - if route.startswith(root_path): - normalized_route = route[len(root_path):] - if normalized_route: # guard against route == root_path exactly - for mapped_route in LiteLLMRoutes.mapped_pass_through_routes.value: - if normalized_route.startswith(mapped_route): - return True - # Route lacks expected prefix (or is exactly root_path) — not a mapped pass-through route - else: + normalized_route = normalize_route_for_root_path(route) + if normalized_route is not None: for mapped_route in LiteLLMRoutes.mapped_pass_through_routes.value: - if route.startswith(mapped_route): + if normalized_route.startswith(mapped_route): return True # Fast path: check if any registered route key contains this path diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index 2f9d27568e..5a4e6dd8b5 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -5145,6 +5145,16 @@ def get_server_root_path() -> str: return os.getenv("SERVER_ROOT_PATH", "") +def normalize_route_for_root_path(route: str) -> Optional[str]: + """Strip SERVER_ROOT_PATH prefix. Returns de-prefixed route, or None if route is not under root path.""" + root_path = get_server_root_path() + if root_path and root_path != "/": + if route.startswith(root_path + "/"): + return route[len(root_path):] + return None + return route + + def get_prisma_client_or_throw(message: str): from litellm.proxy.proxy_server import prisma_client From 1af7f11dae5e313db1fafb2ccdeae662fbc52f17 Mon Sep 17 00:00:00 2001 From: joereyna Date: Thu, 12 Mar 2026 08:16:00 -0700 Subject: [PATCH 5/5] fix: extract normalize_route_for_root_path to deduplicate root-path stripping; fix mock target --- .../proxy/pass_through_endpoints/test_pass_through_endpoints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 5af24f9612..50459bf18a 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 @@ -2383,7 +2383,7 @@ def test_mapped_pass_through_routes_with_server_root_path(): ) with patch( - "litellm.proxy.pass_through_endpoints.pass_through_endpoints.get_server_root_path" + "litellm.proxy.utils.get_server_root_path" ) as mock_get_root: mock_get_root.return_value = "/litellm"