use std::collections::HashMap;
use std::path::Path;
use anyhow::Context;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct Config {
#[serde(default)]
pub server: ServerConfig,
#[serde(default)]
pub logging: LoggingConfig,
pub providers: Vec<ProviderConfig>,
#[serde(default)]
pub routers: Vec<RouterRule>,
#[serde(default)]
pub plugins: HashMap<String, PluginEntryConfig>,
#[serde(default)]
pub classifiers: HashMap<String, ClassifierEntryConfig>,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(default)]
pub struct EntryConfig {
pub enabled: bool,
#[serde(flatten)]
pub settings: serde_json::Map<String, serde_json::Value>,
}
pub type PluginEntryConfig = EntryConfig;
pub type ClassifierEntryConfig = EntryConfig;
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum RouterRule {
Prefix {
model_prefix: String,
provider: String,
rewrite_model: Option<String>,
},
Tag {
tag: String,
provider: String,
rewrite_model: Option<String>,
},
Price {
#[serde(default)]
providers: Vec<String>,
max_cost_per_1m_tokens: Option<f64>,
},
Latency {
#[serde(default)]
providers: Vec<String>,
max_latency_ms: Option<f64>,
},
Throughput {
#[serde(default)]
providers: Vec<String>,
min_tokens_per_sec: Option<f64>,
},
Fallback {
#[serde(default)]
providers: Vec<String>,
#[serde(default = "default_quality_bias")]
quality_bias: f64,
rewrite_model: Option<String>,
},
Discover { provider: String },
Random {
#[serde(default)]
providers: Vec<String>,
rewrite_model: Option<String>,
#[serde(default)]
candidates: Vec<RandomCandidate>,
},
}
#[derive(Debug, Clone, Deserialize)]
pub struct RandomCandidate {
pub provider: String,
pub model: String,
}
fn default_quality_bias() -> f64 {
0.5
}
#[derive(Debug, Deserialize)]
#[serde(default)]
pub struct LoggingConfig {
pub enabled: bool,
pub path: String,
}
impl Default for LoggingConfig {
fn default() -> Self {
LoggingConfig {
enabled: false,
path: "logs/requests.jsonl".to_string(),
}
}
}
#[derive(Debug, Deserialize)]
#[serde(default)]
pub struct ServerConfig {
pub host: String,
pub port: u16,
pub dashboard: bool,
}
impl Default for ServerConfig {
fn default() -> Self {
ServerConfig {
host: "0.0.0.0".to_string(),
port: 8090,
dashboard: false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ProviderFormat {
OpenAi,
Anthropic,
Ollama,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ProviderConfig {
pub name: String,
pub format: ProviderFormat,
pub base_url: String,
pub api_key_env: Option<String>,
#[serde(default)]
pub strict: bool,
#[serde(default)]
pub cost_per_1m_tokens: f64,
#[serde(default)]
pub quality: f64,
pub latency_ms: Option<f64>,
pub throughput_tokens_per_sec: Option<f64>,
}
impl Config {
pub fn load(path: &Path) -> anyhow::Result<Self> {
let text = std::fs::read_to_string(path)
.with_context(|| format!("reading config file {}", path.display()))?;
toml::from_str(&text).with_context(|| format!("parsing config file {}", path.display()))
}
}