From 6e954e1375fc25b3465d49f570f98109d29f6f29 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 10 Jul 2026 12:11:01 +0000 Subject: [PATCH] fix(proxy): match slashed model names on GET/DELETE /fallback/{model} --- .../fallback_management_endpoints.py | 4 +- .../test_fallback_management_endpoints.py | 66 ++++++++++++++++++- 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/litellm/proxy/management_endpoints/fallback_management_endpoints.py b/litellm/proxy/management_endpoints/fallback_management_endpoints.py index dc594923166..fedf3c6ba12 100644 --- a/litellm/proxy/management_endpoints/fallback_management_endpoints.py +++ b/litellm/proxy/management_endpoints/fallback_management_endpoints.py @@ -190,7 +190,7 @@ async def create_fallback( @router.get( - "/fallback/{model}", + "/fallback/{model:path}", tags=["Fallback Management"], dependencies=[Depends(user_api_key_auth)], response_model=FallbackGetResponse, @@ -247,7 +247,7 @@ async def get_fallback( @router.delete( - "/fallback/{model}", + "/fallback/{model:path}", tags=["Fallback Management"], dependencies=[Depends(user_api_key_auth)], response_model=FallbackDeleteResponse, diff --git a/tests/test_litellm/proxy/test_fallback_management_endpoints.py b/tests/test_litellm/proxy/test_fallback_management_endpoints.py index 054dafbf5a7..0b60f98472d 100644 --- a/tests/test_litellm/proxy/test_fallback_management_endpoints.py +++ b/tests/test_litellm/proxy/test_fallback_management_endpoints.py @@ -11,14 +11,19 @@ Tests: from unittest.mock import AsyncMock, MagicMock, patch import pytest -from fastapi import HTTPException +from fastapi import FastAPI, HTTPException +from fastapi.testclient import TestClient +from litellm.proxy.auth.user_api_key_auth import user_api_key_auth from litellm.proxy.management_endpoints.fallback_management_endpoints import ( FallbackCreateRequest, create_fallback, delete_fallback, get_fallback, ) +from litellm.proxy.management_endpoints.fallback_management_endpoints import ( + router as fallback_router, +) class TestFallbackCreateRequest: @@ -553,3 +558,62 @@ class TestDeleteFallback: assert exc_info.value.status_code == 400 assert "Database storage not enabled" in str(exc_info.value.detail) + + +class TestSlashedModelRouting: + """Routing regression tests for provider-prefixed model names containing a slash. + + These exercise the real FastAPI route matching (not the handler directly), so they + fail if the path parameter reverts to the default `str` converter, which only matches + a single path segment and returns Starlette's 404 for names like `openrouter/gpt-4`. + """ + + @pytest.fixture + def client(self): + app = FastAPI() + app.include_router(fallback_router) + app.dependency_overrides[user_api_key_auth] = lambda: MagicMock() + return TestClient(app) + + def test_get_fallback_slashed_model_reaches_handler(self, client): + router = MagicMock() + router.fallbacks = [{"openrouter/gpt-4": ["gpt-4", "claude-3-haiku"]}] + router.context_window_fallbacks = [] + router.content_policy_fallbacks = [] + + with patch("litellm.proxy.proxy_server.llm_router", router): + response = client.get("/fallback/openrouter/gpt-4") + + assert response.status_code == 200 + body = response.json() + assert body["model"] == "openrouter/gpt-4" + assert body["fallback_models"] == ["gpt-4", "claude-3-haiku"] + + def test_delete_fallback_slashed_model_reaches_handler(self, client): + router = MagicMock() + router.fallbacks = [{"openrouter/gpt-4": ["gpt-4"]}] + router.context_window_fallbacks = [] + router.content_policy_fallbacks = [] + + prisma_client = MagicMock() + prisma_client.db.litellm_config.upsert = AsyncMock() + + proxy_config = MagicMock() + proxy_config.get_config = AsyncMock( + return_value={ + "router_settings": {"fallbacks": [{"openrouter/gpt-4": ["gpt-4"]}]} + } + ) + + with ( + patch("litellm.proxy.proxy_server.llm_router", router), + patch("litellm.proxy.proxy_server.prisma_client", prisma_client), + patch("litellm.proxy.proxy_server.proxy_config", proxy_config), + patch("litellm.proxy.proxy_server.store_model_in_db", True), + ): + response = client.delete("/fallback/openrouter/gpt-4") + + assert response.status_code == 200 + body = response.json() + assert body["model"] == "openrouter/gpt-4" + assert "deleted" in body["message"].lower()