Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions litellm/proxy/pass_through_endpoints/pass_through_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -2561,6 +2561,43 @@ def _is_path_registered(app: FastAPI, path: str, methods: list[str]) -> bool:
return True
return False

# Every generic native-provider route (files, batches, and any future ones) is
# registered as "/{provider}/v1/...", so this literal path-parameter name is a
# reliable, future-proof marker -- no need to enumerate specific provider routes.
_GENERIC_PROVIDER_PATH_MARKER: Final = "{provider}"

@staticmethod
def _move_before_generic_provider_routes(app: FastAPI) -> None:
"""
Custom pass-through routes registered from config.yaml are always appended to
app.routes, since they're added during proxy startup, strictly after every
built-in router (including the generic "/{provider}/v1/files" and
"/{provider}/v1/batches" routes) is mounted at module-import time. Starlette
resolves overlapping path templates by registration order, so an appended
custom route can never win against those generic routes -- they always match
first and misinterpret the custom prefix as a provider name (see
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.

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]
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

@staticmethod
def add_api_route_if_not_exists(
app: FastAPI,
Expand Down Expand Up @@ -2596,6 +2633,7 @@ def add_api_route_if_not_exists(
methods=methods,
dependencies=dependencies,
)
SafeRouteAdder._move_before_generic_provider_routes(app=app)
verbose_proxy_logger.debug(
"Successfully added route: %s with methods %s",
path,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3066,6 +3066,75 @@ def test_native_provider_routes_are_unchanged(method, path, expected_name):
assert _resolve_route_name(method, path) == expected_name


def test_custom_pass_through_endpoint_prefix_wins_over_native_provider_routes():
"""
A pass_through_endpoints entry registered under an arbitrary, non-built-in
prefix (e.g. a self-hosted Anthropic-compatible endpoint reached via a
"/claude-aws" prefix) must win over the native /{provider}/v1/files and
/{provider}/v1/batches routes, which would otherwise misinterpret the
custom prefix as a provider name and 422/500 instead of forwarding
(see https://github.com/BerriAI/litellm/issues/37925).
"""
from litellm.proxy.pass_through_endpoints.pass_through_endpoints import (
InitPassThroughEndpointHelpers,
)
from litellm.proxy.proxy_server import app

for suffix in ("files", "batches"):
InitPassThroughEndpointHelpers.add_exact_path_route(
app=app,
path=f"/claude-aws/v1/{suffix}",
target=f"https://example.com/v1/{suffix}",
custom_headers=None,
forward_headers=False,
merge_query_params=False,
dependencies=None,
cost_per_request=None,
endpoint_id=f"test-claude-aws-{suffix}",
)

assert _resolve_route_name("POST", "/claude-aws/v1/files") == "endpoint_func"
assert _resolve_route_name("POST", "/claude-aws/v1/batches") == "endpoint_func"

# registering a custom prefix must not disturb resolution of unrelated,
# already-registered native-provider routes
assert _resolve_route_name("POST", "/openai/v1/files") == "create_file"
assert _resolve_route_name("GET", "/azure/v1/files") == "list_files"
assert _resolve_route_name("POST", "/v1/files") == "create_file"
assert _resolve_route_name("POST", "/v1/batches") == "create_batch"


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
deployment without the files/batches routers mounted), the newly-appended custom
route is left exactly where it was appended -- a safe no-op fallback.
"""
from litellm.proxy.pass_through_endpoints.pass_through_endpoints import (
SafeRouteAdder,
)

class _FakeRoute:
def __init__(self, path):
self.path = path

class _FakeRouter:
def __init__(self, routes):
self.routes = routes

class _FakeApp:
def __init__(self, routes):
self.routes = routes
self.router = _FakeRouter(routes)

routes = [_FakeRoute("/health"), _FakeRoute("/claude-aws/v1/files")]
app = _FakeApp(routes)

SafeRouteAdder._move_before_generic_provider_routes(app=app)

assert app.router.routes == routes


class TestCursorProxyRoute:
"""Tests for the Cursor Cloud Agents pass-through route."""

Expand Down
Loading