fix(proxy): stop /{provider}/v1/files and /v1/batches from shadowing custom pass_through_endpoints

Custom pass_through_endpoints entries from config.yaml are registered
during proxy startup, strictly after every built-in router (including
the generic /{provider}/v1/files and /v1/batches routes) is mounted at
module-import time. Since they're always appended to app.routes, the
generic native-provider routes always match first regardless of the
configured prefix, misinterpreting it as a provider name.

SafeRouteAdder now repositions a newly-added route immediately before
the first route whose path template contains "{provider}" -- the
shared marker for every such generic route, present and future -- so
a custom pass-through path always wins the match instead.

Fixes #37925
This commit is contained in:
IdoPort 2026-08-23 13:02:37 +03:00
parent f005afa146
commit 26bcafccb0
2 changed files with 70 additions and 0 deletions

View file

@ -2561,6 +2561,37 @@ class SafeRouteAdder:
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.
"""
routes = app.routes
new_route = 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:
routes.pop()
routes.insert(index, new_route)
return
@staticmethod
def add_api_route_if_not_exists(
app: FastAPI,
@ -2596,6 +2627,7 @@ class SafeRouteAdder:
methods=methods,
dependencies=dependencies,
)
SafeRouteAdder._move_before_generic_provider_routes(app=app)
verbose_proxy_logger.debug(
"Successfully added route: %s with methods %s",
path,

View file

@ -3066,6 +3066,44 @@ 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"
class TestCursorProxyRoute:
"""Tests for the Cursor Cloud Agents pass-through route."""