From 3b3c0d6254f317f66ac5ae8c181474a59c785c0d Mon Sep 17 00:00:00 2001 From: muhamed hamed <111616619+muhamedhamedvl@users.noreply.github.com> Date: Thu, 11 Jun 2026 23:53:16 +0300 Subject: [PATCH] fix: detect HuggingFace token when downloading cookbook models (#3459) Co-authored-by: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com> --- routes/cookbook_helpers.py | 20 ++++++++++++++++++ routes/cookbook_routes.py | 13 +++++------- src/tool_implementations.py | 3 ++- static/js/cookbook-hwfit.js | 10 ++++----- tests/test_cookbook_hf_token.py | 37 +++++++++++++++++++++++++++++++++ 5 files changed, 68 insertions(+), 15 deletions(-) create mode 100644 tests/test_cookbook_hf_token.py diff --git a/routes/cookbook_helpers.py b/routes/cookbook_helpers.py index 53bdde80e..c2f93cb77 100644 --- a/routes/cookbook_helpers.py +++ b/routes/cookbook_helpers.py @@ -1,12 +1,14 @@ """cookbook_helpers.py — validators + small helpers shared by the cookbook routes. Extracted from cookbook_routes.py; the routes module imports the symbols it needs.""" +import json import logging import ntpath import os import posixpath import re import shlex +from pathlib import Path from fastapi import HTTPException from pydantic import BaseModel @@ -90,6 +92,24 @@ def _validate_token(v: str | None) -> str | None: return v +def load_stored_hf_token(*, state_path: Path | str | None = None) -> str: + """Return the decrypted HF token from cookbook_state.json, else env fallback.""" + path = Path(state_path) if state_path else Path(os.environ.get("DATA_DIR", "data")) / "cookbook_state.json" + token = "" + if path.exists(): + try: + state = json.loads(path.read_text(encoding="utf-8")) + env = state.get("env") if isinstance(state, dict) else {} + if isinstance(env, dict) and env.get("hfToken"): + from src.secret_storage import decrypt + token = decrypt(env.get("hfToken") or "") + except Exception: + token = "" + if not token: + token = (os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN") or "").strip() + return token + + def _validate_local_dir(v: str | None) -> str | None: if v is None or v == "": return None diff --git a/routes/cookbook_routes.py b/routes/cookbook_routes.py index 40cfec31d..edbba3ad7 100644 --- a/routes/cookbook_routes.py +++ b/routes/cookbook_routes.py @@ -40,6 +40,10 @@ from routes.cookbook_helpers import ( _ps_squote, _bash_squote, _validate_serve_cmd, _parse_serve_phase, _safe_env_prefix, _local_tooling_path_export, _append_serve_preflight_exit_lines, _append_serve_exit_code_lines, _append_llama_cpp_linux_accel_build_lines, _cached_model_scan_script, + load_stored_hf_token, + _append_vllm_linux_preflight_lines, _ollama_bind_from_cmd, _pip_install_fallback_chain, + _pip_install_no_cache, _user_shell_path_bootstrap, _venv_safe_local_pip_install_cmd, + _diagnose_serve_output, run_ssh_command_async, _ollama_bind_from_cmd, _pip_install_fallback_chain, _pip_install_no_cache, _user_shell_path_bootstrap, _venv_safe_local_pip_install_cmd, ModelDownloadRequest, ServeRequest, @@ -234,14 +238,7 @@ def setup_cookbook_routes() -> APIRouter: return state def _load_stored_hf_token() -> str: - if not _cookbook_state_path.exists(): - return "" - try: - state = json.loads(_cookbook_state_path.read_text(encoding="utf-8")) - env = state.get("env") if isinstance(state, dict) else {} - return _decrypt_secret(env.get("hfToken") if isinstance(env, dict) else "") - except Exception: - return "" + return load_stored_hf_token(state_path=_cookbook_state_path) def _cookbook_ssh_dir() -> Path: # The Docker image keeps cookbook keys under /app/.ssh; that path only diff --git a/src/tool_implementations.py b/src/tool_implementations.py index 27c05f139..33cc8dc11 100644 --- a/src/tool_implementations.py +++ b/src/tool_implementations.py @@ -2054,13 +2054,14 @@ async def _cookbook_env_for_host(host: str) -> Dict[str, Any]: else: env_prefix = f'eval "$(conda shell.bash hook)" && conda activate {env_path}' + from routes.cookbook_helpers import load_stored_hf_token return { "env_prefix": env_prefix, "env_type": env_kind, "env_path": env_path, "gpus": env_root.get("gpus") or "", "platform": platform, - "hf_token": env_root.get("hfToken") or "", + "hf_token": load_stored_hf_token(), "ssh_port": ssh_port, } diff --git a/static/js/cookbook-hwfit.js b/static/js/cookbook-hwfit.js index d8652d02e..29feb9279 100644 --- a/static/js/cookbook-hwfit.js +++ b/static/js/cookbook-hwfit.js @@ -1506,12 +1506,10 @@ export function _hwfitInit() { clearTimeout(_hwfitDebounce); _hwfitDebounce = setTimeout(() => _hwfitFetch(), 400); }); - // HF Token - const hfToken = document.getElementById('hwfit-hftoken'); - if (hfToken) { - hfToken.addEventListener('change', () => { _envState.hfToken = hfToken.value.trim(); _persistEnvState(); }); - hfToken.addEventListener('input', () => { _envState.hfToken = hfToken.value.trim(); }); - } + // HF token save is owned by cookbook.js (_wireTabEvents) — do not wire a + // second change/input handler here. The old duplicate ran after cookbook.js + // cleared the input on save and overwrote _envState.hfToken with "", so the + // debounced state sync never persisted the token to cookbook_state.json. // Rebuild all server select dropdowns with current servers function _rebuildServerSelect() { diff --git a/tests/test_cookbook_hf_token.py b/tests/test_cookbook_hf_token.py new file mode 100644 index 000000000..4299158a9 --- /dev/null +++ b/tests/test_cookbook_hf_token.py @@ -0,0 +1,37 @@ +"""Cookbook HF token persistence and lookup.""" + +import json +import os + +import pytest + +from routes.cookbook_helpers import load_stored_hf_token +from src.secret_storage import encrypt + + +def test_load_stored_hf_token_reads_encrypted_state(tmp_path, monkeypatch): + monkeypatch.setenv("DATA_DIR", str(tmp_path)) + state_path = tmp_path / "cookbook_state.json" + state_path.write_text( + json.dumps({"env": {"hfToken": encrypt("hf_test_token_12345")}}), + encoding="utf-8", + ) + assert load_stored_hf_token() == "hf_test_token_12345" + assert load_stored_hf_token(state_path=state_path) == "hf_test_token_12345" + + +def test_load_stored_hf_token_falls_back_to_env_when_state_missing(tmp_path, monkeypatch): + monkeypatch.setenv("DATA_DIR", str(tmp_path)) + monkeypatch.setenv("HF_TOKEN", "hf_from_env") + assert load_stored_hf_token() == "hf_from_env" + + +def test_load_stored_hf_token_prefers_state_over_env(tmp_path, monkeypatch): + monkeypatch.setenv("DATA_DIR", str(tmp_path)) + monkeypatch.setenv("HF_TOKEN", "hf_from_env") + state_path = tmp_path / "cookbook_state.json" + state_path.write_text( + json.dumps({"env": {"hfToken": encrypt("hf_from_state")}}), + encoding="utf-8", + ) + assert load_stored_hf_token() == "hf_from_state"