#![allow(
clippy::struct_excessive_bools,
clippy::case_sensitive_file_extension_comparisons,
reason = "Independent native discovery flags and case-sensitive GGUF filename policy."
)]
use crate::{Result, invalid};
use regex::RegexBuilder;
use std::path::PathBuf;
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct File {
pub path: String,
pub url: String,
pub local_path: PathBuf,
pub final_path: PathBuf,
pub oid: String,
pub repo: String,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Model {
pub path: String,
pub url: String,
pub hf_repo: String,
pub hf_file: String,
}
impl Model {
#[must_use]
pub const fn is_empty(&self) -> bool {
self.path.is_empty() && self.hf_repo.is_empty()
}
}
#[derive(Clone, Debug, Default)]
pub struct Options {
pub mmproj: bool,
pub mtp: bool,
pub dflash: bool,
pub eagle3: bool,
pub dspark: bool,
pub offline: bool,
pub token: String,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Plan {
pub primary: File,
pub model_files: Vec<File>,
pub mmproj: File,
pub mtp: File,
pub dflash: File,
pub eagle3: File,
pub dspark: File,
pub preset: File,
}
pub fn split_repo_tag(value: &str) -> Result<(&str, &str)> {
let parts: Vec<_> = value.split(':').collect();
let repo = parts[0];
if repo.split('/').count() != 2 {
return Err(invalid("HF repository requires owner/model[:quant]"));
}
Ok((
repo,
if parts.len() > 1 {
parts[parts.len() - 1]
} else {
""
},
))
}
#[derive(Default)]
struct Split<'a> {
prefix: &'a str,
tag: String,
index: u32,
count: u32,
}
fn split(path: &str) -> Split<'_> {
let Some(mut prefix) = path.strip_suffix(".gguf") else {
return Split::default();
};
let mut index = 1;
let mut count = 1;
if prefix.len() >= 16 {
let start = prefix.len() - 15;
let tail = &prefix.as_bytes()[start..];
if start > 0
&& tail[0] == b'-'
&& tail[1..6].iter().all(u8::is_ascii_digit)
&& tail[6..10].eq_ignore_ascii_case(b"-of-")
&& tail[10..15].iter().all(u8::is_ascii_digit)
{
index = prefix[start + 1..start + 6].parse().unwrap_or(0);
count = prefix[start + 10..].parse().unwrap_or(0);
prefix = &prefix[..start];
}
}
let tag = prefix.rsplit_once(['-', '.']).map_or("", |(_, tag)| tag);
let tag = if !tag.is_empty() && tag.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_') {
tag.to_ascii_uppercase()
} else {
String::new()
};
Split {
prefix,
tag,
index,
count,
}
}
fn bits(tag: &str) -> i32 {
let Some(pos) = tag.find(|c: char| c.is_ascii_digit()) else {
return 0;
};
tag[pos..]
.chars()
.take_while(char::is_ascii_digit)
.collect::<String>()
.parse()
.unwrap_or(0)
}
fn is_model(path: &str) -> bool {
let basename = path.rsplit('/').next().unwrap_or(path);
path.ends_with(".gguf")
&& ["mmproj", "imatrix", "mtp-", "eagle3-", "dflash-", "dspark-"]
.iter()
.all(|word| !basename.contains(word))
}
fn find_model(files: &[File], tag: &str) -> Result<File> {
let tags = if tag.is_empty() {
vec!["Q4_K_M", "Q8_0"]
} else {
vec![tag]
};
for tag in tags {
let pattern = RegexBuilder::new(&format!("{tag}[.-]"))
.case_insensitive(true)
.build()?;
for file in files {
let info = split(&file.path);
if is_model(&file.path)
&& pattern.is_match(&file.path)
&& (info.count <= 1 || info.index == 1)
{
return Ok(file.clone());
}
}
}
Ok(if tag.is_empty() {
files
.iter()
.find(|f| {
let info = split(&f.path);
is_model(&f.path) && (info.count <= 1 || info.index == 1)
})
.cloned()
.unwrap_or_default()
} else {
File::default()
})
}
fn sibling(files: &[File], model: &str, keyword: &str, tag: &str) -> File {
let tag = tag.to_ascii_uppercase();
let model_tag = split(model).tag;
let model_bits = bits(if tag.is_empty() { &model_tag } else { &tag });
let model_dir: Vec<_> = model.split('/').collect();
let model_dir = &model_dir[..model_dir.len() - 1];
let mut best: Option<(&File, usize, bool, i32)> = None;
for file in files {
if !file.path.ends_with(".gguf") || !file.path.contains(keyword) {
continue;
}
let parts: Vec<_> = file.path.split('/').collect();
let dir = &parts[..parts.len() - 1];
if !model_dir.starts_with(dir) {
continue;
}
let depth = dir.len();
let exact = !tag.is_empty() && file.path.to_ascii_uppercase().contains(&format!("-{tag}."));
let diff = (bits(&split(&file.path).tag) - model_bits).abs();
if best.is_none_or(|(_, bd, be, bf)| {
depth > bd || (depth == bd && exact && !be) || (depth == bd && exact == be && diff < bf)
}) {
best = Some((file, depth, exact, diff));
}
}
best.map_or_else(File::default, |(file, _, _, _)| file.clone())
}
pub fn select(files: &[File], model: &Model, options: &Options) -> Result<Plan> {
let (_, tag) = split_repo_tag(&model.hf_repo)?;
let mut plan = Plan::default();
if let Some(preset) = files.iter().find(|f| f.path == "preset.ini") {
plan.preset = preset.clone();
return Ok(plan);
}
let primary = if model.hf_file.is_empty() {
find_model(files, tag)?
} else {
let Some(file) = files.iter().find(|f| f.path == model.hf_file) else {
return Ok(plan);
};
file.clone()
};
if primary.path.is_empty()
&& !(options.mtp || options.dflash || options.eagle3 || options.dspark)
{
return Ok(plan);
}
if !primary.path.is_empty() {
let info = split(&primary.path);
plan.model_files = if info.count <= 1 {
vec![primary.clone()]
} else {
files
.iter()
.filter(|f| {
let other = split(&f.path);
other.count == info.count && other.prefix == info.prefix
})
.cloned()
.collect()
};
plan.primary = primary.clone();
if options.mmproj {
plan.mmproj = sibling(files, &primary.path, "mmproj", "");
}
}
if options.mtp {
plan.mtp = sibling(files, &primary.path, "mtp-", tag);
}
if options.dflash {
plan.dflash = sibling(files, &primary.path, "dflash-", tag);
}
if options.eagle3 {
plan.eagle3 = sibling(files, &primary.path, "eagle3-", tag);
}
if options.dspark {
plan.dspark = sibling(files, &primary.path, "dspark-", tag);
}
Ok(plan)
}
#[path = "resolution/cache.rs"]
mod cache;
#[path = "resolution/params.rs"]
mod params;
pub use cache::Resolver;
pub use params::{Handler, Params, SpeculativeType};
#[cfg(test)]
mod tests {
use super::*;
fn files(paths: &[&str]) -> Vec<File> {
paths
.iter()
.map(|path| File {
path: (*path).into(),
..File::default()
})
.collect()
}
#[test]
fn shard_order_and_ancestor_sidecars_follow_metadata() -> Result<()> {
let source = files(&[
"x/a-Q8_0-00002-of-00002.gguf",
"mtp-a-Q8_0.gguf",
"x/mtp-a-F16.gguf",
"x/a-Q8_0-00001-of-00002.gguf",
"x/y/mtp-a-Q8_0.gguf",
]);
let model = Model {
hf_repo: "owner/repo:Q8_0".into(),
..Model::default()
};
let plan = select(
&source,
&model,
&Options {
mtp: true,
..Options::default()
},
)?;
assert_eq!(plan.primary.path, "x/a-Q8_0-00001-of-00002.gguf");
assert_eq!(plan.model_files.len(), 2);
assert_eq!(plan.model_files[0].path, source[0].path);
assert_eq!(plan.mtp.path, "x/mtp-a-F16.gguf");
Ok(())
}
#[test]
fn explicit_missing_filename_prevents_sidecar_fallback() -> Result<()> {
let source = files(&["mtp-a-Q8_0.gguf"]);
let mut model = Model {
hf_repo: "o/r:Q8_0".into(),
..Model::default()
};
let options = Options {
mtp: true,
..Options::default()
};
assert_eq!(select(&source, &model, &options)?.mtp.path, source[0].path);
model.hf_file = "missing.gguf".into();
assert_eq!(select(&source, &model, &options)?, Plan::default());
Ok(())
}
}