1use serde::{Deserialize, Serialize};
8use std::{collections::BTreeMap, path::PathBuf};
9
10#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
12pub struct RuntimeConfigSnapshot {
13 pub entries: Vec<RuntimeConfigEntry>,
15}
16
17impl RuntimeConfigSnapshot {
18 pub fn capture_current() -> Self {
20 Self::from_env_vars(std::env::vars())
21 }
22
23 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 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 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 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 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#[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#[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#[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#[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}