From 1bf3ab53884ba62c0a24540e0b9b3972b41dc843 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2026 14:45:31 +0000 Subject: [PATCH 1/8] fix(proxy): resolve router model aliases in /utils/supported_openai_params Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/proxy/proxy_server.py | 7 +++-- .../proxy/proxy_server/test_routes_utils.py | 31 ++++++++++++++++++- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 887716a383a..d1771cc9088 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -12478,11 +12478,14 @@ async def supported_openai_params(model: str): --header 'Authorization: Bearer sk-1234' ``` """ + global llm_router try: - model, custom_llm_provider, _, _ = litellm.get_llm_provider(model=model) + deployments: Final = llm_router.get_model_list(model_name=model) if llm_router is not None else None + model_to_map: Final = deployments[0]["litellm_params"]["model"] if deployments else model + litellm_model, custom_llm_provider, _, _ = litellm.get_llm_provider(model=model_to_map) return { "supported_openai_params": litellm.get_supported_openai_params( - model=model, custom_llm_provider=custom_llm_provider + model=litellm_model, custom_llm_provider=custom_llm_provider ) } except Exception: diff --git a/tests/test_litellm/proxy/proxy_server/test_routes_utils.py b/tests/test_litellm/proxy/proxy_server/test_routes_utils.py index 35b5c72f92e..7615c5e66db 100644 --- a/tests/test_litellm/proxy/proxy_server/test_routes_utils.py +++ b/tests/test_litellm/proxy/proxy_server/test_routes_utils.py @@ -9,7 +9,7 @@ Pins (PR2): from __future__ import annotations import asyncio -from unittest.mock import AsyncMock, MagicMock +from unittest.mock import MagicMock import pytest @@ -124,6 +124,35 @@ def test_supported_openai_params_happy_path(client, auth_as, patched_supported_p } +def test_supported_openai_params_resolves_router_alias(client, auth_as, monkeypatch): + """A router ``model_name`` alias unknown to the cost map (e.g. ``claude-opus-4-6-cached``) resolves via the router's underlying ``litellm_params.model`` instead of 400ing.""" + router = MagicMock() + router.get_model_list.return_value = [ + {"model_name": "claude-opus-4-6-cached", "litellm_params": {"model": "anthropic/claude-opus-4-6"}} + ] + monkeypatch.setattr(proxy_server, "llm_router", router) + seen = [] + + def _get_llm_provider(model): + seen.append(model) + return (model, "anthropic", None, None) + + monkeypatch.setattr(litellm, "get_llm_provider", _get_llm_provider) + monkeypatch.setattr( + litellm, + "get_supported_openai_params", + lambda model, custom_llm_provider=None: ["max_tokens"], + ) + + with auth_as(): + response = client.get("/utils/supported_openai_params", params={"model": "claude-opus-4-6-cached"}) + + assert response.status_code == 200 + assert response.json() == {"supported_openai_params": ["max_tokens"]} + router.get_model_list.assert_called_once_with(model_name="claude-opus-4-6-cached") + assert seen == ["anthropic/claude-opus-4-6"] + + def test_supported_openai_params_invalid_model(client, auth_as, monkeypatch): """Pins ``GET /utils/supported_openai_params`` (error: unknown model).""" From ba817aa9bbf45b10e37103de59afab36792f13ff Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2026 14:54:50 +0000 Subject: [PATCH 2/8] fix(proxy): avoid NotRequired access on litellm_params model Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/proxy/proxy_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index d1771cc9088..693769446f4 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -12481,7 +12481,7 @@ async def supported_openai_params(model: str): global llm_router try: deployments: Final = llm_router.get_model_list(model_name=model) if llm_router is not None else None - model_to_map: Final = deployments[0]["litellm_params"]["model"] if deployments else model + model_to_map: Final = (deployments[0]["litellm_params"].get("model") or model) if deployments else model litellm_model, custom_llm_provider, _, _ = litellm.get_llm_provider(model=model_to_map) return { "supported_openai_params": litellm.get_supported_openai_params( From 2d01397e4da5093cae5b79296bf1f8805c58ab78 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2026 15:03:52 +0000 Subject: [PATCH 3/8] test: pin llm_router in supported_openai_params tests Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- tests/test_litellm/proxy/proxy_server/test_routes_utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_litellm/proxy/proxy_server/test_routes_utils.py b/tests/test_litellm/proxy/proxy_server/test_routes_utils.py index 7615c5e66db..4d6ce0812a4 100644 --- a/tests/test_litellm/proxy/proxy_server/test_routes_utils.py +++ b/tests/test_litellm/proxy/proxy_server/test_routes_utils.py @@ -99,6 +99,7 @@ def test_token_counter_missing_input_returns_400( @pytest.fixture def patched_supported_params(monkeypatch): + monkeypatch.setattr(proxy_server, "llm_router", None) monkeypatch.setattr( litellm, "get_llm_provider", @@ -125,7 +126,7 @@ def test_supported_openai_params_happy_path(client, auth_as, patched_supported_p def test_supported_openai_params_resolves_router_alias(client, auth_as, monkeypatch): - """A router ``model_name`` alias unknown to the cost map (e.g. ``claude-opus-4-6-cached``) resolves via the router's underlying ``litellm_params.model`` instead of 400ing.""" + """A router alias unknown to the cost map resolves via the deployment's ``litellm_params.model``.""" router = MagicMock() router.get_model_list.return_value = [ {"model_name": "claude-opus-4-6-cached", "litellm_params": {"model": "anthropic/claude-opus-4-6"}} @@ -159,6 +160,7 @@ def test_supported_openai_params_invalid_model(client, auth_as, monkeypatch): def _raise(model): raise Exception("unknown") + monkeypatch.setattr(proxy_server, "llm_router", None) monkeypatch.setattr(litellm, "get_llm_provider", _raise) with auth_as(): response = client.get("/utils/supported_openai_params", params={"model": "??"}) From 11a74719028860a72b57d4afd43e44c95422488a Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Mon, 31 Aug 2026 11:08:31 -0700 Subject: [PATCH 4/8] refactor(proxy): resolve supported_openai_params aliases via Router.resolved_litellm_models --- litellm/proxy/proxy_server.py | 7 ++-- .../proxy/proxy_server/test_routes_utils.py | 33 +++++++------------ 2 files changed, 16 insertions(+), 24 deletions(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 693769446f4..170845babdb 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -12480,9 +12480,10 @@ async def supported_openai_params(model: str): """ global llm_router try: - deployments: Final = llm_router.get_model_list(model_name=model) if llm_router is not None else None - model_to_map: Final = (deployments[0]["litellm_params"].get("model") or model) if deployments else model - litellm_model, custom_llm_provider, _, _ = litellm.get_llm_provider(model=model_to_map) + resolved_models: Final = llm_router.resolved_litellm_models(model) if llm_router is not None else () + litellm_model, custom_llm_provider, _, _ = litellm.get_llm_provider( + model=resolved_models[0] if resolved_models else model + ) return { "supported_openai_params": litellm.get_supported_openai_params( model=litellm_model, custom_llm_provider=custom_llm_provider diff --git a/tests/test_litellm/proxy/proxy_server/test_routes_utils.py b/tests/test_litellm/proxy/proxy_server/test_routes_utils.py index 4d6ce0812a4..629d829ef8a 100644 --- a/tests/test_litellm/proxy/proxy_server/test_routes_utils.py +++ b/tests/test_litellm/proxy/proxy_server/test_routes_utils.py @@ -9,7 +9,6 @@ Pins (PR2): from __future__ import annotations import asyncio -from unittest.mock import MagicMock import pytest @@ -126,32 +125,24 @@ def test_supported_openai_params_happy_path(client, auth_as, patched_supported_p def test_supported_openai_params_resolves_router_alias(client, auth_as, monkeypatch): - """A router alias unknown to the cost map resolves via the deployment's ``litellm_params.model``.""" - router = MagicMock() - router.get_model_list.return_value = [ - {"model_name": "claude-opus-4-6-cached", "litellm_params": {"model": "anthropic/claude-opus-4-6"}} - ] - monkeypatch.setattr(proxy_server, "llm_router", router) - seen = [] - - def _get_llm_provider(model): - seen.append(model) - return (model, "anthropic", None, None) - - monkeypatch.setattr(litellm, "get_llm_provider", _get_llm_provider) - monkeypatch.setattr( - litellm, - "get_supported_openai_params", - lambda model, custom_llm_provider=None: ["max_tokens"], + """A router alias absent from the cost map resolves through the deployment's underlying model.""" + router = litellm.Router( + model_list=[ + { + "model_name": "claude-opus-4-6-cached", + "litellm_params": {"model": "anthropic/claude-opus-4-6", "api_key": "sk-test"}, + } + ] ) + monkeypatch.setattr(proxy_server, "llm_router", router) with auth_as(): response = client.get("/utils/supported_openai_params", params={"model": "claude-opus-4-6-cached"}) assert response.status_code == 200 - assert response.json() == {"supported_openai_params": ["max_tokens"]} - router.get_model_list.assert_called_once_with(model_name="claude-opus-4-6-cached") - assert seen == ["anthropic/claude-opus-4-6"] + expected = litellm.get_supported_openai_params(model="claude-opus-4-6", custom_llm_provider="anthropic") + assert response.json() == {"supported_openai_params": expected} + assert "max_tokens" in response.json()["supported_openai_params"] def test_supported_openai_params_invalid_model(client, auth_as, monkeypatch): From 779b3010d4f4a9e45185df06acb6e8eabc18e170 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Mon, 31 Aug 2026 12:06:35 -0700 Subject: [PATCH 5/8] fix(proxy): never run OAuth device flows when resolving model names Resolving github_copilot/chatgpt names through get_llm_provider runs the provider's OAuth device flow synchronously on the event loop. Adopt the declared provider in PatternMatchRouter.get_pattern, which the auth layer's zero-cost budget check walks on every request against wildcard routers, and in /utils/supported_openai_params. --- litellm/proxy/proxy_server.py | 16 ++++-- .../router_utils/pattern_match_deployments.py | 25 +++++---- .../proxy/proxy_server/test_routes_utils.py | 51 +++++++++++++++++++ .../test_pattern_match_deployments.py | 51 +++++++++++++++++++ 4 files changed, 127 insertions(+), 16 deletions(-) create mode 100644 tests/test_litellm/router_utils/test_pattern_match_deployments.py diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 170845babdb..c5b1e251398 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -12478,11 +12478,21 @@ async def supported_openai_params(model: str): --header 'Authorization: Bearer sk-1234' ``` """ + from litellm.litellm_core_utils.get_llm_provider_logic import declared_authenticating_provider + global llm_router try: - resolved_models: Final = llm_router.resolved_litellm_models(model) if llm_router is not None else () - litellm_model, custom_llm_provider, _, _ = litellm.get_llm_provider( - model=resolved_models[0] if resolved_models else model + resolved_models: Final = ( + llm_router.resolved_litellm_models(model) + if llm_router is not None and declared_authenticating_provider(model) is None + else () + ) + target_model: Final = resolved_models[0] if resolved_models else model + declared_provider: Final = declared_authenticating_provider(target_model) + litellm_model, custom_llm_provider = ( + (target_model.removeprefix(f"{declared_provider}/"), declared_provider) + if declared_provider is not None + else litellm.get_llm_provider(model=target_model)[:2] ) return { "supported_openai_params": litellm.get_supported_openai_params( diff --git a/litellm/router_utils/pattern_match_deployments.py b/litellm/router_utils/pattern_match_deployments.py index 0d5ef01bc04..850ca74b387 100644 --- a/litellm/router_utils/pattern_match_deployments.py +++ b/litellm/router_utils/pattern_match_deployments.py @@ -8,7 +8,7 @@ from re import Match from typing import Final from litellm._logging import verbose_router_logger -from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider +from litellm.litellm_core_utils.get_llm_provider_logic import declared_authenticating_provider, get_llm_provider class PatternUtils: @@ -215,18 +215,17 @@ class PatternMatchRouter: Returns: bool: True if pattern exists, False otherwise """ - if custom_llm_provider is None: - try: - ( - _, - custom_llm_provider, - _, - _, - ) = get_llm_provider(model=model) - except Exception: - # get_llm_provider raises exception when provider is unknown - pass - return self.route(model) or self.route(f"{custom_llm_provider}/{model}") + provider: Final = ( + custom_llm_provider or declared_authenticating_provider(model) or self._resolved_provider(model) + ) + return self.route(model) or self.route(f"{provider}/{model}") + + @staticmethod + def _resolved_provider(model: str) -> str | None: + try: + return get_llm_provider(model=model)[1] + except Exception: # noqa: BLE001 # get_llm_provider raises when the provider is unknown; the name then routes as-is + return None def get_deployments_by_pattern(self, model: str, custom_llm_provider: str | None = None) -> list[dict]: """ diff --git a/tests/test_litellm/proxy/proxy_server/test_routes_utils.py b/tests/test_litellm/proxy/proxy_server/test_routes_utils.py index 629d829ef8a..43ef5023985 100644 --- a/tests/test_litellm/proxy/proxy_server/test_routes_utils.py +++ b/tests/test_litellm/proxy/proxy_server/test_routes_utils.py @@ -9,11 +9,13 @@ Pins (PR2): from __future__ import annotations import asyncio +import json import pytest import litellm from litellm.proxy import proxy_server +from litellm.router_utils import pattern_match_deployments from .conftest import normalize # type: ignore[import-not-found] @@ -145,6 +147,55 @@ def test_supported_openai_params_resolves_router_alias(client, auth_as, monkeypa assert "max_tokens" in response.json()["supported_openai_params"] +def test_supported_openai_params_never_runs_oauth_for_authenticating_providers(client, auth_as, monkeypatch, tmp_path): + """Regression: github_copilot/chatgpt names answer from their declaration; resolving them + through ``get_llm_provider`` would run the provider's OAuth device flow and block the event loop.""" + monkeypatch.setenv("GITHUB_COPILOT_TOKEN_DIR", str(tmp_path)) + (tmp_path / "access-token").write_text("fake-access-token") + (tmp_path / "api-key.json").write_text( + json.dumps( + { + "token": "fake-api-key", + "expires_at": 4102444800, + "endpoints": {"api": "https://api.githubcopilot.com"}, + } + ) + ) + router = litellm.Router( + model_list=[ + { + "model_name": "copilot-alias", + "litellm_params": {"model": "github_copilot/gpt-4o"}, + }, + { + "model_name": "openai/*", + "litellm_params": {"model": "openai/*"}, + }, + ] + ) + monkeypatch.setattr(proxy_server, "llm_router", router) + + resolution_attempts: list[str] = [] + + def _oauth_tripwire(model, *args, **kwargs): + resolution_attempts.append(model) + raise AssertionError("get_llm_provider would run the OAuth device flow") + + monkeypatch.setattr(litellm, "get_llm_provider", _oauth_tripwire) + monkeypatch.setattr(pattern_match_deployments, "get_llm_provider", _oauth_tripwire) + expected = litellm.get_supported_openai_params(model="gpt-4o", custom_llm_provider="github_copilot") + + with auth_as(): + via_alias = client.get("/utils/supported_openai_params", params={"model": "copilot-alias"}) + via_direct_name = client.get("/utils/supported_openai_params", params={"model": "github_copilot/gpt-4o"}) + + assert via_alias.status_code == 200 + assert via_alias.json() == {"supported_openai_params": expected} + assert via_direct_name.status_code == 200 + assert via_direct_name.json() == {"supported_openai_params": expected} + assert resolution_attempts == [] + + def test_supported_openai_params_invalid_model(client, auth_as, monkeypatch): """Pins ``GET /utils/supported_openai_params`` (error: unknown model).""" diff --git a/tests/test_litellm/router_utils/test_pattern_match_deployments.py b/tests/test_litellm/router_utils/test_pattern_match_deployments.py new file mode 100644 index 00000000000..2fef84c8785 --- /dev/null +++ b/tests/test_litellm/router_utils/test_pattern_match_deployments.py @@ -0,0 +1,51 @@ +"""Behavior pins for ``litellm/router_utils/pattern_match_deployments.py``.""" + +from __future__ import annotations + +from litellm.router_utils import pattern_match_deployments +from litellm.router_utils.pattern_match_deployments import PatternMatchRouter + + +def _wildcard_deployment(model_name: str) -> dict: + return {"model_name": model_name, "litellm_params": {"model": model_name}} + + +def _matched_models(matches: list[dict] | None) -> list[str]: + return [deployment["litellm_params"]["model"] for deployment in matches or []] + + +def test_get_pattern_never_resolves_declared_authenticating_providers(monkeypatch): + """Regression: resolving a github_copilot/chatgpt name through ``get_llm_provider`` runs the + provider's OAuth device flow; the auth layer walks every wildcard router on every request, so + a single metadata lookup for an unserved name would block the proxy's event loop.""" + resolution_attempts: list[str] = [] + + def _oauth_tripwire(model, *args, **kwargs): + resolution_attempts.append(model) + raise AssertionError("get_llm_provider would run the OAuth device flow") + + monkeypatch.setattr(pattern_match_deployments, "get_llm_provider", _oauth_tripwire) + + unmatched_router = PatternMatchRouter() + unmatched_router.add_pattern("anthropic/*", _wildcard_deployment("anthropic/*")) + assert unmatched_router.get_pattern("github_copilot/gpt-4o") is None + + matched_router = PatternMatchRouter() + matched_router.add_pattern("github_copilot/*", _wildcard_deployment("github_copilot/*")) + assert _matched_models(matched_router.get_pattern("github_copilot/gpt-4o")) == ["github_copilot/gpt-4o"] + assert _matched_models(matched_router.get_pattern("gpt-4o", custom_llm_provider="github_copilot")) == [ + "github_copilot/gpt-4o" + ] + + assert resolution_attempts == [] + + +def test_get_pattern_still_resolves_unqualified_names(monkeypatch): + monkeypatch.setattr( + pattern_match_deployments, + "get_llm_provider", + lambda model, **kwargs: (model, "openai", None, None), + ) + router = PatternMatchRouter() + router.add_pattern("openai/*", _wildcard_deployment("openai/*")) + assert _matched_models(router.get_pattern("gpt-4o")) == ["openai/gpt-4o"] From b134dbfe7361cd30ee3e5976588149b106b53e6a Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Mon, 31 Aug 2026 12:19:51 -0700 Subject: [PATCH 6/8] test: exempt _resolved_provider in router_code_coverage gate --- tests/code_coverage_tests/router_code_coverage.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/code_coverage_tests/router_code_coverage.py b/tests/code_coverage_tests/router_code_coverage.py index c541c035db7..a5e00799519 100644 --- a/tests/code_coverage_tests/router_code_coverage.py +++ b/tests/code_coverage_tests/router_code_coverage.py @@ -81,6 +81,7 @@ ignored_function_names = [ "_merge_tools_from_deployment", # Tested indirectly via _update_kwargs_with_deployment (test files lack "router" in name) "_invalidate_access_groups_cache", # Tested indirectly via set_model_list, upsert_model etc. (test files lack "router" in name) "has_buffered_provider_output", # Property, so its reads in test_router.py are never an ast.Call + "_resolved_provider", # Tested via get_pattern in test_pattern_match_deployments.py (file lacks "router" in name) ] From ec02c9a6d2b06f131baf46b8771a6d153fd1fd6f Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Mon, 31 Aug 2026 13:34:14 -0700 Subject: [PATCH 7/8] fix(router): bare authenticating-provider names declare nothing --- .../get_llm_provider_logic.py | 2 +- litellm/proxy/proxy_server.py | 6 +----- .../test_get_supported_openai_params.py | 2 ++ .../proxy/proxy_server/test_routes_utils.py | 21 +++++++++++++++++++ .../test_pattern_match_deployments.py | 13 ++++++++++++ 5 files changed, 38 insertions(+), 6 deletions(-) diff --git a/litellm/litellm_core_utils/get_llm_provider_logic.py b/litellm/litellm_core_utils/get_llm_provider_logic.py index 74a1d3e5008..4c0e0dae9ae 100644 --- a/litellm/litellm_core_utils/get_llm_provider_logic.py +++ b/litellm/litellm_core_utils/get_llm_provider_logic.py @@ -135,7 +135,7 @@ def declared_authenticating_provider(model: str, custom_llm_provider: str | None and for a declared pair the resolver's answer is the declaration itself, so metadata callers adopt the declaration instead of resolving. """ - declared: Final = custom_llm_provider or model.split("/", 1)[0] + declared: Final = custom_llm_provider or (model.split("/", 1)[0] if "/" in model else None) return declared if declared in PROVIDERS_THAT_AUTHENTICATE_ON_PROVIDER_INFO else None diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index c5b1e251398..5f641e0b552 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -12482,11 +12482,7 @@ async def supported_openai_params(model: str): global llm_router try: - resolved_models: Final = ( - llm_router.resolved_litellm_models(model) - if llm_router is not None and declared_authenticating_provider(model) is None - else () - ) + resolved_models: Final = llm_router.resolved_litellm_models(model) if llm_router is not None else () target_model: Final = resolved_models[0] if resolved_models else model declared_provider: Final = declared_authenticating_provider(target_model) litellm_model, custom_llm_provider = ( diff --git a/tests/test_litellm/litellm_core_utils/test_get_supported_openai_params.py b/tests/test_litellm/litellm_core_utils/test_get_supported_openai_params.py index cb4e72ab3ad..722818598af 100644 --- a/tests/test_litellm/litellm_core_utils/test_get_supported_openai_params.py +++ b/tests/test_litellm/litellm_core_utils/test_get_supported_openai_params.py @@ -188,6 +188,8 @@ class TestDeclaredAuthenticatingProvider: ("gpt-4o", "github_copilot", "github_copilot"), ("openai/gpt-4o", None, None), ("gpt-4o", "openai", None), + ("github_copilot", None, None), + ("chatgpt", None, None), ], ) def test_names_only_the_providers_whose_resolution_authenticates(self, model, provider, expected): diff --git a/tests/test_litellm/proxy/proxy_server/test_routes_utils.py b/tests/test_litellm/proxy/proxy_server/test_routes_utils.py index 43ef5023985..1e1436fcef8 100644 --- a/tests/test_litellm/proxy/proxy_server/test_routes_utils.py +++ b/tests/test_litellm/proxy/proxy_server/test_routes_utils.py @@ -147,6 +147,27 @@ def test_supported_openai_params_resolves_router_alias(client, auth_as, monkeypa assert "max_tokens" in response.json()["supported_openai_params"] +def test_supported_openai_params_declared_prefix_alias_resolves_through_router(client, auth_as, monkeypatch): + """Regression: an alias whose name starts with an authenticating provider's prefix skipped + router resolution and answered with that provider's params instead of the deployment's.""" + router = litellm.Router( + model_list=[ + { + "model_name": "github_copilot/gpt-4o", + "litellm_params": {"model": "anthropic/claude-opus-4-6", "api_key": "sk-test"}, + } + ] + ) + monkeypatch.setattr(proxy_server, "llm_router", router) + + with auth_as(): + response = client.get("/utils/supported_openai_params", params={"model": "github_copilot/gpt-4o"}) + + assert response.status_code == 200 + expected = litellm.get_supported_openai_params(model="claude-opus-4-6", custom_llm_provider="anthropic") + assert response.json() == {"supported_openai_params": expected} + + def test_supported_openai_params_never_runs_oauth_for_authenticating_providers(client, auth_as, monkeypatch, tmp_path): """Regression: github_copilot/chatgpt names answer from their declaration; resolving them through ``get_llm_provider`` would run the provider's OAuth device flow and block the event loop.""" diff --git a/tests/test_litellm/router_utils/test_pattern_match_deployments.py b/tests/test_litellm/router_utils/test_pattern_match_deployments.py index 2fef84c8785..af43644a305 100644 --- a/tests/test_litellm/router_utils/test_pattern_match_deployments.py +++ b/tests/test_litellm/router_utils/test_pattern_match_deployments.py @@ -40,6 +40,19 @@ def test_get_pattern_never_resolves_declared_authenticating_providers(monkeypatc assert resolution_attempts == [] +def test_get_pattern_bare_provider_name_never_matches_that_providers_wildcard(monkeypatch): + """Regression: a bare ``github_copilot`` adopted itself as its provider and retried as + ``github_copilot/github_copilot``, false-matching the wildcard for a name no deployment serves.""" + + def _unknown_provider(model, *args, **kwargs): + raise ValueError(f"unknown provider for {model}") + + monkeypatch.setattr(pattern_match_deployments, "get_llm_provider", _unknown_provider) + router = PatternMatchRouter() + router.add_pattern("github_copilot/*", _wildcard_deployment("github_copilot/*")) + assert router.get_pattern("github_copilot") is None + + def test_get_pattern_still_resolves_unqualified_names(monkeypatch): monkeypatch.setattr( pattern_match_deployments, From 28d0ac5339ba565d275242504e882853b6a33435 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:06:14 -0700 Subject: [PATCH 8/8] fix(router): guard the declared-provider check for requests without a model --- .../litellm_core_utils/get_llm_provider_logic.py | 4 ++-- litellm/router_utils/pattern_match_deployments.py | 6 +++--- .../router_utils/test_pattern_match_deployments.py | 14 ++++++++++++++ 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/litellm/litellm_core_utils/get_llm_provider_logic.py b/litellm/litellm_core_utils/get_llm_provider_logic.py index 5474725966c..ce51fb19970 100644 --- a/litellm/litellm_core_utils/get_llm_provider_logic.py +++ b/litellm/litellm_core_utils/get_llm_provider_logic.py @@ -127,7 +127,7 @@ def handle_anthropic_text_model_custom_llm_provider( return model, custom_llm_provider -def declared_authenticating_provider(model: str, custom_llm_provider: str | None = None) -> str | None: +def declared_authenticating_provider(model: str | None, custom_llm_provider: str | None = None) -> str | None: """The authenticating provider this pair already names, or None. get_llm_provider runs the OAuth device flow for github_copilot and chatgpt, because their @@ -135,7 +135,7 @@ def declared_authenticating_provider(model: str, custom_llm_provider: str | None and for a declared pair the resolver's answer is the declaration itself, so metadata callers adopt the declaration instead of resolving. """ - declared: Final = custom_llm_provider or (model.split("/", 1)[0] if "/" in model else None) + declared: Final = custom_llm_provider or (model.split("/", 1)[0] if model and "/" in model else None) return declared if declared in PROVIDERS_THAT_AUTHENTICATE_ON_PROVIDER_INFO else None diff --git a/litellm/router_utils/pattern_match_deployments.py b/litellm/router_utils/pattern_match_deployments.py index 850ca74b387..0775e0a4039 100644 --- a/litellm/router_utils/pattern_match_deployments.py +++ b/litellm/router_utils/pattern_match_deployments.py @@ -204,7 +204,7 @@ class PatternMatchRouter: return litellm_deployment_litellm_model - def get_pattern(self, model: str, custom_llm_provider: str | None = None) -> list[dict] | None: + def get_pattern(self, model: str | None, custom_llm_provider: str | None = None) -> list[dict] | None: """ Check if a pattern exists for the given model and custom llm provider @@ -221,9 +221,9 @@ class PatternMatchRouter: return self.route(model) or self.route(f"{provider}/{model}") @staticmethod - def _resolved_provider(model: str) -> str | None: + def _resolved_provider(model: str | None) -> str | None: try: - return get_llm_provider(model=model)[1] + return get_llm_provider(model=model)[1] if model else None except Exception: # noqa: BLE001 # get_llm_provider raises when the provider is unknown; the name then routes as-is return None diff --git a/tests/test_litellm/router_utils/test_pattern_match_deployments.py b/tests/test_litellm/router_utils/test_pattern_match_deployments.py index af43644a305..795d448ef5f 100644 --- a/tests/test_litellm/router_utils/test_pattern_match_deployments.py +++ b/tests/test_litellm/router_utils/test_pattern_match_deployments.py @@ -53,6 +53,20 @@ def test_get_pattern_bare_provider_name_never_matches_that_providers_wildcard(mo assert router.get_pattern("github_copilot") is None +def test_get_pattern_missing_model_returns_none(monkeypatch): + """Regression: a request without a model reaches the auth layer's pattern walk as ``None``; the + declared-provider guard raised ``TypeError`` where the old inline resolve swallowed every + resolver error, so the proxy's missing-model 400 became a crash.""" + + def _unknown_provider(model, *args, **kwargs): + raise ValueError(f"unknown provider for {model}") + + monkeypatch.setattr(pattern_match_deployments, "get_llm_provider", _unknown_provider) + router = PatternMatchRouter() + router.add_pattern("openai/*", _wildcard_deployment("openai/*")) + assert router.get_pattern(None) is None + + def test_get_pattern_still_resolves_unqualified_names(monkeypatch): monkeypatch.setattr( pattern_match_deployments,