From 55be5dc7ae6b527f9c804c24acd8a8ef4d2c2949 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Tue, 3 Nov 2026 03:30:27 -0800 Subject: [PATCH] fix(proxy): list videos only on a deployment the router sends to the asked provider A provider-only list picked a team deployment whose unprefixed model (claude-haiku-4-5) the credential lookup reads as OpenAI while the router dispatches it as Anthropic, so the list answered 400. The pick now skips a deployment whose router provider differs. Also updates the DashScope list test to the error the deployment's key now reaches. --- .../openai_files_endpoints/common_utils.py | 22 ++++++++---- .../providers/test_dashscope_video_wire.py | 2 +- .../proxy/video_endpoints/test_endpoints.py | 35 +++++++++++++++++++ 3 files changed, 51 insertions(+), 8 deletions(-) diff --git a/litellm/proxy/openai_files_endpoints/common_utils.py b/litellm/proxy/openai_files_endpoints/common_utils.py index 911a18e8dfe..4e8af2bc082 100644 --- a/litellm/proxy/openai_files_endpoints/common_utils.py +++ b/litellm/proxy/openai_files_endpoints/common_utils.py @@ -23,6 +23,7 @@ from typing_extensions import NotRequired, ReadOnly, TypedDict from litellm.batches.batch_utils import batch_cost_is_final from litellm.constants import MAX_FILE_LIST_LIMIT +from litellm.litellm_core_utils.routing_common_utils import provider_for_generic_call from litellm.proxy._types import ProxyException from litellm.repositories.managed_file_repository import ManagedFileRepository from litellm.repositories.table_repositories import ( @@ -575,15 +576,22 @@ def get_team_provider_deployment_id( ) -> str | None: if llm_router is None: return None - match: Final = next( - _authorized_provider_credentials(llm_router, user_api_key_dict, custom_llm_provider, True), None + registry: Final = llm_router.registry() + authorized_deployments: Final = ( + registry.credentials(model_id, form="deployment", team_id=user_api_key_dict.team_id) + for model_id, _ in _authorized_provider_credentials(llm_router, user_api_key_dict, custom_llm_provider, True) ) - deployment: Final = ( - None - if match is None - else llm_router.registry().credentials(match[0], form="deployment", team_id=user_api_key_dict.team_id) + routed_deployment: Final = next( + ( + deployment + for deployment in authorized_deployments + if deployment is not None + and provider_for_generic_call(deployment.litellm_params.model_dump(exclude_none=True)) + == custom_llm_provider + ), + None, ) - return None if deployment is None else deployment.model_info.id + return None if routed_deployment is None else routed_deployment.model_info.id def _authorized_provider_credentials( diff --git a/tests/integration/providers/test_dashscope_video_wire.py b/tests/integration/providers/test_dashscope_video_wire.py index 7ad873e2496..b049fd768c3 100644 --- a/tests/integration/providers/test_dashscope_video_wire.py +++ b/tests/integration/providers/test_dashscope_video_wire.py @@ -472,7 +472,7 @@ def test_the_proxy_has_no_video_delete_route_so_a_dashscope_task_is_never_touche @pytest.mark.parametrize( ("operation", "message"), - [("remix", "Video remix is not supported by DashScope"), ("list", "DashScope API key is required")], + [("remix", "Video remix is not supported by DashScope"), ("list", "Video list is not supported by DashScope")], ) def test_operations_dashscope_has_no_api_for_answer_an_error_and_send_nothing_upstream( gateway: Gateway, operation: str, message: str diff --git a/tests/unit/proxy/video_endpoints/test_endpoints.py b/tests/unit/proxy/video_endpoints/test_endpoints.py index 8ebf390a971..0675e6eecb7 100644 --- a/tests/unit/proxy/video_endpoints/test_endpoints.py +++ b/tests/unit/proxy/video_endpoints/test_endpoints.py @@ -753,6 +753,41 @@ def test_list__provider_only_never_borrows_a_deployment_the_key_cannot_use( assert upstream.calls.last.request.url.host != _DEPLOYMENT_ARK_HOST +def test_list__provider_only_skips_a_deployment_the_router_sends_to_another_provider( + proxy_client_for_key_models: Callable[[list[str]], TestClient], monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setattr( + proxy_server, + "llm_router", + Router( + model_list=[ + { + "model_name": "haiku", + "litellm_params": {"model": "claude-haiku-4-5", "api_key": "deployment-anthropic-key"}, + "model_info": {"team_id": "team-video"}, + }, + { + "model_name": "sora", + "litellm_params": {"model": "openai/sora-2", "api_key": "deployment-openai-key"}, + }, + ] + ), + ) + team_key: Final = UserAPIKeyAuth(api_key="sk-video-list", team_id="team-video") + monkeypatch.setitem(proxy_server.app.dependency_overrides, user_api_key_auth, lambda: team_key) + client: Final = TestClient(proxy_server.app) + + with respx.mock(assert_all_called=False) as respx_mock: + upstream: Final = respx_mock.get("https://api.openai.com/v1/videos").mock( + return_value=httpx.Response(200, json={"object": "list", "data": []}) + ) + response: Final = client.get("/v1/videos", params={"custom_llm_provider": "openai"}) + + assert response.status_code == 200, response.text + assert upstream.call_count == 1 + assert upstream.calls.last.request.headers["authorization"] == "Bearer deployment-openai-key" + + def test_list__provider_only_on_a_proxy_without_a_router_is_a_clean_client_error( proxy_client_for_key_models: Callable[[list[str]], TestClient], monkeypatch: pytest.MonkeyPatch ) -> None: