1use crate::ThinkingLevel;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
11#[serde(rename_all = "lowercase")]
12pub enum RouterTier {
13 High,
15 Medium,
17 Low,
19}
20
21impl RouterTier {
22 pub fn rank(&self) -> u8 {
24 match self {
25 Self::Low => 0,
26 Self::Medium => 1,
27 Self::High => 2,
28 }
29 }
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
34#[serde(rename_all = "lowercase")]
35pub enum RouterPhase {
36 #[default]
38 Implementation,
39 Planning,
41 Lightweight,
43}
44
45impl RouterPhase {
46 pub fn weight(&self) -> f64 {
48 match self {
49 Self::Planning => 0.8,
50 Self::Implementation => 0.5,
51 Self::Lightweight => 0.2,
52 }
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
60#[serde(rename_all = "camelCase")]
61pub enum DecisionMethod {
62 Heuristic,
64 LlmClassifier,
66 PinOverride,
68 RuleMatch,
70 ScenarioMatch,
72 ContextUpgrade,
74 BudgetDowngrade,
76}
77
78#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
82pub struct RoutingScore(pub f64);
83
84impl RoutingScore {
85 pub fn new(raw: f64) -> Self {
87 Self(raw.clamp(0.0, 1.0))
88 }
89
90 pub fn to_tier(&self, high_threshold: f64, low_threshold: f64) -> RouterTier {
92 if self.0 >= high_threshold {
93 RouterTier::High
94 } else if self.0 <= low_threshold {
95 RouterTier::Low
96 } else {
97 RouterTier::Medium
98 }
99 }
100
101 pub fn needs_refinement(&self, margin: f64) -> bool {
104 let near_high = (self.0 - 0.65).abs() < margin;
105 let near_low = (self.0 - 0.35).abs() < margin;
106 near_high || near_low
107 }
108
109 pub fn raw(&self) -> f64 {
111 self.0
112 }
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct RoutingDecision {
120 pub profile: String,
122 pub tier: RouterTier,
124 pub phase: RouterPhase,
126 pub target_provider: String,
128 pub target_model_id: String,
130 pub target_label: String,
132 pub reasoning: String,
134 pub thinking: ThinkingLevel,
136 pub timestamp: i64,
138 pub score: f64,
140 pub is_fallback: bool,
142 pub is_context_triggered: bool,
144 pub is_budget_forced: bool,
146 #[serde(default)]
148 pub is_vision_triggered: bool,
149 #[serde(default)]
151 pub vision_images: usize,
152 pub decision_method: DecisionMethod,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct RoutedTierConfig {
161 pub model: String,
163 #[serde(default)]
165 pub thinking: Option<ThinkingLevel>,
166 #[serde(default)]
168 pub fallbacks: Vec<String>,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct RouterProfile {
174 pub high: RoutedTierConfig,
176 pub medium: RoutedTierConfig,
178 pub low: RoutedTierConfig,
180}
181
182impl RouterProfile {
183 pub fn tier_config(&self, tier: RouterTier) -> &RoutedTierConfig {
185 match tier {
186 RouterTier::High => &self.high,
187 RouterTier::Medium => &self.medium,
188 RouterTier::Low => &self.low,
189 }
190 }
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct ScoringWeights {
198 #[serde(default = "default_structural")]
200 pub structural: f64,
201 #[serde(default = "default_behavioral")]
203 pub behavioral: f64,
204 #[serde(default = "default_context")]
206 pub context_budget: f64,
207 #[serde(default = "default_vision")]
209 pub vision: f64,
210 #[serde(default = "default_message")]
212 pub message: f64,
213}
214
215fn default_structural() -> f64 {
216 0.25
217}
218fn default_behavioral() -> f64 {
219 0.20
220}
221fn default_context() -> f64 {
222 0.15
223}
224fn default_vision() -> f64 {
225 0.10
226}
227fn default_message() -> f64 {
228 0.30
229}
230
231impl Default for ScoringWeights {
232 fn default() -> Self {
233 Self {
234 structural: default_structural(),
235 behavioral: default_behavioral(),
236 context_budget: default_context(),
237 vision: default_vision(),
238 message: default_message(),
239 }
240 }
241}
242
243#[derive(Debug, Clone, Serialize, Deserialize)]
247pub struct RouterConfig {
248 #[serde(default = "default_profile_name")]
250 pub default_profile: String,
251 #[serde(default)]
253 pub classifier_model: Option<String>,
254 #[serde(default)]
256 pub context_upgrade_threshold: Option<usize>,
257 #[serde(default)]
259 pub max_session_budget: Option<f64>,
260 #[serde(default)]
262 pub profiles: HashMap<String, RouterProfile>,
263 #[serde(default)]
265 pub weights: ScoringWeights,
266 #[serde(default)]
268 pub pin_tier: Option<RouterTier>,
269 #[serde(default)]
271 pub phase_bias: Option<f64>,
272}
273
274fn default_profile_name() -> String {
275 "auto".to_string()
276}
277
278impl Default for RouterConfig {
279 fn default() -> Self {
280 Self {
281 default_profile: default_profile_name(),
282 classifier_model: None,
283 context_upgrade_threshold: None,
284 max_session_budget: None,
285 profiles: HashMap::new(),
286 weights: ScoringWeights::default(),
287 pin_tier: None,
288 phase_bias: None,
289 }
290 }
291}
292
293impl RouterConfig {
294 pub fn new(
296 default_profile: String,
297 classifier_model: Option<String>,
298 context_upgrade_threshold: Option<usize>,
299 max_session_budget: Option<f64>,
300 profiles: HashMap<String, RouterProfile>,
301 weights: ScoringWeights,
302 ) -> Self {
303 Self {
304 default_profile,
305 classifier_model,
306 context_upgrade_threshold,
307 max_session_budget,
308 profiles,
309 weights,
310 pin_tier: None,
311 phase_bias: None,
312 }
313 }
314
315 #[allow(clippy::too_many_arguments)]
317 pub fn with_pinning(
318 default_profile: String,
319 classifier_model: Option<String>,
320 context_upgrade_threshold: Option<usize>,
321 max_session_budget: Option<f64>,
322 profiles: HashMap<String, RouterProfile>,
323 weights: ScoringWeights,
324 pin_tier: Option<RouterTier>,
325 phase_bias: Option<f64>,
326 ) -> Self {
327 Self {
328 default_profile,
329 classifier_model,
330 context_upgrade_threshold,
331 max_session_budget,
332 profiles,
333 weights,
334 pin_tier,
335 phase_bias,
336 }
337 }
338}
339
340#[derive(Debug, Clone, Default, Serialize, Deserialize)]
342pub struct RouterState {
343 #[serde(default)]
345 pub accumulated_cost: f64,
346 #[serde(default)]
348 pub decision_history: Vec<RoutingDecision>,
349}