switchyard_runner/
runner.rs1use std::collections::BTreeMap;
7use std::path::Path;
8use std::sync::Arc;
9use std::time::Duration;
10
11use libsy::RoutingOutcome;
12use serde_json::Value;
13use switchyard_llm_client::TranslatingLlmClient;
14use switchyard_protocol::{ModelId, WireFormat};
15
16use crate::config;
17use crate::{ModelCapabilities, Route, RunnerConfig, RunnerError};
18
19pub struct Runner {
21 routes: Vec<(ModelId, Route)>,
22 fallback_base_url: Option<String>,
23 provider_api_keys: Vec<String>,
24 model_properties_probes: BTreeMap<ModelId, ModelPropertiesProbe>,
25}
26
27pub struct ModelInfo<'a> {
29 pub id: &'a ModelId,
30 pub algorithm: &'a str,
31 pub capabilities: ModelCapabilities,
32}
33
34pub struct DecisionDescription {
36 pub selected: DecisionTarget,
37 pub fallbacks: Vec<DecisionTarget>,
38 pub metadata: Option<libsy::OutcomeMetadata>,
40}
41
42#[derive(Clone)]
44pub struct DecisionTarget {
45 pub target: String,
46 pub model: ModelId,
47 pub format: WireFormat,
48 pub base_url: String,
49 pub extra_body: BTreeMap<String, Value>,
50 pub reasoning_effort: Option<String>,
52}
53
54pub(crate) struct ModelPropertiesProbe {
55 pub(crate) model: ModelId,
56 pub(crate) format: WireFormat,
57 pub(crate) client: Arc<TranslatingLlmClient>,
58}
59
60impl Runner {
61 pub fn load(path: impl AsRef<Path>) -> Result<Self, RunnerError> {
63 config::load_runner(path)
64 }
65
66 pub fn from_toml(source: &str) -> Result<Self, RunnerError> {
70 config::runner_from_toml(source)
71 }
72
73 pub fn from_config(config: RunnerConfig) -> Result<Self, RunnerError> {
75 Arc::new(config).build()
76 }
77
78 pub fn new(routes: Vec<(ModelId, Route)>) -> Self {
81 Self {
82 routes,
83 fallback_base_url: None,
84 provider_api_keys: Vec::new(),
85 model_properties_probes: BTreeMap::new(),
86 }
87 }
88
89 pub fn with_provider_api_keys(mut self, keys: Vec<String>) -> Self {
92 self.provider_api_keys = keys;
93 self
94 }
95
96 pub fn provider_api_keys(&self) -> &[String] {
98 &self.provider_api_keys
99 }
100
101 pub(crate) fn with_fallback_url(mut self, fallback_base_url: Option<String>) -> Self {
102 self.fallback_base_url = fallback_base_url;
103 self
104 }
105
106 pub(crate) fn with_model_properties_probes(
107 mut self,
108 probes: BTreeMap<ModelId, ModelPropertiesProbe>,
109 ) -> Self {
110 self.model_properties_probes = probes;
111 self
112 }
113
114 pub fn route(&self, model: &str) -> Option<&Route> {
116 self.routes
117 .iter()
118 .find(|(id, _)| id.as_str() == model)
119 .map(|(_, route)| route)
120 }
121
122 pub fn models(&self) -> impl Iterator<Item = ModelInfo<'_>> {
124 self.routes.iter().map(|(id, route)| ModelInfo {
125 id,
126 algorithm: route.algorithm_name(),
127 capabilities: route.capabilities(),
128 })
129 }
130
131 pub async fn model_properties(&self, model: &str, timeout: Duration) -> Option<Value> {
133 let probe = self.model_properties_probes.get(model)?;
134 probe
135 .client
136 .get_model_properties(&probe.model, probe.format, timeout)
137 .await
138 .ok()
139 }
140
141 pub fn fallback_base_url(&self) -> Option<&str> {
143 self.fallback_base_url.as_deref()
144 }
145
146 pub fn describe_decision(
148 &self,
149 model: &ModelId,
150 outcome: &RoutingOutcome,
151 ) -> Option<DecisionDescription> {
152 let route = self.route(model.as_str())?;
153 let resolve = |selected: &ModelId| {
154 let mut target = route.decision_target(selected)?;
155 let mut url = reqwest::Url::parse(&target.base_url).ok()?;
156 let query: Vec<_> = url
157 .query_pairs()
158 .filter(|(name, _)| !matches!(name.as_ref(), "key" | "api_key"))
159 .map(|(name, value)| (name.into_owned(), value.into_owned()))
160 .collect();
161 if query.len() != url.query_pairs().count() {
162 url.set_query(None);
163 if !query.is_empty() {
164 url.query_pairs_mut().extend_pairs(query);
165 }
166 target.base_url = url.into();
168 }
169 Some(target)
170 };
171 let mut model_ids = outcome.selected_model_ids.iter();
172 Some(DecisionDescription {
173 selected: resolve(model_ids.next()?)?,
174 fallbacks: model_ids.map(resolve).collect::<Option<Vec<_>>>()?,
175 metadata: outcome.metadata.clone(),
176 })
177 }
178}