use std::collections::HashMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use async_trait::async_trait;
use clawft_types::routing::{
AuthContext, EscalationConfig, RoutingConfig,
TierSelectionStrategy, UserPermissions,
};
use super::traits::{
BudgetResult, ChatRequest, CostTrackable, ModelRouter, RateLimitable,
ResponseOutcome, RoutingDecision, TaskProfile,
};
#[derive(Debug, Clone)]
pub struct ModelTier {
pub name: String,
pub models: Vec<String>,
pub complexity_range: [f32; 2],
pub cost_per_1k_tokens: f64,
pub max_context_tokens: usize,
pub ordinal: usize,
}
impl ModelTier {
pub fn matches_complexity(&self, complexity: f32) -> bool {
complexity >= self.complexity_range[0] && complexity <= self.complexity_range[1]
}
}
pub struct NoopCostTracker;
impl CostTrackable for NoopCostTracker {
fn check_budget(
&self,
_sender_id: &str,
_estimated_cost: f64,
_daily_limit: f64,
_monthly_limit: f64,
) -> BudgetResult {
BudgetResult::Approved
}
fn record_estimated(&self, _sender_id: &str, _estimated_cost: f64) {}
fn record_actual(&self, _sender_id: &str, _estimated_cost: f64, _actual_cost: f64) {}
}
pub struct NoopRateLimiter;
impl RateLimitable for NoopRateLimiter {
fn check(&self, _sender_id: &str, _limit: u32) -> bool {
true
}
}
struct TierSelection<'a> {
tier: &'a ModelTier,
escalated: bool,
}
struct TierBudgetResult<'a> {
tier: &'a ModelTier,
constrained: bool,
}
pub struct TieredRouter {
tiers: Vec<ModelTier>,
tier_index: HashMap<String, usize>,
selection_strategy: TierSelectionStrategy,
round_robin_counters: Vec<AtomicUsize>,
escalation_config: EscalationConfig,
fallback_model: Option<String>,
cost_tracker: Option<Arc<dyn CostTrackable>>,
rate_limiter: Option<Arc<dyn RateLimitable>>,
}
impl TieredRouter {
pub fn new(config: RoutingConfig) -> Self {
let tiers: Vec<ModelTier> = config
.tiers
.iter()
.enumerate()
.map(|(i, t)| ModelTier {
name: t.name.clone(),
models: t.models.clone(),
complexity_range: t.complexity_range,
cost_per_1k_tokens: t.cost_per_1k_tokens,
max_context_tokens: t.max_context_tokens,
ordinal: i,
})
.collect();
let tier_index: HashMap<String, usize> =
tiers.iter().map(|t| (t.name.clone(), t.ordinal)).collect();
let round_robin_counters: Vec<AtomicUsize> =
(0..tiers.len()).map(|_| AtomicUsize::new(0)).collect();
let selection_strategy = config
.selection_strategy
.unwrap_or(TierSelectionStrategy::PreferenceOrder);
Self {
tiers,
tier_index,
selection_strategy,
round_robin_counters,
escalation_config: config.escalation,
fallback_model: config.fallback_model,
cost_tracker: None,
rate_limiter: None,
}
}
pub fn with_cost_tracker(mut self, tracker: Arc<dyn CostTrackable>) -> Self {
self.cost_tracker = Some(tracker);
self
}
pub fn with_rate_limiter(mut self, limiter: Arc<dyn RateLimitable>) -> Self {
self.rate_limiter = Some(limiter);
self
}
fn filter_tiers_by_permissions<'a>(
&'a self,
permissions: &UserPermissions,
) -> Vec<&'a ModelTier> {
let max_ordinal = self
.tier_index
.get(&permissions.max_tier)
.copied()
.unwrap_or(usize::MAX);
self.tiers
.iter()
.filter(|t| t.ordinal <= max_ordinal)
.collect()
}
fn select_tier<'a>(
&'a self,
complexity: f32,
allowed_tiers: &[&'a ModelTier],
permissions: &UserPermissions,
) -> TierSelection<'a> {
let matching: Vec<&&ModelTier> = allowed_tiers
.iter()
.filter(|t| t.matches_complexity(complexity))
.collect();
if let Some(best) = matching.iter().max_by_key(|t| t.ordinal) {
return TierSelection {
tier: best,
escalated: false,
};
}
if permissions.escalation_allowed
&& self.escalation_config.enabled
&& complexity > permissions.escalation_threshold
{
let max_ordinal = self
.tier_index
.get(&permissions.max_tier)
.copied()
.unwrap_or(0);
let max_escalation = self.escalation_config.max_escalation_tiers;
let escalation_candidates: Vec<&ModelTier> = self
.tiers
.iter()
.filter(|t| {
t.ordinal > max_ordinal
&& t.ordinal <= max_ordinal + max_escalation as usize
&& t.matches_complexity(complexity)
})
.collect();
if let Some(best) = escalation_candidates.iter().max_by_key(|t| t.ordinal) {
tracing::info!(
user_max_tier = %permissions.max_tier,
escalated_to = %best.name,
complexity = %complexity,
"escalation applied: promoting beyond max_tier"
);
return TierSelection {
tier: best,
escalated: true,
};
}
}
if let Some(highest) = allowed_tiers.iter().max_by_key(|t| t.ordinal) {
return TierSelection {
tier: highest,
escalated: false,
};
}
TierSelection {
tier: &self.tiers[0],
escalated: false,
}
}
fn apply_budget_constraints<'a>(
&'a self,
selected: &'a ModelTier,
allowed_tiers: &[&'a ModelTier],
auth: &AuthContext,
permissions: &UserPermissions,
) -> TierBudgetResult<'a> {
let cost_tracker = match &self.cost_tracker {
Some(ct) => ct,
None => return TierBudgetResult { tier: selected, constrained: false },
};
if permissions.cost_budget_daily_usd <= 0.0
&& permissions.cost_budget_monthly_usd <= 0.0
{
return TierBudgetResult {
tier: selected,
constrained: false,
};
}
let estimated_cost = selected.cost_per_1k_tokens;
let budget_check = cost_tracker.check_budget(
&auth.sender_id,
estimated_cost,
permissions.cost_budget_daily_usd,
permissions.cost_budget_monthly_usd,
);
if budget_check == BudgetResult::Approved {
return TierBudgetResult {
tier: selected,
constrained: false,
};
}
tracing::info!(
user = %auth.sender_id,
selected_tier = %selected.name,
estimated_cost = %estimated_cost,
daily_limit = %permissions.cost_budget_daily_usd,
monthly_limit = %permissions.cost_budget_monthly_usd,
"budget constraint triggered: attempting tier downgrade"
);
let mut candidates: Vec<&&ModelTier> = allowed_tiers
.iter()
.filter(|t| t.ordinal < selected.ordinal)
.collect();
candidates.sort_by(|a, b| b.ordinal.cmp(&a.ordinal));
for candidate in candidates {
let candidate_cost = candidate.cost_per_1k_tokens;
let candidate_check = cost_tracker.check_budget(
&auth.sender_id,
candidate_cost,
permissions.cost_budget_daily_usd,
permissions.cost_budget_monthly_usd,
);
if candidate_check == BudgetResult::Approved {
return TierBudgetResult {
tier: candidate,
constrained: true,
};
}
}
if let Some(cheapest) = allowed_tiers.iter().min_by_key(|t| t.ordinal) {
return TierBudgetResult {
tier: cheapest,
constrained: true,
};
}
TierBudgetResult {
tier: selected,
constrained: true,
}
}
fn select_model(
&self,
tier: &ModelTier,
permissions: &UserPermissions,
) -> Option<(String, String)> {
let available = self.filter_models_by_permissions(&tier.models, permissions);
if available.is_empty() {
return None;
}
let selected = match &self.selection_strategy {
TierSelectionStrategy::PreferenceOrder => available[0].clone(),
TierSelectionStrategy::RoundRobin => {
let counter = &self.round_robin_counters[tier.ordinal];
let idx = counter.fetch_add(1, Ordering::Relaxed) % available.len();
available[idx].clone()
}
TierSelectionStrategy::LowestCost => {
available[0].clone()
}
TierSelectionStrategy::Random => {
let seed = crate::runtime::now_millis();
let idx = (seed as usize) % available.len();
available[idx].clone()
}
_ => available[0].clone(),
};
Some(split_provider_model(&selected))
}
fn filter_models_by_permissions(
&self,
models: &[String],
permissions: &UserPermissions,
) -> Vec<String> {
models
.iter()
.filter(|m| {
let allowed = permissions.model_access.is_empty()
|| permissions
.model_access
.iter()
.any(|p| model_matches_pattern(m, p));
let denied = permissions
.model_denylist
.iter()
.any(|p| model_matches_pattern(m, p));
allowed && !denied
})
.cloned()
.collect()
}
fn fallback_chain(
&self,
primary_tier: &ModelTier,
allowed_tiers: &[&ModelTier],
permissions: &UserPermissions,
) -> Option<(String, String, String)> {
let max_ordinal = self
.tier_index
.get(&permissions.max_tier)
.copied()
.unwrap_or(0);
let mut lower_tiers: Vec<&&ModelTier> = allowed_tiers
.iter()
.filter(|t| t.ordinal < primary_tier.ordinal)
.collect();
lower_tiers.sort_by(|a, b| b.ordinal.cmp(&a.ordinal));
for tier in lower_tiers {
if tier.ordinal > max_ordinal {
continue;
}
if let Some((provider, model)) = self.select_model(tier, permissions) {
let reason = format!(
"fallback from tier '{}' to tier '{}'",
primary_tier.name, tier.name
);
return Some((provider, model, reason));
}
}
if let Some(ref fallback) = self.fallback_model {
let fallback_tier_ordinal = self
.tiers
.iter()
.find(|t| t.models.iter().any(|m| m == fallback))
.map(|t| t.ordinal);
match fallback_tier_ordinal {
Some(ordinal) if ordinal > max_ordinal => {
tracing::info!(
fallback_model = %fallback,
fallback_tier_ordinal = %ordinal,
user_max_ordinal = %max_ordinal,
"fallback chain: fallback_model denied -- tier above user max_tier"
);
}
_ => {
let (provider, model) = split_provider_model(fallback);
let reason = format!(
"fallback to configured fallback_model '{}'",
fallback
);
return Some((provider, model, reason));
}
}
}
None
}
fn rate_limited_decision(&self, permissions: &UserPermissions) -> RoutingDecision {
let max_ordinal = self
.tier_index
.get(&permissions.max_tier)
.copied()
.unwrap_or(0);
if let Some(ref fallback) = self.fallback_model {
let fallback_tier_ordinal = self
.tiers
.iter()
.find(|t| t.models.iter().any(|m| m == fallback))
.map(|t| t.ordinal);
if let Some(ordinal) = fallback_tier_ordinal
&& ordinal > max_ordinal {
return RoutingDecision {
provider: String::new(),
model: String::new(),
reason: "rate limited: fallback model not permitted for user tier"
.into(),
..Default::default()
};
}
let (provider, model) = split_provider_model(fallback);
RoutingDecision {
provider,
model,
reason: "rate limited: using fallback model".into(),
..Default::default()
}
} else {
RoutingDecision {
provider: String::new(),
model: String::new(),
reason: "rate limited: no fallback model configured".into(),
budget_constrained: true,
..Default::default()
}
}
}
fn no_tiers_available_decision(&self) -> RoutingDecision {
if let Some(ref fallback) = self.fallback_model {
let (provider, model) = split_provider_model(fallback);
RoutingDecision {
provider,
model,
reason: "no tiers available: using fallback model".into(),
..Default::default()
}
} else {
RoutingDecision {
provider: String::new(),
model: String::new(),
reason: "no tiers or fallback model available".into(),
..Default::default()
}
}
}
}
#[async_trait]
impl ModelRouter for TieredRouter {
async fn route(
&self,
request: &ChatRequest,
profile: &TaskProfile,
) -> RoutingDecision {
let auth = request
.auth_context
.as_ref()
.cloned()
.unwrap_or_default();
let default_permissions = UserPermissions::default();
let permissions = request
.auth_context
.as_ref()
.map(|a| &a.permissions)
.unwrap_or(&default_permissions);
tracing::info!(
sender = %auth.sender_id,
channel = %auth.channel,
complexity = %profile.complexity,
max_tier = %permissions.max_tier,
level = %permissions.level,
"route: starting tiered routing decision"
);
if let Some(ref limiter) = self.rate_limiter
&& permissions.rate_limit > 0
&& !limiter.check(&auth.sender_id, permissions.rate_limit)
{
tracing::info!(sender = %auth.sender_id, "route: rate limited");
return self.rate_limited_decision(permissions);
}
let allowed_tiers = self.filter_tiers_by_permissions(permissions);
if allowed_tiers.is_empty() {
tracing::info!(sender = %auth.sender_id, "route: no tiers available");
return self.no_tiers_available_decision();
}
let tier_selection =
self.select_tier(profile.complexity, &allowed_tiers, permissions);
let budget_result = self.apply_budget_constraints(
tier_selection.tier,
&allowed_tiers,
&auth,
permissions,
);
let final_tier = budget_result.tier;
let escalated = tier_selection.escalated;
let budget_constrained = budget_result.constrained;
let (provider, model) = match self.select_model(final_tier, permissions) {
Some(pm) => pm,
None => {
match self.fallback_chain(final_tier, &allowed_tiers, permissions) {
Some((p, m, reason)) => {
return RoutingDecision {
provider: p,
model: m,
reason,
tier: Some(final_tier.name.clone()),
cost_estimate_usd: Some(final_tier.cost_per_1k_tokens),
escalated,
budget_constrained,
sender_id: Some(auth.sender_id.clone()),
};
}
None => return self.no_tiers_available_decision(),
}
}
};
let cost_estimate = final_tier.cost_per_1k_tokens;
if let Some(ref tracker) = self.cost_tracker {
tracker.record_estimated(&auth.sender_id, cost_estimate);
}
tracing::info!(
sender = %auth.sender_id,
provider = %provider,
model = %model,
tier = %final_tier.name,
cost_estimate_usd = %cost_estimate,
escalated = %escalated,
budget_constrained = %budget_constrained,
"route: decision made"
);
RoutingDecision {
provider,
model,
reason: format!(
"tiered routing: complexity={:.2}, tier={}, level={}, user={}",
profile.complexity, final_tier.name, permissions.level, auth.sender_id,
),
tier: Some(final_tier.name.clone()),
cost_estimate_usd: Some(cost_estimate),
escalated,
budget_constrained,
sender_id: Some(auth.sender_id.clone()),
}
}
fn update(&self, decision: &RoutingDecision, _outcome: &ResponseOutcome) {
if let (Some(cost), Some(sender_id)) =
(decision.cost_estimate_usd, &decision.sender_id)
&& let Some(ref tracker) = self.cost_tracker
{
tracker.record_actual(sender_id, cost, cost);
}
}
}
impl std::fmt::Debug for TieredRouter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TieredRouter")
.field(
"tiers",
&self.tiers.iter().map(|t| &t.name).collect::<Vec<_>>(),
)
.field("selection_strategy", &self.selection_strategy)
.field("escalation_config", &self.escalation_config)
.field("fallback_model", &self.fallback_model)
.finish()
}
}
fn model_matches_pattern(model: &str, pattern: &str) -> bool {
if pattern == "*" {
return true;
}
if let Some(prefix) = pattern.strip_suffix('*') {
return model.starts_with(prefix);
}
model == pattern
}
fn split_provider_model(s: &str) -> (String, String) {
if let Some(idx) = s.find('/') {
(s[..idx].to_string(), s[idx + 1..].to_string())
} else {
("openai".to_string(), s.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pipeline::traits::{LlmMessage, TaskType};
use clawft_types::routing::ModelTierConfig;
use std::sync::Mutex;
fn make_tier_config(
name: &str,
models: Vec<&str>,
range: [f32; 2],
cost: f64,
) -> ModelTierConfig {
ModelTierConfig {
name: name.into(),
models: models.into_iter().map(|s| s.to_string()).collect(),
complexity_range: range,
cost_per_1k_tokens: cost,
max_context_tokens: 8192,
}
}
fn standard_tiers() -> Vec<ModelTierConfig> {
vec![
make_tier_config("free", vec!["groq/llama-3.1-8b"], [0.0, 0.3], 0.0),
make_tier_config(
"standard",
vec!["anthropic/claude-haiku-3.5", "openai/gpt-4o-mini"],
[0.0, 0.7],
0.001,
),
make_tier_config(
"premium",
vec!["anthropic/claude-sonnet-4", "openai/gpt-4o"],
[0.3, 1.0],
0.01,
),
make_tier_config(
"elite",
vec!["anthropic/claude-opus-4"],
[0.7, 1.0],
0.05,
),
]
}
fn make_config(tiers: Vec<ModelTierConfig>) -> RoutingConfig {
RoutingConfig {
mode: "tiered".into(),
tiers,
selection_strategy: Some(TierSelectionStrategy::PreferenceOrder),
fallback_model: Some("groq/llama-3.1-8b".into()),
escalation: EscalationConfig {
enabled: true,
threshold: 0.6,
max_escalation_tiers: 1,
},
..RoutingConfig::default()
}
}
fn make_request() -> ChatRequest {
ChatRequest {
messages: vec![LlmMessage {
role: "user".into(),
content: "hello".into(),
tool_call_id: None,
tool_calls: None,
}],
tools: vec![],
model: None,
max_tokens: None,
temperature: None,
auth_context: None,
complexity_boost: 0.0,
}
}
fn make_request_with_auth(auth: AuthContext) -> ChatRequest {
ChatRequest {
messages: vec![LlmMessage {
role: "user".into(),
content: "hello".into(),
tool_call_id: None,
tool_calls: None,
}],
tools: vec![],
model: None,
max_tokens: None,
temperature: None,
auth_context: Some(auth),
complexity_boost: 0.0,
}
}
fn make_profile(complexity: f32) -> TaskProfile {
TaskProfile {
task_type: TaskType::Chat,
complexity,
keywords: vec![],
}
}
fn admin_permissions() -> UserPermissions {
UserPermissions {
level: 2,
max_tier: "elite".into(),
escalation_allowed: true,
escalation_threshold: 0.0,
rate_limit: 0,
cost_budget_daily_usd: 0.0,
cost_budget_monthly_usd: 0.0,
..UserPermissions::default()
}
}
fn user_permissions() -> UserPermissions {
UserPermissions {
level: 1,
max_tier: "standard".into(),
escalation_allowed: true,
escalation_threshold: 0.6,
rate_limit: 60,
cost_budget_daily_usd: 5.0,
cost_budget_monthly_usd: 50.0,
..UserPermissions::default()
}
}
fn make_auth(sender: &str, permissions: UserPermissions) -> AuthContext {
AuthContext {
sender_id: sender.into(),
channel: "test".into(),
permissions,
}
}
struct MockCostTracker {
deny: Mutex<bool>,
}
impl MockCostTracker {
fn new(deny: bool) -> Self {
Self {
deny: Mutex::new(deny),
}
}
}
impl CostTrackable for MockCostTracker {
fn check_budget(
&self,
_sender_id: &str,
_estimated_cost: f64,
_daily_limit: f64,
_monthly_limit: f64,
) -> BudgetResult {
if *self.deny.lock().unwrap() {
BudgetResult::DailyLimitExceeded {
spent: 10.0,
limit: 5.0,
}
} else {
BudgetResult::Approved
}
}
fn record_estimated(&self, _sender_id: &str, _estimated_cost: f64) {}
fn record_actual(&self, _sender_id: &str, _estimated_cost: f64, _actual_cost: f64) {}
}
struct MockRateLimiter {
allow: bool,
}
impl RateLimitable for MockRateLimiter {
fn check(&self, _sender_id: &str, _limit: u32) -> bool {
self.allow
}
}
#[test]
fn tier_matches_complexity_in_range() {
let tier = ModelTier {
name: "standard".into(),
models: vec![],
complexity_range: [0.0, 0.7],
cost_per_1k_tokens: 0.001,
max_context_tokens: 8192,
ordinal: 0,
};
assert!(tier.matches_complexity(0.5));
}
#[test]
fn tier_matches_complexity_at_lower_bound() {
let tier = ModelTier {
name: "free".into(),
models: vec![],
complexity_range: [0.0, 0.3],
cost_per_1k_tokens: 0.0,
max_context_tokens: 8192,
ordinal: 0,
};
assert!(tier.matches_complexity(0.0));
}
#[test]
fn tier_matches_complexity_at_upper_bound() {
let tier = ModelTier {
name: "standard".into(),
models: vec![],
complexity_range: [0.0, 0.7],
cost_per_1k_tokens: 0.001,
max_context_tokens: 8192,
ordinal: 0,
};
assert!(tier.matches_complexity(0.7));
}
#[test]
fn tier_does_not_match_below_range() {
let tier = ModelTier {
name: "premium".into(),
models: vec![],
complexity_range: [0.3, 1.0],
cost_per_1k_tokens: 0.01,
max_context_tokens: 8192,
ordinal: 0,
};
assert!(!tier.matches_complexity(0.0));
}
#[test]
fn tier_does_not_match_above_range() {
let tier = ModelTier {
name: "free".into(),
models: vec![],
complexity_range: [0.0, 0.3],
cost_per_1k_tokens: 0.0,
max_context_tokens: 8192,
ordinal: 0,
};
assert!(!tier.matches_complexity(1.0));
}
#[test]
fn select_tier_picks_highest_quality_match() {
let config = make_config(standard_tiers());
let router = TieredRouter::new(config);
let perms = admin_permissions();
let allowed = router.filter_tiers_by_permissions(&perms);
let selection = router.select_tier(0.5, &allowed, &perms);
assert_eq!(selection.tier.name, "premium");
assert!(!selection.escalated);
}
#[test]
fn select_tier_with_single_matching_tier() {
let config = make_config(standard_tiers());
let router = TieredRouter::new(config);
let perms = admin_permissions();
let allowed = router.filter_tiers_by_permissions(&perms);
let selection = router.select_tier(0.1, &allowed, &perms);
assert_eq!(selection.tier.name, "standard");
}
#[test]
fn select_tier_falls_back_to_highest_allowed() {
let config = make_config(vec![
make_tier_config("low", vec!["a/m1"], [0.0, 0.2], 0.0),
make_tier_config("high", vec!["a/m2"], [0.8, 1.0], 0.01),
]);
let router = TieredRouter::new(config);
let perms = UserPermissions {
max_tier: "high".into(),
escalation_allowed: false,
..admin_permissions()
};
let allowed = router.filter_tiers_by_permissions(&perms);
let selection = router.select_tier(0.5, &allowed, &perms);
assert_eq!(selection.tier.name, "high");
}
#[test]
fn escalation_promotes_to_next_tier() {
let config = make_config(standard_tiers());
let router = TieredRouter::new(config);
let perms = UserPermissions {
max_tier: "standard".into(),
escalation_allowed: true,
escalation_threshold: 0.6,
..UserPermissions::default()
};
let allowed = router.filter_tiers_by_permissions(&perms);
let selection = router.select_tier(0.8, &allowed, &perms);
assert_eq!(selection.tier.name, "premium");
assert!(selection.escalated);
}
#[test]
fn escalation_respects_max_escalation_tiers() {
let mut config = make_config(standard_tiers());
config.escalation.max_escalation_tiers = 1;
let router = TieredRouter::new(config);
let perms = UserPermissions {
max_tier: "standard".into(),
escalation_allowed: true,
escalation_threshold: 0.6,
..UserPermissions::default()
};
let allowed = router.filter_tiers_by_permissions(&perms);
let selection = router.select_tier(0.8, &allowed, &perms);
assert_eq!(selection.tier.name, "premium");
assert!(selection.escalated);
}
#[test]
fn escalation_denied_when_not_allowed() {
let config = make_config(standard_tiers());
let router = TieredRouter::new(config);
let perms = UserPermissions {
max_tier: "standard".into(),
escalation_allowed: false,
..UserPermissions::default()
};
let allowed = router.filter_tiers_by_permissions(&perms);
let selection = router.select_tier(0.9, &allowed, &perms);
assert_eq!(selection.tier.name, "standard");
assert!(!selection.escalated);
}
#[test]
fn escalation_denied_below_threshold() {
let config = make_config(standard_tiers());
let router = TieredRouter::new(config);
let perms = UserPermissions {
max_tier: "free".into(),
escalation_allowed: true,
escalation_threshold: 0.9,
..UserPermissions::default()
};
let allowed = router.filter_tiers_by_permissions(&perms);
let selection = router.select_tier(0.5, &allowed, &perms);
assert!(!selection.escalated);
}
#[test]
fn escalation_denied_when_config_disabled() {
let mut config = make_config(standard_tiers());
config.escalation.enabled = false;
let router = TieredRouter::new(config);
let perms = UserPermissions {
max_tier: "standard".into(),
escalation_allowed: true,
escalation_threshold: 0.6,
..UserPermissions::default()
};
let allowed = router.filter_tiers_by_permissions(&perms);
let selection = router.select_tier(0.9, &allowed, &perms);
assert!(!selection.escalated);
}
#[tokio::test]
async fn budget_unlimited_skips_check() {
let config = make_config(standard_tiers());
let tracker = Arc::new(MockCostTracker::new(true)); let router = TieredRouter::new(config).with_cost_tracker(tracker);
let perms = UserPermissions {
cost_budget_daily_usd: 0.0,
cost_budget_monthly_usd: 0.0,
..admin_permissions()
};
let auth = make_auth("alice", perms.clone());
let req = make_request_with_auth(auth);
let decision = router.route(&req, &make_profile(0.5)).await;
assert!(!decision.budget_constrained);
}
#[tokio::test]
async fn budget_sufficient_allows_tier() {
let config = make_config(standard_tiers());
let tracker = Arc::new(MockCostTracker::new(false)); let router = TieredRouter::new(config).with_cost_tracker(tracker);
let perms = UserPermissions {
cost_budget_daily_usd: 10.0,
cost_budget_monthly_usd: 100.0,
..admin_permissions()
};
let auth = make_auth("alice", perms);
let req = make_request_with_auth(auth);
let decision = router.route(&req, &make_profile(0.5)).await;
assert!(!decision.budget_constrained);
assert_eq!(decision.tier.as_deref(), Some("premium"));
}
#[tokio::test]
async fn budget_insufficient_downgrades_tier() {
struct SelectiveDenyTracker;
impl CostTrackable for SelectiveDenyTracker {
fn check_budget(
&self,
_sender_id: &str,
estimated_cost: f64,
_daily_limit: f64,
_monthly_limit: f64,
) -> BudgetResult {
if estimated_cost > 0.005 {
BudgetResult::DailyLimitExceeded {
spent: 4.99,
limit: 5.0,
}
} else {
BudgetResult::Approved
}
}
fn record_estimated(&self, _: &str, _: f64) {}
fn record_actual(&self, _: &str, _: f64, _: f64) {}
}
let config = make_config(standard_tiers());
let tracker = Arc::new(SelectiveDenyTracker);
let router = TieredRouter::new(config).with_cost_tracker(tracker);
let perms = UserPermissions {
cost_budget_daily_usd: 5.0,
cost_budget_monthly_usd: 50.0,
..admin_permissions()
};
let auth = make_auth("alice", perms);
let req = make_request_with_auth(auth);
let decision = router.route(&req, &make_profile(0.5)).await;
assert!(decision.budget_constrained);
assert_eq!(decision.tier.as_deref(), Some("standard"));
}
#[tokio::test]
async fn budget_exhausted_uses_cheapest_tier() {
let config = make_config(standard_tiers());
let tracker = Arc::new(MockCostTracker::new(true)); let router = TieredRouter::new(config).with_cost_tracker(tracker);
let perms = UserPermissions {
cost_budget_daily_usd: 5.0,
cost_budget_monthly_usd: 50.0,
..admin_permissions()
};
let auth = make_auth("alice", perms);
let req = make_request_with_auth(auth);
let decision = router.route(&req, &make_profile(0.5)).await;
assert!(decision.budget_constrained);
assert_eq!(decision.tier.as_deref(), Some("free"));
}
#[test]
fn preference_order_picks_first_model() {
let config = make_config(standard_tiers());
let router = TieredRouter::new(config);
let tier = &router.tiers[1]; let perms = admin_permissions();
let (_, model) = router.select_model(tier, &perms).unwrap();
assert_eq!(model, "claude-haiku-3.5");
}
#[test]
fn round_robin_rotates_models() {
let mut config = make_config(standard_tiers());
config.selection_strategy = Some(TierSelectionStrategy::RoundRobin);
let router = TieredRouter::new(config);
let tier = &router.tiers[1]; let perms = admin_permissions();
let (_, m1) = router.select_model(tier, &perms).unwrap();
let (_, m2) = router.select_model(tier, &perms).unwrap();
let (_, m3) = router.select_model(tier, &perms).unwrap();
assert_ne!(m1, m2);
assert_eq!(m1, m3);
}
#[test]
fn lowest_cost_picks_first_model() {
let mut config = make_config(standard_tiers());
config.selection_strategy = Some(TierSelectionStrategy::LowestCost);
let router = TieredRouter::new(config);
let tier = &router.tiers[1]; let perms = admin_permissions();
let (_, model) = router.select_model(tier, &perms).unwrap();
assert_eq!(model, "claude-haiku-3.5");
}
#[test]
fn random_returns_valid_model() {
let mut config = make_config(standard_tiers());
config.selection_strategy = Some(TierSelectionStrategy::Random);
let router = TieredRouter::new(config);
let tier = &router.tiers[1]; let perms = admin_permissions();
let (provider, model) = router.select_model(tier, &perms).unwrap();
let full_name = format!("{}/{}", provider, model);
assert!(
tier.models.contains(&full_name),
"model {} not in tier models {:?}",
full_name,
tier.models
);
}
#[test]
fn filter_tiers_by_max_tier() {
let config = make_config(standard_tiers());
let router = TieredRouter::new(config);
let perms = UserPermissions {
max_tier: "standard".into(),
..UserPermissions::default()
};
let allowed = router.filter_tiers_by_permissions(&perms);
let names: Vec<&str> = allowed.iter().map(|t| t.name.as_str()).collect();
assert_eq!(names, vec!["free", "standard"]);
}
#[test]
fn filter_tiers_unknown_max_tier_allows_all() {
let config = make_config(standard_tiers());
let router = TieredRouter::new(config);
let perms = UserPermissions {
max_tier: "nonexistent".into(),
..UserPermissions::default()
};
let allowed = router.filter_tiers_by_permissions(&perms);
assert_eq!(allowed.len(), 4);
}
#[test]
fn model_access_allowlist_filters() {
let config = make_config(standard_tiers());
let router = TieredRouter::new(config);
let perms = UserPermissions {
model_access: vec!["anthropic/*".into()],
..admin_permissions()
};
let tier = &router.tiers[1]; let available = router.filter_models_by_permissions(&tier.models, &perms);
assert_eq!(available, vec!["anthropic/claude-haiku-3.5"]);
}
#[test]
fn model_denylist_filters() {
let config = make_config(standard_tiers());
let router = TieredRouter::new(config);
let perms = UserPermissions {
model_denylist: vec!["openai/gpt-4o-mini".into()],
..admin_permissions()
};
let tier = &router.tiers[1]; let available = router.filter_models_by_permissions(&tier.models, &perms);
assert_eq!(available, vec!["anthropic/claude-haiku-3.5"]);
}
#[test]
fn empty_model_access_allows_all() {
let config = make_config(standard_tiers());
let router = TieredRouter::new(config);
let perms = UserPermissions {
model_access: vec![],
..admin_permissions()
};
let tier = &router.tiers[1]; let available = router.filter_models_by_permissions(&tier.models, &perms);
assert_eq!(available.len(), 2);
}
#[test]
fn fallback_to_lower_tier() {
let config = make_config(standard_tiers());
let router = TieredRouter::new(config);
let primary = &router.tiers[2]; let perms = UserPermissions {
model_denylist: vec!["anthropic/claude-sonnet-4".into(), "openai/gpt-4o".into()],
..admin_permissions()
};
let allowed = router.filter_tiers_by_permissions(&perms);
let result = router.fallback_chain(primary, &allowed, &perms);
assert!(result.is_some());
let (_, _, reason) = result.unwrap();
assert!(reason.contains("fallback"));
}
#[test]
fn fallback_to_fallback_model() {
let config = make_config(vec![
make_tier_config("only", vec!["denied/model"], [0.0, 1.0], 0.0),
]);
let router = TieredRouter::new(config);
let primary = &router.tiers[0];
let perms = UserPermissions {
model_denylist: vec!["denied/model".into()],
max_tier: "only".into(),
..admin_permissions()
};
let allowed = router.filter_tiers_by_permissions(&perms);
let result = router.fallback_chain(primary, &allowed, &perms);
assert!(result.is_some());
let (provider, model, reason) = result.unwrap();
assert_eq!(provider, "groq");
assert_eq!(model, "llama-3.1-8b");
assert!(reason.contains("fallback_model"));
}
#[test]
fn fallback_returns_none_when_no_fallback() {
let mut config = make_config(vec![
make_tier_config("only", vec!["denied/model"], [0.0, 1.0], 0.0),
]);
config.fallback_model = None;
let router = TieredRouter::new(config);
let primary = &router.tiers[0];
let perms = UserPermissions {
model_denylist: vec!["denied/model".into()],
max_tier: "only".into(),
..admin_permissions()
};
let allowed = router.filter_tiers_by_permissions(&perms);
let result = router.fallback_chain(primary, &allowed, &perms);
assert!(result.is_none());
}
#[test]
fn fallback_model_denied_above_max_tier() {
let mut config = make_config(standard_tiers());
config.fallback_model = Some("anthropic/claude-sonnet-4".into());
let router = TieredRouter::new(config);
let primary = &router.tiers[0]; let perms = UserPermissions {
model_denylist: vec!["groq/llama-3.1-8b".into()],
max_tier: "free".into(),
..UserPermissions::default()
};
let allowed = router.filter_tiers_by_permissions(&perms);
let result = router.fallback_chain(primary, &allowed, &perms);
assert!(result.is_none());
}
#[test]
fn rate_limited_fallback_denied_above_max_tier() {
let mut config = make_config(standard_tiers());
config.fallback_model = Some("anthropic/claude-opus-4".into());
let router = TieredRouter::new(config);
let perms = UserPermissions {
max_tier: "free".into(),
..UserPermissions::default()
};
let decision = router.rate_limited_decision(&perms);
assert!(decision.provider.is_empty());
assert!(decision.reason.contains("not permitted"));
}
#[tokio::test]
async fn route_low_complexity_to_free_tier() {
let config = make_config(standard_tiers());
let router = TieredRouter::new(config);
let perms = UserPermissions {
max_tier: "free".into(),
..UserPermissions::default()
};
let auth = make_auth("user1", perms);
let req = make_request_with_auth(auth);
let decision = router.route(&req, &make_profile(0.1)).await;
assert_eq!(decision.tier.as_deref(), Some("free"));
assert_eq!(decision.model, "llama-3.1-8b");
assert!(!decision.escalated);
}
#[tokio::test]
async fn route_high_complexity_to_premium() {
let config = make_config(standard_tiers());
let router = TieredRouter::new(config);
let auth = make_auth("admin1", admin_permissions());
let req = make_request_with_auth(auth);
let decision = router.route(&req, &make_profile(0.9)).await;
assert_eq!(decision.tier.as_deref(), Some("elite"));
assert!(!decision.escalated);
}
#[tokio::test]
async fn route_no_auth_context_uses_zero_trust() {
let config = make_config(standard_tiers());
let router = TieredRouter::new(config);
let req = make_request(); let decision = router.route(&req, &make_profile(0.1)).await;
assert_eq!(decision.tier.as_deref(), Some("free"));
}
#[tokio::test]
async fn route_rate_limited_returns_fallback() {
let config = make_config(standard_tiers());
let limiter = Arc::new(MockRateLimiter { allow: false });
let router = TieredRouter::new(config).with_rate_limiter(limiter);
let perms = user_permissions();
let auth = make_auth("user1", perms);
let req = make_request_with_auth(auth);
let decision = router.route(&req, &make_profile(0.5)).await;
assert!(decision.reason.contains("rate limited"));
}
#[tokio::test]
async fn route_update_does_not_panic() {
let config = make_config(standard_tiers());
let router = TieredRouter::new(config);
let decision = RoutingDecision {
provider: "test".into(),
model: "test".into(),
reason: "test".into(),
cost_estimate_usd: Some(0.01),
..Default::default()
};
let outcome = ResponseOutcome {
success: true,
quality: crate::pipeline::traits::QualityScore {
overall: 1.0,
relevance: 1.0,
coherence: 1.0,
},
latency_ms: 100,
};
router.update(&decision, &outcome);
}
#[tokio::test]
async fn empty_tiers_config_uses_fallback() {
let mut config = make_config(vec![]);
config.fallback_model = Some("groq/llama-3.1-8b".into());
let router = TieredRouter::new(config);
let req = make_request();
let decision = router.route(&req, &make_profile(0.5)).await;
assert_eq!(decision.provider, "groq");
assert_eq!(decision.model, "llama-3.1-8b");
assert!(decision.reason.contains("no tiers"));
}
#[tokio::test]
async fn single_tier_config_always_uses_it() {
let config = make_config(vec![make_tier_config(
"only",
vec!["test/model"],
[0.0, 1.0],
0.0,
)]);
let router = TieredRouter::new(config);
let perms = UserPermissions {
max_tier: "only".into(),
..admin_permissions()
};
let auth = make_auth("user1", perms);
let req = make_request_with_auth(auth);
let decision = router.route(&req, &make_profile(0.1)).await;
assert_eq!(decision.tier.as_deref(), Some("only"));
let decision2 = router.route(&req, &make_profile(0.9)).await;
assert_eq!(decision2.tier.as_deref(), Some("only"));
}
#[test]
fn model_matches_pattern_exact() {
assert!(model_matches_pattern(
"anthropic/claude-haiku-3.5",
"anthropic/claude-haiku-3.5"
));
}
#[test]
fn model_matches_pattern_wildcard() {
assert!(model_matches_pattern(
"anthropic/claude-haiku-3.5",
"anthropic/*"
));
}
#[test]
fn model_matches_pattern_star() {
assert!(model_matches_pattern("anything/at_all", "*"));
}
#[test]
fn model_matches_pattern_no_match() {
assert!(!model_matches_pattern(
"openai/gpt-4o",
"anthropic/*"
));
}
#[tokio::test]
async fn missing_auth_context_defaults_to_zero_trust() {
let config = make_config(standard_tiers());
let router = TieredRouter::new(config);
let req = make_request(); let decision = router.route(&req, &make_profile(0.1)).await;
assert_eq!(decision.tier.as_deref(), Some("free"));
}
#[tokio::test]
async fn empty_tiers_no_fallback() {
let mut config = make_config(vec![]);
config.fallback_model = None;
let router = TieredRouter::new(config);
let req = make_request();
let decision = router.route(&req, &make_profile(0.5)).await;
assert!(decision.provider.is_empty());
assert!(decision.reason.contains("no tiers"));
}
#[test]
fn split_provider_model_with_slash() {
let (p, m) = split_provider_model("anthropic/claude-opus-4");
assert_eq!(p, "anthropic");
assert_eq!(m, "claude-opus-4");
}
#[test]
fn split_provider_model_without_slash() {
let (p, m) = split_provider_model("gpt-4o");
assert_eq!(p, "openai");
assert_eq!(m, "gpt-4o");
}
#[test]
fn noop_cost_tracker_always_approves() {
let tracker = NoopCostTracker;
let result = tracker.check_budget("any", 100.0, 1.0, 1.0);
assert_eq!(result, BudgetResult::Approved);
}
#[test]
fn noop_rate_limiter_always_allows() {
let limiter = NoopRateLimiter;
assert!(limiter.check("any", 1));
}
#[test]
fn debug_impl_does_not_panic() {
let config = make_config(standard_tiers());
let router = TieredRouter::new(config);
let debug_str = format!("{:?}", router);
assert!(debug_str.contains("TieredRouter"));
}
}