Skip to main content

superfluid_daemon/
speculate_resolve.rs

1//! `--speculate` without knowing drafter formats.
2
3use std::cmp::Reverse;
4use std::io::Read;
5use std::path::{Component, Path, PathBuf};
6use std::sync::atomic::{AtomicBool, Ordering};
7
8const PATH_STRATEGIES: [&str; 4] = ["dflash", "dspark", "eagle3", "draft-model"];
9
10const AUTO_KINDS: [&str; 3] = ["dspark", "dflash", "eagle3"];
11
12const HUB_DEPTH: usize = 3;
13
14const CATALOG_URL: &str = "https://raw.githubusercontent.com/basecompute/baseRT/main/base-convert/crates/base-hub/catalog.json";
15
16const CATALOG_MAX_AGE_SECS: u64 = 24 * 3600;
17
18static FETCH_CATALOG: AtomicBool = AtomicBool::new(false);
19
20/// Whether a stale catalog may be refreshed over the network (`serve` without `--offline`).
21pub fn allow_catalog_fetch(yes: bool) {
22    FETCH_CATALOG.store(yes, Ordering::Relaxed);
23}
24
25#[derive(serde::Deserialize, Default)]
26struct CatalogFile {
27    #[serde(default)]
28    models: Vec<CatalogRow>,
29}
30
31#[derive(serde::Deserialize, Clone)]
32struct CatalogRow {
33    id: String,
34    #[serde(default)]
35    file: String,
36    #[serde(default)]
37    speculator_for: Option<String>,
38    #[serde(default)]
39    speculator_rank: Option<u32>,
40    #[serde(default)]
41    quant: String,
42    #[serde(default)]
43    size: Option<u64>,
44    #[serde(default)]
45    sha256: Option<String>,
46}
47
48#[derive(Default)]
49pub struct Pairings {
50    rows: Vec<CatalogRow>,
51    digests: Option<PathBuf>,
52}
53
54impl Pairings {
55    pub fn load(hub: Option<&Path>) -> Self {
56        let cache = crate::runtimes::default_home().join("cache");
57        let mut p = Self::from_json(&catalog_json(&cache.join("hub-catalog.json"), FETCH_CATALOG.load(Ordering::Relaxed)));
58        if let Some(theirs) = hub.and_then(|h| std::fs::read_to_string(h.join(".catalog-cache.json")).ok()) {
59            p.rows.extend(Self::from_json(&theirs).rows);
60        }
61        p.digests = Some(cache.join("sha256"));
62        p
63    }
64
65    pub fn from_json(json: &str) -> Self {
66        Self { rows: serde_json::from_str::<CatalogFile>(json).map(|c| c.models).unwrap_or_default(), digests: None }
67    }
68
69    fn pinned_row(&self, path: &Path) -> Option<&CatalogRow> {
70        let name = path.file_name()?.to_str()?;
71        let pinned = |r: &&CatalogRow| r.sha256.as_deref().is_some_and(|d| !d.is_empty());
72        if name == "model.base" {
73            let id = self.id_of(path)?;
74            let variant = path.parent()?.file_name()?.to_str()?;
75            return self.rows.iter().filter(pinned).find(|r| r.id == id && r.quant == variant);
76        }
77        self.rows.iter().filter(pinned).find(|r| r.file == name)
78    }
79
80    /// Why a bundle the catalog pins may not be used: its bytes are not the ones the catalog
81    /// lists, which is a partial or corrupt download, or another file under a catalog name.
82    pub fn digest_refusal(&self, path: &Path) -> Option<String> {
83        let row = self.pinned_row(path)?;
84        let want = row.sha256.as_deref()?.to_ascii_lowercase();
85        let meta = std::fs::metadata(path).ok()?;
86        let what = format!("{} {}", row.id, if row.quant.is_empty() { &row.file } else { &row.quant });
87        let got = match row.size {
88            Some(size) if size != meta.len() => format!("{} bytes, not {size}", meta.len()),
89            _ => match file_sha256(path, &meta, self.digests.as_deref())? {
90                got if got == want => return None,
91                got => format!("sha256 {got}, not {want}"),
92            },
93        };
94        Some(format!(
95            "{} is not the catalog's {what} ({got}); pull it again, or rename it if it is a conversion of your own",
96            path.display()
97        ))
98    }
99
100    fn id_of(&self, path: &Path) -> Option<String> {
101        let name = path.file_name()?.to_str()?;
102        if name == "model.base" {
103            let repo = path.parent()?.parent()?;
104            let ns = repo.parent()?.file_name()?.to_str()?;
105            return Some(format!("{ns}/{}", repo.file_name()?.to_str()?));
106        }
107        self.rows.iter().find(|r| r.file == name).map(|r| r.id.clone())
108    }
109
110    fn rank(&self, drafter: &Path, target_id: &str) -> Option<u32> {
111        let id = self.id_of(drafter)?;
112        let rows = || self.rows.iter().filter(|r| r.id == id);
113        if !rows().any(|r| r.speculator_for.as_deref() == Some(target_id)) {
114            return None;
115        }
116        Some(rows().find_map(|r| r.speculator_rank).unwrap_or(u32::MAX))
117    }
118}
119
120/// The hub catalog kept at `own`, refreshed from `CATALOG_URL` once a day when `fetch` allows.
121fn catalog_json(own: &Path, fetch: bool) -> String {
122    let age = std::fs::metadata(own).and_then(|m| m.modified()).ok().and_then(|t| t.elapsed().ok());
123    if fetch && age.is_none_or(|a| a.as_secs() >= CATALOG_MAX_AGE_SECS) {
124        if let Ok(v) = superfluid_adapter_kit::install::fetch_json(CATALOG_URL) {
125            let body = v.to_string();
126            if own.parent().is_some_and(|d| std::fs::create_dir_all(d).is_ok()) && std::fs::write(own, &body).is_ok() {
127                return body;
128            }
129        }
130    }
131    std::fs::read_to_string(own).unwrap_or_default()
132}
133
134/// The file's sha256, remembered under `cache` against its device, inode, size, mtime and ctime
135/// so a multi-gigabyte bundle is hashed once rather than at every start. `None` if unreadable.
136fn file_sha256(path: &Path, meta: &std::fs::Metadata, cache: Option<&Path>) -> Option<String> {
137    use sha2::Digest;
138    use std::os::unix::ffi::OsStrExt;
139    use std::os::unix::fs::MetadataExt;
140    let stamp = |m: &std::fs::Metadata| {
141        format!(
142            "{} {} {} {}.{} {}.{}",
143            m.dev(), m.ino(), m.len(), m.mtime(), m.mtime_nsec(), m.ctime(), m.ctime_nsec()
144        )
145    };
146    let hex = |d: &[u8]| d.iter().map(|b| format!("{b:02x}")).collect::<String>();
147    let before = stamp(meta);
148    let entry = cache.and_then(|dir| {
149        let canon = path.canonicalize().ok()?;
150        Some(dir.join(hex(&sha2::Sha256::digest(canon.as_os_str().as_bytes()))))
151    });
152    if let Some(remembered) = entry.as_ref().and_then(|e| std::fs::read_to_string(e).ok()) {
153        if let Some((st, digest)) = remembered.trim().split_once('\n') {
154            if st == before {
155                return Some(digest.to_string());
156            }
157        }
158    }
159    let mut f = std::fs::File::open(path).ok()?;
160    if meta.len() >= 1 << 30 {
161        eprintln!("superfluid: checking {} against the hub catalog's sha256 (once per file)", path.display());
162    }
163    let mut h = sha2::Sha256::new();
164    let mut buf = vec![0u8; 1 << 20];
165    loop {
166        match f.read(&mut buf) {
167            Ok(0) => break,
168            Ok(n) => h.update(&buf[..n]),
169            Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {}
170            Err(_) => return None,
171        }
172    }
173    let digest = hex(&h.finalize());
174    let unchanged = std::fs::metadata(path).is_ok_and(|m| stamp(&m) == before);
175    if let (Some(entry), true) = (entry, unchanged) {
176        if let Some(dir) = entry.parent() {
177            let tmp = entry.with_extension(format!("tmp{}", std::process::id()));
178            let wrote = std::fs::create_dir_all(dir).is_ok()
179                && std::fs::write(&tmp, format!("{before}\n{digest}\n")).is_ok()
180                && std::fs::rename(&tmp, &entry).is_ok();
181            if !wrote {
182                let _ = std::fs::remove_file(&tmp);
183            }
184        }
185    }
186    Some(digest)
187}
188
189pub fn read_base_header(path: &Path) -> Option<serde_json::Value> {
190    let mut f = std::fs::File::open(path).ok()?;
191    let mut prefix = [0u8; 16];
192    f.read_exact(&mut prefix).ok()?;
193    if &prefix[..4] != b"BASE" || u32::from_le_bytes(prefix[4..8].try_into().ok()?) != 1 {
194        return None;
195    }
196    let len = u64::from_le_bytes(prefix[8..16].try_into().ok()?);
197    if len == 0 || len > (256 << 20) {
198        return None;
199    }
200    let mut json = vec![0u8; len as usize];
201    f.read_exact(&mut json).ok()?;
202    serde_json::from_slice(&json).ok()
203}
204
205pub fn header_speculator_kind(h: &serde_json::Value) -> Option<&str> {
206    if let Some(k) = h
207        .get("speculator")
208        .and_then(|s| s.get("config"))
209        .and_then(|c| c.get("kind"))
210        .and_then(|k| k.as_str())
211    {
212        return Some(k);
213    }
214    if h.get("arch").and_then(|a| a.as_str()) != Some("glm_dsa") {
215        return None;
216    }
217    let layers = h
218        .get("config")
219        .and_then(|c| c.get("num_hidden_layers"))
220        .and_then(|n| n.as_u64())?;
221    let want = format!("layers.{layers}.eh_proj.weight");
222    h.get("tensors")
223        .and_then(|t| t.as_array())?
224        .iter()
225        .any(|t| t.get("name").and_then(|n| n.as_str()) == Some(want.as_str()))
226        .then_some("mtp")
227}
228
229pub fn strategy_for_drafter_header(h: &serde_json::Value) -> &'static str {
230    match h.get("arch").and_then(|v| v.as_str()).unwrap_or("") {
231        "dflash" => "dflash",
232        "dspark" => "dspark",
233        "eagle3" => "eagle3",
234        _ => "draft-model",
235    }
236}
237
238pub fn resolve(directive: &str, target: &Path) -> Result<Option<String>, String> {
239    let d = directive.trim();
240    if d == "off" || d.is_empty() {
241        return Ok(None);
242    }
243    if d == "prompt-lookup" {
244        return Ok(Some(d.to_string()));
245    }
246    if d == "mtp-head" {
247        if let Some(h) = read_base_header(target) {
248            let kind = header_speculator_kind(&h);
249            if kind != Some("mtp") {
250                let built = h
251                    .get("baserT_version")
252                    .and_then(|v| v.as_str())
253                    .map(|v| format!(" (converted by baseRT {v})"))
254                    .unwrap_or_default();
255                return Err(match kind {
256                    None => format!(
257                        "--speculate mtp-head: {} carries no speculator head{built}; reconvert it with \
258                         base-convert >= 0.2.5 from a checkpoint that ships an MTP head, or use \
259                         --speculate auto to pair an installed drafter",
260                        target.display()
261                    ),
262                    Some(k) => format!(
263                        "--speculate mtp-head: {}'s speculator head is `{k}`, not an MTP head",
264                        target.display()
265                    ),
266                });
267            }
268        }
269        return Ok(Some(d.to_string()));
270    }
271    if let Some((kind, path)) = d.split_once(':') {
272        if PATH_STRATEGIES.contains(&kind) {
273            let path = expand_tilde(path);
274            if let Some(why) = catalog_refusal(Path::new(&path)) {
275                return Err(format!("--speculate {d}: {why}"));
276            }
277            return Ok(Some(format!("{kind}:{path}")));
278        }
279    }
280    if d == "auto" {
281        return resolve_all(d, target).map(|ranked| ranked.into_iter().next());
282    }
283    let d = expand_tilde(d);
284    let d = d.as_str();
285    let p = Path::new(d);
286    if p.is_file() {
287        let h = read_base_header(p)
288            .ok_or_else(|| format!("--speculate {d}: not a .base bundle (convert the drafter first)"))?;
289        if let Some(why) = catalog_refusal(p) {
290            return Err(format!("--speculate {d}: {why}"));
291        }
292        return Ok(Some(format!("{}:{}", strategy_for_drafter_header(&h), d)));
293    }
294    Err(format!(
295        "--speculate {d}: expected auto | prompt-lookup | mtp-head | a drafter .base path | \
296         dflash/dspark/eagle3/draft-model:<path>"
297    ))
298}
299
300pub fn resolve_all(directive: &str, target: &Path) -> Result<Vec<String>, String> {
301    let d = directive.trim();
302    if d != "auto" {
303        return resolve(d, target).map(|r| r.into_iter().collect());
304    }
305    let h = read_base_header(target)
306        .ok_or_else(|| format!("--speculate auto: cannot read the bundle header of {}", target.display()))?;
307    let head = header_speculator_kind(&h);
308    let mut ranked = Vec::new();
309    if head == Some("mtp") {
310        ranked.push("mtp-head".to_string());
311    }
312    let pairings = Pairings::load(hub_roots().first().map(PathBuf::as_path));
313    ranked.extend(
314        find_drafters(target, &h, &auto_search_dirs(target), &pairings)
315            .into_iter()
316            .map(|(kind, p)| format!("{kind}:{}", p.display())),
317    );
318    Ok(ranked)
319}
320
321fn catalog_refusal(path: &Path) -> Option<String> {
322    if !path.is_file() {
323        return None;
324    }
325    Pairings::load(hub_roots().first().map(PathBuf::as_path)).digest_refusal(path)
326}
327
328fn expand_tilde(path: &str) -> String {
329    let rest = match path.strip_prefix('~') {
330        Some("") => "",
331        Some(r) if r.starts_with('/') => r,
332        _ => return path.to_string(),
333    };
334    match std::env::var_os("HOME").filter(|h| !h.is_empty()) {
335        Some(home) => format!("{}{rest}", Path::new(&home).display()),
336        None => path.to_string(),
337    }
338}
339
340#[derive(Debug, Clone, Copy, PartialEq, Eq)]
341pub enum SpecLoad {
342    EmbeddedHead,
343    Sidecar,
344    DraftModel,
345}
346
347pub fn sizing_path(resolved: &str, target: &Path) -> Option<(PathBuf, SpecLoad)> {
348    if resolved == "mtp-head" {
349        return Some((target.to_path_buf(), SpecLoad::EmbeddedHead));
350    }
351    match resolved.split_once(':') {
352        Some(("draft-model", path)) => Some((PathBuf::from(path), SpecLoad::DraftModel)),
353        Some((kind, path)) if PATH_STRATEGIES.contains(&kind) => Some((PathBuf::from(path), SpecLoad::Sidecar)),
354        _ => None,
355    }
356}
357
358pub fn auto_search_dirs(target: &Path) -> Vec<(PathBuf, usize)> {
359    let dir = match target.parent() {
360        Some(p) if !p.as_os_str().is_empty() => p.to_path_buf(),
361        _ => PathBuf::from("."),
362    };
363    let mut dirs = vec![(dir.clone(), 0), (dir.join("drafters"), 0)];
364    let up = match dir.parent() {
365        Some(p) if !p.as_os_str().is_empty() => Some(p.to_path_buf()),
366        Some(_) if matches!(dir.components().next_back(), Some(Component::Normal(_))) => Some(PathBuf::from(".")),
367        Some(_) => Some(dir.join("..")),
368        None => None,
369    };
370    if let Some(up) = up {
371        dirs.push((up.join("drafters"), 0));
372    }
373    for hub in hub_roots() {
374        dirs.push((hub, HUB_DEPTH));
375    }
376    dirs
377}
378
379fn hub_roots() -> Vec<PathBuf> {
380    let env = |k| std::env::var_os(k).filter(|v| !v.is_empty());
381    hub_roots_from(env("BASERT_MODELS_DIR"), env("HOME"), env("XDG_CACHE_HOME"))
382}
383
384fn hub_roots_from(
385    models_dir: Option<std::ffi::OsString>,
386    home: Option<std::ffi::OsString>,
387    xdg_cache: Option<std::ffi::OsString>,
388) -> Vec<PathBuf> {
389    if let Some(v) = models_dir {
390        return vec![PathBuf::from(v)];
391    }
392    let Some(home) = home.map(PathBuf::from) else { return vec![] };
393    let platform = if cfg!(target_os = "macos") {
394        home.join("Library/Caches")
395    } else {
396        xdg_cache.map(PathBuf::from).filter(|p| p.is_absolute()).unwrap_or_else(|| home.join(".cache"))
397    };
398    let mut roots = vec![platform.join("baseRT/models")];
399    let legacy = home.join(".cache/baseRT/models");
400    if !roots.contains(&legacy) {
401        roots.push(legacy);
402    }
403    roots
404}
405
406pub fn find_drafter(
407    target: &Path,
408    th: &serde_json::Value,
409    dirs: &[(PathBuf, usize)],
410    pairings: &Pairings,
411) -> Option<(&'static str, PathBuf)> {
412    find_drafters(target, th, dirs, pairings).into_iter().next()
413}
414
415pub fn find_drafters(
416    target: &Path,
417    th: &serde_json::Value,
418    dirs: &[(PathBuf, usize)],
419    pairings: &Pairings,
420) -> Vec<(&'static str, PathBuf)> {
421    let target_names = bundle_names(target, th);
422    let target_id = pairings.id_of(target);
423    let target_bits = storage_bits(th);
424    let mut files = Vec::new();
425    for (dir, depth) in dirs {
426        collect_bundles(dir, *depth, &mut files);
427    }
428    let self_path = target.canonicalize().ok();
429    let mut seen = std::collections::HashSet::new();
430    let mut ranked: Vec<(_, &'static str, PathBuf)> = Vec::new();
431    for path in files {
432        let canon = path.canonicalize().unwrap_or_else(|_| path.clone());
433        if Some(&canon) == self_path.as_ref() || !seen.insert(canon) {
434            continue;
435        }
436        let Some(dh) = read_base_header(&path) else { continue };
437        let kind = strategy_for_drafter_header(&dh);
438        let Some(kind_rank) = AUTO_KINDS.iter().position(|k| *k == kind) else { continue };
439        if !fits(th, &dh) {
440            continue;
441        }
442        if let Some(why) = pairings.digest_refusal(&path) {
443            eprintln!("superfluid: speculation: skipping a drafter: {why}");
444            continue;
445        }
446        let named = bundle_names(&path, &dh)
447            .iter()
448            .any(|d| target_names.iter().any(|t| d.contains(t.as_str())));
449        let same_quant = target_bits.is_some() && storage_bits(&dh) == target_bits;
450        let created = dh.get("created").and_then(|v| v.as_str()).map_or(0, created_secs);
451        let paired = target_id.as_deref().and_then(|t| pairings.rank(&path, t));
452        let key = (paired.is_none(), paired, !named, kind_rank, !same_quant, Reverse(created), path.clone());
453        ranked.push((key, kind, path));
454    }
455    ranked.sort_by(|a, b| a.0.cmp(&b.0));
456    ranked.into_iter().map(|(_, kind, p)| (kind, p)).collect()
457}
458
459fn created_secs(s: &str) -> u64 {
460    let s = s.trim();
461    s.parse::<u64>().ok().or_else(|| rfc3339_secs(s)).unwrap_or(0)
462}
463
464fn rfc3339_secs(s: &str) -> Option<u64> {
465    let num = |r: std::ops::Range<usize>| {
466        let t = s.get(r)?;
467        t.bytes().all(|b| b.is_ascii_digit()).then(|| t.parse::<i64>().ok())?
468    };
469    let b = s.as_bytes();
470    if b.len() < 19 || b[4] != b'-' || b[7] != b'-' || !matches!(b[10], b'T' | b't' | b' ') || b[13] != b':' || b[16] != b':'
471    {
472        return None;
473    }
474    let (y, mo, d, h, mi, sec) = (num(0..4)?, num(5..7)?, num(8..10)?, num(11..13)?, num(14..16)?, num(17..19)?);
475    if !(1..=12).contains(&mo) || !(1..=31).contains(&d) || h > 23 || mi > 59 || sec > 60 {
476        return None;
477    }
478    let mut rest = &s[19..];
479    if let Some(frac) = rest.strip_prefix('.') {
480        rest = frac.trim_start_matches(|c: char| c.is_ascii_digit());
481    }
482    let offset = match rest {
483        "" | "Z" | "z" => 0,
484        o if o.len() == 6 && o.as_bytes()[3] == b':' => {
485            let sign = match o.as_bytes()[0] {
486                b'+' => 1,
487                b'-' => -1,
488                _ => return None,
489            };
490            let (oh, om) = (o.get(1..3)?, o.get(4..6)?);
491            let digits = |t: &str| t.bytes().all(|b| b.is_ascii_digit()).then(|| t.parse::<i64>().ok())?;
492            sign * (digits(oh)? * 3600 + digits(om)? * 60)
493        }
494        _ => return None,
495    };
496    let y = if mo <= 2 { y - 1 } else { y };
497    let era = y.div_euclid(400);
498    let yoe = y - era * 400;
499    let doy = (153 * (if mo > 2 { mo - 3 } else { mo + 9 }) + 2) / 5 + d - 1;
500    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
501    let days = era * 146_097 + doe - 719_468;
502    u64::try_from(days * 86_400 + h * 3600 + mi * 60 + sec - offset).ok()
503}
504
505fn fits(th: &serde_json::Value, dh: &serde_json::Value) -> bool {
506    let num = |h: &serde_json::Value, k: &str| h.get("config").and_then(|c| c.get(k)).and_then(|v| v.as_u64());
507    let (Some(hidden), Some(vocab)) = (num(th, "hidden_size"), num(th, "vocab_size")) else {
508        return false;
509    };
510    if num(dh, "hidden_size") != Some(hidden) || num(dh, "vocab_size") != Some(vocab) {
511        return false;
512    }
513    let layers = num(th, "num_hidden_layers");
514    if let Some(n) = num(dh, "num_target_layers") {
515        if layers != Some(n) {
516            return false;
517        }
518    }
519    if let Some(layers) = layers {
520        for key in ["target_layer_ids", "aux_layer_ids"] {
521            let ids = dh.get("config").and_then(|c| c.get(key)).and_then(|v| v.as_array());
522            if ids.is_some_and(|ids| ids.iter().any(|i| i.as_u64().is_none_or(|i| i >= layers))) {
523                return false;
524            }
525        }
526    }
527    let backend = |h: &serde_json::Value| match h.get("target_backend").and_then(|v| v.as_str()).unwrap_or("metal") {
528        "cuda" | "cuda_sm121" => "cuda".to_owned(),
529        other => other.to_owned(),
530    };
531    backend(th) == backend(dh)
532}
533
534fn bundle_names(path: &Path, h: &serde_json::Value) -> Vec<String> {
535    let mut raw = Vec::new();
536    if let Some(s) = h.get("source").and_then(|s| s.get("filename")).and_then(|v| v.as_str()) {
537        raw.push(s.to_string());
538    }
539    match path.file_stem().and_then(|s| s.to_str()) {
540        Some("model") => {
541            if let Some(repo) = path.parent().and_then(Path::parent).and_then(Path::file_name) {
542                raw.push(repo.to_string_lossy().into_owned());
543            }
544        }
545        Some(stem) => raw.push(stem.to_string()),
546        None => {}
547    }
548    raw.iter()
549        .map(|s| strip_artifact_suffixes(s))
550        .map(|s| s.chars().filter(|c| c.is_ascii_alphanumeric() || *c == '.').collect::<String>())
551        .filter(|s| s.len() >= 3 && !is_generic_name(s))
552        .collect()
553}
554
555fn storage_bits(h: &serde_json::Value) -> Option<String> {
556    let layer = h.get("tensors").and_then(|t| t.as_array()).and_then(|ts| {
557        ts.iter().find(|t| {
558            let name = t.get("name").and_then(|n| n.as_str()).unwrap_or("");
559            name.starts_with("layers.")
560                && name.ends_with(".weight")
561                && t.get("shape").and_then(|s| s.as_array()).is_some_and(|s| s.len() == 2)
562        })
563    });
564    let raw = layer
565        .and_then(|t| t.get("dtype").and_then(|d| d.as_str()))
566        .or_else(|| h.get("quant_scheme").and_then(|q| q.as_str()))?
567        .to_ascii_lowercase();
568    Some(raw.strip_prefix("base_").map(str::to_string).unwrap_or(raw))
569}
570
571fn is_generic_name(s: &str) -> bool {
572    let base = s.trim_end_matches(|c: char| c.is_ascii_digit());
573    let base = base.strip_suffix("of").unwrap_or(base).trim_end_matches(|c: char| c.is_ascii_digit());
574    matches!(
575        base,
576        "model" | "models" | "weight" | "weights" | "checkpoint" | "checkpoints" | "ckpt" | "pytorchmodel"
577            | "consolidated" | "adapter" | "adaptermodel"
578    )
579}
580
581fn strip_artifact_suffixes(name: &str) -> String {
582    let mut s = name.to_ascii_lowercase();
583    loop {
584        let before = s.len();
585        for ext in [".base", ".gguf", ".safetensors"] {
586            if let Some(t) = s.strip_suffix(ext) {
587                s.truncate(t.len());
588            }
589        }
590        let cut = s
591            .char_indices()
592            .rev()
593            .filter(|&(i, c)| i > 0 && matches!(c, '-' | '_' | '.'))
594            .map(|(i, _)| i)
595            .find(|&i| is_artifact_tag(&s[i + 1..]));
596        if let Some(i) = cut {
597            s.truncate(i);
598        }
599        if s.len() == before {
600            return s;
601        }
602    }
603}
604
605fn is_artifact_tag(t: &str) -> bool {
606    const WORDS: [&str; 12] =
607        ["tiled", "f16", "bf16", "f32", "fp16", "fp8", "int4", "int8", "mxfp4", "nvfp4", "4bit", "8bit"];
608    if WORDS.contains(&t) {
609        return true;
610    }
611    let t = t.strip_prefix("mtp").unwrap_or(t);
612    let t = t.strip_prefix('i').unwrap_or(t);
613    let Some(rest) = t.strip_prefix('q') else { return false };
614    let mut parts = rest.split('_');
615    let bits = parts.next().unwrap_or("");
616    !bits.is_empty()
617        && bits.bytes().all(|b| b.is_ascii_digit())
618        && parts.all(|p| matches!(p, "0" | "1" | "k" | "s" | "m" | "l" | "xs" | "xxs" | "xl"))
619}
620
621fn collect_bundles(dir: &Path, depth: usize, out: &mut Vec<PathBuf>) {
622    let Ok(entries) = std::fs::read_dir(dir) else { return };
623    for e in entries.flatten() {
624        let p = e.path();
625        let Ok(ft) = e.file_type() else { continue };
626        if (ft.is_file() || ft.is_symlink()) && p.extension().is_some_and(|x| x == "base") && p.is_file() {
627            out.push(p);
628        } else if ft.is_dir() && depth > 0 && !e.file_name().to_string_lossy().starts_with('.') {
629            collect_bundles(&p, depth - 1, out);
630        }
631    }
632}
633
634#[cfg(test)]
635mod tests {
636    use super::*;
637    use std::io::Write;
638
639    fn bundle(dir: &Path, name: &str, header: serde_json::Value) -> std::path::PathBuf {
640        bundle_v(dir, name, header, 1)
641    }
642
643    fn bundle_v(dir: &Path, name: &str, header: serde_json::Value, version: u32) -> std::path::PathBuf {
644        let p = dir.join(name);
645        std::fs::create_dir_all(p.parent().unwrap()).unwrap();
646        let json = serde_json::to_vec(&header).unwrap();
647        let mut f = std::fs::File::create(&p).unwrap();
648        f.write_all(b"BASE").unwrap();
649        f.write_all(&version.to_le_bytes()).unwrap();
650        f.write_all(&(json.len() as u64).to_le_bytes()).unwrap();
651        f.write_all(&json).unwrap();
652        p
653    }
654
655    fn scratch(tag: &str) -> PathBuf {
656        let dir = std::env::temp_dir().join(format!("spec-resolve-{tag}-{}", std::process::id()));
657        let _ = std::fs::remove_dir_all(&dir);
658        std::fs::create_dir_all(&dir).unwrap();
659        dir
660    }
661
662    fn target(name: &str, quant: &str) -> serde_json::Value {
663        serde_json::json!({"arch": "qwen", "quant_scheme": quant, "target_backend": "metal",
664            "source": {"filename": name},
665            "config": {"hidden_size": 2560, "vocab_size": 151936, "num_hidden_layers": 36}})
666    }
667
668    fn drafter(kind: &str, name: &str, quant: &str, created: u64) -> serde_json::Value {
669        serde_json::json!({"arch": kind, "quant_scheme": quant, "target_backend": "metal",
670            "created": created.to_string(), "source": {"filename": name},
671            "config": {"hidden_size": 2560, "vocab_size": 151936, "num_hidden_layers": 5,
672                "num_target_layers": 36, "target_layer_ids": [1, 9, 17, 25, 33], "speculator_kind": kind}})
673    }
674
675    fn pick(t: &Path, dirs: &[(PathBuf, usize)]) -> Option<String> {
676        pick_with(t, dirs, &Pairings::default())
677    }
678
679    fn pick_with(t: &Path, dirs: &[(PathBuf, usize)], cat: &Pairings) -> Option<String> {
680        let h = read_base_header(t).unwrap();
681        find_drafter(t, &h, dirs, cat).map(|(k, p)| format!("{k}:{}", p.file_name().unwrap().to_string_lossy()))
682    }
683
684    #[test]
685    fn drafter_kind_comes_from_the_header() {
686        let dir = scratch("kind");
687        let t = bundle(&dir, "t.base", serde_json::json!({"arch": "qwen35"}));
688        for (arch, want) in [("dflash", "dflash"), ("dspark", "dspark"), ("eagle3", "eagle3"), ("qwen3", "draft-model")] {
689            let p = bundle(&dir, &format!("{arch}.base"), serde_json::json!({"arch": arch}));
690            let got = resolve(p.to_str().unwrap(), &t).unwrap().unwrap();
691            assert_eq!(got, format!("{want}:{}", p.display()));
692        }
693        assert_eq!(resolve("dflash:/x.base", &t).unwrap().as_deref(), Some("dflash:/x.base"));
694        assert_eq!(resolve("prompt-lookup", &t).unwrap().as_deref(), Some("prompt-lookup"));
695        let home = std::env::var("HOME").unwrap();
696        assert_eq!(resolve("dflash:~/d/x.base", &t).unwrap().unwrap(), format!("dflash:{home}/d/x.base"));
697        assert_eq!(resolve("draft-model:~", &t).unwrap().unwrap(), format!("draft-model:{home}"));
698        assert_eq!(resolve("dspark:~user/x.base", &t).unwrap().as_deref(), Some("dspark:~user/x.base"));
699        if let Ok(rel) = dir.join("dflash.base").strip_prefix(&home).map(|r| r.to_path_buf()) {
700            let got = resolve(&format!("~/{}", rel.display()), &t).unwrap().unwrap();
701            assert_eq!(got, format!("dflash:{home}/{}", rel.display()));
702        }
703        assert_eq!(resolve("auto", &t).unwrap(), None);
704        let m = bundle(&dir, "m.base", serde_json::json!({"arch": "qwen35", "speculator": {"config": {"kind": "mtp"}}}));
705        assert_eq!(resolve("auto", &m).unwrap().as_deref(), Some("mtp-head"));
706        assert_eq!(resolve("mtp-head", &m).unwrap().as_deref(), Some("mtp-head"));
707        let old = bundle(&dir, "old-mtp.base", serde_json::json!({"arch": "qwen35", "baserT_version": "0.2.3"}));
708        let err = resolve("mtp-head", &old).unwrap_err();
709        assert!(err.contains("carries no speculator head") && err.contains("baseRT 0.2.3"), "{err}");
710        let other = bundle(&dir, "eagle.base", serde_json::json!({"arch": "qwen35", "speculator": {"config": {"kind": "eagle3"}}}));
711        assert!(resolve("mtp-head", &other).unwrap_err().contains("not an MTP head"));
712        assert_eq!(resolve("mtp-head", Path::new("/nonexistent/x.base")).unwrap().as_deref(), Some("mtp-head"));
713        let junk = dir.join("junk.bin");
714        std::fs::write(&junk, b"not a bundle").unwrap();
715        assert!(resolve(junk.to_str().unwrap(), &t).is_err());
716        assert!(resolve("medusa", &t).is_err());
717        std::fs::remove_dir_all(&dir).ok();
718    }
719
720    #[test]
721    fn sizing_counts_what_the_strategy_loads() {
722        let t = Path::new("/m/Qwen3.8-27B-Q4.base");
723        assert_eq!(sizing_path("mtp-head", t), Some((t.to_path_buf(), SpecLoad::EmbeddedHead)));
724        assert_eq!(sizing_path("dflash:/d/x.base", t), Some((PathBuf::from("/d/x.base"), SpecLoad::Sidecar)));
725        assert_eq!(sizing_path("dspark:/d/y.base", t), Some((PathBuf::from("/d/y.base"), SpecLoad::Sidecar)));
726        assert_eq!(sizing_path("eagle3:/d/z.base", t), Some((PathBuf::from("/d/z.base"), SpecLoad::Sidecar)));
727        assert_eq!(sizing_path("draft-model:/d/lm.base", t), Some((PathBuf::from("/d/lm.base"), SpecLoad::DraftModel)));
728        assert_eq!(sizing_path("draft-model:/m/b.base", t), Some((PathBuf::from("/m/b.base"), SpecLoad::DraftModel)));
729        assert_eq!(sizing_path("prompt-lookup", t), None);
730    }
731
732    #[test]
733    fn the_hub_cache_is_where_basert_pull_installs() {
734        let os = |s: &str| Some(std::ffi::OsString::from(s));
735        assert_eq!(hub_roots_from(os("/m"), os("/h"), None), vec![PathBuf::from("/m")]);
736        let roots = hub_roots_from(None, os("/h"), os("/xdg"));
737        if cfg!(target_os = "macos") {
738            assert_eq!(roots, vec![PathBuf::from("/h/Library/Caches/baseRT/models"), PathBuf::from("/h/.cache/baseRT/models")]);
739        } else {
740            assert_eq!(roots, vec![PathBuf::from("/xdg/baseRT/models"), PathBuf::from("/h/.cache/baseRT/models")]);
741            assert_eq!(hub_roots_from(None, os("/h"), os("rel")), vec![PathBuf::from("/h/.cache/baseRT/models")]);
742        }
743        assert!(hub_roots_from(None, None, None).is_empty());
744    }
745
746    #[test]
747    fn a_glm_nextn_layer_is_an_mtp_head() {
748        let dir = scratch("glm-nextn");
749        let tensors = |extra: &str| {
750            serde_json::json!([{"name": "layers.0.self_attn.q_a_proj.weight"}, {"name": extra}])
751        };
752        let with = bundle(
753            &dir,
754            "glm.base",
755            serde_json::json!({"arch": "glm_dsa", "config": {"num_hidden_layers": 2},
756                               "tensors": tensors("layers.2.eh_proj.weight")}),
757        );
758        assert_eq!(resolve("mtp-head", &with).unwrap().as_deref(), Some("mtp-head"));
759        assert_eq!(resolve_all("auto", &with).unwrap().first().map(String::as_str), Some("mtp-head"));
760        let without = bundle(
761            &dir,
762            "glm-plain.base",
763            serde_json::json!({"arch": "glm_dsa", "config": {"num_hidden_layers": 3},
764                               "tensors": tensors("layers.2.eh_proj.weight")}),
765        );
766        assert!(resolve("mtp-head", &without).is_err());
767        let other = bundle(
768            &dir,
769            "other.base",
770            serde_json::json!({"arch": "qwen35", "config": {"num_hidden_layers": 2},
771                               "tensors": tensors("layers.2.eh_proj.weight")}),
772        );
773        assert!(resolve("mtp-head", &other).is_err());
774    }
775
776    #[test]
777    fn auto_prefers_the_head_over_an_installed_drafter() {
778        let root = scratch("head");
779        let mut h = target("Qwen3-4B", "base_q4");
780        h["speculator"] = serde_json::json!({"config": {"kind": "mtp"}});
781        let t = bundle(&root, "qwen3-4b/Qwen3-4B-Q4.base", h);
782        bundle(&root, "drafters/Qwen3-4B-DFlash.base", drafter("dflash", "Qwen3-4B-DFlash", "base_q4", 1));
783        assert_eq!(resolve("auto", &t).unwrap().as_deref(), Some("mtp-head"));
784        std::fs::remove_dir_all(&root).ok();
785    }
786
787    #[test]
788    fn auto_finds_a_drafter_in_the_models_layout() {
789        let root = scratch("layout");
790        let t = bundle(&root, "qwen3-4b/Qwen3-4B-Q4.base", target("Qwen3-4B", "base_q4"));
791        let dirs = auto_search_dirs(&t)[..3].to_vec();
792        bundle(&root, "drafters/Qwen3-4B_eagle3.base", drafter("eagle3", "Qwen3-4B_eagle3", "base_q4", 1));
793        assert_eq!(pick(&t, &dirs).as_deref(), Some("eagle3:Qwen3-4B_eagle3.base"));
794        bundle(&root, "drafters/Qwen3-4B-DFlash-b16.base", drafter("dflash", "Qwen3-4B-DFlash-b16", "base_q4", 1));
795        assert_eq!(pick(&t, &dirs).as_deref(), Some("dflash:Qwen3-4B-DFlash-b16.base"));
796        bundle(&root, "drafters/dspark_qwen3_4b_block7.base", drafter("dspark", "dspark_qwen3_4b_block7", "base_q4", 1));
797        assert_eq!(pick(&t, &dirs).as_deref(), Some("dspark:dspark_qwen3_4b_block7.base"));
798        bundle(&root, "drafters/dspark_qwen3_4b_block7-q8.base", drafter("dspark", "dspark_qwen3_4b_block7", "base_q8", 9));
799        bundle(&root, "drafters/dspark_qwen3_4b_block7-new.base", drafter("dspark", "dspark_qwen3_4b_block7", "base_q4", 5));
800        assert_eq!(pick(&t, &dirs).as_deref(), Some("dspark:dspark_qwen3_4b_block7-new.base"));
801        let mut lm = drafter("qwen", "Qwen3-0.6B", "base_q4", 99);
802        lm["config"]["num_hidden_layers"] = serde_json::json!(28);
803        bundle(&root, "drafters/Qwen3-0.6B.base", lm);
804        assert_eq!(pick(&t, &dirs).as_deref(), Some("dspark:dspark_qwen3_4b_block7-new.base"));
805        std::fs::remove_dir_all(&root).ok();
806    }
807
808    #[test]
809    fn auto_keeps_every_fitting_drafter_in_rank_order_for_fallback() {
810        let root = scratch("ranked");
811        let t = bundle(&root, "qwen3-4b/Qwen3-4B-Q4.base", target("Qwen3-4B", "base_q4"));
812        let dirs = auto_search_dirs(&t)[..3].to_vec();
813        bundle(&root, "drafters/Qwen3-4B_eagle3.base", drafter("eagle3", "Qwen3-4B_eagle3", "base_q4", 1));
814        bundle(&root, "drafters/Qwen3-4B-DFlash-b16.base", drafter("dflash", "Qwen3-4B-DFlash-b16", "base_q4", 1));
815        bundle(&root, "drafters/dspark_qwen3_4b_block7.base", drafter("dspark", "dspark_qwen3_4b_block7", "base_q4", 1));
816        let mut wide = drafter("dspark", "dspark_qwen3_4b_wide", "base_q4", 9);
817        wide["config"]["hidden_size"] = serde_json::json!(4096);
818        bundle(&root, "drafters/dspark_qwen3_4b_wide.base", wide);
819        let h = read_base_header(&t).unwrap();
820        let ranked: Vec<String> = find_drafters(&t, &h, &dirs, &Pairings::default())
821            .into_iter()
822            .map(|(k, p)| format!("{k}:{}", p.file_name().unwrap().to_string_lossy()))
823            .collect();
824        assert_eq!(
825            ranked,
826            ["dspark:dspark_qwen3_4b_block7.base", "dflash:Qwen3-4B-DFlash-b16.base", "eagle3:Qwen3-4B_eagle3.base"]
827        );
828        assert_eq!(pick(&t, &dirs).as_deref(), Some(ranked[0].as_str()));
829        assert_eq!(resolve_all("prompt-lookup", &t).unwrap(), ["prompt-lookup"]);
830        assert!(resolve_all("off", &t).unwrap().is_empty());
831        std::fs::remove_dir_all(&root).ok();
832    }
833
834    #[test]
835    fn created_reads_epoch_and_rfc3339() {
836        assert_eq!(created_secs("1790218976"), 1_790_218_976);
837        assert_eq!(created_secs("2026-04-24T12:00:00Z"), 1_777_032_000);
838        assert_eq!(created_secs("2026-04-24T12:00:00.250Z"), 1_777_032_000);
839        assert_eq!(created_secs("2026-04-24T12:00:00+01:30"), 1_777_026_600);
840        assert_eq!(created_secs("1970-01-01T00:00:00Z"), 0);
841        assert_eq!(created_secs("2024-02-29T00:00:00Z"), 1_709_164_800);
842        for junk in ["", "yesterday", "2026-13-01T00:00:00Z", "2026-04-24T12:00:00+0100", "2026-04-24"] {
843            assert_eq!(created_secs(junk), 0, "{junk}");
844        }
845    }
846
847    #[test]
848    fn auto_prefers_the_newest_iso_stamped_conversion() {
849        let root = scratch("iso");
850        let t = bundle(&root, "m/Qwen3-4B-Q4.base", target("Qwen3-4B", "base_q4"));
851        let dirs = auto_search_dirs(&t)[..3].to_vec();
852        let stamped = |c: &str| {
853            let mut h = drafter("dspark", "Qwen3-4B-DSpark", "base_q4", 0);
854            h["created"] = serde_json::json!(c);
855            h
856        };
857        bundle(&root, "drafters/a.base", stamped("2026-04-24T12:00:00Z"));
858        bundle(&root, "drafters/b.base", stamped("2026-09-01T00:00:00Z"));
859        bundle(&root, "drafters/c.base", stamped("1780000000"));
860        assert_eq!(pick(&t, &dirs).as_deref(), Some("dspark:b.base"));
861        std::fs::remove_dir_all(&root).ok();
862    }
863
864    #[test]
865    fn auto_skips_a_format_version_the_engine_will_not_open() {
866        let root = scratch("fmtver");
867        let t = bundle(&root, "m/Qwen3-4B-Q4.base", target("Qwen3-4B", "base_q4"));
868        let dirs = auto_search_dirs(&t)[..3].to_vec();
869        let v2 = bundle_v(&root, "drafters/a.base", drafter("dspark", "Qwen3-4B-DSpark", "base_q4", 2_000_000_000), 2);
870        bundle(&root, "drafters/b.base", drafter("dspark", "Qwen3-4B-DSpark", "base_q4", 1_000_000_000));
871        assert!(read_base_header(&v2).is_none());
872        assert_eq!(pick(&t, &dirs).as_deref(), Some("dspark:b.base"));
873        std::fs::remove_dir_all(&root).ok();
874    }
875
876    #[test]
877    fn relative_targets_search_one_level_up_too() {
878        let up = |t: &str| auto_search_dirs(Path::new(t))[2].0.clone();
879        assert_eq!(up("models/m.base"), PathBuf::from("./drafters"));
880        assert_eq!(up("a/models/m.base"), PathBuf::from("a/drafters"));
881        assert_eq!(up("m.base"), PathBuf::from("./../drafters"));
882        assert_eq!(up("/x/models/m.base"), PathBuf::from("/x/drafters"));
883    }
884
885    #[test]
886    fn auto_prefers_a_drafter_named_for_the_target_over_a_same_shape_one() {
887        let root = scratch("named");
888        let t = bundle(&root, "qwen38/Qwen3.8-4B-Q4.base", target("Qwen3.8-4B", "base_q4"));
889        let dirs = auto_search_dirs(&t)[..3].to_vec();
890        bundle(&root, "drafters/Qwen3.6-4B-DSpark.base", drafter("dspark", "Qwen3.6-4B-DSpark", "base_q4", 1));
891        assert_eq!(pick(&t, &dirs).as_deref(), Some("dspark:Qwen3.6-4B-DSpark.base"));
892        bundle(&root, "drafters/Qwen3.8-4B-DFlash.base", drafter("dflash", "Qwen3.8-4B-DFlash", "base_q4", 1));
893        assert_eq!(pick(&t, &dirs).as_deref(), Some("dflash:Qwen3.8-4B-DFlash.base"));
894        std::fs::remove_dir_all(&root).ok();
895    }
896
897    #[test]
898    fn a_sidecar_ranks_by_its_own_storage_width_not_its_targets_scheme() {
899        let root = scratch("storage");
900        let t = bundle(&root, "qwen3-4b/Qwen3-4B-Q4.base", target("Qwen3-4B", "base_q4"));
901        let dirs = auto_search_dirs(&t)[..3].to_vec();
902        let with_layers = |mut h: serde_json::Value, dtype: &str| {
903            h["tensors"] =
904                serde_json::json!([{"name": "layers.0.mlp.down_proj.weight", "dtype": dtype, "shape": [2560, 9728]}]);
905            h
906        };
907        bundle(
908            &root,
909            "drafters/Qwen3-4B-DFlash-b16.base",
910            with_layers(drafter("dflash", "Qwen3-4B-DFlash-b16", "base_q4", 9), "f16"),
911        );
912        bundle(
913            &root,
914            "drafters/Qwen3-4B-DFlash-b16-q4.base",
915            with_layers(drafter("dflash", "Qwen3-4B-DFlash-b16", "base_q4", 1), "base_q4"),
916        );
917        assert_eq!(pick(&t, &dirs).as_deref(), Some("dflash:Qwen3-4B-DFlash-b16-q4.base"));
918        assert_eq!(storage_bits(&read_base_header(&t).unwrap()).as_deref(), Some("q4"));
919        assert_eq!(storage_bits(&with_layers(drafter("dflash", "x", "base_q4", 1), "f16")).as_deref(), Some("f16"));
920        std::fs::remove_dir_all(&root).ok();
921    }
922
923    #[test]
924    fn a_generic_provenance_name_does_not_make_a_sibling_look_named() {
925        let root = scratch("generic");
926        let t = bundle(&root, "qwen38/Qwen3.8-4B-Q4.base", target("model.safetensors", "base_q4"));
927        let dirs = auto_search_dirs(&t)[..3].to_vec();
928        bundle(
929            &root,
930            "drafters/Qwen3.6-4B-DSpark.base",
931            drafter("dspark", "model-00001-of-00002.safetensors", "base_q4", 9),
932        );
933        bundle(&root, "drafters/Qwen3.8-4B-DFlash.base", drafter("dflash", "model.safetensors", "base_q4", 1));
934        assert_eq!(pick(&t, &dirs).as_deref(), Some("dflash:Qwen3.8-4B-DFlash.base"));
935        for (name, generic) in [
936            ("model", true),
937            ("model00001of00004", true),
938            ("pytorchmodel", true),
939            ("consolidated00", true),
940            ("qwen3.8-4b", false),
941            ("modelscope7b", false),
942        ] {
943            assert_eq!(is_generic_name(name), generic, "{name}");
944        }
945        std::fs::remove_dir_all(&root).ok();
946    }
947
948    #[test]
949    fn artifact_suffixes_do_not_hide_the_model_name() {
950        for (raw, want) in [
951            ("Qwen3.8-27B-Q8.base", "qwen3.8-27b"),
952            ("Qwen3.8-27B-Q8.gguf", "qwen3.8-27b"),
953            ("Qwen3.8-27B-mtpq4-Q4", "qwen3.8-27b"),
954            ("Qwen3-4B-Q4_K_M.gguf", "qwen3-4b"),
955            ("qwen3-4b-iq4_xs", "qwen3-4b"),
956            ("Qwen3.8-27B-DFlash2-Q4-tiled", "qwen3.8-27b-dflash2"),
957            ("Qwen3-8B-BF16", "qwen3-8b"),
958            ("Qwen3-8B-F16", "qwen3-8b"),
959            ("Qwen3-4B-DFlash-b16", "qwen3-4b-dflash-b16"),
960            ("Qwen3.5-2B-Base", "qwen3.5-2b-base"),
961            ("qwen3-30b-a3b", "qwen3-30b-a3b"),
962            ("Q4", "q4"),
963        ] {
964            assert_eq!(strip_artifact_suffixes(raw), want, "{raw}");
965        }
966    }
967
968    #[test]
969    fn a_quant_tagged_target_still_prefers_its_named_drafter() {
970        let root = scratch("quantname");
971        let mut th = target("Qwen3.8-27B-Q8.gguf", "base_q8");
972        th["source"]["filename"] = serde_json::json!("Qwen3.8-27B-Q8.gguf");
973        let t = bundle(&root, "qwen38/Qwen3.8-27B-Q8.base", th);
974        let dirs = auto_search_dirs(&t)[..3].to_vec();
975        bundle(&root, "drafters/Qwen3.6-27B-DSpark.base", drafter("dspark", "Qwen3.6-27B-DSpark", "base_q8", 1));
976        bundle(&root, "drafters/Qwen3.8-27B-DFlash.base", drafter("dflash", "Qwen3.8-27B-DFlash", "base_q8", 1));
977        assert_eq!(pick(&t, &dirs).as_deref(), Some("dflash:Qwen3.8-27B-DFlash.base"));
978        std::fs::remove_dir_all(&root).ok();
979    }
980
981    #[test]
982    fn auto_rejects_drafters_that_do_not_fit() {
983        let root = scratch("fit");
984        let t = bundle(&root, "m/Qwen3-4B-Q4.base", target("Qwen3-4B", "base_q4"));
985        let dirs = auto_search_dirs(&t)[..3].to_vec();
986        let mut wide = drafter("dflash", "Qwen3-4B-DFlash-a", "base_q4", 1);
987        wide["config"]["hidden_size"] = serde_json::json!(4096);
988        bundle(&root, "drafters/a.base", wide);
989        let mut vocab = drafter("dflash", "Qwen3-4B-DFlash-b", "base_q4", 1);
990        vocab["config"]["vocab_size"] = serde_json::json!(248320);
991        bundle(&root, "drafters/b.base", vocab);
992        let mut deep = drafter("dflash", "Qwen3-4B-DFlash-c", "base_q4", 1);
993        deep["config"]["num_target_layers"] = serde_json::json!(64);
994        bundle(&root, "drafters/c.base", deep);
995        let mut taps = drafter("eagle3", "Qwen3-4B-e3", "base_q4", 1);
996        taps["config"].as_object_mut().unwrap().remove("num_target_layers");
997        taps["config"]["aux_layer_ids"] = serde_json::json!([2, 20, 40]);
998        bundle(&root, "drafters/d.base", taps);
999        let mut cuda = drafter("dspark", "Qwen3-4B-dspark", "base_q4", 1);
1000        cuda["target_backend"] = serde_json::json!("cuda");
1001        bundle(&root, "drafters/e.base", cuda);
1002        assert_eq!(pick(&t, &dirs), None);
1003        std::fs::remove_dir_all(&root).ok();
1004    }
1005
1006    #[test]
1007    fn cuda_tags_a_cuda_build_opens_pair_with_each_other() {
1008        let root = scratch("cudaclass");
1009        let mut th = target("Qwen3-4B", "base_q4");
1010        th["target_backend"] = serde_json::json!("cuda");
1011        let t = bundle(&root, "m/Qwen3-4B-Q4.base", th);
1012        let dirs = auto_search_dirs(&t)[..3].to_vec();
1013        bundle(&root, "drafters/a.base", drafter("dspark", "Qwen3-4B-DSpark", "base_q4", 1));
1014        assert_eq!(pick(&t, &dirs), None);
1015        let mut sm = drafter("dflash", "Qwen3-4B-DFlash", "base_q4", 1);
1016        sm["target_backend"] = serde_json::json!("cuda_sm121");
1017        bundle(&root, "drafters/b.base", sm);
1018        assert_eq!(pick(&t, &dirs).as_deref(), Some("dflash:b.base"));
1019        std::fs::remove_dir_all(&root).ok();
1020    }
1021
1022    #[test]
1023    fn a_missing_backend_is_metal() {
1024        let root = scratch("backend");
1025        let mut th = target("Qwen3-4B", "base_q4");
1026        th.as_object_mut().unwrap().remove("target_backend");
1027        let t = bundle(&root, "m/Qwen3-4B-Q4.base", th);
1028        let dirs = auto_search_dirs(&t)[..3].to_vec();
1029        let mut cuda = drafter("dspark", "Qwen3-4B-DSpark", "base_q4", 1);
1030        cuda["target_backend"] = serde_json::json!("cuda_sm121");
1031        bundle(&root, "drafters/a.base", cuda);
1032        assert_eq!(pick(&t, &dirs), None);
1033        bundle(&root, "drafters/b.base", drafter("dflash", "Qwen3-4B-DFlash", "base_q4", 1));
1034        assert_eq!(pick(&t, &dirs).as_deref(), Some("dflash:b.base"));
1035        let mut old = drafter("dspark", "Qwen3-4B-DSpark", "base_q4", 1);
1036        old.as_object_mut().unwrap().remove("target_backend");
1037        bundle(&root, "drafters/c.base", old);
1038        assert_eq!(pick(&t, &dirs).as_deref(), Some("dspark:c.base"));
1039        std::fs::remove_dir_all(&root).ok();
1040    }
1041
1042    #[test]
1043    fn auto_walks_the_hub_cache_layout() {
1044        let hub = scratch("hub");
1045        let t = bundle(&hub, "basecompute/Qwen3-4B/default-q4/model.base", target("Qwen3-4B", "base_q4"));
1046        bundle(&hub, "basecompute/Qwen3-4B-EAGLE3/default-q4/model.base", drafter("eagle3", "x", "base_q4", 1));
1047        bundle(&hub, ".src/hf/staged/model.base", drafter("dspark", "Qwen3-4B-DSpark", "base_q4", 1));
1048        let got = find_drafter(&t, &read_base_header(&t).unwrap(), &[(hub.clone(), HUB_DEPTH)], &Pairings::default())
1049            .unwrap();
1050        assert_eq!(got.0, "eagle3");
1051        assert!(got.1.ends_with("Qwen3-4B-EAGLE3/default-q4/model.base"), "{}", got.1.display());
1052        std::fs::remove_dir_all(&hub).ok();
1053    }
1054
1055    fn catalog() -> Pairings {
1056        Pairings::from_json(
1057            &serde_json::json!({"models": [
1058                {"id": "basecompute/Qwen3-4B", "file": "Qwen3-4B-Q4.base"},
1059                {"id": "basecompute/Qwen3-4B-EAGLE3", "file": "Qwen3-4B-EAGLE3-Q4.base",
1060                 "speculator_for": "basecompute/Qwen3-4B", "speculator_rank": 1},
1061                {"id": "basecompute/Qwen3-4B-DFlash", "file": "Qwen3-4B-DFlash-Q4.base",
1062                 "speculator_for": "basecompute/Qwen3-4B", "speculator_rank": 2},
1063                {"id": "basecompute/Qwen3-4B-DSpark", "file": "Qwen3-4B-DSpark-Q4.base",
1064                 "speculator_for": "basecompute/Qwen3-4B"},
1065            ]})
1066            .to_string(),
1067        )
1068    }
1069
1070    #[test]
1071    fn auto_takes_the_catalog_pairing_in_rank_order() {
1072        let root = scratch("catalog");
1073        let t = bundle(&root, "qwen3-4b/Qwen3-4B-Q4.base", target("Qwen3-4B", "base_q4"));
1074        let dirs = auto_search_dirs(&t)[..3].to_vec();
1075        let cat = catalog();
1076        bundle(&root, "drafters/dspark_qwen3_4b_block7.base", drafter("dspark", "dspark_qwen3_4b_block7", "base_q4", 9));
1077        bundle(&root, "drafters/Qwen3-4B-DSpark-Q4.base", drafter("dspark", "x", "base_q4", 1));
1078        assert_eq!(pick_with(&t, &dirs, &cat).as_deref(), Some("dspark:Qwen3-4B-DSpark-Q4.base"));
1079        bundle(&root, "drafters/Qwen3-4B-DFlash-Q4.base", drafter("dflash", "x", "base_q4", 1));
1080        assert_eq!(pick_with(&t, &dirs, &cat).as_deref(), Some("dflash:Qwen3-4B-DFlash-Q4.base"));
1081        bundle(&root, "drafters/Qwen3-4B-EAGLE3-Q4.base", drafter("eagle3", "x", "base_q4", 1));
1082        assert_eq!(pick_with(&t, &dirs, &cat).as_deref(), Some("eagle3:Qwen3-4B-EAGLE3-Q4.base"));
1083        assert_eq!(pick(&t, &dirs).as_deref(), Some("dspark:dspark_qwen3_4b_block7.base"));
1084        std::fs::remove_dir_all(&root).ok();
1085    }
1086
1087    #[test]
1088    fn a_pairing_for_another_target_does_not_count() {
1089        let root = scratch("other");
1090        let t = bundle(&root, "m/Qwen3-4B-Instruct-2507-Q4.base", target("Qwen3-4B-Instruct-2507", "base_q4"));
1091        let dirs = auto_search_dirs(&t)[..3].to_vec();
1092        bundle(&root, "drafters/Qwen3-4B-EAGLE3-Q4.base", drafter("eagle3", "Qwen3-4B-EAGLE3", "base_q4", 1));
1093        bundle(&root, "drafters/Qwen3-4B-DSpark-Q4.base", drafter("dspark", "Qwen3-4B-DSpark", "base_q4", 1));
1094        assert_eq!(pick_with(&t, &dirs, &catalog()).as_deref(), Some("dspark:Qwen3-4B-DSpark-Q4.base"));
1095        std::fs::remove_dir_all(&root).ok();
1096    }
1097
1098    #[test]
1099    fn hub_bundles_are_identified_by_their_repo() {
1100        let hub = scratch("hubcat");
1101        let t = bundle(&hub, "basecompute/Qwen3-4B/default-q4/model.base", target("Qwen3-4B", "base_q4"));
1102        bundle(&hub, "basecompute/Qwen3-4B-DSpark/default-q4/model.base", drafter("dspark", "a", "base_q4", 1));
1103        bundle(&hub, "basecompute/Qwen3-4B-EAGLE3/default-q4/model.base", drafter("eagle3", "b", "base_q4", 1));
1104        let got = find_drafter(&t, &read_base_header(&t).unwrap(), &[(hub.clone(), HUB_DEPTH)], &catalog()).unwrap();
1105        assert_eq!(got.0, "eagle3");
1106        std::fs::remove_dir_all(&hub).ok();
1107    }
1108
1109    #[test]
1110    fn the_catalog_is_read_from_the_cache_and_not_fetched_offline() {
1111        let root = scratch("catjson");
1112        let own = root.join("cache").join("hub-catalog.json");
1113        assert_eq!(catalog_json(&own, false), "");
1114        std::fs::create_dir_all(own.parent().unwrap()).unwrap();
1115        std::fs::write(&own, r#"{"models":[{"id":"basecompute/Qwen3-4B","file":"Qwen3-4B-Q4.base"}]}"#).unwrap();
1116        let p = Pairings::from_json(&catalog_json(&own, true));
1117        assert_eq!(p.id_of(Path::new("/x/Qwen3-4B-Q4.base")).as_deref(), Some("basecompute/Qwen3-4B"));
1118        std::fs::remove_dir_all(&root).ok();
1119    }
1120
1121    fn sha256_of(path: &Path) -> String {
1122        use sha2::Digest;
1123        sha2::Sha256::digest(std::fs::read(path).unwrap()).iter().map(|b| format!("{b:02x}")).collect()
1124    }
1125
1126    fn pinned(rows: serde_json::Value, digests: &Path) -> Pairings {
1127        let mut p = Pairings::from_json(&serde_json::json!({ "models": rows }).to_string());
1128        p.digests = Some(digests.to_path_buf());
1129        p
1130    }
1131
1132    #[test]
1133    fn a_drafter_that_is_not_the_catalogs_bytes_is_skipped() {
1134        let hub = scratch("hubsha");
1135        let t = bundle(&hub, "basecompute/Qwen3-4B/default-q4/model.base", target("Qwen3-4B", "base_q4"));
1136        let eagle = bundle(&hub, "basecompute/Qwen3-4B-EAGLE3/default-q4/model.base", drafter("eagle3", "b", "base_q4", 1));
1137        let spark = bundle(&hub, "basecompute/Qwen3-4B-DSpark/default-q4/model.base", drafter("dspark", "a", "base_q4", 1));
1138        let flat = bundle(&hub, "drafters/Qwen3-4B-DFlash-Q4.base", drafter("dflash", "c", "base_q4", 1));
1139        let other = "0".repeat(64);
1140        let rows = |eagle_sha: &str| {
1141            serde_json::json!([
1142                {"id": "basecompute/Qwen3-4B", "quant": "default-q4"},
1143                {"id": "basecompute/Qwen3-4B-EAGLE3", "quant": "default-q4", "sha256": eagle_sha,
1144                 "speculator_for": "basecompute/Qwen3-4B", "speculator_rank": 1},
1145                {"id": "basecompute/Qwen3-4B-DFlash", "file": "Qwen3-4B-DFlash-Q4.base", "quant": "default-q4",
1146                 "size": 1, "sha256": other, "speculator_for": "basecompute/Qwen3-4B", "speculator_rank": 2},
1147                {"id": "basecompute/Qwen3-4B-DSpark", "quant": "default-q4", "sha256": sha256_of(&spark),
1148                 "speculator_for": "basecompute/Qwen3-4B", "speculator_rank": 3},
1149            ])
1150        };
1151        let digests = hub.join(".digests");
1152        let dirs = [(hub.clone(), HUB_DEPTH), (hub.join("drafters"), 0)];
1153        let h = read_base_header(&t).unwrap();
1154        let ranked = |cat: &Pairings| -> Vec<PathBuf> { find_drafters(&t, &h, &dirs, cat).into_iter().map(|(_, p)| p).collect() };
1155
1156        assert_eq!(ranked(&pinned(rows(&sha256_of(&eagle)), &digests)), vec![eagle.clone(), spark.clone()]);
1157        assert_eq!(ranked(&pinned(rows(&other), &digests)), vec![spark.clone()], "a mismatched drafter is skipped");
1158        let why = pinned(rows(&other), &digests).digest_refusal(&flat).unwrap();
1159        assert!(why.contains("Qwen3-4B-DFlash default-q4") && why.contains("pull it again"), "{why}");
1160        std::fs::remove_dir_all(&hub).ok();
1161    }
1162
1163    #[test]
1164    fn a_verified_digest_is_remembered_until_the_file_changes() {
1165        let hub = scratch("shacache");
1166        let d = bundle(&hub, "basecompute/Qwen3-4B-EAGLE3/default-q4/model.base", drafter("eagle3", "b", "base_q4", 1));
1167        let good = sha256_of(&d);
1168        let digests = hub.join(".digests");
1169        let cat = pinned(serde_json::json!([{"id": "basecompute/Qwen3-4B-EAGLE3", "quant": "default-q4", "sha256": good}]), &digests);
1170        assert_eq!(cat.digest_refusal(&d), None);
1171        let entries: Vec<PathBuf> = std::fs::read_dir(&digests).unwrap().map(|e| e.unwrap().path()).collect();
1172        assert_eq!(entries.len(), 1, "{entries:?}");
1173        let remembered = std::fs::read_to_string(&entries[0]).unwrap();
1174        assert!(remembered.ends_with(&format!("{good}\n")), "{remembered}");
1175
1176        std::fs::write(&entries[0], remembered.replace(&good, &"f".repeat(64))).unwrap();
1177        assert!(cat.digest_refusal(&d).is_some(), "an unchanged file is not hashed again");
1178        std::thread::sleep(std::time::Duration::from_millis(20));
1179        std::fs::write(&d, std::fs::read(&d).unwrap()).unwrap();
1180        assert_eq!(cat.digest_refusal(&d), None, "a rewritten file is hashed afresh");
1181        std::fs::remove_dir_all(&hub).ok();
1182    }
1183
1184    #[test]
1185    fn an_explicit_drafter_under_a_catalog_name_must_be_the_catalogs_bytes() {
1186        let dir = scratch("shaexplicit");
1187        let t = bundle(&dir, "t.base", target("Qwen3-1.7B", "base_q4"));
1188        let d = bundle(&dir, "Qwen3-1.7B-EAGLE3-F16.base", drafter("eagle3", "x", "base_f16", 1));
1189        let rows = serde_json::json!([{"id": "basecompute/Qwen3-1.7B-EAGLE3", "file": "Qwen3-1.7B-EAGLE3-F16.base", "size": 1, "sha256": "ff"}]);
1190        let cat = pinned(rows, &dir.join("sha256"));
1191        let err = cat.digest_refusal(&d).unwrap();
1192        assert!(err.contains("basecompute/Qwen3-1.7B-EAGLE3") && err.contains("bytes, not"), "{err}");
1193        let own = bundle(&dir, "my-eagle.base", drafter("eagle3", "x", "base_f16", 1));
1194        assert_eq!(cat.digest_refusal(&own), None, "a name the catalog does not list is not checked");
1195        assert!(resolve(own.to_str().unwrap(), &t).unwrap().is_some());
1196        std::fs::remove_dir_all(&dir).ok();
1197    }
1198}