From 0fa3c06edf4c0bf654bae20788325c55bf865c9d Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Wed, 2 Sep 2026 12:39:06 +0200 Subject: [PATCH] common : resolve - download tags and list cached sidecars A Q4_0-mtp style tag now resolves the sidecar file when no model file matches it, so a solo draft or mmproj download actually pulls the file. Cached sidecar files list as their own entries so the state survives a restart, and removing such a tag deletes only the sidecar. Assisted-by: pi:zai-org/GLM-5.3 --- common/download.cpp | 131 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 115 insertions(+), 16 deletions(-) diff --git a/common/download.cpp b/common/download.cpp index 4b28a708c8..04914093c6 100644 --- a/common/download.cpp +++ b/common/download.cpp @@ -8,6 +8,7 @@ #include "json.h" #include +#include #include #include #include @@ -563,9 +564,37 @@ static hf_cache::hf_files get_split_files(const hf_cache::hf_files & files, return result; } +// sidecar filename tokens, as used in `-` download tags, +// e.g. `Q4_0-mtp` for `mtp-Model-Q4_0.gguf`, `BF16-mmproj` for `mmproj-BF16.gguf` +static const std::vector sidecar_tokens = { + "mtp", "eagle3", "dflash", "dspark", "mmproj", +}; + +static bool iequals(const std::string & a, const std::string & b) { + return a.size() == b.size() && + std::equal(a.begin(), a.end(), b.begin(), [](char x, char y) { + return std::tolower((unsigned char) x) == std::tolower((unsigned char) y); + }); +} + +// split a `-` tag into its parts, e.g. `Q4_0-mtp` -> {`Q4_0`, `mtp`}; +// a bare sidecar tag (`mtp`) yields an empty quant; no sidecar yields an empty token +static std::pair split_sidecar_tag(const std::string & tag) { + for (const auto & t : sidecar_tokens) { + if (tag.size() > t.size() + 1 && iequals(tag.substr(tag.size() - t.size() - 1), "-" + t)) { + return { tag.substr(0, tag.size() - t.size() - 1), t }; + } + if (iequals(tag, t)) { + return { "", t }; + } + } + return { tag, "" }; +} + // pick the best sibling GGUF whose filename contains `keyword` (e.g. "mmproj" / "mtp"), // preferring deeper shared directory prefix with the model, then exact `tag` match, // then closest quantization to the tag when given, or to the model otherwise +// an empty `model` skips the directory constraint: the sidecar is matched by tag alone static hf_cache::hf_file find_best_sibling(const hf_cache::hf_files & files, const std::string & model, const std::string & keyword, @@ -589,7 +618,6 @@ static hf_cache::hf_file find_best_sibling(const hf_cache::hf_files & files, model_bits = extract_quant_bits(model); } auto model_parts = string_split(model, '/'); - auto model_dir = model_parts.end() - 1; for (const auto & f : files) { if (!string_ends_with(f.path, ".gguf") || @@ -600,13 +628,16 @@ static hf_cache::hf_file find_best_sibling(const hf_cache::hf_files & files, auto sib_parts = string_split(f.path, '/'); auto sib_dir = sib_parts.end() - 1; - auto [_, dir] = std::mismatch(model_parts.begin(), model_dir, - sib_parts.begin(), sib_dir); - if (dir != sib_dir) { - continue; + size_t depth = 0; + if (!model.empty()) { + auto model_dir = model_parts.end() - 1; + auto [_, dir] = std::mismatch(model_parts.begin(), model_dir, + sib_parts.begin(), sib_dir); + if (dir != sib_dir) { + continue; + } + depth = dir - sib_parts.begin(); } - - size_t depth = dir - sib_parts.begin(); auto bits = extract_quant_bits(f.path); auto diff = std::abs(bits - model_bits); @@ -765,8 +796,28 @@ common_download_hf_plan common_download_get_hf_plan(const common_params_model & } } else { primary = find_best_model(all, tag); + + // a `-` tag (e.g. `Q4_0-mtp`) requests that sidecar alone; + // the full tag is kept for embedded-draft files (e.g. `Model-Q4_0-mtp.gguf`), + // so this only runs when no model file matched + auto [base_tag, sidecar] = split_sidecar_tag(tag); + + if (primary.path.empty() && !sidecar.empty()) { + auto keyword = sidecar == "mmproj" ? sidecar : sidecar + "-"; + auto found = find_best_sibling(all, "", keyword, base_tag); + + if (!found.path.empty()) { + if (sidecar == "mtp") plan.mtp = found; + else if (sidecar == "eagle3") plan.eagle3 = found; + else if (sidecar == "dflash") plan.dflash = found; + else if (sidecar == "dspark") plan.dspark = found; + else plan.mmproj = found; + } + } + // a requested sidecar can resolve on its own, without a full model of the same tag - if (primary.path.empty() && !opts.download_mtp && !opts.download_dflash && !opts.download_eagle3 && !opts.download_dspark) { + if (primary.path.empty() && sidecar.empty() && + !opts.download_mtp && !opts.download_dflash && !opts.download_eagle3 && !opts.download_dspark) { LOG_ERR("%s: no GGUF files found in repository %s\n", __func__, repo.c_str()); list_available_gguf_files(all); return plan; @@ -794,7 +845,7 @@ common_download_hf_plan common_download_get_hf_plan(const common_params_model & plan.dspark = find_best_dspark(all, primary.path, tag); } - if (primary.path.empty() && + if (primary.path.empty() && plan.mmproj.local_path.empty() && plan.mtp.local_path.empty() && plan.dflash.local_path.empty() && plan.eagle3.local_path.empty() && plan.dspark.local_path.empty()) { LOG_ERR("%s: no GGUF files found in repository %s\n", __func__, repo.c_str()); list_available_gguf_files(all); @@ -969,16 +1020,45 @@ std::vector common_list_cached_models() { for (const auto & f : files) { auto split = get_gguf_split_info(f.path); - if (split.index != 1 || split.tag.empty() || + if (split.index != 1 || split.tag.empty()) { + continue; + } + + bool is_sidecar = split.prefix.find("mmproj") != std::string::npos || split.prefix.find("mtp-") != std::string::npos || split.prefix.find("eagle3-") != std::string::npos || split.prefix.find("dflash-") != std::string::npos || - split.prefix.find("dspark-") != std::string::npos) { - continue; + split.prefix.find("dspark-") != std::string::npos; + + std::string tag = split.tag; + + if (is_sidecar) { + // a sidecar is listed under its own `-` tag, so a + // cached `mtp-Model-Q4_0.gguf` shows up as `:Q4_0-mtp` + auto base = f.path; + if (auto pos = base.rfind('/'); pos != std::string::npos) { + base = base.substr(pos + 1); + } + + std::string sidecar; + for (const auto & t : sidecar_tokens) { + if (base.rfind(t + "-", 0) == 0) { + sidecar = t; + break; + } + } + + // an unrecognized sidecar name cannot be re-requested by tag, stay hidden + if (sidecar.empty()) { + continue; + } + + tag += "-" + sidecar; } - if (seen.insert(f.repo_id + ":" + split.tag).second) { - result.push_back({f.repo_id, split.tag}); + + if (seen.insert(f.repo_id + ":" + tag).second) { + result.push_back({f.repo_id, tag}); } } @@ -1019,6 +1099,15 @@ bool common_download_remove(const std::string & hf_repo_with_tag) { c = (char) std::toupper((unsigned char) c); } + // a sidecar tag (`Q4_0-mtp`) targets the sidecar file alone + auto [base_tag, sidecar] = split_sidecar_tag(tag); + if (!sidecar.empty()) { + tag_upper = base_tag; + for (char & c : tag_upper) { + c = (char) std::toupper((unsigned char) c); + } + } + auto files = hf_cache::get_cached_files(repo_id); if (files.empty()) { return false; @@ -1028,9 +1117,19 @@ bool common_download_remove(const std::string & hf_repo_with_tag) { std::vector to_remove; for (const auto & f : files) { auto split = get_gguf_split_info(f.path); - if (split.tag == tag_upper) { - to_remove.emplace_back(f.local_path); + if (split.tag != tag_upper) { + continue; } + if (!sidecar.empty()) { + auto base = f.path; + if (auto pos = base.rfind('/'); pos != std::string::npos) { + base = base.substr(pos + 1); + } + if (base.rfind(sidecar + "-", 0) != 0) { + continue; + } + } + to_remove.emplace_back(f.local_path); } if (to_remove.empty()) {