From e7739247e4e6792a72a08fffcf8cff6c9806acc8 Mon Sep 17 00:00:00 2001 From: Alex Fournier Date: Sun, 4 Oct 2026 20:14:58 -0700 Subject: [PATCH 1/2] feat(runner): build algorithms with runtime models Signed-off-by: Alex Fournier --- crates/switchyard-runner/src/algorithm.rs | 60 ++++++++++++++++++----- crates/switchyard-runner/src/config.rs | 39 ++------------- crates/switchyard-runner/tests/route.rs | 29 ++++++++--- 3 files changed, 75 insertions(+), 53 deletions(-) diff --git a/crates/switchyard-runner/src/algorithm.rs b/crates/switchyard-runner/src/algorithm.rs index a7e40e751..e2d5501eb 100644 --- a/crates/switchyard-runner/src/algorithm.rs +++ b/crates/switchyard-runner/src/algorithm.rs @@ -15,8 +15,8 @@ use libsy::{ CustomClassifierConfig, CustomClassifierPolicy, DecisionJudgeConfig, EscalationJudgeConfig, GateTrigger, HandoffNoteConfig, LlmCapabilityConfig, LlmClassifierConfig, LlmFallback, LlmTaskClassifier, Noop, Passthrough, PickerMode, PlanExecute, PlanExecuteConfig, Random, - StageRouter, StageRouterConfig, SubagentRouter, SubagentRouterConfig, TaskClassifierConfig, - ToolSemantics, + RuntimeModels, StageRouter, StageRouterConfig, SubagentRouter, SubagentRouterConfig, + TaskClassifierConfig, ToolSemantics, }; use serde::Deserialize; use serde_json::Value; @@ -701,10 +701,7 @@ impl AlgorithmSpec { } /// Target names grouped as the runtime [`Driver`](libsy::Driver) expects them. - pub(crate) fn runtime_model_names( - &self, - route_name: &str, - ) -> AlgorithmResult { + fn runtime_model_names(&self, route_name: &str) -> AlgorithmResult { let parent = match self { Self::Noop { .. } => HashMap::new(), Self::Random { targets, .. } | Self::PrefillRouter { targets, .. } => { @@ -820,22 +817,44 @@ impl AlgorithmSpec { } } - /// Builds this algorithm after resolving configured target names. + /// Builds only the routing algorithm from this specification. + /// + /// Embedding hosts should normally use [`Self::build_with_runtime_models`], which also + /// resolves the model groups supplied to the algorithm at execution time. pub fn build( &self, - context: &str, + route_name: &str, targets: &BTreeMap, ) -> AlgorithmResult> { - build_algorithm(context, self, targets) + build_algorithm(route_name, self, targets) + } + + /// Builds the routing algorithm and its matching runtime model groups. + /// + /// `route_name` identifies the route in validation errors. `targets` maps configured target + /// names to the model IDs served by the embedding host. + pub fn build_with_runtime_models( + &self, + route_name: &str, + targets: &BTreeMap, + ) -> AlgorithmResult<(Arc, RuntimeModels)> { + let algorithm = self.build(route_name, targets)?; + let names = self.runtime_model_names(route_name)?; + let mut models = + RuntimeModels::new(resolve_runtime_models(route_name, names.parent, targets)?); + if let Some(subagent) = names.subagent { + models = models.with_subagent(resolve_runtime_models(route_name, subagent, targets)?); + } + Ok((algorithm, models)) } } /// One route's target names, grouped by category and by routing scope. -pub(crate) struct RuntimeModelNames { +struct RuntimeModelNames { /// Groups the algorithm itself routes over. - pub(crate) parent: HashMap>, + parent: HashMap>, /// Groups delegated sub-agent work routes over, when the route has a `subagents` table. - pub(crate) subagent: Option>>, + subagent: Option>>, } fn category_models( @@ -1595,3 +1614,20 @@ fn resolve_target_model_id( )) }) } + +fn resolve_runtime_models( + route_name: &str, + names: HashMap>, + targets: &BTreeMap, +) -> AlgorithmResult>> { + names + .into_iter() + .map(|(category, names)| { + let models = names + .into_iter() + .map(|name| resolve_target_model_id(route_name, &name, targets)) + .collect::>>()?; + Ok((category, models)) + }) + .collect() +} diff --git a/crates/switchyard-runner/src/config.rs b/crates/switchyard-runner/src/config.rs index 2c85a97ca..58946374b 100644 --- a/crates/switchyard-runner/src/config.rs +++ b/crates/switchyard-runner/src/config.rs @@ -9,7 +9,6 @@ use std::path::Path; use std::sync::Arc; use std::time::Duration; -use libsy::RuntimeModels; use serde::de::DeserializeOwned; use serde::{Deserialize, Deserializer}; use serde_json::Value; @@ -17,7 +16,7 @@ use switchyard_llm_client::{ AuxiliaryOperation, Backend, ClientRouter, DEFAULT_MAX_RETRIES, HttpBackendConfig, ModelConfig, SystemOneClient, TranslatingLlmClient, }; -use switchyard_protocol::{Category, ModelId, RoutedDecisionClient, RoutedLlmClient, WireFormat}; +use switchyard_protocol::{ModelId, RoutedDecisionClient, RoutedLlmClient, WireFormat}; use crate::{ AlgorithmSpec, AuxiliaryTarget, CallerAuthKind, DecisionTarget, ModelCapabilities, Route, @@ -265,9 +264,9 @@ impl DeploymentConfig { "route {route_name} context_window must be greater than zero" ))); } - let algorithm = config + let (algorithm, models) = config .algorithm - .build(route_name, &targets) + .build_with_runtime_models(route_name, &targets) .map_err(|error| RunnerError::configuration_source(error.to_string(), error))?; let (route_clients, caller_auth) = self.build_route_clients(route_name, config, &clients, &decision_clients)?; @@ -280,14 +279,6 @@ impl DeploymentConfig { .into_iter() .filter_map(|name| self.decision_target(name)) .collect(); - let names = config - .algorithm - .runtime_model_names(route_name) - .map_err(|error| RunnerError::configuration_source(error.to_string(), error))?; - let mut models = RuntimeModels::new(resolve_category_models(names.parent, &targets)?); - if let Some(subagent) = names.subagent { - models = models.with_subagent(resolve_category_models(subagent, &targets)?); - } let route = Route::new( algorithm, route_clients, @@ -751,29 +742,6 @@ impl ClientFormat { } } -/// Resolves one scope's configured target names to the models the driver serves. -fn resolve_category_models( - names: HashMap>, - targets: &BTreeMap, -) -> RunnerResult>> { - names - .into_iter() - .map(|(category, names)| { - let models = names - .into_iter() - .map(|name| { - targets.get(&name).cloned().ok_or_else(|| { - RunnerError::configuration(format!( - "route references unknown target {name}" - )) - }) - }) - .collect::>>()?; - Ok((category, models)) - }) - .collect() -} - fn build_backend( client_name: &str, config: &LlmClientConfig, @@ -917,6 +885,7 @@ bogus = true mod deployment_tests { use super::*; use serde_json::json; + use switchyard_protocol::Category; const VALID_CONFIG: &str = r#" schema_version = 1 diff --git a/crates/switchyard-runner/tests/route.rs b/crates/switchyard-runner/tests/route.rs index 68bba5667..add99312b 100644 --- a/crates/switchyard-runner/tests/route.rs +++ b/crates/switchyard-runner/tests/route.rs @@ -7,11 +7,10 @@ use std::sync::{Arc, Mutex}; use async_trait::async_trait; use futures_util::StreamExt; -use libsy::RuntimeModels; use switchyard_llm_client::{ClientRouter, RunObservation}; use switchyard_protocol::{ - Category, LlmClientError, LlmResponse, ModelId, Request, Response, RoutedLlmClient, - text_request, text_response, + LlmClientError, LlmResponse, ModelId, Request, Response, RoutedLlmClient, text_request, + text_response, }; use switchyard_runner::{AlgorithmSpec, ModelCapabilities, Route}; @@ -40,8 +39,8 @@ fn plugin_route(client: Arc) -> Route { "semantic-target".to_string(), ModelId::from("semantic-target"), )]); - let algorithm = spec - .build("switchyard", &targets) + let (algorithm, models) = spec + .build_with_runtime_models("switchyard", &targets) .expect("identity target map should build"); let clients = ClientRouter::new( BTreeMap::from([(ModelId::from("semantic-target"), client)]) @@ -56,10 +55,28 @@ fn plugin_route(client: Arc) -> Route { None, None, Vec::new(), - RuntimeModels::new([(Category::Any, vec![ModelId::from("semantic-target")])].into()), + models, ) } +#[test] +fn runtime_model_builder_rejects_an_unknown_target() { + // Embedding hosts do not get DeploymentConfig's target prevalidation. + let spec = AlgorithmSpec::Passthrough { + target: "missing".to_string(), + subagents: None, + }; + let error = match spec.build_with_runtime_models("embedded", &BTreeMap::new()) { + Ok(_) => panic!("unknown target should fail"), + Err(error) => error, + }; + + assert_eq!( + error.to_string(), + "route embedded references unknown target missing" + ); +} + #[tokio::test] async fn plugin_shaped_route_executes_without_runner_model_or_toml() { let route = plugin_route(Arc::new(StubClient)); From 16dc65533e108f70a7d8826a1866f5ab55bf5c6f Mon Sep 17 00:00:00 2001 From: Alex Fournier Date: Fri, 9 Oct 2026 11:45:29 -0700 Subject: [PATCH 2/2] docs(runner): shorten algorithm builder comments Signed-off-by: Alex Fournier --- crates/switchyard-runner/src/algorithm.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/crates/switchyard-runner/src/algorithm.rs b/crates/switchyard-runner/src/algorithm.rs index e2d5501eb..29e5a3c8a 100644 --- a/crates/switchyard-runner/src/algorithm.rs +++ b/crates/switchyard-runner/src/algorithm.rs @@ -817,10 +817,7 @@ impl AlgorithmSpec { } } - /// Builds only the routing algorithm from this specification. - /// - /// Embedding hosts should normally use [`Self::build_with_runtime_models`], which also - /// resolves the model groups supplied to the algorithm at execution time. + /// Builds this algorithm after resolving configured target names. pub fn build( &self, route_name: &str, @@ -829,10 +826,7 @@ impl AlgorithmSpec { build_algorithm(route_name, self, targets) } - /// Builds the routing algorithm and its matching runtime model groups. - /// - /// `route_name` identifies the route in validation errors. `targets` maps configured target - /// names to the model IDs served by the embedding host. + /// Resolves target names to host model IDs and builds the algorithm and runtime groups. pub fn build_with_runtime_models( &self, route_name: &str,