Merge pull request #39000 from BerriAI/litellm_fix_supported_openai_params_router_alias

fix(proxy): resolve router model aliases in /utils/supported_openai_params
This commit is contained in:
Mateo Wang 2026-09-01 14:40:04 -07:00 committed by GitHub
commit 981c5924a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 203 additions and 19 deletions

View file

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

View file

@ -12481,11 +12481,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:
model, custom_llm_provider, _, _ = litellm.get_llm_provider(model=model)
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 = (
(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(
model=model, custom_llm_provider=custom_llm_provider
model=litellm_model, custom_llm_provider=custom_llm_provider
)
}
except Exception:

View file

@ -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:
@ -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
@ -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 | None) -> str | None:
try:
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
def get_deployments_by_pattern(self, model: str, custom_llm_provider: str | None = None) -> list[dict]:
"""

View file

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

View file

@ -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):

View file

@ -9,12 +9,13 @@ Pins (PR2):
from __future__ import annotations
import asyncio
from unittest.mock import AsyncMock, MagicMock
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]
@ -99,6 +100,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",
@ -124,12 +126,104 @@ 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 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
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_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."""
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)."""
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": "??"})

View file

@ -0,0 +1,78 @@
"""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_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_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,
"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"]