switchyard_runner/
algorithm_factory.rs1use std::collections::BTreeMap;
7use std::sync::Arc;
8
9use libsy::{Algorithm, RuntimeModels, State, StepStream};
10use parking_lot::Mutex;
11use switchyard_protocol::{LlmClientError, ModelId, Request};
12
13use crate::config::{DeploymentConfig, MAX_ELIGIBLE_LANES, TargetBindings};
14use crate::eligibility::{EligibilityPolicy, has_target_requirements};
15use crate::{RunnerError, TargetModel};
16
17pub(crate) type AlgorithmAndModels = (Arc<dyn Algorithm>, RuntimeModels);
18
19pub struct AlgorithmFactory {
21 pub(crate) config: Arc<DeploymentConfig>,
22}
23
24impl AlgorithmFactory {
25 pub fn from_toml(source: &str) -> Result<Self, RunnerError> {
28 let mut config: DeploymentConfig = toml::from_str(source).map_err(|error| {
29 RunnerError::configuration_source(format!("failed to parse TOML: {error}"), error)
30 })?;
31 config.infer_decision_judges();
32 Ok(Self {
33 config: Arc::new(config),
34 })
35 }
36
37 pub fn build(&self) -> Result<BTreeMap<String, AlgorithmAndModels>, RunnerError> {
43 self.config.validate()?;
44 if self.config.requires_target_admission() {
45 return Err(RunnerError::configuration(
46 "target eligibility requires Runner admission; use Runner instead of AlgorithmFactory",
47 ));
48 }
49 self.config
50 .build_algorithms()
51 .map(|entry| {
52 let setup = entry?;
53 Ok((setup.config.id.to_string(), (setup.algorithm, setup.models)))
54 })
55 .collect()
56 }
57
58 pub fn build_configured(&self) -> Result<BTreeMap<String, ConfiguredAlgorithm>, RunnerError> {
63 self.config.validate()?;
64 self.config.validate_target_approvals()?;
65 self.config
66 .build_algorithms()
67 .map(|entry| {
68 let mut setup = entry?;
69 self.config.validate_decision_models(setup.route_name)?;
70 let policy = self.config.eligibility_policy(setup.route_name);
71 if policy.is_some() {
72 setup.models = setup.models.with_target_restriction();
73 }
74 Ok((
75 setup.config.id.to_string(),
76 ConfiguredAlgorithm {
77 target_models: self.config.route_target_models(setup.route_name)?,
78 standard: AlgorithmSetup {
79 algorithm: setup.algorithm,
80 models: Arc::new(setup.models),
81 },
82 eligibility: policy.map(|policy| ConfiguredEligibility {
83 policy,
84 deployment: Arc::clone(&self.config),
85 route_name: setup.route_name.to_owned(),
86 setups: Mutex::default(),
87 }),
88 },
89 ))
90 })
91 .collect()
92 }
93}
94
95#[derive(Clone)]
96struct AlgorithmSetup {
97 algorithm: Arc<dyn Algorithm>,
98 models: Arc<RuntimeModels>,
99}
100
101struct ConfiguredEligibility {
102 policy: EligibilityPolicy,
103 deployment: Arc<DeploymentConfig>,
104 route_name: String,
105 setups: Mutex<BTreeMap<TargetBindings, AlgorithmSetup>>,
106}
107
108impl ConfiguredEligibility {
109 fn select(
110 &self,
111 request: &Request,
112 standard: &AlgorithmSetup,
113 ) -> Result<AlgorithmSetup, RunnerError> {
114 let requirements = self.policy.admit(request)?;
115 let Some(bindings) = self
116 .deployment
117 .select_eligible_targets(&self.route_name, requirements.required_labels())?
118 else {
119 return Ok(standard.clone());
120 };
121 let mut setups = self.setups.lock();
123 if let Some(setup) = setups.get(&bindings) {
124 return Ok(setup.clone());
125 }
126 if setups.len() >= MAX_ELIGIBLE_LANES {
127 return Err(LlmClientError::TemporarilyUnavailable.into());
128 }
129 let (algorithm, models) = self
130 .deployment
131 .build_bound_algorithm(&self.route_name, &bindings)?;
132 let setup = AlgorithmSetup {
133 algorithm,
134 models: Arc::new(models),
135 };
136 setups.insert(bindings, setup.clone());
137 Ok(setup)
138 }
139}
140
141pub struct ConfiguredAlgorithm {
143 standard: AlgorithmSetup,
144 eligibility: Option<ConfiguredEligibility>,
145 target_models: BTreeMap<ModelId, TargetModel>,
146}
147
148impl ConfiguredAlgorithm {
149 pub fn target_models(&self) -> &BTreeMap<ModelId, TargetModel> {
153 &self.target_models
154 }
155
156 pub fn run_stream(&self, request: Request, state: State) -> Result<StepStream, RunnerError> {
161 let setup = match &self.eligibility {
162 Some(eligibility) => eligibility.select(&request, &self.standard)?,
163 None if has_target_requirements(&request) => {
164 return Err(LlmClientError::InvalidRequest {
165 message: "route does not accept target requirements".into(),
166 }
167 .into());
168 }
169 None => self.standard.clone(),
170 };
171 Ok(setup.algorithm.run_stream(request, setup.models, state))
172 }
173}