Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,12 @@ candidate models, their harnesses, and the router name sent with each recorded
decision. Feedback investigations can compare the selection with that original
menu without querying the router again.

Claude and Codex routing reasons summarize the task profile in plain language, such as
"This task has a clear target and limited scope." A separate sentence explains when
model availability requires a higher tier. Custom gateway notices are retained, and
older or unrecognized responses keep their original wording. Diagnostic records keep
the displayed reason in `rationale` and the gateway's full explanation in `raw_rationale`.

On Windows, Claude smart routing uses subagent hooks only. If first-prompt routing
is enabled, ug warns and falls back to subagent routing because the first-prompt
wrapper requires a Unix terminal.
Expand Down
64 changes: 64 additions & 0 deletions src/ucode/smart_routing/rationale.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""Plain-language explanations of structured task-router decisions."""

_PROFILE_REASONS = (
(
"localization_burden",
"open_search",
"This task needs investigation to find what needs attention.",
),
("scope", "cross_cutting", "This task spans several components."),
("environment_burden", "live_external", "This task depends on live systems or data."),
("reasoning_depth", "open_ended", "Solving this task requires exploring possible approaches."),
("verification_strength", "weak", "There is no clear way to check the result of this task."),
("scope", "connected", "This task involves several related components."),
("reasoning_depth", "multi_step", "This task involves several dependent steps."),
("localization_burden", "bounded_search", "This task needs a focused search."),
)


def format_rationale(selection: dict, rationale: str) -> str:
"""Summarize task_v3 profiles, preserving unfamiliar responses and gateway notices."""
params = selection.get("params")
if not isinstance(params, dict) or params.get("router_version") != "task_v3":
return rationale
task_tier, served_tier = params.get("task_tier"), params.get("served_tier")
tiers = ("Low", "Medium", "High")
if task_tier not in tiers or served_tier not in tiers:
return rationale
if tiers.index(served_tier) < tiers.index(task_tier):
return rationale

evidence = params.get("evidence_spans")
if not isinstance(evidence, list) or len(evidence) != 1 or not isinstance(evidence[0], str):
return rationale
ending = f" Request: “{evidence[0]}”."
if task_tier != served_tier:
ending += f" Your configured model availability requires service at {served_tier}."
# Only replace the known generated portion; custom gateway notices follow it.
_, separator, notice = rationale.partition(ending)
if not separator:
return rationale

profile = params.get("task_profile")
if params.get("profiler_status") == "unavailable":
if task_tier != "High" or served_tier != "High":
return rationale
reason = "The task could not be assessed, so a high-capability model was selected."
elif params.get("profiler_status") == "ok" and isinstance(profile, dict):
reason = next(
(text for field, value, text in _PROFILE_REASONS if profile.get(field) == value),
"",
)
if (
not reason
and profile.get("localization_burden") == "explicit_target"
and profile.get("scope") == "localized"
):
reason = "This task has a clear target and limited scope."
if not reason:
return rationale
else:
return rationale
if task_tier != served_tier:
reason += " A higher-tier model was selected because the requested tier isn't available."
return " ".join(part for part in (reason, notice.strip()) if part)
13 changes: 11 additions & 2 deletions src/ucode/smart_routing/routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
from pathlib import Path
from typing import Any

from ucode.smart_routing.rationale import format_rationale

ROUTER_NAME = "task_v3"
ROUTER_NAME_ENV_VAR = "SMART_ROUTER_NAME"
ROUTING_PATH = "/ai-gateway/routing/v1/routes:select"
Expand Down Expand Up @@ -94,6 +96,7 @@ class RoutingDecision:
rationale: str = ""
route_options: tuple[tuple[str, str | None], ...] = ()
router_name: str = ""
raw_rationale: str | None = None

def display_message(
self,
Expand Down Expand Up @@ -262,14 +265,17 @@ def select_route(
model = resolve(raw_model)
if model is None:
return None, f"router selected unsupported model {raw_model!r}"
rationale = payload.get("rationale") if isinstance(payload, dict) else None
raw_rationale = payload.get("rationale")
raw_rationale = raw_rationale if isinstance(raw_rationale, str) else ""
rationale = format_rationale(payload["route_selection"][0], raw_rationale)
return (
RoutingDecision(
model=model,
raw_model=raw_model,
rationale=rationale if isinstance(rationale, str) else "",
rationale=rationale,
route_options=offered_routes,
router_name=router_name,
raw_rationale=raw_rationale if rationale != raw_rationale else None,
),
None,
)
Expand Down Expand Up @@ -424,6 +430,9 @@ def write_decision_record(
# Persisted for diagnosis: an empty value means the gateway returned
# no rationale (vs. a placement bug in how we surface it).
"rationale": decision.rationale,
"raw_rationale": decision.raw_rationale
if decision.raw_rationale is not None
else decision.rationale,
"at": time.time(),
},
)
Expand Down
26 changes: 26 additions & 0 deletions tests/test_routing_rationale.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from ucode.smart_routing.rationale import format_rationale


def test_plain_language_reason():
selection = {
"params": {
"router_version": "task_v3",
"task_tier": "Low",
"served_tier": "Low",
"profiler_status": "ok",
"evidence_spans": ["Inspect the parser."],
"task_profile": {"localization_burden": "explicit_target", "scope": "localized"},
}
}
rationale = (
"Routed to Low because its task-only profile is low: "
"an explicit target, localized scope, direct reasoning. Request: “Inspect the parser.”."
)
assert (
format_rationale(selection, rationale) == "This task has a clear target and limited scope."
)


def test_unknown_response_is_preserved():
rationale = "A custom routing explanation."
assert format_rationale({}, rationale) == rationale
Loading