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