mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-06 07:48:06 +00:00
fix(proxy): list each model id once and keep a named deployment's row over a wildcard's
This commit is contained in:
parent
4b3c273fad
commit
b3f386bb0f
2 changed files with 88 additions and 17 deletions
|
|
@ -267,9 +267,7 @@ def get_complete_model_list(
|
|||
expand_wildcard_routes=expand_wildcard_routes,
|
||||
)
|
||||
|
||||
complete_model_list: Final = unique_models + all_wildcard_models
|
||||
|
||||
return complete_model_list
|
||||
return list(dict.fromkeys(unique_models + all_wildcard_models))
|
||||
|
||||
|
||||
def _hydrate_litellm_credential_name(
|
||||
|
|
@ -399,18 +397,15 @@ def _deployment_team_id(deployment: Mapping[str, object]) -> str | None:
|
|||
|
||||
|
||||
def _cached_wildcard_routing(llm_router: Router | None) -> Callable[[str, str | None], frozenset[str]] | None:
|
||||
return None if llm_router is None else functools.cache(llm_router.wildcard_routed_models)
|
||||
|
||||
|
||||
def _deployed_by_name(llm_router: Router | None) -> Callable[[str, str | None], bool]:
|
||||
if llm_router is None:
|
||||
return None
|
||||
return lambda _model, _team_id: False
|
||||
deployed_names: Final = frozenset(llm_router.model_names)
|
||||
team_deployed_names: Final = frozenset(llm_router.team_model_to_deployment_indices)
|
||||
wildcard_routed_models: Final = llm_router.wildcard_routed_models
|
||||
|
||||
def routed_models(model: str, team_id: str | None) -> frozenset[str]:
|
||||
if model in deployed_names or (team_id, model) in team_deployed_names:
|
||||
return frozenset()
|
||||
return wildcard_routed_models(model, team_id)
|
||||
|
||||
return functools.cache(routed_models)
|
||||
return lambda model, team_id: model in deployed_names or (team_id, model) in team_deployed_names
|
||||
|
||||
|
||||
def _routes_to_model(
|
||||
|
|
@ -522,6 +517,25 @@ def _routed_deployment_model(name_pattern: str, listed_name: str, deployment_mod
|
|||
)
|
||||
|
||||
|
||||
def _names_listed_under_wildcard(
|
||||
deployment: Mapping[str, object],
|
||||
wildcard_pattern: str,
|
||||
litellm_params: LiteLLM_Params | None,
|
||||
routed_models: Callable[[str, str | None], frozenset[str]] | None,
|
||||
deployed_by_name: Callable[[str, str | None], bool],
|
||||
) -> tuple[str, ...]:
|
||||
team_id: Final = _deployment_team_id(deployment)
|
||||
return tuple(
|
||||
name
|
||||
for name in get_known_models_from_wildcard(
|
||||
wildcard_model=wildcard_pattern,
|
||||
litellm_params=litellm_params,
|
||||
routes_to_model=_routes_to_model(routed_models, team_id),
|
||||
)
|
||||
if not deployed_by_name(name, team_id)
|
||||
)
|
||||
|
||||
|
||||
def expand_wildcard_deployments_for_model_info(
|
||||
deployments: list[dict[str, object]],
|
||||
llm_router: Router | None = None,
|
||||
|
|
@ -535,6 +549,7 @@ def expand_wildcard_deployments_for_model_info(
|
|||
"""
|
||||
expanded: Final[list[dict[str, object]]] = []
|
||||
routed_models: Final = _cached_wildcard_routing(llm_router)
|
||||
deployed_by_name: Final = _deployed_by_name(llm_router)
|
||||
for deployment in deployments:
|
||||
model_name = str(deployment.get("model_name") or "")
|
||||
raw_params = deployment.get("litellm_params")
|
||||
|
|
@ -559,10 +574,8 @@ def expand_wildcard_deployments_for_model_info(
|
|||
except Exception:
|
||||
expanded.append(deployment)
|
||||
continue
|
||||
expanded_names = get_known_models_from_wildcard(
|
||||
wildcard_model=wildcard_pattern,
|
||||
litellm_params=litellm_params,
|
||||
routes_to_model=_routes_to_model(routed_models, _deployment_team_id(deployment)),
|
||||
expanded_names = _names_listed_under_wildcard(
|
||||
deployment, wildcard_pattern, litellm_params, routed_models, deployed_by_name
|
||||
)
|
||||
if not expanded_names:
|
||||
expanded.append(deployment)
|
||||
|
|
|
|||
|
|
@ -1248,7 +1248,7 @@ def _openai_catalog_behind_a_catch_all(
|
|||
("model_name_team-x_gpt-4o", {"team_id": "team-x", "team_public_model_name": "gpt-4o"}, "team-x"),
|
||||
],
|
||||
)
|
||||
def test_catch_all_wildcard_never_lists_a_name_a_concrete_deployment_serves(
|
||||
def test_catch_all_wildcard_lists_a_name_a_concrete_deployment_serves_once(
|
||||
monkeypatch: pytest.MonkeyPatch, concrete_name: str, concrete_model_info: Mapping[str, str], team_id: str | None
|
||||
) -> None:
|
||||
from litellm.proxy.auth.model_checks import get_complete_model_list
|
||||
|
|
@ -1283,6 +1283,64 @@ def test_catch_all_wildcard_model_info_keeps_only_the_concrete_row_for_a_deploye
|
|||
]
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _openrouter_catalog_beside_a_named_deployment(
|
||||
monkeypatch: pytest.MonkeyPatch, wildcard_name: str, named: str
|
||||
) -> Generator[Router, None, None]:
|
||||
import litellm
|
||||
|
||||
monkeypatch.setattr(litellm, "check_provider_endpoint", True)
|
||||
monkeypatch.delenv("OPENROUTER_API_BASE", raising=False)
|
||||
with respx.mock(assert_all_called=False) as respx_mock:
|
||||
respx_mock.get("https://openrouter.ai/api/v1/models/user").mock(
|
||||
return_value=httpx.Response(
|
||||
200, json={"data": [{"id": "openai/gpt-5.4-mini"}, {"id": "anthropic/claude-haiku-5-5"}]}
|
||||
)
|
||||
)
|
||||
yield Router(
|
||||
model_list=[
|
||||
{
|
||||
"model_name": wildcard_name,
|
||||
"litellm_params": {"model": "openrouter/*", "api_key": f"sk-or-wildcard-{wildcard_name}"},
|
||||
},
|
||||
{
|
||||
"model_name": named,
|
||||
"litellm_params": {"model": "openrouter/openai/gpt-5.4-mini", "api_key": "sk-or-named"},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("wildcard_name", "named"),
|
||||
[("openrouter/*", "openrouter/openai/gpt-5.4-mini"), ("or-team/*", "or-team/openai/gpt-5.4-mini")],
|
||||
)
|
||||
def test_provider_wildcard_lists_a_name_a_named_deployment_serves_once(
|
||||
monkeypatch: pytest.MonkeyPatch, wildcard_name: str, named: str
|
||||
) -> None:
|
||||
from litellm.proxy.auth.model_checks import expand_wildcard_deployments_for_model_info, get_complete_model_list
|
||||
|
||||
with _openrouter_catalog_beside_a_named_deployment(monkeypatch, wildcard_name, named) as router:
|
||||
listed: Final = get_complete_model_list(
|
||||
key_models=[],
|
||||
team_models=[],
|
||||
proxy_model_list=list(router.model_names),
|
||||
user_model=None,
|
||||
infer_model_from_keys=False,
|
||||
llm_router=router,
|
||||
)
|
||||
rows: Final = expand_wildcard_deployments_for_model_info(
|
||||
[dict(deployment) for deployment in router.get_model_list() or ()], llm_router=router
|
||||
)
|
||||
|
||||
vendor_prefix: Final = wildcard_name.removesuffix("*")
|
||||
assert sorted(listed) == [f"{vendor_prefix}anthropic/claude-haiku-5-5", named]
|
||||
assert sorted((row["model_name"], row["litellm_params"]["api_key"]) for row in rows) == [
|
||||
(f"{vendor_prefix}anthropic/claude-haiku-5-5", f"sk-or-wildcard-{wildcard_name}"),
|
||||
(named, "sk-or-named"),
|
||||
]
|
||||
|
||||
|
||||
@respx.mock
|
||||
def test_custom_prefix_that_starts_a_discovered_id_still_prefixes_every_listed_model(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue