use serde::Deserialize; use std::path::Path; #[derive(Debug, Clone, Deserialize)] pub struct Config { pub gateway: GatewayConfig, pub storage: StorageConfig, #[serde(default)] pub catalog: CatalogConfig, #[serde(default)] pub query: QueryConfig, pub sidecar: SidecarConfig, #[serde(default)] pub ai: AiConfig, #[serde(default)] pub auth: AuthConfig, #[serde(default)] pub observability: ObservabilityConfig, #[serde(default)] pub agent: AgentSettings, } /// Phase 16.2 — background autotune agent settings. /// /// Duplicated from `vectord::agent::AgentConfig` because `shared` can't /// depend on `vectord` (vectord already depends on shared). The gateway /// copies these into the vectord config at startup. #[derive(Debug, Clone, Deserialize)] pub struct AgentSettings { #[serde(default)] pub enabled: bool, #[serde(default = "default_cycle_interval_secs")] pub cycle_interval_secs: u64, #[serde(default = "default_cooldown_secs")] pub cooldown_between_trials_secs: u64, #[serde(default = "default_min_recall")] pub min_recall: f32, #[serde(default = "default_max_trials_per_hour")] pub max_trials_per_hour: u32, } impl Default for AgentSettings { fn default() -> Self { Self { enabled: false, cycle_interval_secs: default_cycle_interval_secs(), cooldown_between_trials_secs: default_cooldown_secs(), min_recall: default_min_recall(), max_trials_per_hour: default_max_trials_per_hour(), } } } fn default_cycle_interval_secs() -> u64 { 60 } fn default_cooldown_secs() -> u64 { 30 } fn default_min_recall() -> f32 { 0.9 } fn default_max_trials_per_hour() -> u32 { 30 } #[derive(Debug, Clone, Deserialize)] pub struct GatewayConfig { #[serde(default = "default_host")] pub host: String, #[serde(default = "default_gateway_port")] pub port: u16, } #[derive(Debug, Clone, Deserialize)] pub struct StorageConfig { /// Legacy single-backend root. If `buckets` is empty, this is used to /// create an implicit `primary` bucket at this path — preserves the /// pre-federation config shape. #[serde(default = "default_storage_root")] pub root: String, /// Where profile buckets are rooted when auto-provisioned. #[serde(default = "default_profile_root")] pub profile_root: String, /// Name of the bucket used for read fallback when a target bucket is /// unreachable. If `None`, no fallback — reads fail hard. #[serde(default)] pub rescue_bucket: Option, /// Explicitly configured buckets. Empty = backward-compat single-bucket /// mode driven by `root`. #[serde(default)] pub buckets: Vec, } #[derive(Debug, Clone, Deserialize)] pub struct BucketConfig { pub name: String, pub backend: String, // "local" | "s3" /// Local filesystem root (for backend = "local") pub root: Option, /// S3 bucket name (for backend = "s3") pub bucket: Option, pub region: Option, pub endpoint: Option, /// Handle for the secrets provider — never the literal credential. pub secret_ref: Option, } #[derive(Debug, Clone, Deserialize, Default)] pub struct CatalogConfig { #[serde(default = "default_manifest_prefix")] pub manifest_prefix: String, } #[derive(Debug, Clone, Deserialize, Default)] pub struct QueryConfig { pub max_rows_per_query: Option, } #[derive(Debug, Clone, Deserialize)] pub struct SidecarConfig { #[serde(default = "default_sidecar_url")] pub url: String, } #[derive(Debug, Clone, Deserialize, Default)] pub struct AiConfig { #[serde(default = "default_embed_model")] pub embed_model: String, #[serde(default = "default_gen_model")] pub gen_model: String, #[serde(default = "default_rerank_model")] pub rerank_model: String, } #[derive(Debug, Clone, Deserialize, Default)] pub struct AuthConfig { #[serde(default)] pub enabled: bool, pub api_key: Option, } #[derive(Debug, Clone, Deserialize, Default)] pub struct ObservabilityConfig { #[serde(default = "default_exporter")] pub exporter: String, #[serde(default = "default_service_name")] pub service_name: String, } // Defaults fn default_host() -> String { "0.0.0.0".to_string() } fn default_gateway_port() -> u16 { 3100 } fn default_storage_root() -> String { "./data".to_string() } fn default_profile_root() -> String { "./data/_profiles".to_string() } fn default_manifest_prefix() -> String { "_catalog/manifests".to_string() } fn default_sidecar_url() -> String { "http://localhost:3200".to_string() } fn default_embed_model() -> String { "nomic-embed-text".to_string() } fn default_gen_model() -> String { "qwen2.5".to_string() } fn default_rerank_model() -> String { "qwen2.5".to_string() } fn default_exporter() -> String { "stdout".to_string() } fn default_service_name() -> String { "lakehouse".to_string() } impl Config { pub fn load(path: &str) -> Result { let path = Path::new(path); if !path.exists() { return Err(format!("config file not found: {}", path.display())); } let content = std::fs::read_to_string(path) .map_err(|e| format!("failed to read config: {e}"))?; toml::from_str(&content) .map_err(|e| format!("failed to parse config: {e}")) } pub fn load_or_default() -> Self { // Try lakehouse.toml in current dir, then /etc/lakehouse/lakehouse.toml for path in &["lakehouse.toml", "/etc/lakehouse/lakehouse.toml"] { if let Ok(config) = Self::load(path) { tracing::info!("loaded config from {path}"); return config; } } tracing::warn!("no config file found, using defaults"); Self::default() } } impl Default for Config { fn default() -> Self { Self { gateway: GatewayConfig { host: default_host(), port: default_gateway_port() }, storage: StorageConfig { root: default_storage_root(), profile_root: default_profile_root(), rescue_bucket: None, buckets: Vec::new(), }, catalog: CatalogConfig::default(), query: QueryConfig::default(), sidecar: SidecarConfig { url: default_sidecar_url() }, ai: AiConfig::default(), auth: AuthConfig::default(), observability: ObservabilityConfig::default(), agent: AgentSettings::default(), } } }