Skip to content

docs(stage-router): correct the decision flow and rewrite threshold tuning - #288

Open
sabhatinas wants to merge 1 commit into
mainfrom
sabhatinas/stage-router-docs
Open

docs(stage-router): correct the decision flow and rewrite threshold tuning#288
sabhatinas wants to merge 1 commit into
mainfrom
sabhatinas/stage-router-docs

Conversation

@sabhatinas

@sabhatinas sabhatinas commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Docs only. No code change.

Decision flow was wrong

  • The old diagram gated on confidence >= threshold and jumped straight to "signals pick capable/efficient", which hid both hard rules and the sign check. It also implied the score is the confidence.
  • New diagram shows the real cascade: critical-error/compaction override → tests-passed de-escalation → scorer → threshold → sign → classifier/fall-open.

Show where the score comes from

Added the actual formula and what it implies, rather than asserting "~0.46":

raw   = 0.10 × (severity/0.7 + spinning + exploring − production_intensity)
score = tanh(5.0 × raw)        → [-1, +1],  confidence = |score|

with a table of what 1 / 1.5 / 2 / 3 corroborating signals actually score.

Threshold tuning rewritten

  • Replaced the 0.0 / 0.5 / 0.7 / 1.0 recommendation table — it read as a menu of portable settings, which they are not.
  • Added number lines showing the three bands and who owns the middle: efficient_first sends [-1, t) to efficient and [t, +1] to capable; capable_first sends [-1, -t] to efficient and (-t, +1] to capable.
  • Added a table converting t into "maxed signals needed to leave the default tier".
  • Changed the starting recommendation to 0.3 and said plainly that swapping either model changes the trajectories, so the score distribution moves and the same t buys a different split. Recalibrate per tier pair.

Calibration rewritten

  • Dropped the probe-set stratification table and the RESCUE/LOSS/SAFE/HARD quadrant construction. It demanded two matched runs and a 20-task stratified sample before you could pick a number, which is more ceremony than picking a threshold needs.
  • Now: replay any run with real tool traffic through the switchyard-stage-router-scorer skill, histogram the scores, put the cut line where the mass tells you to, sweep, and check escalations land where the efficient tier actually fails.
  • Added an ASCII histogram showing the threshold as a cut line on the distribution.
  • Added a section on recent_turn_window — short windows react fast and flap, long windows need sustained trouble and react late — and noted it is not independent of t.
  • Replaced "derived from SWE-Bench Pro Python-75" with the honest version: calibrated on Terminal-Bench 2.1 and more recently SWE-Bench Pro, which is why you should re-derive rather than adopt.

Route configuration

Replaced the single-provider OpenRouter gpt-4o/4o-mini example with the two-provider shape we actually benchmark: Anthropic Messages capable tier, OpenAI Chat efficient tier, per-target extra_body, handoff notes, and the --routing-log-file flag.

Verified with mkdocs build --strict.

Summary by CodeRabbit

  • Documentation
    • Updated routing guidance to explain signed scores, routing bands, thresholds, and hard overrides.
    • Added score-calibration recommendations using replayed runs and configurable thresholds.
    • Refreshed configuration examples with provider-specific settings, handoff notes, and per-target options.
    • Added startup guidance for routing-decision logging and route ID alignment.

…uning

Signed-off-by: sabhatinas <sabhatinas@nvidia.com>
@sabhatinas
sabhatinas requested a review from a team as a code owner August 4, 2026 19:23
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1

🚀 View preview at
https://NVIDIA-NeMo.github.io/Switchyard/pr-preview/pr-288/

Built to branch gh-pages at 2026-08-04 19:24 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The routing documentation now defines signed confidence scoring, capable and efficient routing bands, hard overrides, replay-based calibration, provider-specific configuration, handoff behavior, and startup logging requirements.

Changes

Stage router routing

Layer / File(s) Summary
Scoring and decision flow
docs/routing_algorithms/stage_router_routing.md
Documents the signed score, bounded confidence model, corroboration, hard overrides, classifier path, and picker fallbacks.
Threshold bands and calibration
docs/routing_algorithms/stage_router_routing.md
Defines picker-specific threshold bands and replay-based score-distribution and window calibration.
Provider configuration and startup guidance
docs/routing_algorithms/stage_router_routing.md
Updates provider targets, per-target options, handoff settings, routing-decision logging, and route ID requirements.

