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