@@ -747,6 +747,124 @@ std::pair<long, std::vector<char>> common_remote_get_content(const std::string &
747747
748748#endif // LLAMA_USE_CURL
749749
750+ //
751+ // Docker registry functions
752+ //
753+
754+ static std::string common_docker_get_token (const std::string & repo) {
755+ std::string url = " https://auth.docker.io/token?service=registry.docker.io&scope=repository:" + repo + " :pull" ;
756+
757+ common_remote_params params;
758+ auto res = common_remote_get_content (url, params);
759+
760+ if (res.first != 200 ) {
761+ throw std::runtime_error (" Failed to get Docker registry token, HTTP code: " + std::to_string (res.first ));
762+ }
763+
764+ std::string response_str (res.second .begin (), res.second .end ());
765+ nlohmann::ordered_json response = nlohmann::ordered_json::parse (response_str);
766+
767+ if (!response.contains (" token" )) {
768+ throw std::runtime_error (" Docker registry token response missing 'token' field" );
769+ }
770+
771+ return response[" token" ].get <std::string>();
772+ }
773+
774+ static std::string common_docker_resolve_model (const std::string & docker) {
775+ // Parse ai/smollm2:135M-Q4_K_M
776+ size_t colon_pos = docker.find (' :' );
777+ std::string repo, tag;
778+ if (colon_pos != std::string::npos) {
779+ repo = docker.substr (0 , colon_pos);
780+ tag = docker.substr (colon_pos + 1 );
781+ } else {
782+ repo = docker;
783+ tag = " latest" ;
784+ }
785+
786+ // ai/ is the default
787+ size_t slash_pos = docker.find (' /' );
788+ if (slash_pos == std::string::npos) {
789+ repo.insert (0 , " ai/" );
790+ }
791+
792+ LOG_INF (" %s: Downloading Docker Model: %s:%s\n " , __func__, repo.c_str (), tag.c_str ());
793+ try {
794+ // --- helper: digest validation ---
795+ auto validate_oci_digest = [](const std::string & digest) -> std::string {
796+ // Expected: algo:hex ; start with sha256 (64 hex chars)
797+ // You can extend this map if supporting other algorithms in future.
798+ static const std::regex re (" ^sha256:([a-fA-F0-9]{64})$" );
799+ std::smatch m;
800+ if (!std::regex_match (digest, m, re)) {
801+ throw std::runtime_error (" Invalid OCI digest format received in manifest: " + digest);
802+ }
803+ // normalize hex to lowercase
804+ std::string normalized = digest;
805+ std::transform (normalized.begin ()+7 , normalized.end (), normalized.begin ()+7 , [](unsigned char c){
806+ return std::tolower (c);
807+ });
808+ return normalized;
809+ };
810+
811+ std::string token = common_docker_get_token (repo); // Get authentication token
812+
813+ // Get manifest
814+ const std::string url_prefix = " https://registry-1.docker.io/v2/" + repo;
815+ std::string manifest_url = url_prefix + " /manifests/" + tag;
816+ common_remote_params manifest_params;
817+ manifest_params.headers .push_back (" Authorization: Bearer " + token);
818+ manifest_params.headers .push_back (
819+ " Accept: application/vnd.docker.distribution.manifest.v2+json,application/vnd.oci.image.manifest.v1+json" );
820+ auto manifest_res = common_remote_get_content (manifest_url, manifest_params);
821+ if (manifest_res.first != 200 ) {
822+ throw std::runtime_error (" Failed to get Docker manifest, HTTP code: " + std::to_string (manifest_res.first ));
823+ }
824+
825+ std::string manifest_str (manifest_res.second .begin (), manifest_res.second .end ());
826+ nlohmann::ordered_json manifest = nlohmann::ordered_json::parse (manifest_str);
827+ std::string gguf_digest; // Find the GGUF layer
828+ if (manifest.contains (" layers" )) {
829+ for (const auto & layer : manifest[" layers" ]) {
830+ if (layer.contains (" mediaType" )) {
831+ std::string media_type = layer[" mediaType" ].get <std::string>();
832+ if (media_type == " application/vnd.docker.ai.gguf.v3" ||
833+ media_type.find (" gguf" ) != std::string::npos) {
834+ gguf_digest = layer[" digest" ].get <std::string>();
835+ break ;
836+ }
837+ }
838+ }
839+ }
840+
841+ if (gguf_digest.empty ()) {
842+ throw std::runtime_error (" No GGUF layer found in Docker manifest" );
843+ }
844+
845+ // Validate & normalize digest
846+ gguf_digest = validate_oci_digest (gguf_digest);
847+ LOG_DBG (" %s: Using validated digest: %s\n " , __func__, gguf_digest.c_str ());
848+
849+ // Prepare local filename
850+ std::string model_filename = repo;
851+ std::replace (model_filename.begin (), model_filename.end (), ' /' , ' _' );
852+ model_filename += " _" + tag + " .gguf" ;
853+ std::string local_path = fs_get_cache_file (model_filename);
854+
855+ const std::string blob_url = url_prefix + " /blobs/" + gguf_digest;
856+ if (!common_download_file_single (blob_url, local_path, token, false )) {
857+ throw std::runtime_error (" Failed to download Docker Model" );
858+ }
859+
860+ LOG_INF (" %s: Downloaded Docker Model to: %s\n " , __func__, local_path.c_str ());
861+ return local_path;
862+ } catch (const std::exception & e) {
863+ LOG_ERR (" %s: Docker Model download failed: %s\n " , __func__, e.what ());
864+ throw ;
865+ }
866+ }
867+
750868//
751869// utils
752870//
@@ -797,7 +915,9 @@ static handle_model_result common_params_handle_model(
797915 handle_model_result result;
798916 // handle pre-fill default model path and url based on hf_repo and hf_file
799917 {
800- if (!model.hf_repo .empty ()) {
918+ if (!model.docker_repo .empty ()) { // Handle Docker URLs by resolving them to local paths
919+ model.path = common_docker_resolve_model (model.docker_repo );
920+ } else if (!model.hf_repo .empty ()) {
801921 // short-hand to avoid specifying --hf-file -> default it to --model
802922 if (model.hf_file .empty ()) {
803923 if (model.path .empty ()) {
@@ -2638,6 +2758,15 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
26382758 params.model .url = value;
26392759 }
26402760 ).set_env (" LLAMA_ARG_MODEL_URL" ));
2761+ add_opt (common_arg (
2762+ { " -dr" , " --docker-repo" }, " [<repo>/]<model>[:quant]" ,
2763+ " Docker Hub model repository. repo is optional, default to ai/. quant is optional, default to :latest.\n "
2764+ " example: gemma3\n "
2765+ " (default: unused)" ,
2766+ [](common_params & params, const std::string & value) {
2767+ params.model .docker_repo = value;
2768+ }
2769+ ).set_env (" LLAMA_ARG_DOCKER_REPO" ));
26412770 add_opt (common_arg (
26422771 {" -hf" , " -hfr" , " --hf-repo" }, " <user>/<model>[:quant]" ,
26432772 " Hugging Face model repository; quant is optional, case-insensitive, default to Q4_K_M, or falls back to the first file in the repo if Q4_K_M doesn't exist.\n "
0 commit comments