feat(ui): show Capability and FUSE v2 routing forecasts

This commit is contained in:
Tin Chi Lo 2026-09-19 17:04:14 -07:00
parent 1fcef68ab7
commit 7c79c7efad
5 changed files with 393 additions and 6 deletions

View file

@ -1776,7 +1776,7 @@ class ComplexityRouter(CustomLogger):
conversation_continuing: bool = True,
tier_litellm_params: Mapping[str, object] | None = None,
context_escalation_original_tier: ComplexityTier | str | None = None,
heuristic_v2_forecast: StandardLoggingHeuristicV2Forecast | None = None,
previous_decision: StandardLoggingRoutingDecision | None = None,
) -> StandardLoggingRoutingDecision:
"""Assemble the per-request provenance record for this router's decision.
@ -1836,8 +1836,15 @@ class ComplexityRouter(CustomLogger):
masked_tier_litellm_params: Final = mask_credentials_in_payload(tier_litellm_params)
if isinstance(masked_tier_litellm_params, Mapping):
decision["tier_litellm_params"] = masked_tier_litellm_params
return (
decision if heuristic_v2_forecast is None else {**decision, "heuristic_v2_forecast": heuristic_v2_forecast}
if previous_decision is None:
return decision
forecast_fields: Final = {
field: value
for field, value in previous_decision.items()
if field.startswith("classifier_") or field == "heuristic_v2_forecast"
}
return cast( # cast-ok: retaining optional keys from a typed decision preserves their declared values
StandardLoggingRoutingDecision, {**forecast_fields, **decision}
)
async def aclassify(
@ -3569,7 +3576,7 @@ class ComplexityRouter(CustomLogger):
context_escalation_original_tier=(
decision.get("context_escalation_original_tier") if decision is not None else None
),
heuristic_v2_forecast=decision.get("heuristic_v2_forecast") if decision is not None else None,
previous_decision=decision,
)
from litellm.types.router import PreRoutingHookResponse as HookResponse
@ -3749,7 +3756,7 @@ class ComplexityRouter(CustomLogger):
conversation_continuing=bool(decision.get("conversation_continuing", True)),
tier_litellm_params=self._litellm_params_for_model(candidate_tier, new_model),
context_escalation_original_tier=decision.get("context_escalation_original_tier"),
heuristic_v2_forecast=decision.get("heuristic_v2_forecast"),
previous_decision=decision,
)
return response.model_copy(
update={ # mutable-ok: model_copy types update as a plain dict
@ -3794,7 +3801,7 @@ class ComplexityRouter(CustomLogger):
conversation_continuing=bool(decision.get("conversation_continuing", True)),
tier_litellm_params=self._litellm_params_for_model(None, default_model),
context_escalation_original_tier=decision.get("context_escalation_original_tier"),
heuristic_v2_forecast=decision.get("heuristic_v2_forecast"),
previous_decision=decision,
)
return response.model_copy(
update={ # mutable-ok: model_copy types update as a plain dict

View file

@ -78,6 +78,7 @@ from litellm.router_strategy.complexity_router.jev_classifier import (
JevSystemOneResponse,
JevUsage,
)
from litellm.router_strategy.complexity_router.llm_v2 import LLM_V2_PROMPT_VERSION
from litellm.router_strategy.complexity_router.tier_predictor import (
TierGlobalStatistic,
TrainedTierArtifact,
@ -3207,6 +3208,41 @@ class TestCapabilityClassifier:
)
assert response.model == "capable-model"
assert response.routing_decision["cause"] == "capability_classifier_fallback"
assert "classifier_p_solve" not in response.routing_decision
assert "classifier_threshold" not in response.routing_decision
@pytest.mark.asyncio
@pytest.mark.parametrize("bypass", ("literal_keyword_match", "session_affinity_pin", "housekeeping"))
async def test_bypasses_do_not_reuse_the_previous_capability_forecast(
self,
mock_router_instance: MagicMock,
bypass: Literal["literal_keyword_match", "session_affinity_pin", "housekeeping"],
) -> None:
mock_router_instance.acompletion = AsyncMock(return_value=_llm_response(_capability_reply(p_solve=0.8)))
mock_router_instance.cache = DualCache()
router: Final = self._router(
mock_router_instance,
session_affinity=bypass == "session_affinity_pin",
keyword_tier_rules=[{"keywords": ["quick lookup"], "tier": "SIMPLE"}],
)
original: Final = await router.async_pre_routing_hook(
model="capability-router",
request_kwargs={"metadata": {"session_id": "forecast-bypass"}},
messages=[{"role": "user", "content": "Hello!"}],
)
result: Final = await router.async_pre_routing_hook(
model="capability-router",
request_kwargs={"metadata": {"session_id": "forecast-bypass"}},
messages=[{"role": "user", "content": TITLE_ASK if bypass == "housekeeping" else "quick lookup"}],
)
assert original is not None and original.routing_decision is not None
assert original.routing_decision["classifier_p_solve"] == 0.8
assert result is not None and result.routing_decision is not None
assert result.routing_decision["cause"] == bypass
assert "classifier_p_solve" not in result.routing_decision
assert "classifier_threshold" not in result.routing_decision
mock_router_instance.acompletion.assert_awaited_once()
CUSTOM_TIER_LABELS: Dict[str, str] = {
@ -14557,6 +14593,121 @@ class TestModalityRouting:
@pytest.mark.usefixtures("local_model_cost_map")
class TestHealthFallbackDispatch:
@pytest.mark.asyncio
@pytest.mark.parametrize("classifier", ("capability", "llm_v2"))
@pytest.mark.parametrize("calibrated", (False, True), ids=("raw", "calibrated"))
@pytest.mark.parametrize("rewrite", ("modality_escalation", "health_failover", "health_default_fallback"))
async def test_classifier_forecasts_survive_placement_rewrites(
self,
classifier: Literal["capability", "llm_v2"],
calibrated: bool,
rewrite: Literal["modality_escalation", "health_failover", "health_default_fallback"],
) -> None:
calibration: Final = {"slope": 0.8, "intercept": 0.1}
classifier_config: Final = (
{
"capability_classifier_config": {
"efficient_tier": "SIMPLE",
"capable_tier": "REASONING",
"base_threshold": 0.0,
"threshold_step": 0.1,
**({"calibration": {"version": "test-v1", **calibration}} if calibrated else {}),
}
}
if classifier == "capability"
else {
"llm_v2_config": {
"efficient_profile": "Small coding solver",
"capable_profile": "Large coding solver",
"harness": "Repository tools",
"max_quality_gap": 0.0,
**(
{
"calibration": {
"version": "test-v1",
"prompt_version": LLM_V2_PROMPT_VERSION,
"efficient": calibration,
"capable": calibration,
}
}
if calibrated
else {}
),
}
}
)
router: Final = self._router(
config={
"classifier_type": classifier,
"classifier_llm_config": {"model": "fallback", "timeout_ms": 10000},
"tiers": {"SIMPLE": "primary", "REASONING": "peer"},
"tier_labels": {"SIMPLE": "Entry", "REASONING": "Advanced"},
"modality_routing": True,
**classifier_config,
}
)
verdict: Final = (
_capability_reply(p_solve=0.0)
if classifier == "capability"
else json.dumps(
{
"crux": "Preserve existing behavior",
"demands": {"reasoning": "routine", "scope": "localized", "specification": "clear"},
"verification": "relevant",
"forecasts": {
"efficient": {"likely_failure": "Miss an edge case", "p_solve": 0.0},
"capable": {"likely_failure": "Miss an edge case", "p_solve": 0.0},
},
}
)
)
judge_response: Final = litellm.ModelResponse(
choices=[{"message": {"role": "assistant", "content": verdict}}],
usage={"prompt_tokens": 10, "completion_tokens": 1, "total_tokens": 11},
)
with respx.mock(assert_all_mocked=True) as upstream:
upstream.post(host="fallback.test").respond(json=judge_response.model_dump())
original: Final = await router.async_pre_routing_hook(
model="health-router", request_kwargs={}, messages=[{"role": "user", "content": "Hello!"}]
)
for deployment in router.model_list:
deployment["model_info"]["supports_vision"] = (
rewrite != "modality_escalation" or deployment["model_name"] != "primary"
)
if rewrite != "modality_escalation":
self._unavailable(router, "primary-id", "cooldown")
if rewrite == "health_default_fallback":
self._unavailable(router, "peer-id", "cooldown")
result: Final = await router.async_pre_routing_hook(
model="health-router", request_kwargs={}, messages=TestModalityRouting.IMAGE_MESSAGE
)
assert original is not None and original.routing_decision is not None
assert original.model == "primary"
assert result is not None and result.routing_decision is not None
decision: Final = result.routing_decision
assert decision["cause"] == rewrite
assert result.model == ("fallback" if rewrite == "health_default_fallback" else "peer")
expected: Final = {
field: value for field, value in original.routing_decision.items() if field.startswith("classifier_")
}
assert expected["classifier_p_solve" if classifier == "capability" else "classifier_efficient_p_solve"] == 0.0
assert ("classifier_calibration_version" in expected) is calibrated
assert {field: value for field, value in decision.items() if field.startswith("classifier_")} == expected
if rewrite == "health_default_fallback":
assert "tier" not in decision and "tier_label" not in decision
else:
assert decision["tier"] == "REASONING"
assert decision["tier_label"] == "Advanced"
redacted: Final = Router._redact_prompt_text_if_needed(
request_kwargs={"metadata": {"headers": {"x-litellm-enable-message-redaction": True}}},
routing_decision=decision,
)
assert "classifier_crux" not in redacted and "signals" not in redacted
assert {field: value for field, value in redacted.items() if field.startswith("classifier_")} == {
field: value for field, value in expected.items() if field != "classifier_crux"
}
@pytest.mark.asyncio
@pytest.mark.parametrize("peer", (True, False), ids=("peer_failover", "default_fallback"))
async def test_health_rewrites_preserve_the_original_heuristic_v2_forecast(self, peer: bool) -> None:

View file

@ -477,6 +477,10 @@ async def test_user_turn_mode_reuses_forecast_until_a_new_user_requirement() ->
assert first.model == second.model == "efficient"
assert first.routing_decision["cause"] == "llm_v2_classifier"
assert first.routing_decision["classifier_cost"] == 0.001
assert second is not None and second.routing_decision is not None
assert second.routing_decision["cause"] == "user_turn_continuation"
assert "classifier_efficient_p_solve" not in second.routing_decision
assert "classifier_capable_p_solve" not in second.routing_decision
client.acompletion.assert_awaited_once()
client.acompletion.return_value = _response(_verdict(0.3, 0.9).model_dump_json())
updated: Final = await router.async_pre_routing_hook(

View file

@ -113,6 +113,148 @@ describe("RoutingDecisionCard", () => {
},
);
it.each(["capability_classifier", "modality_escalation"])(
"shows the recorded Capability forecast for %s",
(cause) => {
render(
<RoutingDecisionCard
decision={{
cause,
tier: "COMPLEX",
tier_label: "Deep",
classifier_p_solve: 0,
classifier_calibrated_p_solve: 0.864,
classifier_threshold: 0.82,
classifier_capability_boundary: "uncertain",
classifier_primary_rule: "UNC-2",
classifier_calibration_version: "calibration-1",
}}
/>,
);
expect(screen.getByText("Capability estimates")).toBeInTheDocument();
expect(screen.getByText("Efficient model solve chance")).toBeInTheDocument();
expect(screen.getAllByText(/^\d+\.\d%$/).map((value) => value.textContent)).toEqual(["0.0%", "86.4%", "82.0%"]);
expect(screen.getByText("Raw")).toBeInTheDocument();
expect(screen.getByText("Calibrated")).toBeInTheDocument();
expect(screen.getByText("Threshold")).toBeInTheDocument();
expect(screen.getByText("uncertain")).toBeInTheDocument();
expect(screen.getByText("UNC-2")).toBeInTheDocument();
expect(screen.getByText("calibration-1")).toBeInTheDocument();
expect(screen.getByText("Deep")).toBeInTheDocument();
expect(screen.queryByText("FUSE v2 estimates")).not.toBeInTheDocument();
},
);
it("omits absent Capability fields while preserving a recorded zero threshold", () => {
render(
<RoutingDecisionCard
decision={{ cause: "capability_classifier", classifier_p_solve: 0.25, classifier_threshold: 0 }}
/>,
);
expect(screen.getAllByText(/^\d+\.\d%$/).map((value) => value.textContent)).toEqual(["25.0%", "0.0%"]);
for (const label of ["Calibrated", "Calibration", "Boundary", "Rule"]) {
expect(screen.queryByText(label)).not.toBeInTheDocument();
}
});
it.each(["llm_v2_classifier", "default_fallback"])(
"shows the original calibrated FUSE v2 forecast for %s",
(cause) => {
render(
<RoutingDecisionCard
decision={{
cause,
routed_model: "fallback-model",
classifier_efficient_p_solve: 0.25,
classifier_capable_p_solve: 0.91,
classifier_calibrated_efficient_p_solve: 0.75,
classifier_calibrated_capable_p_solve: 0.8,
classifier_max_quality_gap: 0.1,
classifier_calibration_version: "calibration-2",
signals: ["llm-v2:verification=tests"],
}}
/>,
);
expect(screen.getByText("FUSE v2 estimates")).toBeInTheDocument();
expect(screen.getAllByText(/^\d+\.\d%$/).map((value) => value.textContent)).toEqual([
"25.0%",
"91.0%",
"75.0%",
"80.0%",
]);
expect(screen.getByText("Efficient (raw)")).toBeInTheDocument();
expect(screen.getByText("Capable (raw)")).toBeInTheDocument();
expect(screen.getByText("Efficient (calibrated)")).toBeInTheDocument();
expect(screen.getByText("Capable (calibrated)")).toBeInTheDocument();
expect(screen.getAllByText(/percentage points$/).map((value) => value.textContent)).toEqual([
"5.0 percentage points",
"10.0 percentage points",
]);
expect(screen.getByText("Applied gap")).toBeInTheDocument();
expect(screen.getByText("Allowed gap")).toBeInTheDocument();
expect(screen.getByText("calibration-2")).toBeInTheDocument();
expect(screen.getByText("llm-v2:verification=tests")).toBeInTheDocument();
expect(screen.getByText("fallback-model")).toBeInTheDocument();
expect(screen.queryByText("Capability estimates")).not.toBeInTheDocument();
},
);
it("uses raw FUSE v2 probabilities without calibration and preserves negative and zero gaps", () => {
render(
<RoutingDecisionCard
decision={{
cause: "llm_v2_classifier",
classifier_efficient_p_solve: 0.5,
classifier_capable_p_solve: 0,
classifier_max_quality_gap: 0,
}}
/>,
);
expect(screen.getAllByText(/^\d+\.\d%$/).map((value) => value.textContent)).toEqual(["50.0%", "0.0%"]);
expect(screen.getAllByText(/percentage points$/).map((value) => value.textContent)).toEqual([
"-50.0 percentage points",
"0.0 percentage points",
]);
expect(screen.queryByText(/calibrated|Calibration/)).not.toBeInTheDocument();
});
it.each([
{ classifier_efficient_p_solve: 0, classifier_max_quality_gap: 0.2 },
{
classifier_efficient_p_solve: 0.4,
classifier_capable_p_solve: 0.9,
classifier_calibrated_efficient_p_solve: 0,
classifier_calibration_version: "partial-calibration",
classifier_max_quality_gap: 0.2,
},
])("shows partial FUSE v2 estimates without inventing an applied gap: %j", (fields) => {
render(<RoutingDecisionCard decision={{ cause: "llm_v2_classifier", ...fields }} />);
expect(screen.getByText("FUSE v2 estimates")).toBeInTheDocument();
expect(screen.getByText("0.0%")).toBeInTheDocument();
expect(screen.getByText("20.0 percentage points")).toBeInTheDocument();
expect(screen.queryByText("Applied gap")).not.toBeInTheDocument();
expect(screen.queryByText("Capable (calibrated)")).not.toBeInTheDocument();
});
it.each([
["capability_classifier", "Capability"],
["llm_v2_classifier", "FUSE v2"],
["capability_classifier_fallback", "Capable tier, Capability classifier failed"],
["llm_v2_fallback", "Capable tier, FUSE v2 classifier failed"],
["session_affinity_pin", "Pinned to session"],
])("labels %s without inventing a missing forecast", (cause, label) => {
render(<RoutingDecisionCard decision={{ cause, tier: "MEDIUM" }} />);
expect(screen.getByText(label)).toBeInTheDocument();
expect(screen.getByText("MEDIUM")).toBeInTheDocument();
expect(screen.queryByText(/estimates/)).not.toBeInTheDocument();
});
it("uses the persisted boundary snapshot, not today's defaults", () => {
// Same score, boundaries the operator had configured lower: it lands in a
// different band, and the card must say so.

View file

@ -24,6 +24,17 @@ export interface RoutingDecision {
matched_keyword?: string;
escalation_keyword?: string;
classifier_model?: string;
classifier_p_solve?: number;
classifier_calibrated_p_solve?: number;
classifier_threshold?: number;
classifier_capability_boundary?: string;
classifier_primary_rule?: string;
classifier_calibration_version?: string;
classifier_efficient_p_solve?: number;
classifier_capable_p_solve?: number;
classifier_calibrated_efficient_p_solve?: number;
classifier_calibrated_capable_p_solve?: number;
classifier_max_quality_gap?: number;
escalated?: boolean;
tier_boundaries?: RoutingDecisionTierBoundaries;
reasoning_override_min_score?: number;
@ -91,6 +102,10 @@ function describeReasoningOverride(tierLabel: string | undefined, floor: number
const CONSTANT_CAUSE_LABELS: Record<string, string> = {
heuristic_scorer: "Heuristic scorer",
heuristic_v2: "Heuristic v2",
capability_classifier: "Capability",
capability_classifier_fallback: "Capable tier, Capability classifier failed",
llm_v2_classifier: "FUSE v2",
llm_v2_fallback: "Capable tier, FUSE v2 classifier failed",
heuristic_first_short_circuit: "Heuristic scorer, classifier skipped",
hybrid_short_circuit: "Heuristic scorer, score clear of every boundary",
classifier_plugin: "Custom classifier plugin",
@ -156,6 +171,71 @@ function Row({ label, children }: { label: string; children: React.ReactNode })
);
}
function PercentageRow({ label, value, unit = "%" }: { label: string; value?: number; unit?: string }) {
if (value === undefined) return null;
return (
<Row label={label}>
<span className="tabular-nums">{`${(value * 100).toFixed(1)}${unit}`}</span>
</Row>
);
}
function CapabilityForecast({ decision }: { decision: RoutingDecision }) {
const {
classifier_p_solve: raw,
classifier_calibrated_p_solve: calibrated,
classifier_threshold: threshold,
classifier_capability_boundary: boundary,
classifier_primary_rule: rule,
classifier_calibration_version: version,
} = decision;
if ([raw, calibrated, threshold, boundary, rule].every((value) => value === undefined)) return null;
return (
<div className="mt-3 border-t pt-3">
<div className="mb-1 text-sm font-medium">Capability estimates</div>
<div className="mb-2 text-xs text-muted-foreground">Efficient model solve chance</div>
<PercentageRow label="Raw" value={raw} />
<PercentageRow label="Calibrated" value={calibrated} />
<PercentageRow label="Threshold" value={threshold} />
{boundary && <Row label="Boundary">{boundary}</Row>}
{rule && <Row label="Rule">{rule}</Row>}
{version && <Row label="Calibration">{version}</Row>}
</div>
);
}
function FuseV2Forecast({ decision }: { decision: RoutingDecision }) {
const {
classifier_efficient_p_solve: rawEfficient,
classifier_capable_p_solve: rawCapable,
classifier_calibrated_efficient_p_solve: calibratedEfficient,
classifier_calibrated_capable_p_solve: calibratedCapable,
classifier_max_quality_gap: allowedGap,
classifier_calibration_version: version,
} = decision;
if ([rawEfficient, rawCapable, calibratedEfficient, calibratedCapable, allowedGap].every((v) => v === undefined)) {
return null;
}
const isCalibrated = [calibratedEfficient, calibratedCapable, version].some((value) => value !== undefined);
const efficient = isCalibrated ? calibratedEfficient : rawEfficient;
const capable = isCalibrated ? calibratedCapable : rawCapable;
const gap = efficient !== undefined && capable !== undefined ? capable - efficient : undefined;
return (
<div className="mt-3 border-t pt-3">
<div className="mb-1 text-sm font-medium">FUSE v2 estimates</div>
<PercentageRow label="Efficient (raw)" value={rawEfficient} />
<PercentageRow label="Capable (raw)" value={rawCapable} />
<PercentageRow label="Efficient (calibrated)" value={calibratedEfficient} />
<PercentageRow label="Capable (calibrated)" value={calibratedCapable} />
<PercentageRow label="Applied gap" value={gap} unit=" percentage points" />
<PercentageRow label="Allowed gap" value={allowedGap} unit=" percentage points" />
{version && <Row label="Calibration">{version}</Row>}
</div>
);
}
export function RoutingDecisionCard({
decision,
className,
@ -247,6 +327,9 @@ export function RoutingDecisionCard({
</div>
)}
<CapabilityForecast decision={decision} />
<FuseV2Forecast decision={decision} />
{signals && signals.length > 0 && (
<Row label="Signals">
<span className="flex flex-wrap gap-1">