diff --git a/hermes_cli/local_runtime/catalog.json b/hermes_cli/local_runtime/catalog.json index b66e263f3c..eacf41c8de 100644 --- a/hermes_cli/local_runtime/catalog.json +++ b/hermes_cli/local_runtime/catalog.json @@ -50,15 +50,18 @@ "files": [ { "path": "UD-IQ4_XS/Qwen3.8-Flash-Next-UD-IQ4_XS-00001-of-00003.gguf", - "size_bytes": 10946624 + "size_bytes": 10946624, + "revision": "38bb39ee97821de2c9009abb7e93950eec396e66" }, { "path": "UD-IQ4_XS/Qwen3.8-Flash-Next-UD-IQ4_XS-00002-of-00003.gguf", - "size_bytes": 49835229856 + "size_bytes": 49835229856, + "revision": "38bb39ee97821de2c9009abb7e93950eec396e66" }, { "path": "UD-IQ4_XS/Qwen3.8-Flash-Next-UD-IQ4_XS-00003-of-00003.gguf", - "size_bytes": 43836407744 + "size_bytes": 43836407744, + "revision": "38bb39ee97821de2c9009abb7e93950eec396e66" } ], "lazy_bytes": 28800138240 @@ -75,12 +78,14 @@ "mmproj": { "path": "mmproj-BF16.gguf", "size_bytes": 907542944, - "local": "mmproj-Qwen3.8-Flash-Next-BF16.gguf" + "local": "mmproj-Qwen3.8-Flash-Next-BF16.gguf", + "revision": "38bb39ee97821de2c9009abb7e93950eec396e66" }, "mtp_head": { "repo": "ggml-org/Qwen3.8-Flash-Next-GGUF", "path": "mtp-Qwen3.8-Flash-Next-Q8_0.gguf", - "size_bytes": 4137429280 + "size_bytes": 4137429280, + "revision": "052beeaca7bec4a303e59cc7bc630c4f3a1b845d" }, "min_engine": "b11370", "quality": 95, diff --git a/hermes_cli/local_runtime/catalog.py b/hermes_cli/local_runtime/catalog.py index ab822bf4b2..7f2933ce51 100644 --- a/hermes_cli/local_runtime/catalog.py +++ b/hermes_cli/local_runtime/catalog.py @@ -39,6 +39,7 @@ class AssetFile: size_bytes: int local: str | None = None repo: str | None = None # HF repo when it is not the entry's own (a companion published elsewhere) + revision: str = "main" # commit whose bytes size_bytes and the entry's pricing describe @property def local_name(self) -> str: @@ -301,7 +302,7 @@ def _asset_from(d: "dict | None") -> "AssetFile | None": if not d: return None return AssetFile(path=d["path"], size_bytes=int(d["size_bytes"]), local=d.get("local"), - repo=d.get("repo")) + repo=d.get("repo"), revision=d.get("revision", "main")) # Scalar CatalogEntry fields parsed from JSON: key -> (coerce, default); None default = required. diff --git a/hermes_cli/web_routers/local_models.py b/hermes_cli/web_routers/local_models.py index d1e43d9d55..46542d7e36 100644 --- a/hermes_cli/web_routers/local_models.py +++ b/hermes_cli/web_routers/local_models.py @@ -397,8 +397,8 @@ def _assign_default(job: Dict[str, Any], model_id: str) -> None: # ── downloads: ranged parallel streams ─────────────────────── -def _hf_url(repo: str, path: str) -> str: - return f"https://huggingface.co/{repo}/resolve/main/{path}" +def _hf_url(repo: str, path: str, revision: str = "main") -> str: + return f"https://huggingface.co/{repo}/resolve/{revision}/{path}" def _model_id_for(gguf: Path) -> str: @@ -418,8 +418,9 @@ def _variant_files_on_disk(model_id: str) -> "list[Path]": def _download_plan(entry, variant) -> list: """Everything a variant needs: split parts + mmproj/draft/MTP head assets, as (url, dest, bytes) tuples.""" - plan = [(_hf_url(entry.repo, a.path), bootstrap.models_dir() / a.local_name, a.size_bytes) for a in variant.files] - plan += [(_hf_url(a.repo or entry.repo, a.path), bootstrap.assets_dir() / a.local_name, a.size_bytes) + plan = [(_hf_url(entry.repo, a.path, a.revision), bootstrap.models_dir() / a.local_name, a.size_bytes) + for a in variant.files] + plan += [(_hf_url(a.repo or entry.repo, a.path, a.revision), bootstrap.assets_dir() / a.local_name, a.size_bytes) for a in (entry.mmproj, entry.draft, entry.mtp_head) if a is not None] return plan diff --git a/tests/hermes_cli/test_local_mtp_head.py b/tests/hermes_cli/test_local_mtp_head.py index 731b2c5616..bffdcd1689 100644 --- a/tests/hermes_cli/test_local_mtp_head.py +++ b/tests/hermes_cli/test_local_mtp_head.py @@ -27,15 +27,15 @@ ENTRY = CatalogEntry( id="head-model", display_name="Head Model", description="", repo="org/Head-Model-GGUF", variants=(BUILD,), n_ctx_train=262144, full_layers=12, recurrent_layers=36, per_layer_f16=2048, moe=True, n_vocab=248320, mtp_draft_depth=3, - mtp_head=AssetFile("mtp-Head-Model-Q8_0.gguf", 4 * GIB, repo="other/Head-Model-GGUF")) + mtp_head=AssetFile("mtp-Head-Model-Q8_0.gguf", 4 * GIB, repo="other/Head-Model-GGUF", revision="0123abc")) def test_the_head_downloads_with_the_model_from_its_own_repo_and_is_priced_before_download(): without_head = replace(ENTRY, mtp_head=None) urls = [url for url, _, _ in local_models._download_plan(ENTRY, BUILD)] - assert "https://huggingface.co/other/Head-Model-GGUF/resolve/main/mtp-Head-Model-Q8_0.gguf" in urls - assert all("/org/Head-Model-GGUF/" in url for url in urls[:2]) + assert "https://huggingface.co/other/Head-Model-GGUF/resolve/0123abc/mtp-Head-Model-Q8_0.gguf" in urls + assert all("/org/Head-Model-GGUF/resolve/main/" in url for url in urls[:2]) assert ENTRY.mtp_capable and not without_head.mtp_capable with_plan = ENTRY.launch_plan(BUILD, UNIFIED_128) without_plan = without_head.launch_plan(BUILD, UNIFIED_128)