diff --git a/hermes_cli/web_routers/local_models.py b/hermes_cli/web_routers/local_models.py index cb771e482a..1044053fc1 100644 --- a/hermes_cli/web_routers/local_models.py +++ b/hermes_cli/web_routers/local_models.py @@ -40,15 +40,54 @@ logger = logging.getLogger(__name__) router = APIRouter() -_GIB = 1 << 30 _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 / _GIB:.1f} GB" + return f"{n / (1 << 30):.1f} GB" def _k_label(tokens: int) -> str: @@ -64,6 +103,20 @@ def _http_error(status: int, prefix: str = ""): 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, @@ -94,13 +147,17 @@ def _finish(job: Dict[str, Any], detail: str) -> None: 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) -> None: - """Run ``body`` on a daemon thread; an exception marks the job errored (warning - ``fail_msg`` when given). ``on_exit`` always runs last.""" +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 "