| 839 | } |
| 840 | |
| 841 | std::string common_docker_resolve_model(const std::string & docker) { |
| 842 | // Parse ai/smollm2:135M-Q4_0 |
| 843 | size_t colon_pos = docker.find(':'); |
| 844 | std::string repo, tag; |
| 845 | if (colon_pos != std::string::npos) { |
| 846 | repo = docker.substr(0, colon_pos); |
| 847 | tag = docker.substr(colon_pos + 1); |
| 848 | } else { |
| 849 | repo = docker; |
| 850 | tag = "latest"; |
| 851 | } |
| 852 | |
| 853 | // ai/ is the default |
| 854 | size_t slash_pos = docker.find('/'); |
| 855 | if (slash_pos == std::string::npos) { |
| 856 | repo.insert(0, "ai/"); |
| 857 | } |
| 858 | |
| 859 | LOG_INF("%s: Downloading Docker Model: %s:%s\n", __func__, repo.c_str(), tag.c_str()); |
| 860 | try { |
| 861 | // --- helper: digest validation --- |
| 862 | auto validate_oci_digest = [](const std::string & digest) -> std::string { |
| 863 | // Expected: algo:hex ; start with sha256 (64 hex chars) |
| 864 | // You can extend this map if supporting other algorithms in future. |
| 865 | static const std::regex re("^sha256:([a-fA-F0-9]{64})$"); |
| 866 | std::smatch m; |
| 867 | if (!std::regex_match(digest, m, re)) { |
| 868 | throw std::runtime_error("Invalid OCI digest format received in manifest: " + digest); |
| 869 | } |
| 870 | // normalize hex to lowercase |
| 871 | std::string normalized = digest; |
| 872 | std::transform(normalized.begin()+7, normalized.end(), normalized.begin()+7, [](unsigned char c){ |
| 873 | return std::tolower(c); |
| 874 | }); |
| 875 | return normalized; |
| 876 | }; |
| 877 | |
| 878 | std::string token = common_docker_get_token(repo); // Get authentication token |
| 879 | |
| 880 | // Get manifest |
| 881 | // TODO: cache the manifest response so that it appears in the model list |
| 882 | const std::string url_prefix = "https://registry-1.docker.io/v2/" + repo; |
| 883 | std::string manifest_url = url_prefix + "/manifests/" + tag; |
| 884 | common_remote_params manifest_params; |
| 885 | manifest_params.headers.push_back({"Authorization", "Bearer " + token}); |
| 886 | manifest_params.headers.push_back({"Accept", |
| 887 | "application/vnd.docker.distribution.manifest.v2+json,application/vnd.oci.image.manifest.v1+json" |
| 888 | }); |
| 889 | auto manifest_res = common_remote_get_content(manifest_url, manifest_params); |
| 890 | if (manifest_res.first != 200) { |
| 891 | throw std::runtime_error("Failed to get Docker manifest, HTTP code: " + std::to_string(manifest_res.first)); |
| 892 | } |
| 893 | |
| 894 | std::string manifest_str(manifest_res.second.begin(), manifest_res.second.end()); |
| 895 | nlohmann::ordered_json manifest = nlohmann::ordered_json::parse(manifest_str); |
| 896 | std::string gguf_digest; // Find the GGUF layer |
| 897 | if (manifest.contains("layers")) { |
| 898 | for (const auto & layer : manifest["layers"]) { |
no test coverage detected