@@ -747,6 +747,124 @@ std::pair<long, std::vector<char>> common_remote_get_content(const std::string &
747
747
748
748
#endif // LLAMA_USE_CURL
749
749
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
+
750
868
//
751
869
// utils
752
870
//
@@ -797,7 +915,9 @@ static handle_model_result common_params_handle_model(
797
915
handle_model_result result;
798
916
// handle pre-fill default model path and url based on hf_repo and hf_file
799
917
{
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 ()) {
801
921
// short-hand to avoid specifying --hf-file -> default it to --model
802
922
if (model.hf_file .empty ()) {
803
923
if (model.path .empty ()) {
@@ -2638,6 +2758,15 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
2638
2758
params.model .url = value;
2639
2759
}
2640
2760
).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" ));
2641
2770
add_opt (common_arg (
2642
2771
{" -hf" , " -hfr" , " --hf-repo" }, " <user>/<model>[:quant]" ,
2643
2772
" 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