//! Provider, model and permission configuration shared by server and CLI. use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; fn yes() -> bool { true } #[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum ProviderKind { Anthropic, Openai, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ProviderConfig { pub kind: ProviderKind, #[serde(default)] pub base_url: String, #[serde(default)] pub api_key: String, #[serde(default)] pub auth_header: Option, #[serde(default)] pub auth_prefix: Option, #[serde(default)] pub headers: BTreeMap, #[serde(default)] pub models: Vec, #[serde(default = "yes")] pub enabled: bool, /// Max. simultaneous requests to this provider/host (None = unlimited). #[serde(default, skip_serializing_if = "Option::is_none")] pub max_concurrency: Option, /// Monthly budget in the configured currency (tracked from recorded costs). #[serde(default, skip_serializing_if = "Option::is_none")] pub monthly_budget: Option, /// Optional admin key to read the provider's own cost report /// (Anthropic: sk-ant-admin01-…, organisation-wide). #[serde(default, skip_serializing_if = "String::is_empty")] pub admin_key: String, } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(untagged)] pub enum ModelEntry { Id(String), Full(ModelConfig), } #[derive(Debug, Clone, Deserialize, Serialize, Default)] pub struct ModelConfig { pub id: String, #[serde(default)] pub name: Option, #[serde(default)] pub context: Option, #[serde(default)] pub max_output: Option, #[serde(default)] pub reasoning: bool, #[serde(default)] pub vision: bool, /// Reasoning effort (Anthropic: low|medium|high|xhigh|max, OpenAI: low|medium|high). #[serde(default)] pub effort: Option, /// Prices in currency units per 1M tokens (used for cost statistics). #[serde(default, skip_serializing_if = "Option::is_none")] pub input_price: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub output_price: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub cache_read_price: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub cache_write_price: Option, /// Max. simultaneous requests to this model (None = unlimited). #[serde(default, skip_serializing_if = "Option::is_none")] pub max_concurrency: Option, /// Extra request body fields sent as-is (e.g. temperature, reasoning_budget_tokens). #[serde(default, skip_serializing_if = "serde_json::Map::is_empty")] pub options: serde_json::Map, } impl ModelConfig { /// Cost of one request. Cache prices default to the input price. pub fn cost(&self, u: &crate::types::Usage) -> f64 { let inp = self.input_price.unwrap_or(0.0); let per = |tokens: u64, price: f64| tokens as f64 * price / 1_000_000.0; per(u.input_tokens, inp) + per(u.output_tokens, self.output_price.unwrap_or(0.0)) + per(u.cache_read_tokens, self.cache_read_price.unwrap_or(inp)) + per(u.cache_write_tokens, self.cache_write_price.unwrap_or(inp)) } } impl ModelEntry { pub fn resolved(&self) -> ModelConfig { match self { ModelEntry::Id(id) => ModelConfig { id: id.clone(), ..Default::default() }, ModelEntry::Full(m) => m.clone(), } } } #[derive(Debug, Clone, Deserialize, Serialize, Default)] pub struct PermissionConfig { #[serde(default)] pub bash_rules: BTreeMap, #[serde(flatten)] pub tools: BTreeMap, } /// Replace `${NAME}` with the environment variable (empty if unset). pub fn expand_env(s: &str) -> String { let re = regex::Regex::new(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}").unwrap(); re.replace_all(s, |c: ®ex::Captures| std::env::var(&c[1]).unwrap_or_default()) .into_owned() }