use crate::ThinkingLevel;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
#[serde(rename_all = "lowercase")]
pub enum RouterTier {
High,
Medium,
Low,
}
impl RouterTier {
pub fn rank(&self) -> u8 {
match self {
Self::Low => 0,
Self::Medium => 1,
Self::High => 2,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum RouterPhase {
#[default]
Implementation,
Planning,
Lightweight,
}
impl RouterPhase {
pub fn weight(&self) -> f64 {
match self {
Self::Planning => 0.8,
Self::Implementation => 0.5,
Self::Lightweight => 0.2,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum DecisionMethod {
Heuristic,
LlmClassifier,
PinOverride,
ContextUpgrade,
BudgetDowngrade,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct RoutingScore(pub f64);
impl RoutingScore {
pub fn new(raw: f64) -> Self {
Self(raw.clamp(0.0, 1.0))
}
pub fn to_tier(&self, high_threshold: f64, low_threshold: f64) -> RouterTier {
if self.0 >= high_threshold {
RouterTier::High
} else if self.0 <= low_threshold {
RouterTier::Low
} else {
RouterTier::Medium
}
}
pub fn needs_refinement(&self, margin: f64) -> bool {
let near_high = (self.0 - 0.65).abs() < margin;
let near_low = (self.0 - 0.35).abs() < margin;
near_high || near_low
}
pub fn raw(&self) -> f64 {
self.0
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoutingDecision {
pub profile: String,
pub tier: RouterTier,
pub phase: RouterPhase,
pub target_provider: String,
pub target_model_id: String,
pub target_label: String,
pub reasoning: String,
pub thinking: ThinkingLevel,
pub timestamp: i64,
pub score: f64,
pub is_fallback: bool,
pub is_context_triggered: bool,
pub is_budget_forced: bool,
#[serde(default)]
pub is_vision_triggered: bool,
#[serde(default)]
pub vision_images: usize,
pub decision_method: DecisionMethod,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoutedTierConfig {
pub model: String,
#[serde(default)]
pub thinking: Option<ThinkingLevel>,
#[serde(default)]
pub fallbacks: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RouterProfile {
pub high: RoutedTierConfig,
pub medium: RoutedTierConfig,
pub low: RoutedTierConfig,
}
impl RouterProfile {
pub fn tier_config(&self, tier: RouterTier) -> &RoutedTierConfig {
match tier {
RouterTier::High => &self.high,
RouterTier::Medium => &self.medium,
RouterTier::Low => &self.low,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScoringWeights {
#[serde(default = "default_structural")]
pub structural: f64,
#[serde(default = "default_behavioral")]
pub behavioral: f64,
#[serde(default = "default_context")]
pub context_budget: f64,
#[serde(default = "default_vision")]
pub vision: f64,
}
fn default_structural() -> f64 {
0.35
}
fn default_behavioral() -> f64 {
0.35
}
fn default_context() -> f64 {
0.20
}
fn default_vision() -> f64 {
0.10
}
impl Default for ScoringWeights {
fn default() -> Self {
Self {
structural: default_structural(),
behavioral: default_behavioral(),
context_budget: default_context(),
vision: default_vision(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RouterConfig {
#[serde(default = "default_profile_name")]
pub default_profile: String,
#[serde(default)]
pub classifier_model: Option<String>,
#[serde(default)]
pub context_upgrade_threshold: Option<usize>,
#[serde(default)]
pub max_session_budget: Option<f64>,
#[serde(default)]
pub profiles: HashMap<String, RouterProfile>,
#[serde(default)]
pub weights: ScoringWeights,
}
fn default_profile_name() -> String {
"auto".to_string()
}
impl Default for RouterConfig {
fn default() -> Self {
Self {
default_profile: default_profile_name(),
classifier_model: None,
context_upgrade_threshold: None,
max_session_budget: None,
profiles: HashMap::new(),
weights: ScoringWeights::default(),
}
}
}
impl RouterConfig {
pub fn new(
default_profile: String,
classifier_model: Option<String>,
context_upgrade_threshold: Option<usize>,
max_session_budget: Option<f64>,
profiles: HashMap<String, RouterProfile>,
weights: ScoringWeights,
) -> Self {
Self {
default_profile,
classifier_model,
context_upgrade_threshold,
max_session_budget,
profiles,
weights,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct RouterState {
#[serde(default)]
pub accumulated_cost: f64,
#[serde(default)]
pub decision_history: Vec<RoutingDecision>,
}