1use std::collections::HashMap;
12use std::path::{Path, PathBuf};
13use std::time::Duration;
14
15use serde::{Deserialize, Serialize};
16
17use crate::error::Result;
18
19#[derive(Debug, Clone, Default, Serialize, Deserialize)]
20#[serde(default)]
21pub struct Config {
22 pub cluster: ClusterConfig,
23 pub router: RouterConfig,
24 pub agent: AgentConfig,
25 pub engine: EngineConfig,
26 pub kv: KvConfig,
27 pub security: SecurityConfig,
28 pub metrics: MetricsConfig,
29 pub auth: AuthConfig,
30 pub models: HashMap<String, ModelConfig>,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
38#[serde(default)]
39pub struct ClusterConfig {
40 pub name: String,
41 pub state_backend: StateBackend,
42 pub etcd_endpoints: Vec<String>,
43 pub gossip_seeds: Vec<String>,
44}
45impl Default for ClusterConfig {
46 fn default() -> Self {
47 Self {
48 name: "cognitora".into(),
49 state_backend: StateBackend::Etcd,
50 etcd_endpoints: vec![],
51 gossip_seeds: vec![],
52 }
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
57#[serde(rename_all = "lowercase")]
58pub enum StateBackend {
59 Etcd,
60 Gossip,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
68#[serde(default)]
69pub struct RouterConfig {
70 pub listen_http: String,
72 pub listen_grpc: String,
74 pub listen_admin: String,
76 pub node_id: String,
77 pub score_weights: ScoreWeights,
78 pub admission: AdmissionConfig,
79 pub rate_limit: RateLimitConfig,
80 pub cascade: CascadeConfig,
81 pub disagg: DisaggConfig,
82 pub federation: FederationConfig,
83 pub autoscaler: AutoscalerConfig,
84}
85impl Default for RouterConfig {
86 fn default() -> Self {
87 Self {
88 listen_http: format!("0.0.0.0:{}", crate::ports::ROUTER_HTTP),
89 listen_grpc: format!("0.0.0.0:{}", crate::ports::ROUTER_GRPC),
90 listen_admin: format!("0.0.0.0:{}", crate::ports::ROUTER_ADMIN),
91 node_id: default_node_id("router"),
92 score_weights: ScoreWeights::default(),
93 admission: AdmissionConfig::default(),
94 rate_limit: RateLimitConfig::default(),
95 cascade: CascadeConfig::default(),
96 disagg: DisaggConfig::default(),
97 federation: FederationConfig::default(),
98 autoscaler: AutoscalerConfig::default(),
99 }
100 }
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
104#[serde(default)]
105pub struct ScoreWeights {
106 pub kv: f32,
107 pub load: f32,
108 pub power: f32,
109 pub capacity: f32,
110}
111impl Default for ScoreWeights {
112 fn default() -> Self {
113 Self {
114 kv: 0.55,
115 load: 0.25,
116 power: 0.10,
117 capacity: 0.10,
118 }
119 }
120}
121impl ScoreWeights {
122 pub fn validate(&self) -> Result<()> {
124 let sum = self.kv + self.load + self.power + self.capacity;
125 if (sum - 1.0).abs() > 0.01 {
126 return Err(crate::Error::Config(format!(
127 "router.score_weights must sum to 1.0 (got {sum:.3})"
128 )));
129 }
130 Ok(())
131 }
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
135#[serde(default)]
136pub struct AdmissionConfig {
137 pub max_queue: u32,
138 #[serde(with = "humantime_serde")]
139 pub ttft_slo: Duration,
140 pub max_concurrent_per_replica: u32,
141}
142impl Default for AdmissionConfig {
143 fn default() -> Self {
144 Self {
145 max_queue: 1024,
146 ttft_slo: Duration::from_millis(800),
147 max_concurrent_per_replica: 16,
148 }
149 }
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
153#[serde(default)]
154pub struct RateLimitConfig {
155 pub rps: u32,
156 pub burst: u32,
157 pub redis_url: Option<String>,
158}
159impl Default for RateLimitConfig {
160 fn default() -> Self {
161 Self {
162 rps: 50,
163 burst: 200,
164 redis_url: None,
165 }
166 }
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
170#[serde(default)]
171pub struct CascadeConfig {
172 pub enabled: bool,
174 pub confidence_threshold: f32,
176}
177impl Default for CascadeConfig {
178 fn default() -> Self {
179 Self {
180 enabled: false,
181 confidence_threshold: -1.5,
182 }
183 }
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize)]
187#[serde(default)]
188pub struct DisaggConfig {
189 pub enabled: bool,
191 pub colocate_below_tokens: u32,
193}
194impl Default for DisaggConfig {
195 fn default() -> Self {
196 Self {
197 enabled: false,
198 colocate_below_tokens: 256,
199 }
200 }
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize)]
204#[serde(default)]
205#[derive(Default)]
206pub struct FederationConfig {
207 pub enabled: bool,
209 pub peers: Vec<String>,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize)]
214#[serde(default)]
215pub struct AutoscalerConfig {
216 pub enabled: bool,
219 pub idle_util_pct: f32,
221 pub high_watt_threshold: f32,
223 pub deadline_admission: bool,
226}
227impl Default for AutoscalerConfig {
228 fn default() -> Self {
229 Self {
230 enabled: false,
231 idle_util_pct: 15.0,
232 high_watt_threshold: 350.0,
233 deadline_admission: false,
234 }
235 }
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize)]
243#[serde(default)]
244pub struct AgentConfig {
245 pub listen: String,
246 pub role: NodeRoleCfg,
247 pub node_id: String,
248 pub kv_uds: PathBuf,
249 pub gpu_index: Option<u32>,
250
251 #[serde(default)]
254 pub vllm_url: Option<String>,
255 #[serde(default)]
256 pub vllm_cmd: Option<Vec<String>>,
257}
258impl Default for AgentConfig {
259 fn default() -> Self {
260 Self {
261 listen: format!("0.0.0.0:{}", crate::ports::AGENT_GRPC),
262 role: NodeRoleCfg::Both,
263 node_id: default_node_id("agent"),
264 kv_uds: PathBuf::from("/run/cognitora/kv.sock"),
265 gpu_index: None,
266 vllm_url: None,
267 vllm_cmd: None,
268 }
269 }
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize)]
292#[serde(default)]
293pub struct EngineConfig {
294 pub kind: EngineKind,
295 pub url: String,
297 pub kv_offload: KvOffload,
301 pub vllm: VllmEngineConfig,
303 pub sglang: SglangEngineConfig,
305 pub llama_cpp: LlamaCppEngineConfig,
307}
308impl Default for EngineConfig {
309 fn default() -> Self {
310 Self {
311 kind: EngineKind::Vllm,
312 url: format!("http://127.0.0.1:{}", crate::ports::VLLM_HTTP),
313 kv_offload: KvOffload::None,
314 vllm: VllmEngineConfig::default(),
315 sglang: SglangEngineConfig::default(),
316 llama_cpp: LlamaCppEngineConfig::default(),
317 }
318 }
319}
320
321#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
322#[serde(rename_all = "snake_case")]
323pub enum EngineKind {
324 Vllm,
325 Sglang,
326 LlamaCpp,
327 OpenaiCompat,
328}
329
330#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
351#[serde(rename_all = "snake_case")]
352pub enum KvOffload {
353 #[default]
356 None,
357 Nixl,
360 Lmcache,
364 Hicache,
367 Kvbm,
371}
372
373impl KvOffload {
374 pub fn as_str(self) -> &'static str {
376 match self {
377 Self::None => "none",
378 Self::Nixl => "nixl",
379 Self::Lmcache => "lmcache",
380 Self::Hicache => "hicache",
381 Self::Kvbm => "kvbm",
382 }
383 }
384}
385
386#[derive(Debug, Clone, Serialize, Deserialize)]
387#[serde(default)]
388pub struct VllmEngineConfig {
389 pub binary: String,
391 pub extra_args: Vec<String>,
394}
395impl Default for VllmEngineConfig {
396 fn default() -> Self {
397 Self {
398 binary: "vllm".into(),
399 extra_args: vec!["--enable-chunked-prefill".into()],
400 }
401 }
402}
403
404#[derive(Debug, Clone, Serialize, Deserialize)]
408#[serde(default)]
409pub struct SglangEngineConfig {
410 pub binary: String,
413 pub host: String,
415 pub port: u16,
417 pub context_length: u32,
420 pub mem_fraction_static: f32,
423 pub extra_args: Vec<String>,
425}
426impl Default for SglangEngineConfig {
427 fn default() -> Self {
428 Self {
429 binary: "python".into(),
430 host: "127.0.0.1".into(),
431 port: crate::ports::VLLM_HTTP,
432 context_length: 4096,
433 mem_fraction_static: 0.85,
434 extra_args: vec![],
435 }
436 }
437}
438
439#[derive(Debug, Clone, Serialize, Deserialize)]
440#[serde(default)]
441pub struct LlamaCppEngineConfig {
442 pub binary: String,
445 pub mode: LlamaCppMode,
448 pub host: String,
449 pub port: u16,
450 pub n_ctx: u32,
452 pub n_threads: u32,
454 pub n_gpu_layers: i32,
457 pub extra_args: Vec<String>,
459}
460impl Default for LlamaCppEngineConfig {
461 fn default() -> Self {
462 Self {
463 binary: "python".into(),
464 mode: LlamaCppMode::PythonServer,
465 host: "127.0.0.1".into(),
466 port: crate::ports::VLLM_HTTP,
467 n_ctx: 4096,
468 n_threads: 4,
469 n_gpu_layers: 0,
470 extra_args: vec![],
471 }
472 }
473}
474
475#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
476#[serde(rename_all = "snake_case")]
477pub enum LlamaCppMode {
478 PythonServer,
479 Binary,
480}
481
482#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
483#[serde(rename_all = "lowercase")]
484pub enum NodeRoleCfg {
485 Decode,
486 Prefill,
487 Both,
488}
489
490#[derive(Debug, Clone, Serialize, Deserialize)]
495#[serde(default)]
496pub struct KvConfig {
497 pub listen: String,
498 pub uds: PathBuf,
499 pub ram_gib: u32,
500 pub ssd_dir: PathBuf,
501 pub ssd_gib: u32,
502 pub index_dir: PathBuf,
503 pub transport: KvTransport,
504 pub quic_listen: String,
505 pub block_size_tokens: u32,
506}
507impl Default for KvConfig {
508 fn default() -> Self {
509 Self {
510 listen: format!("0.0.0.0:{}", crate::ports::KV_GRPC),
511 uds: PathBuf::from("/run/cognitora/kv.sock"),
512 ram_gib: 32,
513 ssd_dir: PathBuf::from("/var/lib/cognitora/kv"),
514 ssd_gib: 1024,
515 index_dir: PathBuf::from("/var/lib/cognitora/index"),
516 transport: KvTransport::Quic,
517 quic_listen: format!("0.0.0.0:{}", crate::ports::KV_QUIC),
518 block_size_tokens: 16,
519 }
520 }
521}
522
523#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
524#[serde(rename_all = "lowercase")]
525pub enum KvTransport {
526 Quic,
527 Rdma,
528}
529
530#[derive(Debug, Clone, Default, Serialize, Deserialize)]
535#[serde(default)]
536pub struct SecurityConfig {
537 pub ca_file: Option<PathBuf>,
538 pub cert_file: Option<PathBuf>,
539 pub key_file: Option<PathBuf>,
540 pub require_mtls: bool,
541}
542
543#[derive(Debug, Clone, Default, Serialize, Deserialize)]
548#[serde(default)]
549pub struct AuthConfig {
550 pub enabled: bool,
553 pub oidc_issuer: Option<String>,
554 pub oidc_audience: Option<String>,
555 pub api_keys_file: Option<PathBuf>,
556}
557
558#[derive(Debug, Clone, Serialize, Deserialize)]
563#[serde(default)]
564pub struct MetricsConfig {
565 pub listen: String,
566 pub redfish_url: Option<String>,
567 pub redfish_user: Option<String>,
568 pub redfish_password: Option<String>,
569 pub ipmi_fallback: bool,
570 #[serde(with = "humantime_serde")]
571 pub scrape_interval: Duration,
572}
573impl Default for MetricsConfig {
574 fn default() -> Self {
575 Self {
576 listen: format!("0.0.0.0:{}", crate::ports::METRICS_HTTP),
577 redfish_url: None,
578 redfish_user: None,
579 redfish_password: None,
580 ipmi_fallback: false,
581 scrape_interval: Duration::from_secs(15),
582 }
583 }
584}
585
586#[derive(Debug, Clone, Serialize, Deserialize)]
591#[serde(default)]
592pub struct ModelConfig {
593 pub cascade: Vec<String>,
594 pub prefill_replicas: u32,
595 pub decode_replicas: u32,
596 pub tp: u32,
597 pub max_model_len: Option<u32>,
598 pub extra_args: Vec<String>,
599 pub path: Option<PathBuf>,
603}
604impl Default for ModelConfig {
605 fn default() -> Self {
606 Self {
607 cascade: vec![],
608 prefill_replicas: 1,
609 decode_replicas: 2,
610 tp: 1,
611 max_model_len: None,
612 extra_args: vec![],
613 path: None,
614 }
615 }
616}
617
618impl Config {
623 pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
625 let path = path.as_ref();
626 if !path.exists() {
627 tracing::warn!(path = %path.display(), "config file not found, using defaults");
628 return Ok(Self::default());
629 }
630 let data = std::fs::read_to_string(path)?;
631 let cfg: Self = toml::from_str(&data).map_err(|e| crate::Error::Config(e.to_string()))?;
632 cfg.validate()?;
633 Ok(cfg)
634 }
635
636 pub fn locate(arg: Option<&Path>) -> PathBuf {
638 if let Some(p) = arg {
639 return p.to_path_buf();
640 }
641 if let Ok(env) = std::env::var("CGN_CONFIG") {
642 return PathBuf::from(env);
643 }
644 PathBuf::from(crate::DEFAULT_CONFIG_PATH)
645 }
646
647 fn validate(&self) -> Result<()> {
648 self.router.score_weights.validate()?;
649 Ok(())
650 }
651}
652
653fn default_node_id(role: &str) -> String {
654 format!(
655 "{}-{}-{}",
656 hostname_or(role),
657 role,
658 &uuid::Uuid::new_v4().simple().to_string()[..8]
659 )
660}
661
662fn hostname_or(default: &str) -> String {
663 if let Ok(h) = std::env::var("HOSTNAME") {
664 if !h.is_empty() {
665 return h;
666 }
667 }
668 if let Ok(s) = std::fs::read_to_string("/etc/hostname") {
669 let trimmed = s.trim();
670 if !trimmed.is_empty() {
671 return trimmed.to_string();
672 }
673 }
674 default.to_string()
675}
676
677#[cfg(test)]
678mod tests {
679 use super::*;
680 use tempfile::TempDir;
681
682 #[test]
683 fn missing_file_yields_default() {
684 let cfg = Config::load("/no/such/path/cognitora.toml").unwrap();
685 assert_eq!(cfg.cluster.name, "cognitora");
686 assert_eq!(cfg.router.score_weights.kv, 0.55);
687 }
688
689 #[test]
690 fn parses_minimal_toml() {
691 let dir = TempDir::new().unwrap();
692 let p = dir.path().join("cognitora.toml");
693 std::fs::write(
694 &p,
695 r#"
696[cluster]
697name = "prod-eu"
698
699[router.score_weights]
700kv = 0.6
701load = 0.2
702power = 0.1
703capacity = 0.1
704 "#,
705 )
706 .unwrap();
707 let cfg = Config::load(&p).unwrap();
708 assert_eq!(cfg.cluster.name, "prod-eu");
709 assert!((cfg.router.score_weights.kv - 0.6).abs() < 1e-6);
710 }
711
712 #[test]
713 fn weights_must_sum_to_one() {
714 let dir = TempDir::new().unwrap();
715 let p = dir.path().join("c.toml");
716 std::fs::write(
717 &p,
718 r#"
719[router.score_weights]
720kv = 0.9
721load = 0.2
722power = 0.1
723capacity = 0.1
724 "#,
725 )
726 .unwrap();
727 assert!(Config::load(&p).is_err());
728 }
729}