Skip to main content

ferrum_types/
runtime_config.rs

1//! Runtime configuration snapshot and small env parsing helpers.
2//!
3//! This is intentionally a narrow data surface first: it makes effective
4//! `FERRUM_*` overrides visible in health and bench artifacts while the
5//! hot-path env reads are migrated to typed config structs.
6
7use serde::{Deserialize, Serialize};
8use std::{collections::BTreeMap, path::PathBuf};
9
10/// Stable snapshot of non-default runtime configuration visible to the process.
11#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
12pub struct RuntimeConfigSnapshot {
13    /// Sorted by key for stable JSON and machine-readable diffs.
14    pub entries: Vec<RuntimeConfigEntry>,
15}
16
17impl RuntimeConfigSnapshot {
18    /// Capture all currently set `FERRUM_*` env overrides.
19    pub fn capture_current() -> Self {
20        Self::from_env_vars(std::env::vars())
21    }
22
23    /// Build a snapshot from a supplied environment map or iterator.
24    pub fn from_env_vars<I, K, V>(vars: I) -> Self
25    where
26        I: IntoIterator<Item = (K, V)>,
27        K: Into<String>,
28        V: Into<String>,
29    {
30        let mut sorted = BTreeMap::new();
31        for (key, value) in vars {
32            let key = key.into();
33            if key.starts_with("FERRUM_") {
34                sorted.insert(key, value.into());
35            }
36        }
37
38        Self {
39            entries: sorted
40                .into_iter()
41                .map(|(key, effective_value)| RuntimeConfigEntry {
42                    affects: infer_effects(&key),
43                    key,
44                    effective_value,
45                    source: RuntimeConfigSource::Env,
46                })
47                .collect(),
48        }
49    }
50
51    /// Build a stable snapshot from explicit entries. Later entries for the
52    /// same key replace earlier entries.
53    pub fn from_entries<I>(entries: I) -> Self
54    where
55        I: IntoIterator<Item = RuntimeConfigEntry>,
56    {
57        let mut sorted = BTreeMap::new();
58        for entry in entries {
59            sorted.insert(entry.key.clone(), entry);
60        }
61        Self {
62            entries: sorted.into_values().collect(),
63        }
64    }
65
66    /// Insert or replace one effective value, preserving stable key order.
67    pub fn upsert(
68        &mut self,
69        key: impl Into<String>,
70        effective_value: impl Into<String>,
71        source: RuntimeConfigSource,
72    ) {
73        self.upsert_entry(RuntimeConfigEntry::new(key, effective_value, source));
74    }
75
76    /// Insert or replace one explicit entry, preserving stable key order.
77    pub fn upsert_entry(&mut self, entry: RuntimeConfigEntry) {
78        let mut entries = std::mem::take(&mut self.entries);
79        entries.retain(|existing| existing.key != entry.key);
80        entries.push(entry);
81        *self = Self::from_entries(entries);
82    }
83
84    /// Return a snapshot with one additional effective value.
85    pub fn with_entry(
86        mut self,
87        key: impl Into<String>,
88        effective_value: impl Into<String>,
89        source: RuntimeConfigSource,
90    ) -> Self {
91        self.upsert(key, effective_value, source);
92        self
93    }
94}
95
96/// One effective config value in a runtime snapshot.
97#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
98pub struct RuntimeConfigEntry {
99    pub key: String,
100    pub effective_value: String,
101    pub source: RuntimeConfigSource,
102    pub affects: Vec<RuntimeConfigEffect>,
103}
104
105impl RuntimeConfigEntry {
106    pub fn new(
107        key: impl Into<String>,
108        effective_value: impl Into<String>,
109        source: RuntimeConfigSource,
110    ) -> Self {
111        let key = key.into();
112        Self {
113            affects: infer_effects(&key),
114            key,
115            effective_value: effective_value.into(),
116            source,
117        }
118    }
119}
120
121/// Source of an effective config value.
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
123#[serde(rename_all = "snake_case")]
124pub enum RuntimeConfigSource {
125    Default,
126    ConfigFile,
127    Cli,
128    Env,
129    ScriptCase,
130    MemoryProfile,
131}
132
133/// Impact classes used by config snapshots and artifact diffs.
134#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
135#[serde(rename_all = "snake_case")]
136pub enum RuntimeConfigEffect {
137    Correctness,
138    Performance,
139    Memory,
140    Diagnostics,
141}
142
143/// Tri-state env override used by paths that distinguish unset from forced off.
144#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
145#[serde(rename_all = "snake_case")]
146pub enum EnvTriState {
147    Default,
148    ForcedOff,
149    ForcedOn,
150}
151
152pub fn parse_bool_env_value(raw: &str) -> Result<bool, String> {
153    match raw.trim().to_ascii_lowercase().as_str() {
154        "1" | "true" | "yes" | "on" => Ok(true),
155        "0" | "false" | "no" | "off" => Ok(false),
156        other => Err(format!("invalid boolean env value: {other:?}")),
157    }
158}
159
160pub fn parse_usize_env_value(raw: &str) -> Result<usize, String> {
161    raw.trim()
162        .parse::<usize>()
163        .map_err(|_| format!("invalid integer env value: {raw:?}"))
164}
165
166pub fn parse_path_env_value(raw: &str) -> Result<PathBuf, String> {
167    let trimmed = raw.trim();
168    if trimmed.is_empty() {
169        return Err("path env value must not be empty".to_string());
170    }
171    Ok(PathBuf::from(trimmed))
172}
173
174pub fn parse_tri_state_env_value(raw: Option<&str>) -> Result<EnvTriState, String> {
175    let Some(raw) = raw else {
176        return Ok(EnvTriState::Default);
177    };
178    if raw.trim().is_empty() {
179        return Ok(EnvTriState::Default);
180    }
181    Ok(if parse_bool_env_value(raw)? {
182        EnvTriState::ForcedOn
183    } else {
184        EnvTriState::ForcedOff
185    })
186}
187
188fn infer_effects(key: &str) -> Vec<RuntimeConfigEffect> {
189    let mut effects = Vec::new();
190
191    if key.contains("DIAG")
192        || key.contains("PROF")
193        || key.contains("TRACE")
194        || key.contains("DUMP")
195        || key.contains("LOG_CONFIG")
196        || key.contains("CAPTURE")
197        || key.contains("DEBUG")
198    {
199        effects.push(RuntimeConfigEffect::Diagnostics);
200    }
201
202    if key.contains("KV")
203        || key.contains("BATCHED_TOKENS")
204        || key.contains("PAGED_MAX_SEQS")
205        || key.contains("MODEL_LEN")
206        || key.contains("MEMORY")
207    {
208        effects.push(RuntimeConfigEffect::Memory);
209    }
210
211    if key.contains("PREFIX_CACHE")
212        || key.contains("MODEL_PATH")
213        || key.contains("MODEL_LEN")
214        || key.contains("SPEC_")
215        || key.contains("REF_")
216        || key.contains("DTYPE")
217    {
218        effects.push(RuntimeConfigEffect::Correctness);
219    }
220
221    if effects.is_empty()
222        || key.contains("MOE")
223        || key.contains("VLLM")
224        || key.contains("MARLIN")
225        || key.contains("PAGED")
226        || key.contains("GRAPH")
227        || key.contains("SCHED")
228        || key.contains("BATCH")
229        || key.contains("ATTN")
230        || key.contains("FLASH")
231        || key.contains("CUDA")
232        || key.contains("TRITON")
233        || key.contains("GREEDY")
234        || key.contains("FA")
235    {
236        effects.push(RuntimeConfigEffect::Performance);
237    }
238
239    effects.sort();
240    effects.dedup();
241    effects
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn parses_boolean_values() {
250        assert_eq!(parse_bool_env_value("1").unwrap(), true);
251        assert_eq!(parse_bool_env_value("off").unwrap(), false);
252        assert!(parse_bool_env_value("maybe").is_err());
253    }
254
255    #[test]
256    fn parses_integer_values() {
257        assert_eq!(parse_usize_env_value("4096").unwrap(), 4096);
258        assert!(parse_usize_env_value("-1").is_err());
259        assert!(parse_usize_env_value("many").is_err());
260    }
261
262    #[test]
263    fn parses_path_values() {
264        assert_eq!(
265            parse_path_env_value("/tmp/model").unwrap(),
266            PathBuf::from("/tmp/model")
267        );
268        assert!(parse_path_env_value("   ").is_err());
269    }
270
271    #[test]
272    fn parses_tri_state_values() {
273        assert_eq!(
274            parse_tri_state_env_value(None).unwrap(),
275            EnvTriState::Default
276        );
277        assert_eq!(
278            parse_tri_state_env_value(Some("0")).unwrap(),
279            EnvTriState::ForcedOff
280        );
281        assert_eq!(
282            parse_tri_state_env_value(Some("on")).unwrap(),
283            EnvTriState::ForcedOn
284        );
285        assert!(parse_tri_state_env_value(Some("auto")).is_err());
286    }
287
288    #[test]
289    fn snapshot_is_sorted_and_classified() {
290        let snapshot = RuntimeConfigSnapshot::from_env_vars([
291            ("OTHER_ENV", "ignored"),
292            ("FERRUM_PREFIX_CACHE", "1"),
293            ("FERRUM_MOE_GRAPH", "1"),
294        ]);
295        let keys: Vec<_> = snapshot
296            .entries
297            .iter()
298            .map(|entry| entry.key.as_str())
299            .collect();
300        assert_eq!(keys, vec!["FERRUM_MOE_GRAPH", "FERRUM_PREFIX_CACHE"]);
301        assert_eq!(snapshot.entries[0].source, RuntimeConfigSource::Env);
302        assert!(snapshot.entries[0]
303            .affects
304            .contains(&RuntimeConfigEffect::Performance));
305        assert!(snapshot.entries[1]
306            .affects
307            .contains(&RuntimeConfigEffect::Correctness));
308    }
309
310    #[test]
311    fn upsert_preserves_non_env_source_and_stable_order() {
312        let mut snapshot = RuntimeConfigSnapshot::from_env_vars([
313            ("FERRUM_KV_DTYPE", "fp16"),
314            ("FERRUM_MOE_GRAPH", "1"),
315        ]);
316        snapshot.upsert("FERRUM_KV_DTYPE", "int8", RuntimeConfigSource::Cli);
317        snapshot.upsert(
318            "FERRUM_PROFILE_JSONL",
319            "/tmp/profile.jsonl",
320            RuntimeConfigSource::Cli,
321        );
322
323        let keys: Vec<_> = snapshot
324            .entries
325            .iter()
326            .map(|entry| entry.key.as_str())
327            .collect();
328        assert_eq!(
329            keys,
330            [
331                "FERRUM_KV_DTYPE",
332                "FERRUM_MOE_GRAPH",
333                "FERRUM_PROFILE_JSONL"
334            ]
335        );
336        let kv = snapshot
337            .entries
338            .iter()
339            .find(|entry| entry.key == "FERRUM_KV_DTYPE")
340            .unwrap();
341        assert_eq!(kv.effective_value, "int8");
342        assert_eq!(kv.source, RuntimeConfigSource::Cli);
343        assert!(kv.affects.contains(&RuntimeConfigEffect::Correctness));
344
345        let profile = snapshot
346            .entries
347            .iter()
348            .find(|entry| entry.key == "FERRUM_PROFILE_JSONL")
349            .unwrap();
350        assert_eq!(profile.source, RuntimeConfigSource::Cli);
351        assert!(profile.affects.contains(&RuntimeConfigEffect::Diagnostics));
352    }
353}