Estimated code review effort: 2 (Simple) | ~10 minutes

Poem

I’m a rabbit tuning routes tonight,
With signed scores hopping left and right.
Thresholds sweep, providers flow,
Logs record each route we know.
Efficient hops make docs just right!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the documentation scope and the two main changes: correcting the decision flow and rewriting threshold-tuning guidance.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/routing_algorithms/stage_router_routing.md`:
- Around line 68-76: Update the Mermaid flowchart’s de-escalation decision to
require tests_passed, a recent write or edit, and severity <= 0.0 before
selecting the efficient tier. Update the classifier path so an invalid or absent
classifier tier falls through to the picker’s default fall_open tier, matching
the runtime behavior in the stage-router picker.
- Around line 200-204: Update the “Caveat on efficient outcomes” section to
remove the claim that stage-router efficient performance is always at least as
good as standalone efficient performance or constitutes a conservative lower
bound. State that inherited conversation history may help or hurt, and treat the
two runs as non-equivalent baselines unless matched benchmark results establish
a lower bound.
- Around line 258-263: Update the routing-log example in the documentation to
use a path writable by the server user, or explicitly document the required
ownership and permissions for /var/lib/switchyard. In the request example, set
the model value to "switchyard" so it matches routes.stage.id and selects the
intended route.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: e43c89fd-971a-4c60-ae0e-1ece77160281

📥 Commits

Reviewing files that changed from the base of the PR and between 6e63ad9 and e87b43c.

📒 Files selected for processing (1)
  • docs/routing_algorithms/stage_router_routing.md

Comment on lines +68 to +76
h -->|no| dz{"tests passed<br/>+ recent write?"}
dz -->|yes| eff["efficient (tests_passed)"]
dz -->|no| sc["raw = 0.10 × (severity/0.7 + spinning + exploring − production)<br/>score = tanh(5 × raw)"]
sc --> g{"|score| >= threshold?"}
g -->|"yes, score > 0"| cap2["capable (dimensions)"]
g -->|"yes, score < 0"| eff2["efficient (dimensions)"]
g -->|no| c{"classifier set?"}
c -->|yes| k["classifier picks capable/efficient"]
c -->|no| d["use picker default tier"]
c -->|yes| k["classifier picks tier"]
c -->|no| d["picker default tier (fall_open)"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Align the flowchart with the runtime fallbacks.

The de-escalation branch must also require no windowed error (severity <= 0.0) and must accept a recent edit as well as a recent write. The classifier branch must show a fall-open path when the classifier returns no valid tier. Update Lines 68-76 to match switchyard/lib/processors/stage_router/picker.py:54-88.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/routing_algorithms/stage_router_routing.md` around lines 68 - 76, Update
the Mermaid flowchart’s de-escalation decision to require tests_passed, a recent
write or edit, and severity <= 0.0 before selecting the efficient tier. Update
the classifier path so an invalid or absent classifier tier falls through to the
picker’s default fall_open tier, matching the runtime behavior in the
stage-router picker.

Comment on lines +200 to +204
**Caveat on efficient outcomes.** In stage-router the efficient model inherits
conversation history up to the escalation point, whereas a pure-efficient run
starts fresh. So efficient performs at least as well inside stage-router as it
does alone, and any comparison against a standalone efficient run is a
conservative lower bound.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- target section ---'
sed -n '175,220p' docs/routing_algorithms/stage_router_routing.md
printf '%s\n' '--- related stage-router references ---'
rg -n -i -C 3 'stage-router|standalone|pure-efficient|efficient model|lower bound|escalat' docs tests .github 2>/dev/null | head -n 300
printf '%s\n' '--- repository files relevant to routing and benchmarks ---'
git ls-files | rg -i 'routing|router|benchmark|stage'

Repository: NVIDIA-NeMo/Switchyard

