Felipe97/llama-cpp-compiled
01.1k
1#include "arg.h"2 3#include "build-info.h"4#include "common.h"5#include "log.h"6#include "download.h"7#include "hf-cache.h"8#include "json.h"9 10#include <algorithm>11#include <filesystem>12#include <fstream>13#include <future>14#include <map>15#include <mutex>16#include <regex>17#include <unordered_set>18#include <string>19#include <thread>20#include <vector>21 22#include "http.h"23 24#ifndef __EMSCRIPTEN__25#ifdef __linux__26#include <linux/limits.h>27#elif defined(_WIN32)28# if !defined(PATH_MAX)29# define PATH_MAX MAX_PATH30# endif31#elif defined(_AIX)32#include <sys/limits.h>33#else34#include <sys/syslimits.h>35#endif36#endif37 38// isatty39#if defined(_WIN32)40#include <io.h>41#else42#include <unistd.h>43#endif44 45//46// downloader47//48 49// validate repo name format: owner/repo50static void write_file(const std::string & fname, const std::string & content) {51 const std::string fname_tmp = fname + ".tmp";52 std::ofstream file(fname_tmp);53 if (!file) {54 throw std::runtime_error(string_format("error: failed to open file '%s'\n", fname.c_str()));55 }56 57 try {58 file << content;59 file.close();60 61 // Makes write atomic62 if (rename(fname_tmp.c_str(), fname.c_str()) != 0) {63 LOG_ERR("%s: unable to rename file: %s to %s\n", __func__, fname_tmp.c_str(), fname.c_str());64 // If rename fails, try to delete the temporary file65 if (remove(fname_tmp.c_str()) != 0) {66 LOG_ERR("%s: unable to delete temporary file: %s\n", __func__, fname_tmp.c_str());67 }68 }69 } catch (...) {70 // If anything fails, try to delete the temporary file71 if (remove(fname_tmp.c_str()) != 0) {72 LOG_ERR("%s: unable to delete temporary file: %s\n", __func__, fname_tmp.c_str());73 }74 75 throw std::runtime_error(string_format("error: failed to write file '%s'\n", fname.c_str()));76 }77}78 79static void write_etag(const std::string & path, const std::string & etag) {80 const std::string etag_path = path + ".etag";81 write_file(etag_path, etag);82 LOG_DBG("%s: file etag saved: %s\n", __func__, etag_path.c_str());83}84 85static std::string read_etag(const std::string & path) {86 const std::string etag_path = path + ".etag";87 if (!std::filesystem::exists(etag_path)) {88 return {};89 }90 std::ifstream etag_in(etag_path);91 if (!etag_in) {92 LOG_ERR("%s: could not open .etag file for reading: %s\n", __func__, etag_path.c_str());93 return {};94 }95 std::string etag;96 std::getline(etag_in, etag);97 return etag;98}99 100static bool is_http_status_ok(int status) {101 return status >= 200 && status < 400;102}103 104std::pair<std::string, std::string> common_download_split_repo_tag(const std::string & hf_repo_with_tag) {105 auto parts = string_split<std::string>(hf_repo_with_tag, ':');106 std::string tag = parts.size() > 1 ? parts.back() : "";107 std::string hf_repo = parts[0];108 if (string_split<std::string>(hf_repo, '/').size() != 2) {109 throw std::invalid_argument("error: invalid HF repo format, expected <user>/<model>[:quant]\n");110 }111 return {hf_repo, tag};112}113 114class ProgressBar : public common_download_callback {115 static inline std::mutex mutex;116 static inline std::map<const ProgressBar *, int> lines;117 static inline int max_line = 0;118 119 std::string filename;120 size_t len = 0;121 122 static void cleanup(const ProgressBar * line) {123 lines.erase(line);124 if (lines.empty()) {125 max_line = 0;126 }127 }128 129 static bool is_output_a_tty() {130#if defined(_WIN32)131 return _isatty(_fileno(stdout));132#else133 return isatty(1);134#endif135 }136 137public:138 ProgressBar() = default;139 140 void on_start(const common_download_progress & p) override {141 filename = p.url;142 143 if (auto pos = filename.rfind('/'); pos != std::string::npos) {144 filename = filename.substr(pos + 1);145 }146 if (auto pos = filename.find('?'); pos != std::string::npos) {147 filename = filename.substr(0, pos);148 }149 for (size_t i = 0; i < filename.size(); ++i) {150 if ((filename[i] & 0xC0) != 0x80) {151 if (len++ == 39) {152 filename.resize(i);153 filename += "…";154 break;155 }156 }157 }158 }159 160 void on_done(const common_download_progress &, bool) override {161 std::lock_guard<std::mutex> lock(mutex);162 cleanup(this);163 }164 165 void on_update(const common_download_progress & p) override {166 if (!p.total || !is_output_a_tty()) {167 return;168 }169 170 std::lock_guard<std::mutex> lock(mutex);171 172 if (lines.find(this) == lines.end()) {173 lines[this] = max_line++;174 std::cout << "\n";175 }176 int lines_up = max_line - lines[this];177 178 size_t bar = (55 - len) * 2;179 size_t pct = (100 * p.downloaded) / p.total;180 size_t pos = (bar * p.downloaded) / p.total;181 182 if (lines_up > 0) {183 std::cout << "\033[" << lines_up << "A";184 }185 std::cout << '\r' << "Downloading " << filename << " ";186 187 for (size_t i = 0; i < bar; i += 2) {188 std::cout << (i + 1 < pos ? "─" : (i < pos ? "╴" : " "));189 }190 std::cout << std::setw(4) << pct << "%\033[K";191 192 if (lines_up > 0) {193 std::cout << "\033[" << lines_up << "B";194 }195 std::cout << '\r' << std::flush;196 197 if (p.downloaded == p.total) {198 cleanup(this);199 }200 }201 202 ProgressBar(const ProgressBar &) = delete;203 ProgressBar & operator=(const ProgressBar &) = delete;204};205 206static bool common_pull_file(httplib::Client & cli,207 const std::string & resolve_path,208 const std::string & path_tmp,209 bool supports_ranges,210 common_download_progress & p,211 common_download_callback * callback) {212 std::ofstream ofs(path_tmp, std::ios::binary | std::ios::app);213 if (!ofs.is_open()) {214 LOG_ERR("%s: error opening local file for writing: %s\n", __func__, path_tmp.c_str());215 return false;216 }217 218 httplib::Headers headers;219 if (supports_ranges && p.downloaded > 0) {220 headers.emplace("Range", "bytes=" + std::to_string(p.downloaded) + "-");221 }222 223 const char * func = __func__; // avoid __func__ inside a lambda224 size_t progress_step = 0;225 226 auto res = cli.Get(resolve_path, headers,227 [&](const httplib::Response &response) {228 if (p.downloaded > 0 && response.status != 206) {229 LOG_WRN("%s: server did not respond with 206 Partial Content for a resume request. Status: %d\n", func, response.status);230 return false;231 }232 if (p.downloaded == 0 && response.status != 200) {233 LOG_WRN("%s: download received non-successful status code: %d\n", func, response.status);234 return false;235 }236 if (p.total == 0 && response.has_header("Content-Length")) {237 try {238 size_t content_length = std::stoull(response.get_header_value("Content-Length"));239 p.total = p.downloaded + content_length;240 } catch (const std::exception &e) {241 LOG_WRN("%s: invalid Content-Length header: %s\n", func, e.what());242 }243 }244 return true;245 },246 [&](const char *data, size_t len) {247 ofs.write(data, len);248 if (!ofs) {249 LOG_ERR("%s: error writing to file: %s\n", func, path_tmp.c_str());250 return false;251 }252 p.downloaded += len;253 progress_step += len;254 255 if (progress_step >= p.total / 1000 || p.downloaded == p.total) {256 if (callback) {257 callback->on_update(p);258 if (callback->is_cancelled()) {259 return false;260 }261 }262 progress_step = 0;263 }264 return true;265 },266 nullptr267 );268 269 if (!res) {270 LOG_ERR("%s: download failed: %s (status: %d)\n",271 __func__,272 httplib::to_string(res.error()).c_str(),273 res ? res->status : -1);274 return false;275 }276 277 return true;278}279 280// download one single file from remote URL to local path281// returns status code or -1 on error282static int common_download_file_single_online(const std::string & url,283 const std::string & path,284 const common_download_opts & opts,285 bool skip_etag) {286 static const int max_attempts = 3;287 static const int retry_delay_seconds = 2;288 289 const bool file_exists = std::filesystem::exists(path);290 291 if (file_exists && skip_etag) {292 LOG_DBG("%s: using cached file: %s\n", __func__, path.c_str());293 return 304; // 304 Not Modified - fake cached response294 }295 296 auto [cli, parts] = common_http_client(url);297 298 httplib::Headers headers;299 for (const auto & h : opts.headers) {300 headers.emplace(h.first, h.second);301 }302 if (headers.find("User-Agent") == headers.end()) {303 headers.emplace("User-Agent", "llama-cpp/" + std::string(llama_build_info()));304 }305 if (!opts.bearer_token.empty()) {306 headers.emplace("Authorization", "Bearer " + opts.bearer_token);307 }308 cli.set_default_headers(headers);309 310 std::string last_etag;311 if (file_exists) {312 last_etag = read_etag(path);313 } else {314 LOG_DBG("%s: no previous model file found %s\n", __func__, path.c_str());315 }316 317 auto head = cli.Head(parts.path);318 if (!head || head->status < 200 || head->status >= 300) {319 LOG_TRC("%s: HEAD failed, status: %d\n", __func__, head ? head->status : -1);320 if (file_exists) {321 LOG_TRC("%s: using cached file (HEAD failed): %s\n", __func__, path.c_str());322 return 304; // 304 Not Modified - fake cached response323 }324 return head ? head->status : -1;325 }326 327 std::string etag;328 if (head->has_header("ETag")) {329 etag = head->get_header_value("ETag");330 }331 332 common_download_progress p;333 p.url = url;334 if (head->has_header("Content-Length")) {335 try {336 p.total = std::stoull(head->get_header_value("Content-Length"));337 } catch (const std::exception& e) {338 LOG_WRN("%s: invalid Content-Length in HEAD response: %s\n", __func__, e.what());339 }340 }341 342 bool supports_ranges = false;343 if (head->has_header("Accept-Ranges")) {344 supports_ranges = head->get_header_value("Accept-Ranges") != "none";345 }346 347 if (file_exists) {348 if (etag.empty()) {349 LOG_DBG("%s: using cached file (no server etag): %s\n", __func__, path.c_str());350 return 304; // 304 Not Modified - fake cached response351 }352 if (!last_etag.empty() && last_etag == etag) {353 LOG_DBG("%s: using cached file (same etag): %s\n", __func__, path.c_str());354 return 304; // 304 Not Modified - fake cached response355 }356 // pass this point, the file exists but is different from the server version, so we need to redownload it357 if (remove(path.c_str()) != 0) {358 LOG_ERR("%s: unable to delete file: %s\n", __func__, path.c_str());359 return -1;360 }361 }362 363 { // silent364 std::error_code ec;365 std::filesystem::create_directories(std::filesystem::path(path).parent_path(), ec);366 }367 368 bool success = false;369 const std::string path_temporary = path + ".downloadInProgress";370 int delay = retry_delay_seconds;371 372 if (opts.callback) {373 opts.callback->on_start(p);374 }375 376 for (int i = 0; i < max_attempts; ++i) {377 if (opts.callback && opts.callback->is_cancelled()) {378 break;379 }380 if (i) {381 LOG_WRN("%s: retrying after %d seconds...\n", __func__, delay);382 std::this_thread::sleep_for(std::chrono::seconds(delay));383 delay *= retry_delay_seconds;384 }385 386 size_t existing_size = 0;387 388 if (std::filesystem::exists(path_temporary)) {389 if (supports_ranges) {390 existing_size = std::filesystem::file_size(path_temporary);391 } else if (remove(path_temporary.c_str()) != 0) {392 LOG_ERR("%s: unable to delete file: %s\n", __func__, path_temporary.c_str());393 break;394 }395 }396 397 p.downloaded = existing_size;398 399 LOG_DBG("%s: downloading from %s to %s (etag:%s)...\n",400 __func__, common_http_show_masked_url(parts).c_str(),401 path_temporary.c_str(), etag.c_str());402 403 if (common_pull_file(cli, parts.path, path_temporary, supports_ranges, p, opts.callback)) {404 if (std::rename(path_temporary.c_str(), path.c_str()) != 0) {405 LOG_ERR("%s: unable to rename file: %s to %s\n", __func__, path_temporary.c_str(), path.c_str());406 break;407 }408 if (!etag.empty() && !skip_etag) {409 write_etag(path, etag);410 }411 success = true;412 break;413 }414 }415 416 if (opts.callback) {417 opts.callback->on_done(p, success);418 }419 if (opts.callback && opts.callback->is_cancelled() &&420 std::filesystem::exists(path_temporary)) {421 if (remove(path_temporary.c_str()) != 0) {422 LOG_ERR("%s: unable to delete temporary file: %s\n", __func__, path_temporary.c_str());423 }424 }425 if (!success) {426 LOG_ERR("%s: download failed after %d attempts\n", __func__, max_attempts);427 return -1; // max attempts reached428 }429 430 return head->status;431}432 433std::pair<long, std::vector<char>> common_remote_get_content(const std::string & url,434 const common_remote_params & params) {435 auto [cli, parts] = common_http_client(url);436 437 httplib::Headers headers;438 for (const auto & h : params.headers) {439 headers.emplace(h.first, h.second);440 }441 if (headers.find("User-Agent") == headers.end()) {442 headers.emplace("User-Agent", "llama-cpp/" + std::string(llama_build_info()));443 }444 445 if (params.timeout > 0) {446 cli.set_read_timeout(params.timeout, 0);447 cli.set_write_timeout(params.timeout, 0);448 }449 450 std::vector<char> buf;451 auto res = cli.Get(parts.path, headers,452 [&](const char *data, size_t len) {453 buf.insert(buf.end(), data, data + len);454 return params.max_size == 0 ||455 buf.size() <= static_cast<size_t>(params.max_size);456 },457 nullptr458 );459 460 if (!res) {461 throw std::runtime_error("error: cannot make GET request");462 }463 464 return { res->status, std::move(buf) };465}466 467int common_download_file_single(const std::string & url,468 const std::string & path,469 const common_download_opts & opts,470 bool skip_etag) {471 if (!opts.offline) {472 ProgressBar tty_cb;473 common_download_opts online_opts = opts;474 if (!online_opts.callback) {475 online_opts.callback = &tty_cb;476 }477 return common_download_file_single_online(url, path, online_opts, skip_etag);478 }479 480 if (!std::filesystem::exists(path)) {481 LOG_ERR("%s: required file is not available in cache (offline mode): %s\n", __func__, path.c_str());482 return -1;483 }484 485 LOG_DBG("%s: using cached file (offline mode): %s\n", __func__, path.c_str());486 487 // notify the callback that the file was cached488 if (opts.callback) {489 common_download_progress p;490 p.url = url;491 p.cached = true;492 opts.callback->on_start(p);493 opts.callback->on_done(p, true);494 }495 496 return 304; // Not Modified - fake cached response497}498 499struct gguf_split_info {500 std::string prefix; // tag included501 std::string tag;502 int index;503 int count;504};505 506static gguf_split_info get_gguf_split_info(const std::string & path) {507 static const std::regex re_split("^(.+)-([0-9]{5})-of-([0-9]{5})$", std::regex::icase);508 static const std::regex re_tag("[-.]([A-Z0-9_]+)$", std::regex::icase);509 std::smatch m;510 511 std::string prefix = path;512 if (!string_remove_suffix(prefix, ".gguf")) {513 return {};514 }515 516 int index = 1;517 int count = 1;518 519 if (std::regex_match(prefix, m, re_split)) {520 index = std::stoi(m[2].str());521 count = std::stoi(m[3].str());522 prefix = m[1].str();523 }524 525 std::string tag;526 if (std::regex_search(prefix, m, re_tag)) {527 tag = m[1].str();528 for (char & c : tag) {529 c = std::toupper((unsigned char)c);530 }531 }532 533 return {std::move(prefix), std::move(tag), index, count};534}535 536// Q4_0 -> 4, F16 -> 16, NVFP4 -> 4, Q8_K_M -> 8, etc537static int extract_quant_bits(const std::string & filename) {538 auto split = get_gguf_split_info(filename);539 540 auto pos = split.tag.find_first_of("0123456789");541 if (pos == std::string::npos) {542 return 0;543 }544 545 return std::stoi(split.tag.substr(pos));546}547 548static hf_cache::hf_files get_split_files(const hf_cache::hf_files & files,549 const hf_cache::hf_file & file) {550 auto split = get_gguf_split_info(file.path);551 552 if (split.count <= 1) {553 return {file};554 }555 hf_cache::hf_files result;556 557 for (const auto & f : files) {558 auto split_f = get_gguf_split_info(f.path);559 if (split_f.count == split.count && split_f.prefix == split.prefix) {560 result.push_back(f);561 }562 }563 return result;564}565 566// pick the best sibling GGUF whose filename contains `keyword` (e.g. "mmproj" / "mtp"),567// preferring deeper shared directory prefix with the model, then exact `tag` match,568// then closest quantization to the tag when given, or to the model otherwise569static hf_cache::hf_file find_best_sibling(const hf_cache::hf_files & files,570 const std::string & model,571 const std::string & keyword,572 const std::string & tag = "") {573 hf_cache::hf_file best;574 size_t best_depth = 0;575 int best_diff = 0;576 bool best_exact = false;577 bool found = false;578 579 std::string tag_upper = tag;580 for (char & c : tag_upper) {581 c = (char) std::toupper((unsigned char) c);582 }583 584 int model_bits = 0;585 if (!tag_upper.empty()) {586 auto pos = tag_upper.find_first_of("0123456789");587 model_bits = pos == std::string::npos ? 0 : std::stoi(tag_upper.substr(pos));588 } else {589 model_bits = extract_quant_bits(model);590 }591 auto model_parts = string_split<std::string>(model, '/');592 auto model_dir = model_parts.end() - 1;593 594 for (const auto & f : files) {595 if (!string_ends_with(f.path, ".gguf") ||596 f.path.find(keyword) == std::string::npos) {597 continue;598 }599 600 auto sib_parts = string_split<std::string>(f.path, '/');601 auto sib_dir = sib_parts.end() - 1;602 603 auto [_, dir] = std::mismatch(model_parts.begin(), model_dir,604 sib_parts.begin(), sib_dir);605 if (dir != sib_dir) {606 continue;607 }608 609 size_t depth = dir - sib_parts.begin();610 auto bits = extract_quant_bits(f.path);611 auto diff = std::abs(bits - model_bits);612 613 std::string path_upper = f.path;614 for (char & c : path_upper) {615 c = (char) std::toupper((unsigned char) c);616 }617 bool exact = !tag_upper.empty() && path_upper.find("-" + tag_upper + ".") != std::string::npos;618 619 if (!found || depth > best_depth ||620 (depth == best_depth && exact && !best_exact) ||621 (depth == best_depth && exact == best_exact && diff < best_diff)) {622 best = f;623 best_depth = depth;624 best_diff = diff;625 best_exact = exact;626 found = true;627 }628 }629 return best;630}631 632static hf_cache::hf_file find_best_mmproj(const hf_cache::hf_files & files,633 const std::string & model) {634 return find_best_sibling(files, model, "mmproj");635}636 637static hf_cache::hf_file find_best_mtp(const hf_cache::hf_files & files,638 const std::string & model,639 const std::string & tag = "") {640 return find_best_sibling(files, model, "mtp-", tag);641}642 643static hf_cache::hf_file find_best_eagle3(const hf_cache::hf_files & files,644 const std::string & model,645 const std::string & tag = "") {646 return find_best_sibling(files, model, "eagle3-", tag);647}648 649static hf_cache::hf_file find_best_dflash(const hf_cache::hf_files & files,650 const std::string & model,651 const std::string & tag = "") {652 return find_best_sibling(files, model, "dflash-", tag);653}654 655static hf_cache::hf_file find_best_dspark(const hf_cache::hf_files & files,656 const std::string & model,657 const std::string & tag = "") {658 return find_best_sibling(files, model, "dspark-", tag);659}660 661static bool gguf_filename_is_model(const std::string & filepath) {662 if (!string_ends_with(filepath, ".gguf")) {663 return false;664 }665 666 std::string filename = filepath;667 if (auto pos = filename.rfind('/'); pos != std::string::npos) {668 filename = filename.substr(pos + 1);669 }670 671 return filename.find("mmproj") == std::string::npos &&672 filename.find("imatrix") == std::string::npos &&673 filename.find("mtp-") == std::string::npos &&674 filename.find("eagle3-") == std::string::npos &&675 filename.find("dflash-") == std::string::npos &&676 filename.find("dspark-") == std::string::npos;677}678 679static hf_cache::hf_file find_best_model(const hf_cache::hf_files & files,680 const std::string & tag) {681 std::vector<std::string> tags;682 683 if (!tag.empty()) {684 tags.push_back(tag);685 } else {686 tags = {"Q4_K_M", "Q8_0"};687 }688 689 for (const auto & t : tags) {690 std::regex pattern(t + "[.-]", std::regex::icase);691 for (const auto & f : files) {692 if (gguf_filename_is_model(f.path) &&693 std::regex_search(f.path, pattern)) {694 auto split = get_gguf_split_info(f.path);695 if (split.count > 1 && split.index != 1) {696 continue;697 }698 return f;699 }700 }701 }702 703 // fallback to first available model only if tag is empty704 if (tag.empty()) {705 for (const auto & f : files) {706 if (gguf_filename_is_model(f.path)) {707 auto split = get_gguf_split_info(f.path);708 if (split.count > 1 && split.index != 1) {709 continue;710 }711 return f;712 }713 }714 }715 716 return {};717}718 719static void list_available_gguf_files(const hf_cache::hf_files & files) {720 LOG_INF("Available GGUF files:\n");721 for (const auto & f : files) {722 if (string_ends_with(f.path, ".gguf")) {723 LOG_INF(" - %s\n", f.path.c_str());724 }725 }726}727 728common_download_hf_plan common_download_get_hf_plan(const common_params_model & model, const common_download_opts & opts) {729 common_download_hf_plan plan;730 hf_cache::hf_files all;731 732 auto [repo, tag] = common_download_split_repo_tag(model.hf_repo);733 734 if (!opts.offline) {735 all = hf_cache::get_repo_files(repo, opts.bearer_token);736 }737 if (all.empty()) {738 all = hf_cache::get_cached_files(repo);739 }740 if (all.empty()) {741 return plan;742 }743 744 // if preset.ini exists in the repo root, download only that file745 for (const auto & f : all) {746 if (f.path == "preset.ini") {747 plan.preset = f;748 return plan;749 }750 }751 752 hf_cache::hf_file primary;753 754 if (!model.hf_file.empty()) {755 for (const auto & f : all) {756 if (f.path == model.hf_file) {757 primary = f;758 break;759 }760 }761 if (primary.path.empty()) {762 LOG_ERR("%s: file '%s' not found in repository\n", __func__, model.hf_file.c_str());763 list_available_gguf_files(all);764 return plan;765 }766 } else {767 primary = find_best_model(all, tag);768 // a requested sidecar can resolve on its own, without a full model of the same tag769 if (primary.path.empty() && !opts.download_mtp && !opts.download_dflash && !opts.download_eagle3 && !opts.download_dspark) {770 LOG_ERR("%s: no GGUF files found in repository %s\n", __func__, repo.c_str());771 list_available_gguf_files(all);772 return plan;773 }774 }775 776 if (!primary.path.empty()) {777 plan.primary = primary;778 plan.model_files = get_split_files(all, primary);779 }780 781 if (opts.download_mmproj && !primary.path.empty()) {782 plan.mmproj = find_best_mmproj(all, primary.path);783 }784 if (opts.download_mtp) {785 plan.mtp = find_best_mtp(all, primary.path, tag);786 }787 if (opts.download_dflash) {788 plan.dflash = find_best_dflash(all, primary.path, tag);789 }790 if (opts.download_eagle3) {791 plan.eagle3 = find_best_eagle3(all, primary.path, tag);792 }793 if (opts.download_dspark) {794 plan.dspark = find_best_dspark(all, primary.path, tag);795 }796 797 if (primary.path.empty() &&798 plan.mtp.local_path.empty() && plan.dflash.local_path.empty() && plan.eagle3.local_path.empty() && plan.dspark.local_path.empty()) {799 LOG_ERR("%s: no GGUF files found in repository %s\n", __func__, repo.c_str());800 list_available_gguf_files(all);801 }802 803 return plan;804}805 806void common_download_run_tasks(const std::vector<common_download_task> & tasks) {807 std::vector<std::future<int>> futures;808 for (const auto & task : tasks) {809 futures.push_back(std::async(std::launch::async,810 [&task]() {811 return common_download_file_single(task.url, task.local_path, task.opts, task.is_hf);812 }813 ));814 }815 816 for (size_t i = 0; i < futures.size(); ++i) {817 std::string url = tasks[i].url;818 int status = futures[i].get();819 bool is_ok = is_http_status_ok(status);820 if (!is_ok) {821 throw std::runtime_error(string_format("Download '%s' failed with status code: %d", url.c_str(), status));822 }823 }824}825 826std::vector<std::string> common_download_get_all_parts(const std::string & url) {827 auto split = get_gguf_split_info(url);828 829 if (split.count <= 1) {830 return {url};831 }832 833 std::vector<std::string> parts;834 for (int i = 1; i <= split.count; i++) {835 auto suffix = string_format("-%05d-of-%05d.gguf", i, split.count);836 parts.push_back(split.prefix + suffix);837 }838 return parts;839}840 841//842// Docker registry functions843//844 845static std::string common_docker_get_token(const std::string & repo) {846 std::string url = "https://auth.docker.io/token?service=registry.docker.io&scope=repository:" + repo + ":pull";847 848 common_remote_params params;849 auto res = common_remote_get_content(url, params);850 851 if (res.first != 200) {852 throw std::runtime_error("Failed to get Docker registry token, HTTP code: " + std::to_string(res.first));853 }854 855 std::string response_str(res.second.begin(), res.second.end());856 common_json response = common_json::parse(response_str);857 858 if (!response.contains("token")) {859 throw std::runtime_error("Docker registry token response missing 'token' field");860 }861 862 return response["token"].get<std::string>();863}864 865std::string common_docker_resolve_model(const std::string & docker) {866 // Parse ai/smollm2:135M-Q4_0867 size_t colon_pos = docker.find(':');868 std::string repo, tag;869 if (colon_pos != std::string::npos) {870 repo = docker.substr(0, colon_pos);871 tag = docker.substr(colon_pos + 1);872 } else {873 repo = docker;874 tag = "latest";875 }876 877 // ai/ is the default878 size_t slash_pos = docker.find('/');879 if (slash_pos == std::string::npos) {880 repo.insert(0, "ai/");881 }882 883 LOG_INF("%s: Downloading Docker Model: %s:%s\n", __func__, repo.c_str(), tag.c_str());884 try {885 // --- helper: digest validation ---886 auto validate_oci_digest = [](const std::string & digest) -> std::string {887 // Expected: algo:hex ; start with sha256 (64 hex chars)888 // You can extend this map if supporting other algorithms in future.889 static const std::regex re("^sha256:([a-fA-F0-9]{64})$");890 std::smatch m;891 if (!std::regex_match(digest, m, re)) {892 throw std::runtime_error("Invalid OCI digest format received in manifest: " + digest);893 }894 // normalize hex to lowercase895 std::string normalized = digest;896 std::transform(normalized.begin()+7, normalized.end(), normalized.begin()+7, [](unsigned char c){897 return std::tolower(c);898 });899 return normalized;900 };901 902 std::string token = common_docker_get_token(repo); // Get authentication token903 904 // Get manifest905 // TODO: cache the manifest response so that it appears in the model list906 const std::string url_prefix = "https://registry-1.docker.io/v2/" + repo;907 std::string manifest_url = url_prefix + "/manifests/" + tag;908 common_remote_params manifest_params;909 manifest_params.headers.push_back({"Authorization", "Bearer " + token});910 manifest_params.headers.push_back({"Accept",911 "application/vnd.docker.distribution.manifest.v2+json,application/vnd.oci.image.manifest.v1+json"912 });913 auto manifest_res = common_remote_get_content(manifest_url, manifest_params);914 if (manifest_res.first != 200) {915 throw std::runtime_error("Failed to get Docker manifest, HTTP code: " + std::to_string(manifest_res.first));916 }917 918 std::string manifest_str(manifest_res.second.begin(), manifest_res.second.end());919 common_json manifest = common_json::parse(manifest_str);920 std::string gguf_digest; // Find the GGUF layer921 if (manifest.contains("layers")) {922 for (const auto & layer : manifest["layers"]) {923 if (layer.contains("mediaType")) {924 std::string media_type = layer["mediaType"].get<std::string>();925 if (media_type == "application/vnd.docker.ai.gguf.v3" ||926 media_type.find("gguf") != std::string::npos) {927 gguf_digest = layer["digest"].get<std::string>();928 break;929 }930 }931 }932 }933 934 if (gguf_digest.empty()) {935 throw std::runtime_error("No GGUF layer found in Docker manifest");936 }937 938 // Validate & normalize digest939 gguf_digest = validate_oci_digest(gguf_digest);940 LOG_DBG("%s: Using validated digest: %s\n", __func__, gguf_digest.c_str());941 942 // Prepare local filename943 std::string model_filename = repo;944 std::replace(model_filename.begin(), model_filename.end(), '/', '_');945 model_filename += "_" + tag + ".gguf";946 std::string local_path = fs_get_cache_file(model_filename);947 948 const std::string blob_url = url_prefix + "/blobs/" + gguf_digest;949 common_download_opts opts;950 opts.bearer_token = token;951 const int http_status = common_download_file_single(blob_url, local_path, opts);952 if (!is_http_status_ok(http_status)) {953 throw std::runtime_error("Failed to download Docker Model");954 }955 956 LOG_INF("%s: Downloaded Docker Model to: %s\n", __func__, local_path.c_str());957 return local_path;958 } catch (const std::exception & e) {959 LOG_ERR("%s: Docker Model download failed: %s\n", __func__, e.what());960 throw;961 }962}963 964std::vector<common_cached_model_info> common_list_cached_models() {965 std::unordered_set<std::string> seen;966 std::vector<common_cached_model_info> result;967 968 auto files = hf_cache::get_cached_files();969 970 for (const auto & f : files) {971 auto split = get_gguf_split_info(f.path);972 if (split.index != 1 || split.tag.empty() ||973 split.prefix.find("mmproj") != std::string::npos ||974 split.prefix.find("mtp-") != std::string::npos ||975 split.prefix.find("eagle3-") != std::string::npos ||976 split.prefix.find("dflash-") != std::string::npos ||977 split.prefix.find("dspark-") != std::string::npos) {978 continue;979 }980 if (seen.insert(f.repo_id + ":" + split.tag).second) {981 result.push_back({f.repo_id, split.tag});982 }983 }984 985 return result;986}987 988std::string common_download_resolve_path(const std::string & hf_repo_with_tag, const std::string & hf_file) {989 auto [repo, tag] = common_download_split_repo_tag(hf_repo_with_tag);990 991 auto files = hf_cache::get_cached_files(repo);992 if (files.empty()) {993 return "";994 }995 996 if (!hf_file.empty()) {997 for (const auto & f : files) {998 if (f.path == hf_file) {999 return f.local_path;1000 }1001 }1002 return "";1003 }1004 1005 return find_best_model(files, tag).local_path;1006}1007 1008bool common_download_remove(const std::string & hf_repo_with_tag) {1009 namespace fs = std::filesystem;1010 1011 auto [repo_id, tag] = common_download_split_repo_tag(hf_repo_with_tag);1012 1013 if (tag.empty()) {1014 return hf_cache::remove_cached_repo(repo_id);1015 }1016 1017 std::string tag_upper = tag;1018 for (char & c : tag_upper) {1019 c = (char) std::toupper((unsigned char) c);1020 }1021 1022 auto files = hf_cache::get_cached_files(repo_id);1023 if (files.empty()) {1024 return false;1025 }1026 1027 // collect snapshot entries whose tag matches1028 std::vector<fs::path> to_remove;1029 for (const auto & f : files) {1030 auto split = get_gguf_split_info(f.path);1031 if (split.tag == tag_upper) {1032 to_remove.emplace_back(f.local_path);1033 }1034 }1035 1036 if (to_remove.empty()) {1037 return false;1038 }1039 1040 // resolve blob paths from symlinks before deleting snapshot entries1041 std::vector<fs::path> blobs_to_check;1042 for (const auto & p : to_remove) {1043 std::error_code ec;1044 if (fs::is_symlink(p, ec)) {1045 auto target = fs::read_symlink(p, ec);1046 if (!ec) {1047 blobs_to_check.push_back((p.parent_path() / target).lexically_normal());1048 }1049 }1050 }1051 1052 // remove snapshot entries1053 for (const auto & p : to_remove) {1054 std::error_code ec;1055 fs::remove(p, ec);1056 if (ec) {1057 LOG_WRN("%s: failed to remove %s: %s\n", __func__, p.string().c_str(), ec.message().c_str());1058 }1059 }1060 1061 if (blobs_to_check.empty()) {1062 return true;1063 }1064 1065 // collect blobs still referenced by remaining snapshot entries1066 std::unordered_set<std::string> still_referenced;1067 for (const auto & f : hf_cache::get_cached_files(repo_id)) {1068 fs::path p(f.local_path);1069 std::error_code ec;1070 if (fs::is_symlink(p, ec)) {1071 auto target = fs::read_symlink(p, ec);1072 if (!ec) {1073 still_referenced.insert((p.parent_path() / target).lexically_normal().string());1074 }1075 }1076 }1077 1078 // remove orphaned blobs1079 for (const auto & blob : blobs_to_check) {1080 if (still_referenced.find(blob.string()) == still_referenced.end()) {1081 std::error_code ec;1082 fs::remove(blob, ec);1083 if (ec) {1084 LOG_WRN("%s: failed to remove blob %s: %s\n", __func__, blob.string().c_str(), ec.message().c_str());1085 }1086 }1087 }1088 1089 return true;1090}1091 