diff --git a/litellm/router.py b/litellm/router.py index 88601e5b97b..f9d4bf1428e 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -11170,28 +11170,31 @@ class Router: def get_candidate_model_ids_for_route(self, model: str, team_id: str | None = None) -> frozenset[str]: """ - Deployment ids that could serve ``model`` for ``team_id``, unioned across the paths - the router resolves a route through: ``model_group_alias``, a routing group, the - ``model_name`` and team indexes, and wildcard pattern routes. Read-only and - side-effect-free, unlike ``_common_checks_available_deployment`` which also applies - fallbacks and can raise. Lets a pre-call check tell a genuine cross-group route from - same-group unavailability without re-deriving that precedence at the call site, and - without leaking deployment ids into request kwargs bound for the provider. + Deployment ids that could serve ``model`` for ``team_id``, following the same + precedence ``_common_checks_available_deployment`` uses to build a candidate pool: + ``model_group_alias``, then a routing group, then the first matching early-resolve + path for a name that is not a ``model_name`` (team route, wildcard pattern via + ``get_deployments_by_pattern``, team pattern router, default deployment), then the + ``model_name`` and team indexes. Delegating to the router's own resolvers keeps this + aligned with how a route actually resolves rather than re-deriving it, and unlike + ``_common_checks_available_deployment`` it is read-only: it does not apply request + fallbacks and (with ``include_team_models`` left off) does not raise. Lets a pre-call + check tell a genuine cross-group route from same-group unavailability without leaking + deployment ids into request kwargs bound for the provider. """ resolved: Final = self._get_model_from_alias(model=model) or model routing_group_members: Final = self._get_routing_group_deployments(model=resolved, team_id=team_id) if routing_group_members is not None: return self._deployment_ids(routing_group_members) - if resolved in self.model_names: - return self._deployment_ids(self._get_all_deployments(model_name=resolved, team_id=team_id)) - team_router: Final = self.team_pattern_routers.get(team_id) if team_id is not None else None - return self._deployment_ids( - ( - *self._get_all_deployments(model_name=resolved, team_id=team_id), - *(self.pattern_router.route(resolved) or ()), - *((team_router.route(resolved) or ()) if team_router is not None else ()), - ) + early: Final = self._try_early_resolve_deployments_for_model_not_in_names( + model=resolved, request_team_id=team_id ) + if early is not None: + early_deployments: Final = early[1] + return self._deployment_ids( + (early_deployments,) if isinstance(early_deployments, Mapping) else early_deployments + ) + return self._deployment_ids(self._get_all_deployments(model_name=resolved, team_id=team_id)) @staticmethod def _deployment_ids(deployments: Sequence[Mapping[str, object]]) -> frozenset[str]: diff --git a/tests/test_litellm/test_router.py b/tests/test_litellm/test_router.py index 1e0327e5ac2..f5b1f79ad73 100644 --- a/tests/test_litellm/test_router.py +++ b/tests/test_litellm/test_router.py @@ -15023,7 +15023,9 @@ def test_get_candidate_model_ids_for_route_covers_model_name_and_pattern(): pre-call check can tell a genuine cross-group route from same-group unavailability. A concrete model group returns its member ids; a wildcard/pattern deployment is included for a concrete model it matches, which the bare model_name index misses. - Regression guard for the LIT-7195 tier-change discriminator's team/pattern gaps. + The unprefixed-name case must resolve through get_deployments_by_pattern (which retries + the provider-qualified form), not a bare pattern_router.route that only sees the literal + name. Regression guard for the LIT-7195 tier-change discriminator's team/pattern gaps. """ router = Router( model_list=[ @@ -15047,6 +15049,9 @@ def test_get_candidate_model_ids_for_route_covers_model_name_and_pattern(): assert router.get_candidate_model_ids_for_route(model="grp") == frozenset({"dep-a", "dep-b"}) assert "dep-wild" in router.get_candidate_model_ids_for_route(model="openai/gpt-4o-some-new-model") + # unprefixed name whose provider resolves to openai: only get_deployments_by_pattern's + # provider-qualified retry matches "openai/*"; a bare route() on the literal name misses it + assert "dep-wild" in router.get_candidate_model_ids_for_route(model="gpt-5") def test_deployment_ids_stringifies_ids_and_skips_entries_without_a_model_info_id():