From ea441ec0a962c9fa718d8ccafc839c3710f46403 Mon Sep 17 00:00:00 2001 From: dingdangmao <2756823972@qq.com> Date: Mon, 5 Oct 2026 04:49:20 +0800 Subject: [PATCH] fix(router): honor reasoning_override_min_score on heuristic_v2 classifier_type=heuristic_v2 routed every request through the TierSuccessPredictor, which reads outcome statistics and never the ask's reasoning markers, so an operator who set reasoning_override_min_score (0 = unconditional) saw 2+ exact reasoning keywords silently ignored and the request classified by the predictor alone. Route the ask through the shared scorer gate first and short-circuit to REASONING when the reasoning-marker override fires, so the documented classifier-wide contract holds on every classification path. Fixes #44527 Signed-off-by: apex-mochen <2756823972@qq.com> --- .../complexity_router/complexity_router.py | 15 +++++++ .../router_strategy/test_complexity_router.py | 39 +++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/litellm/router_strategy/complexity_router/complexity_router.py b/litellm/router_strategy/complexity_router/complexity_router.py index fc0865654c7..8e5ab31584b 100644 --- a/litellm/router_strategy/complexity_router/complexity_router.py +++ b/litellm/router_strategy/complexity_router/complexity_router.py @@ -1944,6 +1944,21 @@ class ComplexityRouter(CustomLogger): return await self._llm_classifier_outcome(prompt, system_prompt, request_kwargs, messages) def _classify_with_heuristic_v2(self, prompt: str) -> ClassificationOutcome: + # The reasoning-marker override is a classifier-wide contract, not a + # heuristic-scorer quirk: an operator who sets reasoning_override_min_score + # (0 = unconditional) expects 2+ markers to promote on every path that + # classifies. The success predictor reads outcome statistics, never the + # ask's markers, so honor the override here through the same gate the + # heuristic scorer uses -- otherwise the setting silently does nothing + # under heuristic_v2 while still being documented as classifier-wide. + overridden: Final = self._score_and_classify(prompt) + if overridden[3] == "reasoning_override": + return ClassificationOutcome( + tier=overridden[0], + score=overridden[1], + signals=overridden[2], + cause="reasoning_override", + ) predictor: Final = self._tier_success_predictor if predictor is None: raise ValueError("heuristic v2 predictor is not configured") diff --git a/tests/unit/router_strategy/test_complexity_router.py b/tests/unit/router_strategy/test_complexity_router.py index 333524ffffc..d408a6591d6 100644 --- a/tests/unit/router_strategy/test_complexity_router.py +++ b/tests/unit/router_strategy/test_complexity_router.py @@ -661,6 +661,45 @@ class TestReasoningMarkerScoring: assert score == 0.25 assert tier == ComplexityTier.REASONING + def test_heuristic_v2_honors_the_zero_floor_override(self, mock_router_instance, basic_config): + """#44527: reasoning_override_min_score=0 must promote on heuristic_v2 too. + + The success predictor reads outcome statistics, never the ask's reasoning + markers, so before the fix a request carrying 2+ exact markers was still + classified by the predictor alone and the operator's explicit 0 floor was + silently ignored. + """ + router = ComplexityRouter( + model_name="test-complexity-router", + litellm_router_instance=mock_router_instance, + complexity_router_config={ + **basic_config, + "classifier_type": "heuristic_v2", + "heuristic_v2_artifact": _heuristic_v2_artifact(), + "reasoning_override_min_score": 0.0, + }, + ) + outcome = asyncio.run(router.aclassify("hi, step by step, pros and cons")) + assert outcome.cause == "reasoning_override" + assert outcome.tier == ComplexityTier.REASONING + + def test_heuristic_v2_respects_a_floor_above_the_markers_score(self, mock_router_instance, basic_config): + """A floor above the markers' weighted score leaves heuristic_v2 to the predictor.""" + router = ComplexityRouter( + model_name="test-complexity-router", + litellm_router_instance=mock_router_instance, + complexity_router_config={ + **basic_config, + "classifier_type": "heuristic_v2", + "heuristic_v2_artifact": _heuristic_v2_artifact(), + "reasoning_override_min_score": 0.99, + }, + ) + outcome = asyncio.run(router.aclassify("hi, step by step, pros and cons")) + assert outcome.cause != "reasoning_override" + assert outcome.cause == "heuristic_v2" + assert outcome.tier == ComplexityTier.COMPLEX + def test_system_prompt_reasoning_not_counted(self, complexity_router): """Reasoning markers in system prompt should not count for override.""" user_prompt = "What is 2+2?"