-
-
Notifications
You must be signed in to change notification settings - Fork 11.6k
feat(router): resolve auto-router routing plugins from proxy YAML config #33251
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8d5e033
64bf705
038f9e1
585121b
5e676d6
76e53d9
e089a41
a41586e
8ea0f19
bae865c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -468,6 +468,38 @@ def _pick_from_tier_value(model: str | list[str], tier_key: str) -> str: | |
| def _tier_pools(self) -> dict[str, list[str]]: | ||
| return {tier: (models if isinstance(models, list) else [models]) for tier, models in self.config.tiers.items()} | ||
|
|
||
| async def _pick_model_for_tier( | ||
| self, | ||
| tier: ComplexityTier, | ||
| raw_messages: list[dict[str, Any]] | None, | ||
| resolved_messages: list[dict[str, Any]] | None, | ||
| request_kwargs: dict, | ||
| ) -> str: | ||
| if not self.config.plugins: | ||
| return self.get_model_for_tier(tier) | ||
|
|
||
| from litellm.types.router import RoutingContext | ||
|
|
||
| tier_key = tier.value | ||
| metadata_key = "litellm_metadata" if "litellm_metadata" in request_kwargs else "metadata" | ||
| context = RoutingContext( | ||
| raw_messages=raw_messages or [], | ||
| structured_messages=resolved_messages or [], | ||
| candidate_models=list(self._tier_pools().get(tier_key, [])), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This populates Useful? React with 👍 / 👎. |
||
| metadata=request_kwargs.get(metadata_key) or {}, | ||
| ) | ||
| for plugin in self.config.plugins: | ||
| context = await plugin.run(context) | ||
|
|
||
| if not context.candidate_models: | ||
| # A plugin narrowing a tier to zero candidates is a policy decision (e.g. no | ||
| # model this tenant's budget allows) -- falling back to default_model here | ||
| # (which was never checked against the plugins) would let that policy be | ||
| # silently bypassed. Raise instead, matching the Router-level plugin | ||
| # pipeline's own fail-closed behavior for the same situation. | ||
| raise ValueError(f"No candidate models left for tier {tier_key} after routing-plugin filtering") | ||
| return self._pick_from_tier_value(context.candidate_models, tier_key) | ||
|
greptile-apps[bot] marked this conversation as resolved.
|
||
|
|
||
| def _ensure_adaptive_router(self) -> Any | None: | ||
| if not self.config.adaptive: | ||
| return None | ||
|
|
@@ -861,10 +893,16 @@ async def async_pre_routing_hook( | |
| When `session_affinity` is enabled and a session_id is resolvable on the request, | ||
| pins the model chosen on the session's first turn and reuses it for every later | ||
| turn, skipping classification entirely. Otherwise delegates to `_classify_and_route`. | ||
|
|
||
| Skipped entirely when `plugins` are configured: reusing a stale pin would bypass | ||
| the plugin pipeline on every turn after the first, since a pinned model was never | ||
| re-checked against a policy plugin whose decision can change between turns (e.g. a | ||
| budget plugin, once the session's spend crosses its cap). | ||
| """ | ||
| from litellm.types.router import PreRoutingHookResponse | ||
|
|
||
| session_id = self._get_session_id_from_request_kwargs(request_kwargs) if self.config.session_affinity else None | ||
| use_session_affinity = self.config.session_affinity and not self.config.plugins | ||
| session_id = self._get_session_id_from_request_kwargs(request_kwargs) if use_session_affinity else None | ||
| cache_key = self._get_session_affinity_cache_key(session_id, request_kwargs) if session_id is not None else None | ||
|
|
||
| if cache_key is not None: | ||
|
|
@@ -947,14 +985,26 @@ async def _classify_and_route( | |
|
|
||
| if user_message is None: | ||
| verbose_router_logger.debug("ComplexityRouter: No user message found, routing to default model") | ||
| if not self.config.plugins and self.config.default_model: | ||
| # No plugins configured: preserve the pre-existing default_model-first | ||
| # priority exactly (changing it would be a silent behavior change for | ||
| # every non-plugin user, not just a security fix). | ||
| routed_model = self.config.default_model | ||
| else: | ||
| # Plugins configured: default_model must never bypass them, so it's not | ||
| # checked here at all -- _pick_model_for_tier -> get_model_for_tier still | ||
| # falls back to it (after the MEDIUM tier) once the plugin pipeline runs. | ||
| routed_model = await self._pick_model_for_tier( | ||
| ComplexityTier.MEDIUM, messages, resolved_messages, request_kwargs | ||
| ) | ||
| return PreRoutingHookResponse( | ||
| model=self.config.default_model or self.get_model_for_tier(ComplexityTier.MEDIUM), | ||
| model=routed_model, | ||
| messages=messages if has_original_messages else None, | ||
| ) | ||
|
greptile-apps[bot] marked this conversation as resolved.
krrish-berri-2 marked this conversation as resolved.
|
||
|
|
||
| override_tier = await self._resolve_keyword_tier_override(user_message, request_kwargs) | ||
| if override_tier is not None: | ||
| routed_model = self.get_model_for_tier(override_tier) | ||
| routed_model = await self._pick_model_for_tier(override_tier, messages, resolved_messages, request_kwargs) | ||
| cause = "semantic_keyword_match" if self.config.semantic_keyword_matching else "literal_keyword_match" | ||
| verbose_router_logger.info( | ||
| f"ComplexityRouter: routing decision cause={cause}, " | ||
|
|
@@ -980,7 +1030,7 @@ async def _classify_and_route( | |
| f"signals={signals}, routed_model={routed_model}" | ||
| ) | ||
| else: | ||
| routed_model = self.get_model_for_tier(tier) | ||
| routed_model = await self._pick_model_for_tier(tier, messages, resolved_messages, request_kwargs) | ||
| verbose_router_logger.info( | ||
| f"ComplexityRouter: routing decision cause=complexity_scorer, tier={tier.value}, " | ||
| f"score={score:.3f}, signals={signals}, routed_model={routed_model}" | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.