From 349dba3be46df9d6ffa0eb0feedacf0408f18ac1 Mon Sep 17 00:00:00 2001 From: Ryan Lempka Date: Wed, 9 Sep 2026 13:38:02 -0500 Subject: [PATCH 1/5] feat(routing): add auto algorithm type Signed-off-by: Ryan Lempka --- crates/switchyard-runner/src/algorithm.rs | 28 +++++++++++++++++++++++ crates/switchyard-runner/src/config.rs | 8 +++++++ docs/reference/toml_schema.md | 5 ++++ docs/routing_algorithms/overview.md | 1 + 4 files changed, 42 insertions(+) diff --git a/crates/switchyard-runner/src/algorithm.rs b/crates/switchyard-runner/src/algorithm.rs index 0e1ede289..1938e5346 100644 --- a/crates/switchyard-runner/src/algorithm.rs +++ b/crates/switchyard-runner/src/algorithm.rs @@ -258,6 +258,18 @@ pub enum AlgorithmSpec { #[serde(default)] subagents: Option, }, + /// Picks a routing strategy automatically. Currently resolves to the same + /// behavior as `stage_router`; change the merged match arms below to + /// repoint it at a different algorithm. + Auto { + #[serde(flatten)] + tiers: StageTierConfig, + picker: PickerMode, + #[serde(default)] + classifier: Option, + #[serde(default)] + subagents: Option, + }, /// A judge picks the tier at each user turn; a stage router runs the turns within it. Composite { /// Judge that picks the tier. Called through its own target. @@ -454,6 +466,9 @@ impl AlgorithmSpec { } Self::StageRouter { tiers, subagents, .. + } + | Self::Auto { + tiers, subagents, .. } => { let mut names = vec![ tiers.capable_target.as_str(), @@ -501,6 +516,11 @@ impl AlgorithmSpec { classifier, subagents, .. + } + | Self::Auto { + classifier, + subagents, + .. } => { if let Some(classifier) = classifier { names.push(&classifier.target); @@ -553,6 +573,7 @@ impl AlgorithmSpec { | Self::Passthrough { .. } | Self::LlmClassifier { .. } | Self::StageRouter { .. } + | Self::Auto { .. } | Self::Composite { .. } | Self::PrefillRouter { .. } => None, } @@ -983,6 +1004,13 @@ fn build_algorithm( classifier, subagents, .. + } + | AlgorithmSpec::Auto { + tiers, + picker, + classifier, + subagents, + .. } => { let StageTierConfig { capable_target, diff --git a/crates/switchyard-runner/src/config.rs b/crates/switchyard-runner/src/config.rs index a4bb309aa..c70ac99bf 100644 --- a/crates/switchyard-runner/src/config.rs +++ b/crates/switchyard-runner/src/config.rs @@ -982,6 +982,14 @@ confidence_threshold = 0.5 assert!(error_message(&config).contains("unknown field")); } + #[test] + fn auto_builds_the_same_route_as_stage_router() -> RunnerResult<()> { + let config = stage_config().replace("type = \"stage_router\"", "type = \"auto\""); + let runner = runner_from_toml(&config)?; + assert!(runner.route("switchyard/stage").is_some()); + Ok(()) + } + #[test] fn composite_stage_block_rejects_an_unknown_field() { let config = composite_config().replace( diff --git a/docs/reference/toml_schema.md b/docs/reference/toml_schema.md index 2708b68db..ab2a87e17 100644 --- a/docs/reference/toml_schema.md +++ b/docs/reference/toml_schema.md @@ -252,6 +252,11 @@ optional `handoff_notes` and `classifier` tables and for tuning. | `classifier.response_format_type` | No | `json_schema` | Structured-output mode for the optional classifier judge. Use `json_object` when the classifier provider does not support JSON Schema; Switchyard adds the schema to the prompt and validates the verdict locally. | | `subagents` | No | unset | Nested `passthrough` or custom `llm_classifier` policy used only for delegated sub-agent work. See [Sub-Agent-Aware Routing](../routing_algorithms/subagent_routing.md). | +### `auto` + +Picks a routing strategy automatically. Currently resolves to `stage_router` +and takes the same fields; the resolution may change in a future release. + ### `composite` Composes other algorithms, letting one set another's diff --git a/docs/routing_algorithms/overview.md b/docs/routing_algorithms/overview.md index 648e6866b..6310bd830 100644 --- a/docs/routing_algorithms/overview.md +++ b/docs/routing_algorithms/overview.md @@ -16,6 +16,7 @@ configuration and tuning. For the vocabulary these pages use, see | [Random Routing](random_routing.md) | You need a fixed traffic split for A/B tests, baselines, or cost experiments. | `random` | | [LLM Classifier Routing](llm_classifier_routing.md) | Request content should decide whether a turn needs the weak or strong tier. | `llm_classifier` | | [Stage-Router Routing](stage_router_routing.md) | Tool-result and agent-progress signals should route most turns without an extra classifier call. | `stage_router` | +| Auto Routing | You don't want to pick a strategy yet. Currently resolves to `stage_router`; the resolution may change. Takes the same fields as `stage_router`. | `auto` | | [Composite Routing](composite_routing.md) | Routing algorithms are composed, one setting the configuration of another before handing off. Today an LLM classifier sets a stage router's default tier. | `composite` | | [Escalation-Router Routing](escalation_router_routing.md) | Start every task on the weak tier and escalate to strong when an LLM judge detects trouble. | `llm_classifier` with `escalation` | | [Advisor-Gate Routing](advisor_gate_routing.md) | One model should serve every turn, with a stronger reviewer approving its "done" claims or sending back a redo plan. | `advisor` | From 09ee1fef5c0765b22ca89e3131b140bee48fe7b0 Mon Sep 17 00:00:00 2001 From: Ryan Lempka Date: Wed, 9 Sep 2026 13:57:02 -0500 Subject: [PATCH 2/5] fix(routing): report correct route type in auto route errors Signed-off-by: Ryan Lempka --- crates/switchyard-runner/src/algorithm.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/switchyard-runner/src/algorithm.rs b/crates/switchyard-runner/src/algorithm.rs index 1938e5346..6e30f76a1 100644 --- a/crates/switchyard-runner/src/algorithm.rs +++ b/crates/switchyard-runner/src/algorithm.rs @@ -1012,6 +1012,11 @@ fn build_algorithm( subagents, .. } => { + let type_name = if matches!(config, AlgorithmSpec::Auto { .. }) { + "auto" + } else { + "stage_router" + }; let StageTierConfig { capable_target, efficient_target, @@ -1021,7 +1026,7 @@ fn build_algorithm( } = tiers; if matches!(picker, PickerMode::CapableFirst) { tracing::warn!( - "stage_router route {route_name} uses picker \"capable_first\", which is experimental: published thresholds and routing results all come from \"efficient_first\", so there is no calibrated confidence_threshold for it and no measured accuracy or cost. Use \"efficient_first\" unless you are running your own calibration." + "{type_name} route {route_name} uses picker \"capable_first\", which is experimental: published thresholds and routing results all come from \"efficient_first\", so there is no calibrated confidence_threshold for it and no measured accuracy or cost. Use \"efficient_first\" unless you are running your own calibration." ); } let capable = resolve_target_model_id(route_name, capable_target, targets)?; @@ -1044,7 +1049,7 @@ fn build_algorithm( .transpose()?; let algorithm = StageRouter::new(capable, efficient, config).map_err(|error| { AlgorithmConfigError::with_source( - format!("stage_router route {route_name}: {error}"), + format!("{type_name} route {route_name}: {error}"), error, ) })?; From 380932283a01d8841c81caa49ad5094e2dba5e23 Mon Sep 17 00:00:00 2001 From: Ryan Lempka Date: Wed, 9 Sep 2026 13:59:39 -0500 Subject: [PATCH 3/5] docs(routing): describe auto routing without naming its current default Signed-off-by: Ryan Lempka --- docs/reference/toml_schema.md | 6 ++++-- docs/routing_algorithms/overview.md | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/reference/toml_schema.md b/docs/reference/toml_schema.md index ab2a87e17..0b9dfb1f3 100644 --- a/docs/reference/toml_schema.md +++ b/docs/reference/toml_schema.md @@ -254,8 +254,10 @@ optional `handoff_notes` and `classifier` tables and for tuning. ### `auto` -Picks a routing strategy automatically. Currently resolves to `stage_router` -and takes the same fields; the resolution may change in a future release. +Uses Switchyard's recommended default routing strategy instead of one you pick +yourself. See [Stage-Router Routing](../routing_algorithms/stage_router_routing.md) +for a deeper dive on the current default, or the [strategy table](../routing_algorithms/overview.md#choose-a-strategy) +to pick one manually. ### `composite` diff --git a/docs/routing_algorithms/overview.md b/docs/routing_algorithms/overview.md index 6310bd830..6350bc24e 100644 --- a/docs/routing_algorithms/overview.md +++ b/docs/routing_algorithms/overview.md @@ -16,7 +16,7 @@ configuration and tuning. For the vocabulary these pages use, see | [Random Routing](random_routing.md) | You need a fixed traffic split for A/B tests, baselines, or cost experiments. | `random` | | [LLM Classifier Routing](llm_classifier_routing.md) | Request content should decide whether a turn needs the weak or strong tier. | `llm_classifier` | | [Stage-Router Routing](stage_router_routing.md) | Tool-result and agent-progress signals should route most turns without an extra classifier call. | `stage_router` | -| Auto Routing | You don't want to pick a strategy yet. Currently resolves to `stage_router`; the resolution may change. Takes the same fields as `stage_router`. | `auto` | +| Auto Routing | You want a recommended default instead of picking a strategy yourself. For a deeper dive on the current default, see [Stage-Router Routing](stage_router_routing.md); for full control, pick one of the strategies above instead. | `auto` | | [Composite Routing](composite_routing.md) | Routing algorithms are composed, one setting the configuration of another before handing off. Today an LLM classifier sets a stage router's default tier. | `composite` | | [Escalation-Router Routing](escalation_router_routing.md) | Start every task on the weak tier and escalate to strong when an LLM judge detects trouble. | `llm_classifier` with `escalation` | | [Advisor-Gate Routing](advisor_gate_routing.md) | One model should serve every turn, with a stronger reviewer approving its "done" claims or sending back a redo plan. | `advisor` | From b464210a2df7ca96a98520727c96ab8db0646e1d Mon Sep 17 00:00:00 2001 From: Ryan Lempka Date: Wed, 9 Sep 2026 14:30:52 -0500 Subject: [PATCH 4/5] feat(routing): give auto its own preset knobs instead of stage_router's fields Signed-off-by: Ryan Lempka --- crates/switchyard-runner/src/algorithm.rs | 61 +++++++++++------------ crates/switchyard-runner/src/config.rs | 14 ++++-- docs/reference/toml_schema.md | 14 ++++-- 3 files changed, 51 insertions(+), 38 deletions(-) diff --git a/crates/switchyard-runner/src/algorithm.rs b/crates/switchyard-runner/src/algorithm.rs index 6e30f76a1..e5db6d477 100644 --- a/crates/switchyard-runner/src/algorithm.rs +++ b/crates/switchyard-runner/src/algorithm.rs @@ -258,17 +258,15 @@ pub enum AlgorithmSpec { #[serde(default)] subagents: Option, }, - /// Picks a routing strategy automatically. Currently resolves to the same - /// behavior as `stage_router`; change the merged match arms below to - /// repoint it at a different algorithm. + /// Picks a routing strategy automatically, preset with recommended knobs. + /// Currently a `stage_router` with `picker = "efficient_first"` and + /// `confidence_threshold = 0.5`; change `build_algorithm`'s `Auto` arm to + /// repoint it at a different algorithm or preset. Auto { - #[serde(flatten)] - tiers: StageTierConfig, - picker: PickerMode, - #[serde(default)] - classifier: Option, - #[serde(default)] - subagents: Option, + /// The capable tier. + capable_target: String, + /// The efficient tier. + efficient_target: String, }, /// A judge picks the tier at each user turn; a stage router runs the turns within it. Composite { @@ -466,9 +464,6 @@ impl AlgorithmSpec { } Self::StageRouter { tiers, subagents, .. - } - | Self::Auto { - tiers, subagents, .. } => { let mut names = vec![ tiers.capable_target.as_str(), @@ -479,6 +474,10 @@ impl AlgorithmSpec { } names } + Self::Auto { + capable_target, + efficient_target, + } => vec![capable_target.as_str(), efficient_target.as_str()], Self::Composite { stage, subagents, .. } => { @@ -516,11 +515,6 @@ impl AlgorithmSpec { classifier, subagents, .. - } - | Self::Auto { - classifier, - subagents, - .. } => { if let Some(classifier) = classifier { names.push(&classifier.target); @@ -1004,19 +998,7 @@ fn build_algorithm( classifier, subagents, .. - } - | AlgorithmSpec::Auto { - tiers, - picker, - classifier, - subagents, - .. } => { - let type_name = if matches!(config, AlgorithmSpec::Auto { .. }) { - "auto" - } else { - "stage_router" - }; let StageTierConfig { capable_target, efficient_target, @@ -1026,7 +1008,7 @@ fn build_algorithm( } = tiers; if matches!(picker, PickerMode::CapableFirst) { tracing::warn!( - "{type_name} route {route_name} uses picker \"capable_first\", which is experimental: published thresholds and routing results all come from \"efficient_first\", so there is no calibrated confidence_threshold for it and no measured accuracy or cost. Use \"efficient_first\" unless you are running your own calibration." + "stage_router route {route_name} uses picker \"capable_first\", which is experimental: published thresholds and routing results all come from \"efficient_first\", so there is no calibrated confidence_threshold for it and no measured accuracy or cost. Use \"efficient_first\" unless you are running your own calibration." ); } let capable = resolve_target_model_id(route_name, capable_target, targets)?; @@ -1049,13 +1031,28 @@ fn build_algorithm( .transpose()?; let algorithm = StageRouter::new(capable, efficient, config).map_err(|error| { AlgorithmConfigError::with_source( - format!("{type_name} route {route_name}: {error}"), + format!("stage_router route {route_name}: {error}"), error, ) })?; let parent: Arc = Arc::new(algorithm); attach_subagent_router(route_name, parent, subagents.as_ref(), targets) } + AlgorithmSpec::Auto { + capable_target, + efficient_target, + } => { + let capable = resolve_target_model_id(route_name, capable_target, targets)?; + let efficient = resolve_target_model_id(route_name, efficient_target, targets)?; + let config = StageRouterConfig::new(PickerMode::EfficientFirst, 0.5); + let algorithm = StageRouter::new(capable, efficient, config).map_err(|error| { + AlgorithmConfigError::with_source( + format!("auto route {route_name}: {error}"), + error, + ) + })?; + Ok(Arc::new(algorithm)) + } AlgorithmSpec::Composite { classifier, stage, diff --git a/crates/switchyard-runner/src/config.rs b/crates/switchyard-runner/src/config.rs index c70ac99bf..5317df264 100644 --- a/crates/switchyard-runner/src/config.rs +++ b/crates/switchyard-runner/src/config.rs @@ -983,10 +983,18 @@ confidence_threshold = 0.5 } #[test] - fn auto_builds_the_same_route_as_stage_router() -> RunnerResult<()> { - let config = stage_config().replace("type = \"stage_router\"", "type = \"auto\""); + fn auto_route_builds_a_stage_router_with_no_extra_fields() -> RunnerResult<()> { + let config = format!( + r#"{VALID_CONFIG} +[routes.auto] +id = "switchyard/auto" +type = "auto" +capable_target = "strong" +efficient_target = "weak" +"# + ); let runner = runner_from_toml(&config)?; - assert!(runner.route("switchyard/stage").is_some()); + assert!(runner.route("switchyard/auto").is_some()); Ok(()) } diff --git a/docs/reference/toml_schema.md b/docs/reference/toml_schema.md index 0b9dfb1f3..a819a6178 100644 --- a/docs/reference/toml_schema.md +++ b/docs/reference/toml_schema.md @@ -255,9 +255,17 @@ optional `handoff_notes` and `classifier` tables and for tuning. ### `auto` Uses Switchyard's recommended default routing strategy instead of one you pick -yourself. See [Stage-Router Routing](../routing_algorithms/stage_router_routing.md) -for a deeper dive on the current default, or the [strategy table](../routing_algorithms/overview.md#choose-a-strategy) -to pick one manually. +yourself: a `stage_router` preset with `picker = "efficient_first"` and +`confidence_threshold = 0.5`, no classifier. See +[Stage-Router Routing](../routing_algorithms/stage_router_routing.md) for a +deeper dive on the current default, or the +[strategy table](../routing_algorithms/overview.md#choose-a-strategy) to pick +one manually. + +| Key | Required | Default | Meaning | +|---|:---:|---|---| +| `capable_target` | Yes | — | Capable tier. | +| `efficient_target` | Yes | — | Efficient tier. | ### `composite` From 866aaefbb11343f07504f0bc9aabbc0986ef5828 Mon Sep 17 00:00:00 2001 From: Ryan Lempka Date: Thu, 10 Sep 2026 10:17:00 -0500 Subject: [PATCH 5/5] docs(getting-started): use auto for the quick start route Signed-off-by: Ryan Lempka --- docs/getting_started.md | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/docs/getting_started.md b/docs/getting_started.md index b6070c865..645ab2545 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -64,7 +64,7 @@ Cargo builds the release binary and installs it into `~/.cargo/bin` by default. The Rust server reads an explicit TOML file. -Create `routes.toml` with an LLM-classifier route: +Create `routes.toml` with an auto route: ```toml schema_version = 1 @@ -84,12 +84,9 @@ llm_client = "openrouter" [routes.smart] id = "switchyard" -type = "llm_classifier" -mode = "capability" -classifier_target = "weak" -strong_target = "strong" -weak_target = "weak" -base_threshold = 0.5 +type = "auto" +capable_target = "strong" +efficient_target = "weak" ``` `format` selects the upstream protocol and must be `openai_chat`, @@ -131,11 +128,12 @@ curl http://localhost:4000/v1/chat/completions \ #### Choose a route type -This guide uses `llm_classifier`, which asks a classifier target whether each -request should use the weak or strong target. The Rust server also supports: +This guide uses `auto`, which routes with Switchyard's recommended default +settings. The Rust server also supports: | Algorithm | Use it when | Config | |---|---|---| +| Auto | You want a recommended default instead of picking a strategy yourself. | `auto` | | [Random](routing_algorithms/random_routing.md) | You need a weighted split for A/B tests or baselines. | `random` | | [LLM classifier](routing_algorithms/llm_classifier_routing.md) | Request content should decide whether to use the weak or strong target. | `llm_classifier` | | [Stage router](routing_algorithms/stage_router_routing.md) | Tool-result and progress signals should select an efficient or capable target. | `stage_router` |