Length of output: 29090


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- stage-router implementation outlines ---'
ast-grep outline crates/switchyard-components/src/stage_router.rs --view compact 2>/dev/null || true
ast-grep outline crates/libsy/src/algorithms/stage.rs --view compact 2>/dev/null || true
ast-grep outline switchyard/lib/processors/stage_router_request_processor.py --view compact 2>/dev/null || true
printf '%s\n' '--- request-processing and tier-switching code ---'
rg -n -C 5 'conversation|history|handoff|tier|EFFICIENT|CAPABLE|request|messages|transcript' \
  crates/switchyard-components/src/stage_router.rs \
  crates/libsy/src/algorithms/stage.rs \
  switchyard/lib/processors/stage_router_request_processor.py \
  switchyard/lib/processors/stage_router/*.py \
  tests/test_stage_router_request_processor.py \
  tests/test_stage_router_profile.py
printf '%s\n' '--- benchmark instructions and staged scoring ---'
rg -n -C 5 -i 'stage.router|efficient|capable|baseline|standalone|history|fresh|comparison' \
  benchmark docs/routing_algorithms/stage_router_routing.md \
  .agents/skills/switchyard-stage-router-scorer/SKILL.md

Repository: NVIDIA-NeMo/Switchyard

Length of output: 50379


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- stage-router documentation context ---'
sed -n '1,175p' docs/routing_algorithms/stage_router_routing.md
printf '%s\n' '--- benchmark references to staged and standalone runs ---'
rg -n -C 4 -i 'stage.?router|pure.?efficient|standalone|baseline|capable|efficient|fresh|history|handoff' \
  benchmark/README.md benchmark/run-baseline.sh benchmark/run_manifest.py benchmark/score_staged_run.py \
  .agents/skills/switchyard-stage-router-scorer/SKILL.md \
  docs/routing_algorithms/stage_router_routing.md
printf '%s\n' '--- tests that assert request/history preservation or tier handoff ---'
rg -n -C 4 -i 'not mutated|messages|handoff|selected_target|selected_model|stage_router' \
  tests/test_stage_router_request_processor.py tests/test_stage_router_handoff_notes.py \
  tests/test_stage_router_classifier.py

Repository: NVIDIA-NeMo/Switchyard

Length of output: 50380


Remove the unsupported lower-bound claim. Inherited conversation history can help or hurt the efficient model. Treat stage-router and standalone efficient runs as non-equivalent baselines unless matched benchmark results establish a lower bound.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/routing_algorithms/stage_router_routing.md` around lines 200 - 204,
Update the “Caveat on efficient outcomes” section to remove the claim that
stage-router efficient performance is always at least as good as standalone
efficient performance or constitutes a conservative lower bound. State that
inherited conversation history may help or hurt, and treat the two runs as
non-equivalent baselines unless matched benchmark results establish a lower
bound.

Comment on lines +258 to +263
Add `--routing-log-file /var/lib/switchyard/routing_requests.jsonl` to record
per-request routing decisions for later analysis.

Keep the route `id` aligned with whatever model alias your agent sends — that
string is what selects this route.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 4 \
  'routing-log-file|route.*id|model.*alias|switchyard-server|argparse|clap' \
  --glob '*.py' --glob '*.rs' --glob '*.toml' .

Repository: NVIDIA-NeMo/Switchyard

Length of output: 50379


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- server CLI definition ---'
cat -n crates/switchyard-server/src/cli.rs | sed -n '1,140p'

printf '%s\n' '--- routing log references ---'
rg -n -C 3 'routing[_-]log|routing log|routing_requests|log_file' .

printf '%s\n' '--- stage-router documentation and request examples ---'
rg -n -C 5 'stage_router|stage-router|routes\.stage|model alias|model_alias|model:' \
  docs switchyard tests --glob '*.md' --glob '*.py' --glob '*.toml' --glob '*.yaml' --glob '*.yml' \
  | head -n 500

printf '%s\n' '--- server startup and config references ---'
rg -n -C 4 'switchyard-server|--config|/var/lib|routes\.toml|routing' \
  docs README.md crates/switchyard-server --glob '*.md' --glob '*.toml' --glob '*.rs' \
  | head -n 500

Repository: NVIDIA-NeMo/Switchyard

Length of output: 50381


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- reviewed document ---'
cat -n docs/routing_algorithms/stage_router_routing.md | sed -n '1,290p'

printf '%s\n' '--- route resolution and request model selection ---'
rg -n -C 8 'resolve_route|selected_model|model.*header|body.*model|metadata.*model|request.*model|route.*model' \
  crates/switchyard-server/src switchyard/cli switchyard/lib \
  --glob '*.rs' --glob '*.py' | head -n 600

printf '%s\n' '--- launcher request/model configuration ---'
rg -n -C 6 'model|route|alias|OPENAI|ANTHROPIC|CODEX|CLAUDE' \
  switchyard/cli/launchers switchyard/cli/launch_command.py docs \
  --glob '*.py' --glob '*.md' | head -n 800

printf '%s\n' '--- deployment users and filesystem paths ---'
rg -n -C 5 'User=|useradd|systemd|/var/lib/switchyard|WorkingDirectory|runuser|sudo|permissions|writable' \
  . --glob '*.service' --glob '*.yml' --glob '*.yaml' --glob '*.toml' --glob '*.md' --glob '*.sh' --glob 'Dockerfile*' \
  | head -n 800

Repository: NVIDIA-NeMo/Switchyard

Length of output: 50381


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- exact Rust route lookup ---'
cat -n crates/switchyard-server/src/lib.rs | sed -n '570,635p'

printf '%s\n' '--- route-bundle registration for stage_router ---'
cat -n switchyard/cli/route_bundle.py | sed -n '1030,1085p'
cat -n switchyard/cli/route_bundle.py | sed -n '420,490p'

printf '%s\n' '--- launcher model/default route flow ---'
rg -n -C 8 'display_model|default_route|profile_routes|default_model|route.*model|model.*route' \
  switchyard/cli/launchers switchyard/cli/launch_command.py \
  --glob '*.py' | head -n 500

printf '%s\n' '--- deployment-related files and path references ---'
git ls-files | rg -i '(^|/)(dockerfile[^/]*|.*systemd.*|.*service$|deploy|deployment|container|helm|compose|install|readme|operations)'
rg -n -C 4 '/var/lib/switchyard|User=|useradd|systemd|docker run|WORKDIR|chmod|chown|routing_requests\.jsonl' \
  $(git ls-files) 2>/dev/null | head -n 500

Repository: NVIDIA-NeMo/Switchyard

Length of output: 50378


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- native TOML route loading and route-ID contract ---'
rg -n -C 8 'routes|route.*id|id.*route|algorithm_for_model|load_server_state|Route' \
  crates/switchyard-server/src/config.rs crates/switchyard-server/src/lib.rs \
  crates/switchyard-server/README.md --glob '*.rs' --glob '*.md' | head -n 500

printf '%s\n' '--- server README startup and route examples ---'
cat -n crates/switchyard-server/README.md | sed -n '1,115p'

printf '%s\n' '--- standalone source-data verifier ---'
python3 - <<'PY'
from pathlib import Path
import re

cli = Path("crates/switchyard-server/src/cli.rs").read_text()
log = Path("crates/switchyard-server/src/routing_log.rs").read_text()
server = Path("crates/switchyard-server/src/lib.rs").read_text()
doc = Path("docs/routing_algorithms/stage_router_routing.md").read_text()

checks = {
    "clap flag field": bool(re.search(r'#\[arg\(long, value_name = "PATH"\)\]\s+routing_log_file:\s+Option<PathBuf>', cli)),
    "CLI applies routing log": "with_routing_log(path)" in cli,
    "log creates parent": "fs::create_dir_all(parent)" in log,
    "log opens append/create": all(x in log for x in ("OpenOptions::new()", ".create(true)", ".append(true)")),
    "server exact model lookup": "state.algorithm_for_model(&requested_model)" in server,
    "document route id": bool(re.search(r'\[routes\.stage\].*?id\s*=\s*"switchyard"', doc, re.S)),
}
for name, result in checks.items():
    print(f"{name}: {'PASS' if result else 'FAIL'}")
PY

Repository: NVIDIA-NeMo/Switchyard

Length of output: 44771


Use a writable routing-log path. --routing-log-file is supported, and the request must send "model": "switchyard" to select routes.stage.id. The server creates parent directories but does not change permissions, so document directory ownership or use a path writable by the server user instead of assuming /var/lib/switchyard.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/routing_algorithms/stage_router_routing.md` around lines 258 - 263,
Update the routing-log example in the documentation to use a path writable by
the server user, or explicitly document the required ownership and permissions
for /var/lib/switchyard. In the request example, set the model value to
"switchyard" so it matches routes.stage.id and selects the intended route.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant