revert: remove Hugging Face model integration

This commit is contained in:
Hermes Agent
2026-08-28 19:30:35 +10:00
parent bc47cf8152
commit 309947d353
7 changed files with 9 additions and 423 deletions
-298
View File
@@ -70,9 +70,6 @@ def _discover_ollama_endpoint() -> None:
LOCAL_OLLAMA = _ollama_base_url()
REMOTE_OLLAMA = "https://ollama.com"
HUGGINGFACE_API = "https://huggingface.co/api"
HUGGINGFACE_DOWNLOAD_ROOT = "huggingface"
HF_REPO_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,95}/[A-Za-z0-9][A-Za-z0-9._-]{0,95}$")
CATALOG_FILE = "catalog.json"
MODEL_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/-]{0,190}$")
MELBOURNE = ZoneInfo("Australia/Melbourne")
@@ -750,14 +747,6 @@ def _json_request(url: str, method: str = "GET", payload: Any = None, timeout: i
return value if isinstance(value, dict) else {}
def _json_list_request(url: str, timeout: int = 30) -> list[dict[str, Any]]:
request = Request(url, headers={"Accept": "application/json", "User-Agent": "Hermes-Ollama-Models/1.7.4"})
with urlopen(request, timeout=timeout) as response:
raw = response.read()
value = json.loads(raw.decode("utf-8")) if raw else []
return [item for item in value if isinstance(item, dict)] if isinstance(value, list) else []
def _valid_name(name: str) -> str:
name = str(name or "").strip()
if not MODEL_RE.fullmatch(name):
@@ -1372,251 +1361,6 @@ def _model_view(raw: dict[str, Any], loaded: dict[str, Any] | None = None, sourc
}
def _huggingface_model_view(raw: dict[str, Any]) -> dict[str, Any]:
repo_id = str(raw.get("id") or raw.get("modelId") or "").strip()
raw_tags = raw.get("tags")
tags = [str(tag).strip() for tag in raw_tags if str(tag).strip()][:32] if isinstance(raw_tags, list) else []
pipeline = str(raw.get("pipeline_tag") or "").strip()
library = str(raw.get("library_name") or "").strip()
searchable = " ".join([repo_id, pipeline, library, *tags])
capabilities = _infer_capabilities(repo_id, library, {}, ["completion"] if pipeline in {"text-generation", "text2text-generation", "image-text-to-text"} else [])
if pipeline == "image-text-to-text" and "vision" not in capabilities:
capabilities.append("vision")
if pipeline in {"text-to-image", "image-to-image", "image-classification"} and "vision" not in capabilities:
capabilities.append("vision")
is_moe = bool(re.search(r"(?:moe|mixture.of.experts|a\d+b)", searchable, re.I))
return {
"name": repo_id,
"source": "huggingface",
"source_label": "Hugging Face",
"downloadable": False,
"installed": False,
"loaded": False,
"size_bytes": 0,
"size_gb": None,
"size_label": "Hub repository",
"loaded_bytes": 0,
"loaded_vram_bytes": 0,
"digest": str(raw.get("sha") or ""),
"modified_at": raw.get("lastModified"),
"family": library or "Hugging Face model",
"architecture": pipeline or "Unknown",
"is_moe": is_moe,
"parameter_size": "unknown",
"activated_parameter_size": None,
"parameter_summary": "unknown",
"description": f"Hugging Face model · {pipeline or 'pipeline unavailable'}" + (f" · {library}" if library else ""),
"quantization": "see repository files",
"format": library or "Hub format",
"context_length": None,
"input_modalities": ["Text", "Image"] if "vision" in capabilities else ["Text"],
"embedding_length": None,
"capabilities": list(dict.fromkeys(capabilities)),
"capability_breakdown": {cap: CAPABILITY_INFO[cap] for cap in capabilities if cap in CAPABILITY_INFO},
"strengths": [pipeline or "model repository", "Hugging Face Hub metadata"],
"expected_ram_gb": None,
"expected_ram_label": "Unknown · inspect repository requirements",
"expected_ram_basis": "Hugging Face does not provide a reliable universal runtime RAM estimate in search results.",
"hf_url": f"https://huggingface.co/{repo_id}",
"hf_downloads": int(raw.get("downloads") or 0),
"hf_likes": int(raw.get("likes") or 0),
"hf_pipeline_tag": pipeline,
"hf_library": library,
"hf_tags": tags,
}
def _search_huggingface(query: str, limit: int = 30) -> list[dict[str, Any]]:
query = str(query or "").strip()
if len(query) < 2:
return []
limit = max(1, min(int(limit), 50))
url = f"{HUGGINGFACE_API}/models?{urlencode({'search': query[:120], 'limit': limit, 'sort': 'downloads', 'direction': '-1', 'full': 'false'})}"
try:
rows = _json_list_request(url, timeout=20)
except (HTTPError, URLError, OSError, ValueError):
return []
return [_huggingface_model_view(row) for row in rows if (row.get("id") or row.get("modelId"))]
def _valid_huggingface_repo(repo_id: str) -> str:
repo_id = str(repo_id or "").strip()
if not HF_REPO_RE.fullmatch(repo_id):
raise HTTPException(400, "Invalid Hugging Face repository id")
return repo_id
def _valid_huggingface_filename(filename: str) -> str:
filename = unquote(str(filename or "")).strip().replace("\\", "/")
path = Path(filename)
if not filename or path.is_absolute() or any(part in {"", ".", ".."} for part in path.parts) or not filename.lower().endswith(".gguf"):
raise HTTPException(400, "Only safe Hugging Face GGUF filenames are supported")
return filename
def _format_bytes(value: int) -> str:
amount = float(max(0, int(value or 0)))
for unit in ("B", "KiB", "MiB", "GiB", "TiB"):
if amount < 1024 or unit == "TiB":
return f"{amount:.1f} {unit}" if unit != "B" else f"{int(amount)} B"
amount /= 1024
return "Unknown"
def _huggingface_repo_files(repo_id: str) -> list[dict[str, Any]]:
repo_id = _valid_huggingface_repo(repo_id)
url = f"{HUGGINGFACE_API}/models/{repo_id}?full=true"
try:
metadata = _json_request(url, timeout=30)
except (HTTPError, URLError, OSError, ValueError) as exc:
raise HTTPException(502, "Hugging Face repository metadata is unavailable") from exc
raw_files: list[dict[str, Any]] = []
siblings = metadata.get("siblings", []) if isinstance(metadata, dict) else []
for item in siblings if isinstance(siblings, list) else []:
if not isinstance(item, dict):
continue
filename = str(item.get("rfilename") or item.get("path") or "").strip()
if not filename.lower().endswith(".gguf"):
continue
try:
filename = _valid_huggingface_filename(filename)
except HTTPException:
continue
lfs: dict[str, Any] = {}
raw_lfs = item.get("lfs")
if isinstance(raw_lfs, dict):
lfs = raw_lfs
size = int(lfs.get("size") or item.get("size") or 0)
raw_files.append({"filename": filename, "size": size, "download_url": f"https://huggingface.co/{repo_id}/resolve/main/{filename}?download=true"})
groups: dict[str, list[dict[str, Any]]] = {}
split_pattern = re.compile(r"^(.*)-\d{5}-of-\d{5}(\.gguf)$", re.I)
for item in raw_files:
match = split_pattern.match(item["filename"])
group_key = f"{match.group(1)}{match.group(2)}" if match else item["filename"]
groups.setdefault(group_key, []).append(item)
files: list[dict[str, Any]] = []
for group_key, group in groups.items():
group.sort(key=lambda item: item["filename"])
size = sum(int(item.get("size") or 0) for item in group)
files.append({
"filename": group[0]["filename"],
"filenames": [item["filename"] for item in group],
"size": size,
"size_label": _format_bytes(size) if size else (f"{len(group)} shards · size unavailable" if len(group) > 1 else "Unknown"),
"file_count": len(group),
"split": len(group) > 1,
"download_url": group[0]["download_url"],
})
return sorted(files, key=lambda item: (item.get("size") or 0, item["filename"]))
def _hf_ollama_import_available() -> bool:
parsed = urlparse(LOCAL_OLLAMA)
return parsed.hostname in {"localhost", "127.0.0.1", "::1"} and not _running_in_container() and bool(shutil.which("ollama"))
def _ollama_model_name_for_hf(repo_id: str, filename: str) -> str:
owner, repo = repo_id.split("/", 1)
stem = re.sub(r"-\d{5}-of-\d{5}$", "", Path(filename).stem.lower())
value = re.sub(r"[^a-z0-9._-]+", "-", f"hf-{owner}-{repo}-{stem}").strip("-._")
return value[:190] or "hf-imported-model"
def _run_huggingface_download(job_id: str, repo_id: str, filename: str) -> None:
temporary: Path | None = None
try:
repo_id = _valid_huggingface_repo(repo_id)
filename = _valid_huggingface_filename(filename)
file_info = next((item for item in _huggingface_repo_files(repo_id) if filename in item.get("filenames", [item["filename"]])), None)
if not file_info:
raise RuntimeError("Requested GGUF file was not found in the public Hugging Face repository")
expected = int(file_info.get("size") or 0)
free_bytes = shutil.disk_usage(_home()).free
if expected and free_bytes < expected + 1024 ** 3:
raise RuntimeError("Insufficient free disk space for the Hugging Face GGUF download")
destination_root = _home() / HUGGINGFACE_DOWNLOAD_ROOT / repo_id
destination_root.mkdir(parents=True, exist_ok=True)
download_files = list(file_info.get("filenames") or [filename])
completed = 0
total = expected
if not total:
for remote_filename in download_files:
head_url = f"https://huggingface.co/{repo_id}/resolve/main/{remote_filename}?download=true"
try:
head_request = Request(head_url, method="HEAD", headers={"User-Agent": "Hermes-Ollama-Models/1.7.4"})
with urlopen(head_request, timeout=30) as head_response:
total += int(head_response.headers.get("Content-Length") or 0)
except (HTTPError, URLError, OSError, ValueError):
continue
free_bytes = shutil.disk_usage(_home()).free
if total and free_bytes < total + 1024 ** 3:
raise RuntimeError("Insufficient free disk space for the complete Hugging Face GGUF download")
for remote_filename in download_files:
destination = destination_root / remote_filename
destination.parent.mkdir(parents=True, exist_ok=True)
temporary = destination.with_name(f".{destination.name}.{job_id}.part")
download_url = f"https://huggingface.co/{repo_id}/resolve/main/{remote_filename}?download=true"
request = Request(download_url, headers={"Accept": "application/octet-stream", "User-Agent": "Hermes-Ollama-Models/1.7.4"})
with urlopen(request, timeout=60) as response, temporary.open("wb") as output:
total = max(total, completed + int(response.headers.get("Content-Length") or 0))
for chunk in iter(lambda: response.read(8 * 1024 * 1024), b""):
output.write(chunk)
completed += len(chunk)
_set_job(job_id, status="downloading", completed=completed, total=total, percent=round(completed * 100 / total, 1) if total else None)
os.replace(temporary, destination)
destination = destination_root / download_files[0]
model_name = _ollama_model_name_for_hf(repo_id, filename)
imported = False
if _hf_ollama_import_available():
modelfile = destination.with_name(f".{destination.name}.Modelfile")
modelfile.write_text(f"FROM {destination}\n", encoding="utf-8")
try:
env = dict(os.environ)
env["OLLAMA_HOST"] = LOCAL_OLLAMA
result = subprocess.run(["ollama", "create", model_name, "-f", str(modelfile)], capture_output=True, text=True, timeout=3600, check=False, env=env)
if result.returncode != 0:
raise RuntimeError((result.stderr or result.stdout or "ollama create failed")[-1000:])
imported = True
finally:
try:
modelfile.unlink()
except FileNotFoundError:
pass
_set_job(job_id, state="completed", status="success", percent=100, path=str(destination), model_name=model_name, imported=imported, message="Downloaded and imported into local Ollama" if imported else "Downloaded GGUF; Ollama import was not available on this filesystem")
except Exception as exc:
if temporary is not None:
try:
temporary.unlink()
except FileNotFoundError:
pass
_set_job(job_id, state="failed", status="error", error=str(exc))
def _search_ollama_catalog(query: str, limit: int = 50) -> list[dict[str, Any]]:
needle = str(query or "").strip().lower()
if len(needle) < 2:
return []
catalog = _ensure_catalog()
rows = list(catalog.get("models", []))
family_map: dict[str, Any] = {}
raw_families = catalog.get("families")
if isinstance(raw_families, dict):
family_map = raw_families
rows.extend(variant for variants in family_map.values() if isinstance(variants, list) for variant in variants if isinstance(variant, dict))
matches: list[dict[str, Any]] = []
seen: set[str] = set()
for raw in rows:
name = str(raw.get("name") or raw.get("model") or "")
if not name or name in seen:
continue
view = _model_view(raw, source="catalog")
haystack = " ".join([name, view.get("family", ""), view.get("description", ""), *view.get("capabilities", []), *view.get("strengths", [])]).lower()
if needle in haystack:
seen.add(name)
matches.append(view)
return matches[:max(1, min(int(limit), 100))]
class _VariantPageParser(HTMLParser):
"""Extract the public Ollama tag rows without depending on third-party HTML packages."""
@@ -1895,11 +1639,6 @@ class ModelRequest(BaseModel):
placement: str = "gpu_ram"
class HuggingFaceDownloadRequest(BaseModel):
repo_id: str
filename: str
class ConnectionRequest(BaseModel):
url: str
role: str = "local"
@@ -2940,43 +2679,6 @@ def catalog_refresh() -> dict[str, Any]:
return {"ok": bool(catalog.get("models")), "updated_at": catalog.get("fetched_at"), "count": len(catalog.get("models", [])), "error": catalog.get("last_error")}
@router.get("/catalog/search")
def catalog_search(q: str = "", limit: int = 30, include_huggingface: bool = False) -> dict[str, Any]:
query = str(q or "").strip()[:120]
if len(query) < 2:
return {"query": query, "results": [], "sources": ["ollama", "huggingface"] if include_huggingface else ["ollama"]}
ollama = _search_ollama_catalog(query, limit=limit)
huggingface = _search_huggingface(query, limit=limit) if include_huggingface else []
combined = ollama + huggingface
return {
"query": query,
"results": combined,
"ollama_results": ollama,
"huggingface_results": huggingface,
"sources": ["ollama", "huggingface"] if include_huggingface else ["ollama"],
}
@router.get("/huggingface/files")
def huggingface_files(repo_id: str) -> dict[str, Any]:
repo_id = _valid_huggingface_repo(repo_id)
files = _huggingface_repo_files(repo_id)
return {"repo_id": repo_id, "files": [{key: value for key, value in item.items() if key != "download_url"} for item in files], "import_supported": _hf_ollama_import_available()}
@router.post("/huggingface/download")
def huggingface_download(body: HuggingFaceDownloadRequest) -> dict[str, Any]:
repo_id = _valid_huggingface_repo(body.repo_id)
filename = _valid_huggingface_filename(body.filename)
if not any(item["filename"] == filename for item in _huggingface_repo_files(repo_id)):
raise HTTPException(400, "Requested GGUF file was not found in the public Hugging Face repository")
job_id = uuid.uuid4().hex
with _jobs_lock:
_jobs[job_id] = {"id": job_id, "name": f"{repo_id}/{filename}", "action": "huggingface-download", "target": "local", "state": "running", "status": "starting", "percent": 0, "created_at": time.time(), "updated_at": time.time()}
threading.Thread(target=_run_huggingface_download, args=(job_id, repo_id, filename), daemon=True, name=f"huggingface-download-{job_id[:8]}").start()
return {"ok": True, "job_id": job_id, "repo_id": repo_id, "filename": filename, "message": "Hugging Face GGUF download started"}
@router.post("/pull")
def pull_model(body: ModelRequest) -> dict[str, Any]:
name = _valid_name(body.name)