diff --git a/litellm/router_strategy/complexity_router/complexity_router.py b/litellm/router_strategy/complexity_router/complexity_router.py index 2dae9393510..2eb69aad2de 100644 --- a/litellm/router_strategy/complexity_router/complexity_router.py +++ b/litellm/router_strategy/complexity_router/complexity_router.py @@ -485,6 +485,7 @@ class ComplexityRouter(CustomLogger): score_threshold=self.config.match_threshold, ), auto_sync="local", + aggregation="max", ) self._semantic_routelayer = routelayer return routelayer diff --git a/tests/test_litellm/router_strategy/test_complexity_router.py b/tests/test_litellm/router_strategy/test_complexity_router.py index eda73f7e7b6..3d92700032b 100644 --- a/tests/test_litellm/router_strategy/test_complexity_router.py +++ b/tests/test_litellm/router_strategy/test_complexity_router.py @@ -1546,6 +1546,40 @@ class TestSemanticKeywordTierRules: assert result.model == "o1-preview" # REASONING via semantic match assert fake_router.async_embedding_calls, "expected an embedding call for the prompt" + @pytest.mark.asyncio + async def test_tier_matches_on_best_utterance_not_diluted_by_others(self, basic_config): + """A tier with several keywords must match if the query is close to ANY of them, + not the average across all of them. A tier's route holds one utterance per keyword; + mean aggregation (the semantic_router library default) scores the query against the + *average* similarity across every utterance in the route, so a real match on one + keyword gets dragged below threshold by the tier's other, unrelated keywords. + """ + fake_router = FakeEmbeddingRouter() + config = { + **basic_config, + "keyword_tier_rules": [ + {"keywords": ["kubernetes deployment", "thanks", "goodbye"], "tier": "REASONING"}, + ], + "semantic_keyword_matching": True, + "embedding_model": "fake-embed", + "match_threshold": 0.5, + } + router = ComplexityRouter( + model_name="test-router", + litellm_router_instance=fake_router, + complexity_router_config=config, + ) + # Only "kubernetes deployment" is close to this query (cos 1.0); "thanks" and + # "goodbye" are orthogonal (cos 0.0). Mean over the three would be ~0.33, below the + # 0.5 threshold; the best (max) utterance alone clears it. + result = await router.async_pre_routing_hook( + model="test-model", + request_kwargs={}, + messages=[{"role": "user", "content": "help me roll out my k8s cluster today"}], + ) + assert result is not None + assert result.model == "o1-preview" # REASONING via best-utterance semantic match + @pytest.mark.asyncio async def test_semantic_embedding_call_carries_caller_metadata(self, basic_config): """The query embedding call must carry the caller's metadata/litellm_metadata