Skip to main content

mur_common/
local_llm.rs

1//! Location and accessors for the bundled local-model base URL.
2//!
3//! Hub starts an MLX inference server on an ephemeral port and writes its
4//! OpenAI-compatible base URL here. Agents — started by launchd and therefore
5//! NOT inheriting Hub's environment — read it back from this file.
6
7use std::path::{Path, PathBuf};
8
9/// HuggingFace repo holding bundled-free local model.
10pub const DEFAULT_LOCAL_MODEL_REPO: &str = "mlx-community/Qwen3.5-2B-MLX-4bit";
11
12/// Directory name (under `<mur_home>/models/`).
13pub const DEFAULT_LOCAL_MODEL_DIR: &str = "Qwen3.5-2B-MLX-4bit";
14
15/// Path to the local model directory under `<mur_home>/models/<model_dir>`.
16pub fn local_model_dir(mur_home: &Path, model_dir: &str) -> PathBuf {
17    mur_home.join("models").join(model_dir)
18}
19
20/// Path to the model-complete marker file (`.complete`) within the model directory.
21pub fn model_complete_marker(model_dir: &Path) -> PathBuf {
22    model_dir.join(".complete")
23}
24
25/// Path to the file holding the local model base URL, under `<mur_home>`.
26pub fn base_url_path(mur_home: &Path) -> PathBuf {
27    mur_home.join("runtime").join("local_llm.url")
28}
29
30/// Atomically write the base URL (temp file + rename).
31pub fn write_base_url(mur_home: &Path, url: &str) -> std::io::Result<()> {
32    let path = base_url_path(mur_home);
33    if let Some(parent) = path.parent() {
34        std::fs::create_dir_all(parent)?;
35    }
36    let tmp = path.with_extension("url.tmp");
37    std::fs::write(&tmp, url.as_bytes())?;
38    std::fs::rename(&tmp, &path)
39}
40
41/// Read the base URL, trimming whitespace. `None` if absent/empty.
42pub fn read_base_url(mur_home: &Path) -> Option<String> {
43    let s = std::fs::read_to_string(base_url_path(mur_home)).ok()?;
44    let t = s.trim();
45    if t.is_empty() {
46        None
47    } else {
48        Some(t.to_string())
49    }
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55    use tempfile::TempDir;
56
57    #[test]
58    fn write_then_read_roundtrips() {
59        let tmp = TempDir::new().unwrap();
60        assert_eq!(read_base_url(tmp.path()), None);
61        write_base_url(tmp.path(), "http://127.0.0.1:50321/v1").unwrap();
62        assert_eq!(
63            read_base_url(tmp.path()),
64            Some("http://127.0.0.1:50321/v1".to_string())
65        );
66    }
67
68    #[test]
69    fn blank_file_reads_as_none() {
70        let tmp = TempDir::new().unwrap();
71        write_base_url(tmp.path(), "   \n").unwrap();
72        assert_eq!(read_base_url(tmp.path()), None);
73    }
74
75    #[test]
76    fn model_dir_and_marker_paths() {
77        let home = Path::new("/tmp/murhome");
78        let dir = local_model_dir(home, DEFAULT_LOCAL_MODEL_DIR);
79        assert_eq!(dir, home.join("models").join("Qwen3.5-2B-MLX-4bit"));
80        assert_eq!(model_complete_marker(&dir), dir.join(".complete"));
81    }
82}