Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 8 additions & 17 deletions litellm/proxy/guardrails/auto_router_compression.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,17 +80,19 @@ def policy_from_litellm_params(litellm_params: Mapping[str, object]) -> AutoRout
def policy_for_model(
llm_router: "Router | None",
model_alias: str,
team_id: str | None,
request_kwargs: Mapping[str, object],
request_tags: Sequence[str],
) -> AutoRouterCompressionPolicy | None:
"""The compression policy of the auto router marker `model_alias` resolves to.
"""The compression policy of the auto router marker `model_alias` resolves to for this caller.

Pre-call arming and the routing hook both resolve through here, so an alias with
several tag-scoped markers cannot suppress under one and then route under another.
Pre-call arming and the routing hook both resolve through here, and here resolves through the
router's own request-scoped deployment lookup, so an alias with several tag-scoped markers
cannot suppress under one and then route under another, and a team router reached by its
public name carries its policy for every principal that can reach it.
"""
if llm_router is None:
return None
deployments: Final = llm_router.get_model_list(model_name=model_alias, team_id=team_id) or ()
deployments: Final = llm_router.deployments_for_request(model_alias, request_kwargs)
markers: Final = tuple(
litellm_params
for deployment in deployments
Expand All @@ -108,17 +110,6 @@ def policy_for_model(
return next((policy for policy in candidates if policy is not None), None)


def team_id_from_request(request_kwargs: Mapping[str, object]) -> str | None:
"""The caller's team id, from whichever metadata bucket this surface writes to."""
for meta_key in ("metadata", "litellm_metadata"):
meta = request_kwargs.get(meta_key)
if isinstance(meta, Mapping):
team_id = meta.get("user_api_key_team_id")
if isinstance(team_id, str):
return team_id
return None


