1use 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
20pub 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 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
120fn 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
134fn 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 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
309pub 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 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}