Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion routes/cookbook_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -485,10 +485,20 @@ def _cached_model_scan_script(model_dirs: list[str] | None = None, add_hf_cache:
" if not p: return",
" p = os.path.expanduser(p)",
" if p not in candidates: candidates.append(p)",
" # Explicit hub-cache env vars (huggingface_hub prefers these).",
" add(os.environ.get('HUGGINGFACE_HUB_CACHE'))",
" add(os.environ.get('HF_HUB_CACHE'))",
" hf_home = os.environ.get('HF_HOME')",
" if hf_home: add(os.path.join(hf_home, 'hub'))",
" if hf_home:",
" # Standard layout is $HF_HOME/hub.",
" add(os.path.join(hf_home, 'hub'))",
" # Some users set HF_HOME to the hub directory itself",
" # (…/huggingface/hub). Keep that path scannable too.",
" add(hf_home)",
" add('~/.cache/huggingface/hub')",
" # macOS (and several HF/Xet clients) default under Library/Caches.",
" # Without this, downloads land there but Serve never lists them.",
" add('~/Library/Caches/huggingface/hub')",
" # Docker images mount ./data/huggingface at /app/.cache/huggingface.",
" # When HOME is /root, expanduser() misses that persisted cache.",
" add('/app/.cache/huggingface/hub')",
Expand Down Expand Up @@ -576,11 +586,15 @@ def _cached_model_scan_script(model_dirs: list[str] | None = None, add_hf_cache:
"scan_ollama()",
]
for model_dir in model_dirs or []:
# Custom modelDirs may be either plain model folders or a full HF hub
# cache (models--*). scan_dir skips models-- entries; scan_hf handles them.
lines.append(f"scan_hf(normalize_model_dir({model_dir!r}))")
lines.append(f"scan_dir({model_dir!r})")
lines.append("print(json.dumps(models))")
return "\n".join(lines) + "\n"



def _ps_squote(v: str) -> str:
"""Escape a value for PowerShell single-quoted string interpolation.
Belt-and-suspenders on top of _validate_token's regex — if the regex
Expand Down
103 changes: 103 additions & 0 deletions tests/test_cookbook_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,109 @@ def test_cached_model_scan_uses_huggingface_cache_env(tmp_path):
assert by_repo["Qwen/Qwen3.6-35B"]["path"] == str(hf_cache)


def test_cached_model_scan_includes_macos_library_caches(tmp_path):
"""#5978: macOS HF downloads often land under ~/Library/Caches/huggingface/hub.
Serve must list them without requiring a manual ~/.cache symlink.
"""
home = tmp_path / "home"
mac_hub = home / "Library" / "Caches" / "huggingface" / "hub"
model = mac_hub / "models--typhoon-ai--typhoon2.5-qwen3-30b-a3b-gguf"
snap = model / "snapshots" / "8aa0ece"
snap.mkdir(parents=True)
gguf = snap / "typhoon2.5-qwen3-30b-a3b-q4_k_m.gguf"
gguf.write_bytes(b"gguf-bytes")
# Empty Linux-style cache so only the macOS path can produce the hit.
(home / ".cache" / "huggingface" / "hub").mkdir(parents=True)

scan_py = tmp_path / "scan_macos_hf.py"
scan_py.write_text(_cached_model_scan_script(), encoding="utf-8")
env = dict(os.environ)
env["HOME"] = str(home)
env.pop("HF_HOME", None)
env.pop("HUGGINGFACE_HUB_CACHE", None)
env.pop("HF_HUB_CACHE", None)
proc = subprocess.run(
[sys.executable, str(scan_py)],
check=True,
capture_output=True,
text=True,
env=env,
)

by_repo = {m["repo_id"]: m for m in json.loads(proc.stdout)}
key = "typhoon-ai/typhoon2.5-qwen3-30b-a3b-gguf"
assert key in by_repo
assert by_repo[key]["path"] == str(mac_hub)
assert by_repo[key]["is_gguf"] is True
assert by_repo[key]["size_bytes"] == len(b"gguf-bytes")


def test_cached_model_scan_hf_home_pointing_at_hub_dir(tmp_path):
"""Users sometimes set HF_HOME to the hub directory itself, not its parent."""
hub = tmp_path / "Library" / "Caches" / "huggingface" / "hub"
model = hub / "models--acme--direct-hub"
(model / "snapshots" / "rev").mkdir(parents=True)
(model / "snapshots" / "rev" / "config.json").write_text("{}", encoding="utf-8")
(model / "snapshots" / "rev" / "model.safetensors").write_bytes(b"w")

empty_home = tmp_path / "empty-home"
empty_home.mkdir()
scan_py = tmp_path / "scan_hf_home_hub.py"
scan_py.write_text(_cached_model_scan_script(), encoding="utf-8")
env = dict(os.environ)
env["HOME"] = str(empty_home)
env["HF_HOME"] = str(hub)
env.pop("HUGGINGFACE_HUB_CACHE", None)
env.pop("HF_HUB_CACHE", None)
proc = subprocess.run(
[sys.executable, str(scan_py)],
check=True,
capture_output=True,
text=True,
env=env,
)

by_repo = {m["repo_id"]: m for m in json.loads(proc.stdout)}
assert "acme/direct-hub" in by_repo
assert by_repo["acme/direct-hub"]["path"] == str(hub)


def test_cached_model_scan_model_dir_with_hf_hub_layout(tmp_path):
"""Custom modelDirs that are HF hub caches (models--*) must still list models.
scan_dir alone skips models-- entries; the scanner must also run scan_hf.
"""
hub_layout = tmp_path / "custom-hf-hub"
model = hub_layout / "models--org--widget-gguf"
snap = model / "snapshots" / "abc"
snap.mkdir(parents=True)
(snap / "widget.gguf").write_bytes(b"gguf")

scan_py = tmp_path / "scan_model_dir_hub.py"
scan_py.write_text(
_cached_model_scan_script([str(hub_layout)]),
encoding="utf-8",
)
empty_home = tmp_path / "home"
empty_home.mkdir()
env = dict(os.environ)
env["HOME"] = str(empty_home)
env.pop("HF_HOME", None)
env.pop("HUGGINGFACE_HUB_CACHE", None)
env.pop("HF_HUB_CACHE", None)
proc = subprocess.run(
[sys.executable, str(scan_py)],
check=True,
capture_output=True,
text=True,
env=env,
)

by_repo = {m["repo_id"]: m for m in json.loads(proc.stdout)}
assert "org/widget-gguf" in by_repo
assert by_repo["org/widget-gguf"]["path"] == str(hub_layout)
assert by_repo["org/widget-gguf"]["is_gguf"] is True


# ── #1219 / #1459: keep big dependency wheel builds off the home pip cache ──

def test_pip_install_no_cache_injects_flag():
Expand Down