Skip to main content

superfluid_daemon/
runtime_pick.rs

1//! Which runtime serves a model.
2
3use std::collections::HashSet;
4use std::path::Path;
5use std::sync::Mutex;
6
7use crate::runtimes::Catalog;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
10pub struct RuntimeId(&'static str);
11
12impl RuntimeId {
13    pub fn new(id: &str) -> RuntimeId {
14        static IDS: Mutex<Option<HashSet<&'static str>>> = Mutex::new(None);
15        let mut ids = IDS.lock().expect("runtime ids");
16        let ids = ids.get_or_insert_with(HashSet::new);
17        if let Some(have) = ids.get(id) {
18            return RuntimeId(have);
19        }
20        let leaked: &'static str = Box::leak(id.to_string().into_boxed_str());
21        ids.insert(leaked);
22        RuntimeId(leaked)
23    }
24
25    pub fn parse(s: &str) -> Option<RuntimeId> {
26        let s = s.trim();
27        let (base, install) = match s.split_once('@') {
28            Some((b, i)) => (b, Some(i)),
29            None => (s, None),
30        };
31        (valid(base) && install.is_none_or(valid_install)).then(|| RuntimeId::new(s))
32    }
33
34    pub fn name(self) -> &'static str {
35        self.0
36    }
37
38    pub fn base(self) -> RuntimeId {
39        match self.0.split_once('@') {
40            Some((b, _)) => RuntimeId::new(b),
41            None => self,
42        }
43    }
44
45    pub fn install(self) -> Option<&'static str> {
46        self.0.split_once('@').map(|(_, i)| i)
47    }
48}
49
50impl std::fmt::Display for RuntimeId {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        f.write_str(self.0)
53    }
54}
55
56pub fn valid(s: &str) -> bool {
57    superfluid_engine::artifact::valid_id(s)
58}
59
60pub fn valid_install(s: &str) -> bool {
61    !s.is_empty() && !s.starts_with('.') && s.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '_' | '+'))
62}
63
64#[derive(Debug, Clone, Default)]
65pub struct RuntimePick {
66    global: Option<String>,
67    per_model: Vec<(String, String)>,
68}
69
70impl RuntimePick {
71    pub fn add(&mut self, value: &str) -> Result<(), String> {
72        let named = |id: &str| -> Result<String, String> {
73            let id = id.trim();
74            if RuntimeId::parse(id).is_some() {
75                Ok(id.to_string())
76            } else {
77                Err(format!(
78                    "--runtime '{id}' is not a runtime id (lowercase letters, digits, '.', '-', '_'), with an install after '@' if one"
79                ))
80            }
81        };
82        match value.split_once('=') {
83            Some((model, id)) => {
84                let model = model.trim();
85                if model.is_empty() {
86                    return Err("--runtime <model>=<id> names no model".to_string());
87                }
88                let id = named(id)?;
89                self.per_model.retain(|(m, _)| m != model);
90                self.per_model.push((model.to_string(), id));
91                Ok(())
92            }
93            None if value.trim() == "auto" => {
94                self.global = None;
95                Ok(())
96            }
97            None => {
98                self.global = Some(named(value)?);
99                Ok(())
100            }
101        }
102    }
103
104    pub fn global(&self) -> Option<&str> {
105        self.global.as_deref()
106    }
107
108    pub fn explicit(&self, path: &Path) -> Option<&str> {
109        let written = path.to_string_lossy();
110        let stem = path.file_stem().map(|s| s.to_string_lossy().into_owned());
111        let id = crate::registry::model_id_for(path);
112        let by_path = self
113            .per_model
114            .iter()
115            .rev()
116            .find(|(m, _)| *m == written || Path::new(m) == path);
117        let by_name = self.per_model.iter().rev().find(|(m, _)| Some(m) == stem.as_ref() || *m == id);
118        match by_path.or(by_name) {
119            Some((_, runtime)) => Some(runtime.as_str()).filter(|r| *r != "auto"),
120            None => self.global.as_deref(),
121        }
122    }
123
124    pub fn resolve(&self, path: &Path, catalog: &Catalog) -> Result<RuntimeId, String> {
125        self.resolve_with(path, None, catalog)
126    }
127
128    pub fn resolve_with(&self, path: &Path, requested: Option<RuntimeId>, catalog: &Catalog) -> Result<RuntimeId, String> {
129        let picked = match requested {
130            Some(id) => Some(id),
131            None => match self.explicit(path) {
132                Some(name) => Some(catalog.named(name).map_err(|why| catalog.not_here(name, path, &why))?),
133                None => None,
134            },
135        };
136        match picked {
137            Some(id) => {
138                catalog.reads(id, path)?;
139                Ok(id)
140            }
141            None => catalog.reader_of(path),
142        }
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn ids_are_interned_and_spelled_as_directories_can_be() {
152        let a = RuntimeId::new("llamacpp");
153        let b = RuntimeId::parse(" llamacpp ").unwrap();
154        assert_eq!(a, b);
155        assert!(std::ptr::eq(a.name(), b.name()), "one copy per id");
156        assert_eq!(a.to_string(), "llamacpp");
157        for bad in ["", "LlamaCpp", "a/b", "..", ".hidden", "sp ace"] {
158            assert_eq!(RuntimeId::parse(bad), None, "{bad:?}");
159        }
160        assert!(valid("llama.cpp") && valid("vllm") && valid("basert-2"));
161        let at = RuntimeId::parse("llamacpp@b11284-cuda-13.4").unwrap();
162        assert_eq!((at.base(), at.install()), (RuntimeId::new("llamacpp"), Some("b11284-cuda-13.4")));
163        assert_eq!((a.base(), a.install()), (a, None));
164        for bad in ["llamacpp@", "llamacpp@../x", "llamacpp@.hidden", "LL@b1"] {
165            assert_eq!(RuntimeId::parse(bad), None, "{bad:?}");
166        }
167    }
168
169    #[test]
170    fn per_model_picks_win_over_the_global_one_and_match_by_stem_or_path() {
171        let dir = std::env::temp_dir().join(format!("superfluid-runtime-pick-{}", std::process::id()));
172        std::fs::create_dir_all(dir.join("a")).unwrap();
173        std::fs::create_dir_all(dir.join("b")).unwrap();
174        let mine = dir.join("mine.gguf");
175        let other = dir.join("other.gguf");
176        let mut pick = RuntimePick::default();
177        pick.add("basert").unwrap();
178        pick.add("mine=llamacpp").unwrap();
179        assert_eq!(pick.explicit(&mine), Some("llamacpp"));
180        assert_eq!(pick.explicit(&other), Some("basert"));
181        let mut pick = RuntimePick::default();
182        pick.add(&format!("{}=llamacpp", other.display())).unwrap();
183        assert_eq!(pick.explicit(&other), Some("llamacpp"));
184        assert_eq!(pick.explicit(&mine), None);
185        let bundle = dir.join("a").join("model.base");
186        let gguf = dir.join("b").join("model.GGUF");
187        let mut pick = RuntimePick::default();
188        pick.add("model=basert").unwrap();
189        pick.add(&format!("{}=llamacpp", gguf.display())).unwrap();
190        assert_eq!(pick.explicit(&bundle), Some("basert"));
191        assert_eq!(pick.explicit(&gguf), Some("llamacpp"));
192        let mut pick = RuntimePick::default();
193        pick.add("mine=llamacpp").unwrap();
194        pick.add("mine=basert").unwrap();
195        assert_eq!(pick.explicit(&mine), Some("basert"));
196        let mut pick = RuntimePick::default();
197        pick.add("basert").unwrap();
198        pick.add("mine=auto").unwrap();
199        assert_eq!(pick.explicit(&mine), None);
200        assert_eq!(pick.explicit(&other), Some("basert"));
201    }
202
203    #[test]
204    fn bad_flag_values_are_named() {
205        let mut pick = RuntimePick::default();
206        let e = pick.add("Llama CPP").unwrap_err();
207        assert!(e.contains("'Llama CPP' is not a runtime id"), "{e}");
208        let e = pick.add("=llamacpp").unwrap_err();
209        assert!(e.contains("names no model"), "{e}");
210        let e = pick.add("x=No/pe").unwrap_err();
211        assert!(e.contains("'No/pe' is not a runtime id"), "{e}");
212        pick.add("vllm").unwrap();
213        assert_eq!(pick.global(), Some("vllm"));
214        pick.add("auto").unwrap();
215        assert_eq!(pick.global(), None);
216    }
217}