From 07d32b87adf1df3b25a2dda71e9ed728c026d853 Mon Sep 17 00:00:00 2001 From: DragonAssassin-one <2819637808@qq.com> Date: Mon, 11 May 2026 11:45:10 +0800 Subject: [PATCH] refactor(router): Reorder async callback filter for deployments to improve request handling --- litellm/router.py | 20 +++---- .../test_router_tag_routing.py | 54 +++++++++++++++++++ 2 files changed, 64 insertions(+), 10 deletions(-) diff --git a/litellm/router.py b/litellm/router.py index e0ed9b85ec1..8694bdad65f 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -9262,16 +9262,6 @@ class Router: ) healthy_deployments = _pre_cooldown_deployments - healthy_deployments = await self.async_callback_filter_deployments( - model=model, - healthy_deployments=healthy_deployments, - messages=( - cast(List[AllMessageValues], messages) if messages is not None else None - ), - request_kwargs=request_kwargs, - parent_otel_span=parent_otel_span, - ) - if self.enable_pre_call_checks and messages is not None: healthy_deployments = self._pre_call_checks( model=model, @@ -9290,6 +9280,16 @@ class Router: ), ) + healthy_deployments = await self.async_callback_filter_deployments( + model=model, + healthy_deployments=healthy_deployments, + messages=( + cast(List[AllMessageValues], messages) if messages is not None else None + ), + request_kwargs=request_kwargs, + parent_otel_span=parent_otel_span, + ) + ## ORDER FILTERING ## -> if user set 'order' in deployments, return deployments with lowest order (e.g. order=1 > order=2) _target_order = (request_kwargs or {}).pop("_target_order", None) healthy_deployments = litellm.utils._get_order_filtered_deployments( diff --git a/tests/test_litellm/router_strategy/test_router_tag_routing.py b/tests/test_litellm/router_strategy/test_router_tag_routing.py index 1fdd3dad4da..10fb25c81d8 100644 --- a/tests/test_litellm/router_strategy/test_router_tag_routing.py +++ b/tests/test_litellm/router_strategy/test_router_tag_routing.py @@ -25,6 +25,9 @@ from dotenv import load_dotenv import litellm from litellm import Router from litellm._logging import verbose_logger +from litellm.router_utils.pre_call_checks.prompt_prefix_affinity_check import ( + PromptPrefixAffinityCheck, +) @pytest.mark.asyncio() @@ -219,6 +222,57 @@ async def test_default_tagged_deployments(): assert response_extra_info["model_id"] == "default-model" + +@pytest.mark.asyncio() +async def test_prompt_prefix_affinity_does_not_bypass_default_tag_routing_for_untagged_requests(): + """ + Ensure prompt-prefix affinity selection cannot bypass tag-based routing defaults. + + For untagged requests, tag routing prefers deployments tagged "default" when present. + Prompt-prefix affinity must select only from that tag-filtered candidate set. + """ + + router = litellm.Router( + model_list=[ + { + "model_name": "gpt-4", + "litellm_params": { + "model": "gpt-4o", + "api_base": "https://exampleopenaiendpoint-production.up.railway.app/", + "tags": ["default"], + }, + "model_info": {"id": "default-model"}, + }, + { + "model_name": "gpt-4", + "litellm_params": { + "model": "gpt-4o-mini", + "api_base": "https://exampleopenaiendpoint-production.up.railway.app/", + "tags": ["teamA"], + }, + "model_info": {"id": "teamA-model"}, + }, + ], + enable_tag_filtering=True, + optional_pre_call_checks=["prompt_prefix_affinity"], + prompt_prefix_affinity_tokens=64, + prompt_prefix_affinity_min_tokens=0, + ) + + # Force the affinity scorer to prefer the non-default deployment if it ever sees it. + def _prefer_team_a(self, prefix_hash: str, deployment_model_id: str) -> int: # noqa: ANN001 + return 2 if deployment_model_id == "teamA-model" else 1 + + with patch.object(PromptPrefixAffinityCheck, "_get_prefix_hash", return_value="hash"): + with patch.object(PromptPrefixAffinityCheck, "_score_deployment", _prefer_team_a): + response = await router.acompletion( + model="gpt-4", + messages=[{"role": "user", "content": "Tell me a joke."}], + mock_response="Tell me a joke.", + ) + + assert response._hidden_params["model_id"] == "default-model" + for _ in range(5): # requests tagged with "default", this should pick model with id == "default-model" response = await router.acompletion(