def _compression_guardrail_classes() -> tuple[type, ...]:
"""The registered guardrail classes whose provider compresses prompts."""
from litellm.proxy.guardrails.guardrail_registry import guardrail_class_registry
Expand Down Expand Up @@ -172,7 +163,7 @@ async def arm_pre_call(
policy: Final = policy_for_model(
llm_router=llm_router,
model_alias=model_alias,
team_id=team_id_from_request(data),
request_kwargs=data,
request_tags=_get_tags_from_request_kwargs(data),
)
if policy is None:
Expand Down
144 changes: 86 additions & 58 deletions litellm/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@
_is_proxy_admin_request,
filter_team_based_models,
filter_web_search_deployments,
get_request_team_id,
resolve_model_group_alias,
truncate_fallback_error_detail,
warn_on_provider_credential_mismatch,
Expand Down Expand Up @@ -12337,27 +12338,7 @@ def _try_early_resolve_deployments_for_model_not_in_names(
if team_deployments:
return model, team_deployments
elif include_team_models:
team_deployments = [
self.model_list[index]
for (_, public_model_name), indices in self.team_model_to_deployment_indices.items()
if public_model_name == model
for index in indices
]
team_ids: Final = {
team_id
for deployment in team_deployments
for team_id in [(deployment.get("model_info") or {}).get("team_id")]
if team_id is not None
}
if len(team_ids) > 1:
raise litellm.BadRequestError(
message=(
f"Model name '{model}' matches deployments from multiple teams. "
"Specify the deployment ID directly to disambiguate."
),
model=model,
llm_provider="",
)
team_deployments = self._team_deployments_across_teams(model)
if team_deployments:
return model, team_deployments

Expand All @@ -12384,6 +12365,45 @@ def _try_early_resolve_deployments_for_model_not_in_names(

return None

def _team_deployments_across_teams(self, model: str) -> list[DeploymentTypedDict]:
"""Every team's deployments under public name `model`, for a proxy admin calling without a team."""
team_deployments: Final = [
self.model_list[index]
for (_, public_model_name), indices in self.team_model_to_deployment_indices.items()
if public_model_name == model
for index in indices
]
team_ids: Final = {
team_id
for deployment in team_deployments
for team_id in [(deployment.get("model_info") or {}).get("team_id")]
if team_id is not None
}
if len(team_ids) > 1:
raise litellm.BadRequestError(
message=(
f"Model name '{model}' matches deployments from multiple teams. "
"Specify the deployment ID directly to disambiguate."
),
model=model,
llm_provider="",
)
return team_deployments

def deployments_for_request(
self, model: str, request_kwargs: Mapping[str, object]
) -> Sequence[DeploymentTypedDict]:
"""The deployments `model` names for this caller, through the same alias, then team-first, then
global, then admin-across-teams resolution `_common_checks_available_deployment` applies, so
strategy selection and compression policy can never disagree with deployment selection about
which marker a name means."""
registered_name: Final = self._get_model_from_alias(model=model) or model
team_id: Final = get_request_team_id(request_kwargs)
deployments: Final = self._get_all_deployments(model_name=registered_name, team_id=team_id)
if deployments or team_id is not None or not _is_proxy_admin_request(request_kwargs):
return deployments
return self._team_deployments_across_teams(registered_name)

@staticmethod
def _is_strategy_marker_deployment(deployment: Mapping[str, object]) -> bool:
litellm_params: Final = deployment.get("litellm_params")
Expand Down Expand Up @@ -12411,11 +12431,7 @@ def _common_checks_available_deployment(
- Dict, if specific model chosen
"""

request_team_id: str | None = None
if request_kwargs is not None:
metadata: Final = request_kwargs.get("metadata") or {}
litellm_metadata: Final = request_kwargs.get("litellm_metadata") or {}
request_team_id = metadata.get("user_api_key_team_id") or litellm_metadata.get("user_api_key_team_id")
request_team_id: Final = get_request_team_id(request_kwargs)
# check if aliases set on litellm model alias map
if specific_deployment is True:
return model, self._get_deployment_by_litellm_model(model=model)
Expand All @@ -12440,7 +12456,9 @@ def _common_checks_available_deployment(
include_team_models=_is_proxy_admin_request(request_kwargs),
)
if early is not None:
return early
if not isinstance(early[1], list):
return early
return early[0], self._drop_strategy_markers(early[0], early[1])

## get healthy deployments
### get all deployments
Expand Down Expand Up @@ -12517,19 +12535,22 @@ def _common_checks_available_deployment(
model
] # update the model to the actual value if an alias has been passed in

marker_flags: Final = tuple(self._is_strategy_marker_deployment(d) for d in healthy_deployments)
if not any(marker_flags):
return model, healthy_deployments
selectable: Final = [ # mutable-ok: matches this function's list contract expected by downstream filters
d for d, is_marker in zip(healthy_deployments, marker_flags, strict=True) if not is_marker
return model, self._drop_strategy_markers(model, healthy_deployments)

def _drop_strategy_markers(
self, model: str, deployments: Sequence[DeploymentTypedDict]
) -> list[DeploymentTypedDict]:
"""A strategy marker is never a callable deployment, whichever resolution arm produced it."""
selectable: Final = [ # mutable-ok: matches _common_checks_available_deployment's list contract
d for d in deployments if not self._is_strategy_marker_deployment(d)
]
if not selectable:
if deployments and not selectable:
raise litellm.BadRequestError(
message=f"You passed in model={model}. {RouterErrors.only_strategy_marker_deployments.value}",
model=model,
llm_provider="",
)
return model, selectable
return selectable

def _filter_deployments_by_model_access_groups(
self,
Expand Down Expand Up @@ -13219,12 +13240,8 @@ def _filter_by_routing_plugin_candidates(

return filtered

def _model_name_has_plain_deployments(self, model: str) -> bool:
indices: Final = self.model_name_to_deployment_indices.get(model) or ()
return any(not self._is_strategy_marker_deployment(self.model_list[idx]) for idx in indices)

def _select_pre_routing_strategy(
self, model: str, request_kwargs: dict
self, model: str, request_kwargs: Mapping[str, object]
) -> "TaggedPreRoutingStrategy[PreRoutingStrategy] | None":
"""
Resolve the pre-routing strategy for `model`, disambiguating deployments
Expand All @@ -13235,19 +13252,27 @@ def _select_pre_routing_strategy(
deployment the strategy was registered from via its (model_name, tags)
pair.

The registries are keyed by the marker deployment's own `model_name`, which
for a team-scoped router is the internal `model_name_{team}_{uuid}` while
the caller sends the team's public name. So the names looked up are the
`model_name`s of whatever deployments this caller's request resolves `model`
to, and `model` itself when it resolves to none.

With tag filtering enabled, router-wide or by the request's
enable_tag_filtering (which the proxy sets from key/team
router_settings), strategies that all carry real tags matching none of
the request's do not capture it when the name also has plain
deployments: returning None hands the request to ordinary tag-aware
deployment selection.
"""
candidates: Final[list[TaggedPreRoutingStrategy[PreRoutingStrategy]]] = [
*self.auto_routers.get(model, []),
*self.complexity_routers.get(model, []),
*self.adaptive_routers.get(model, []),
*self.quality_routers.get(model, []),
]
registries: Final = (self.auto_routers, self.complexity_routers, self.adaptive_routers, self.quality_routers)
if not any(registries):
return None
deployments: Final = self.deployments_for_request(model, request_kwargs)
registered_names: Final = tuple(dict.fromkeys(str(d["model_name"]) for d in deployments)) or (model,)
candidates: Final = tuple(
tagged for registry in registries for name in registered_names for tagged in registry.get(name, [])
)
if not candidates:
return None

Expand All @@ -13265,7 +13290,7 @@ def _select_pre_routing_strategy(
if (
(self.enable_tag_filtering or request_scoped_filtering)
and all(tagged.tags for tagged in candidates)
and self._model_name_has_plain_deployments(model)
and any(not self._is_strategy_marker_deployment(d) for d in deployments)
):
return None
return candidates[0]
Expand Down Expand Up @@ -13377,11 +13402,12 @@ async def async_pre_routing_hook(

Used for the litellm auto-router to modify the request before the routing decision is made.

`model` is whatever the caller asked for, which may be a `model_group_alias` key, while the
strategy registries and the marker deployment are keyed by the marker's own `model_name`, so
every lookup below resolves the alias first. Only the lookups: the caller-facing name stays
the alias, since spend metadata is stamped before routing and the response carries the tier
group the strategy picked.
`model` is whatever the caller asked for, which may be a `model_group_alias` key or a team's
public model name, while the strategy registries and the marker deployment are keyed by the
marker's own `model_name`, so every lookup below resolves the alias first and the team name
through the deployment path. Only the lookups: the caller-facing name stays the alias, since
spend metadata is stamped before routing and the response carries the tier group the
strategy picked.
"""
requested_registered_model_name: Final = self._get_model_from_alias(model=model) or model
registered_model_name: Final = await self._resolve_claude_code_session_router(
Expand Down Expand Up @@ -13418,15 +13444,14 @@ async def async_pre_routing_hook(
messages_for_routing,
model_hop_compression_armed,
policy_for_model,
team_id_from_request,
)

# Same tag-aware lookup the proxy's pre-call arming used, so an alias with
# several tag-scoped markers cannot suppress under one and route under another.
compression_policy: Final = policy_for_model(
llm_router=self,
model_alias=registered_model_name,
team_id=team_id_from_request(request_kwargs),
request_kwargs=request_kwargs,
request_tags=_get_tags_from_request_kwargs(request_kwargs),
)
# Shared compression already ran in the pre-call hook, so reuse it rather than
Expand Down Expand Up @@ -13495,7 +13520,9 @@ async def async_pre_routing_hook(
# Per-tier `litellm_params` on the hook response are deliberate overrides
# the caller applies on top, so those keys are never forwarded here.
marker_params: Final = (
self._forwardable_alias_marker_params(model=registered_model_name, strategy_tags=selected_strategy.tags)
self._forwardable_alias_marker_params(
model=registered_model_name, strategy_tags=selected_strategy.tags, request_kwargs=request_kwargs
)
if pre_routing_hook_response is not None
else ()
)
Expand All @@ -13513,13 +13540,14 @@ async def async_pre_routing_hook(
return pre_routing_hook_response

def _forwardable_alias_marker_params(
self, model: str, strategy_tags: tuple[str, ...]
self, model: str, strategy_tags: tuple[str, ...], request_kwargs: Mapping[str, object]
) -> tuple[tuple[str, object], ...]:
marker_params: Final = tuple(
litellm_params
for idx in self.model_name_to_deployment_indices.get(model, ())
if isinstance(litellm_params := self.model_list[idx].get("litellm_params", {}), dict)
and str(litellm_params.get("model", "")).startswith(AUTO_ROUTER_MODEL_PREFIX)
for deployment in self.deployments_for_request(model, request_kwargs)
if str((litellm_params := deployment["litellm_params"]).get("model", "")).startswith(
AUTO_ROUTER_MODEL_PREFIX
)
)
tag_matched: Final = tuple(
params for params in marker_params if tuple(params.get("tags") or ()) == strategy_tags
Expand Down
14 changes: 13 additions & 1 deletion litellm/router_utils/common_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@ def _is_proxy_admin_request(request_kwargs: Mapping[str, object] | None) -> bool
return getattr(user_api_key_auth, "user_role", None) == "proxy_admin"


def get_request_team_id(request_kwargs: Mapping[str, object] | None) -> str | None:
"""The caller's team id, from whichever metadata bucket this surface writes to."""
if request_kwargs is None:
return None
for bucket_name in ("metadata", "litellm_metadata"):
bucket = request_kwargs.get(bucket_name)
team_id = bucket.get("user_api_key_team_id") if isinstance(bucket, Mapping) else None
if isinstance(team_id, str) and team_id:
return team_id
return None


def resolve_model_group_alias(model_group_alias: object, model: str) -> str | None:
"""
Resolve ``model`` through a ``model_group_alias`` map.
Expand Down Expand Up @@ -110,7 +122,7 @@ def filter_team_based_models(

metadata: Final = request_kwargs.get("metadata") or {}
litellm_metadata: Final = request_kwargs.get("litellm_metadata") or {}
request_team_id: Final = metadata.get("user_api_key_team_id") or litellm_metadata.get("user_api_key_team_id")
request_team_id: Final = get_request_team_id(request_kwargs)
if request_team_id is None and _is_proxy_admin_request(request_kwargs) and isinstance(healthy_deployments, list):
requested_model: Final = (
request_kwargs.get("model") or metadata.get("model_group") or litellm_metadata.get("model_group")
Expand Down
Loading
Loading