"""Local-models dashboard routes — the desktop's window into the managed llama.cpp runtime. Every payload carries plain-language, pre-formatted facts the UI shows verbatim (what will this model do ON THIS MACHINE, how big is the download, what is the runtime doing), never raw internals. Long jobs follow the repo's job pattern: start-POST -> {job_id} -> GET poll with byte progress. """ from __future__ import annotations import asyncio import contextlib import json import logging import os import re import shutil import subprocess import threading import time import urllib.parse import urllib.request import uuid from pathlib import Path from typing import Any, Callable, Dict, Optional from fastapi import APIRouter, HTTPException from pydantic import BaseModel from starlette.concurrency import run_in_threadpool from hermes_cli import config as config_mod, web_deps from hermes_cli.local_runtime import ( binaries, bootstrap, catalog, context_policy, estimator, growth, hardware, hf_browse, load_progress, presets, supervisor, ) from hermes_cli.local_runtime.endpoint import _state_endpoint logger = logging.getLogger(__name__) router = APIRouter() _JOBS: Dict[str, Dict[str, Any]] = {} _JOBS_LOCK = threading.Lock() # One quickstart at a time: the job sequences installs, downloads, a server bounce and a config write — two # racing runs would interleave all four. Held for the job's lifetime, released in the worker. _QUICKSTART_LOCK = threading.Lock() _LLAMACPP_PROVIDERS = ("llamacpp", "llama.cpp", "llama-cpp") _SPLIT_PART_RE = r"-\d{5}-of-\d{5}" # One TCP stream to a CDN rarely fills a fast line; 8 ranged connections into a preallocated file saturate gigabit. _DOWNLOAD_CONNECTIONS = 8 _CHUNK = 4 << 20 _SERVER_START_FAILED = "The local server could not start — check the runtime is installed" class RuntimeInstallBody(BaseModel): backend: Optional[str] = None # None/auto -> detect class ModelDownloadBody(BaseModel): model_id: str class QuickstartBody(BaseModel): model_id: str | None = None # default: the catalog's recommended entry class ServerActionBody(BaseModel): action: str # "stop" | "start" class ModelEjectBody(BaseModel): model_id: str class ModelActivateBody(BaseModel): model_id: str # exact variant id (a staged .gguf stem) class BrowsedDownloadBody(BaseModel): repo: str paths: list[str] # one GGUF, or every part of a split, in order class SideloadBody(BaseModel): path: str # absolute path to a .gguf on this machine def _human_gb(n: int | float) -> str: return f"{n / (1 << 30):.1f} GB" def _k_label(tokens: int) -> str: return f"{tokens // 1024}K" @contextlib.contextmanager def _http_error(status: int, prefix: str = ""): """Map any exception to ``HTTPException(status, f"{prefix}{exc}")``.""" try: yield except Exception as exc: # noqa: BLE001 raise HTTPException(status_code=status, detail=f"{prefix}{exc}") from exc def _quiet(fn: Callable[[], Any], default: Any, *, warn: str | None = None, debug: str | None = None) -> Any: """``fn()`` or ``default`` on any exception — for garnish that must never 500. ``warn`` logs a warning with the exception (%s), ``debug`` a debug line with traceback; silent otherwise.""" try: return fn() except Exception as exc: # noqa: BLE001 if warn: logger.warning(warn, exc) if debug: logger.debug(debug, exc_info=True) return default # ── jobs ───────────────────────────────────────────────────── def _job(kind: str, target: str, model_id: str | None = None) -> Dict[str, Any]: job = { "job_id": uuid.uuid4().hex[:12], "kind": kind, "target": target, "model_id": model_id, # catalog id for downloads; None otherwise "status": "running", # running | done | error "phase": "starting", # human-readable step name "detail": "", "total_bytes": None, "done_bytes": 0, "started_at": time.time(), "error": None, } with _JOBS_LOCK: _JOBS[job["job_id"]] = job return job def _job_view(job: Dict[str, Any]) -> Dict[str, Any]: out = dict(job) if out["total_bytes"]: out["percent"] = min(100, round(out["done_bytes"] / out["total_bytes"] * 100)) return out def _step(job: Dict[str, Any], phase: str, detail: str) -> None: job["phase"] = phase job["detail"] = detail def _finish(job: Dict[str, Any], detail: str) -> None: _step(job, "done", detail) job["status"] = "done" def _spawn_job(job: Dict[str, Any], name: str, body: Callable[[], None], *, fail_msg: str | None = None, on_exit: Callable[[], None] | None = None, download_label: str | None = None) -> None: """Run ``body`` on a daemon thread; an exception marks the job errored (warning ``fail_msg`` when given); ``on_exit`` always runs last. ``download_label`` = download job: finishes as "