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>
This commit is contained in:
dingdangmao 2026-10-05 04:49:20 +08:00
parent 97fc72e5bb
commit ea441ec0a9
2 changed files with 54 additions and 0 deletions

View file

@ -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")

View file

@ -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?"