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