use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ModelInfo {
pub name: String,
pub provider: String,
pub model_type: Option<String>,
pub metadata: HashMap<String, serde_json::Value>,
}
impl ModelInfo {
pub fn new(name: impl Into<String>, provider: impl Into<String>) -> Self {
Self {
name: name.into(),
provider: provider.into(),
model_type: None,
metadata: HashMap::new(),
}
}
pub fn with_type(mut self, t: impl Into<String>) -> Self {
self.model_type = Some(t.into());
self
}
pub fn with_meta(mut self, key: impl Into<String>, val: impl Into<serde_json::Value>) -> Self {
self.metadata.insert(key.into(), val.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderConfig {
pub name: String,
pub slug: String,
pub base_url: Option<String>,
}
impl ProviderConfig {
pub fn new(name: impl Into<String>, slug: impl Into<String>) -> Self {
Self { name: name.into(), slug: slug.into(), base_url: None }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoutingDecision {
pub model: String,
pub provider: String,
pub reasoning: Option<String>,
pub confidence: Option<f32>,
pub metadata: HashMap<String, serde_json::Value>,
}
impl RoutingDecision {
pub fn new(model: impl Into<String>, provider: impl Into<String>) -> Self {
Self {
model: model.into(),
provider: provider.into(),
reasoning: None,
confidence: None,
metadata: HashMap::new(),
}
}
pub fn with_reasoning(mut self, r: impl Into<String>) -> Self {
self.reasoning = Some(r.into());
self
}
pub fn with_confidence(mut self, c: f32) -> Self {
self.confidence = Some(c.clamp(0.0, 1.0));
self
}
pub fn with_meta(mut self, key: impl Into<String>, val: impl Into<serde_json::Value>) -> Self {
self.metadata.insert(key.into(), val.into());
self
}
}
pub fn model_tier(model_name: &str) -> u8 {
let name = model_name.to_lowercase();
if name.contains("nano") {
return 1;
}
if name.contains("opus") || name.contains("gpt-5") || name.contains("o3")
|| name.contains("gpt-4.1") && !name.contains("mini")
{
return 4;
}
if name.contains("sonnet") || name.contains("gpt-4o") {
return 3;
}
2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn model_info_new() {
let m = ModelInfo::new("gpt-4o", "openai");
assert_eq!(m.name, "gpt-4o");
assert_eq!(m.provider, "openai");
assert!(m.model_type.is_none());
}
#[test]
fn model_info_with_type() {
let m = ModelInfo::new("embed-v3", "openai").with_type("embedding");
assert_eq!(m.model_type.as_deref(), Some("embedding"));
}
#[test]
fn routing_decision_builder() {
let d = RoutingDecision::new("claude-sonnet", "anthropic")
.with_reasoning("RoundRobin")
.with_confidence(0.9);
assert_eq!(d.model, "claude-sonnet");
assert_eq!(d.provider, "anthropic");
assert_eq!(d.reasoning.as_deref(), Some("RoundRobin"));
assert!((d.confidence.unwrap() - 0.9).abs() < 1e-6);
}
#[test]
fn confidence_is_clamped() {
let d = RoutingDecision::new("m", "p").with_confidence(2.0);
assert_eq!(d.confidence, Some(1.0));
}
#[test]
fn model_tier_nano() {
assert_eq!(model_tier("gpt-4-nano"), 1);
}
#[test]
fn model_tier_sonnet() {
assert_eq!(model_tier("claude-sonnet-4"), 3);
}
#[test]
fn model_tier_gpt4o() {
assert_eq!(model_tier("gpt-4o"), 3);
}
#[test]
fn model_tier_opus() {
assert_eq!(model_tier("claude-opus-4"), 4);
}
#[test]
fn model_tier_default() {
assert_eq!(model_tier("some-unknown-model"), 2);
}
}