#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum ModelTier {
Premium = 3,
Standard = 2,
Economy = 1,
Local = 0,
}
impl ModelTier {
pub fn downgrade(&self) -> Option<ModelTier> {
match self {
ModelTier::Premium => Some(ModelTier::Standard),
ModelTier::Standard => Some(ModelTier::Economy),
ModelTier::Economy => Some(ModelTier::Local),
ModelTier::Local => None,
}
}
pub fn cost_per_1k_output(&self) -> f64 {
match self {
ModelTier::Premium => 0.075,
ModelTier::Standard => 0.015,
ModelTier::Economy => 0.00125,
ModelTier::Local => 0.0,
}
}
pub fn cost_per_1k_input(&self) -> f64 {
match self {
ModelTier::Premium => 0.015,
ModelTier::Standard => 0.003,
ModelTier::Economy => 0.00025,
ModelTier::Local => 0.0,
}
}
pub fn estimate_cost(&self, input_tokens: u32, output_tokens: u32) -> f64 {
(input_tokens as f64 / 1000.0) * self.cost_per_1k_input()
+ (output_tokens as f64 / 1000.0) * self.cost_per_1k_output()
}
}
#[derive(Debug, Clone)]
pub struct ModelCascade {
current: ModelTier,
pressure_threshold: f64,
total_budget: f64,
}
impl ModelCascade {
pub fn new(tier: ModelTier, pressure_threshold: f64, total_budget: f64) -> Result<Self, crate::error::BudgetError> {
if !(0.0..=1.0).contains(&pressure_threshold) {
return Err(crate::error::BudgetError::InvalidConfig(
format!("pressure_threshold must be in [0.0, 1.0], got {pressure_threshold}"),
));
}
if total_budget <= 0.0 {
return Err(crate::error::BudgetError::InvalidConfig(
"total_budget must be positive".into(),
));
}
Ok(Self { current: tier, pressure_threshold, total_budget })
}
pub fn current(&self) -> ModelTier {
self.current
}
pub(crate) fn set_total_budget(&mut self, total: f64) {
if total > 0.0 {
self.total_budget = total;
}
}
pub fn evaluate(&mut self, remaining: f64) -> Option<ModelTier> {
let fraction_remaining = remaining / self.total_budget;
if fraction_remaining <= self.pressure_threshold {
if let Some(next) = self.current.downgrade() {
self.current = next;
return Some(self.current);
}
}
None
}
pub fn force_downgrade(&mut self) -> Option<ModelTier> {
if let Some(next) = self.current.downgrade() {
self.current = next;
Some(self.current)
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_model_tier_ordering() {
assert!(ModelTier::Premium > ModelTier::Standard);
assert!(ModelTier::Standard > ModelTier::Economy);
assert!(ModelTier::Economy > ModelTier::Local);
}
#[test]
fn test_downgrade_premium_to_standard() {
assert_eq!(ModelTier::Premium.downgrade(), Some(ModelTier::Standard));
}
#[test]
fn test_downgrade_standard_to_economy() {
assert_eq!(ModelTier::Standard.downgrade(), Some(ModelTier::Economy));
}
#[test]
fn test_downgrade_economy_to_local() {
assert_eq!(ModelTier::Economy.downgrade(), Some(ModelTier::Local));
}
#[test]
fn test_downgrade_local_returns_none() {
assert_eq!(ModelTier::Local.downgrade(), None);
}
#[test]
fn test_cost_per_1k_decreases_by_tier() {
assert!(ModelTier::Premium.cost_per_1k_output() > ModelTier::Standard.cost_per_1k_output());
assert!(ModelTier::Standard.cost_per_1k_output() > ModelTier::Economy.cost_per_1k_output());
assert_eq!(ModelTier::Local.cost_per_1k_output(), 0.0);
}
#[test]
fn test_estimate_cost_local_is_zero() {
assert_eq!(ModelTier::Local.estimate_cost(10000, 5000), 0.0);
}
#[test]
fn test_estimate_cost_premium_nonzero() {
let cost = ModelTier::Premium.estimate_cost(1000, 1000);
assert!(cost > 0.0);
}
#[test]
fn test_cascade_new_invalid_threshold() {
let result = ModelCascade::new(ModelTier::Premium, 1.5, 100.0);
assert!(result.is_err());
}
#[test]
fn test_cascade_new_invalid_budget() {
let result = ModelCascade::new(ModelTier::Premium, 0.2, 0.0);
assert!(result.is_err());
}
#[test]
fn test_cascade_evaluate_triggers_downgrade_when_below_threshold() {
let mut cascade = ModelCascade::new(ModelTier::Premium, 0.2, 100.0).expect("new");
let result = cascade.evaluate(15.0);
assert_eq!(result, Some(ModelTier::Standard));
assert_eq!(cascade.current(), ModelTier::Standard);
}
#[test]
fn test_cascade_evaluate_no_downgrade_when_above_threshold() {
let mut cascade = ModelCascade::new(ModelTier::Premium, 0.2, 100.0).expect("new");
let result = cascade.evaluate(50.0);
assert_eq!(result, None);
assert_eq!(cascade.current(), ModelTier::Premium);
}
#[test]
fn test_cascade_local_cannot_downgrade() {
let mut cascade = ModelCascade::new(ModelTier::Local, 0.5, 100.0).expect("new");
let result = cascade.evaluate(10.0); assert_eq!(result, None); }
#[test]
fn test_cascade_force_downgrade_chain() {
let mut cascade = ModelCascade::new(ModelTier::Premium, 0.1, 100.0).expect("new");
assert_eq!(cascade.force_downgrade(), Some(ModelTier::Standard));
assert_eq!(cascade.force_downgrade(), Some(ModelTier::Economy));
assert_eq!(cascade.force_downgrade(), Some(ModelTier::Local));
assert_eq!(cascade.force_downgrade(), None);
}
#[test]
fn test_model_tier_serde_roundtrip() {
let tier = ModelTier::Standard;
let json = serde_json::to_string(&tier).expect("serialize");
let restored: ModelTier = serde_json::from_str(&json).expect("deserialize");
assert_eq!(restored, tier);
}
}