From 5e47e69e99bc370a9bbf27d304a4cd7f899b17ef Mon Sep 17 00:00:00 2001 From: ooovenenoso <120500656+ooovenenoso@users.noreply.github.com> Date: Mon, 1 Jun 2026 10:10:08 -0400 Subject: [PATCH] Allow serving cached local llama.cpp models Co-authored-by: Kevin <120500656+oooindefatigable@users.noreply.github.com> --- routes/cookbook_helpers.py | 13 +++++++++++++ routes/cookbook_routes.py | 12 +++++++----- tests/test_cookbook_helpers.py | 15 +++++++++++++++ 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/routes/cookbook_helpers.py b/routes/cookbook_helpers.py index ee98c8d..e468a5a 100644 --- a/routes/cookbook_helpers.py +++ b/routes/cookbook_helpers.py @@ -16,6 +16,11 @@ logger = logging.getLogger(__name__) # HuggingFace repo IDs are /, both alphanumerics plus ._- # Rejecting anything else up front closes off shell-interpolation vectors. _REPO_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*/[A-Za-z0-9][A-Za-z0-9._-]*$") +# Cached models scanned from a custom/local model dir are keyed by their leaf +# folder name (no slash), e.g. `DeepSeek-R1-UD-IQ4_XS`. The serve command uses +# the real on-disk path separately; this identifier is only for UI/task +# bookkeeping, so serving should accept the same safe glyph set as repo IDs. +_LOCAL_MODEL_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$") # Include pattern is a glob: allow typical safe glyphs only. _INCLUDE_RE = re.compile(r"^[A-Za-z0-9._\-*?/\[\]]+$") # Remote host: user@host (optionally with :port-free hostname parts). @@ -40,6 +45,14 @@ def _validate_repo_id(v: str | None) -> str: return v +def _validate_serve_model_id(v: str | None) -> str: + if not v: + raise HTTPException(400, "repo_id is required") + if _REPO_ID_RE.match(v) or _LOCAL_MODEL_ID_RE.match(v): + return v + raise HTTPException(400, "Invalid repo_id — must be / or a cached local model id using [A-Za-z0-9._-]") + + def _validate_include(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 cc10763..5718167 100644 --- a/routes/cookbook_routes.py +++ b/routes/cookbook_routes.py @@ -33,7 +33,7 @@ logger = logging.getLogger(__name__) from routes.cookbook_helpers import ( _SSH_PORT_RE, _REMOTE_HOST_RE, _SESSION_ID_RE, - _validate_repo_id, _validate_include, _validate_remote_host, _validate_token, + _validate_repo_id, _validate_serve_model_id, _validate_include, _validate_remote_host, _validate_token, _validate_local_dir, _validate_ssh_port, _validate_gpus, _shell_path, _ps_squote, _bash_squote, _validate_serve_cmd, _parse_serve_phase, _safe_env_prefix, _local_tooling_path_export, _append_serve_preflight_exit_lines, @@ -776,9 +776,11 @@ def setup_cookbook_routes() -> APIRouter: """Launch a model server in a tmux session (or PowerShell background process on Windows). `repo_id` is dual-purpose: a HuggingFace repo (`/`) for - model-serve commands, OR a bare pip package name when the cmd is a - `python -m pip install …`. We only enforce the strict HF format on - the model paths. + model-serve commands, a cached local-model id (the folder name reported + by `/api/model/cached`) for models scanned from a custom model dir, OR a + bare pip package name when the cmd is a `python -m pip install …`. We + keep strict validation, but serving local cached models must not require + a fake org/name wrapper. """ require_admin(request) # Defence-in-depth: reject values that could break out of shell contexts. @@ -807,7 +809,7 @@ def setup_cookbook_routes() -> APIRouter: ): raise HTTPException(400, "Invalid pip package name") else: - _validate_repo_id(req.repo_id) + _validate_serve_model_id(req.repo_id) TMUX_LOG_DIR.mkdir(parents=True, exist_ok=True) session_id = f"serve-{uuid.uuid4().hex[:8]}" remote = req.remote_host diff --git a/tests/test_cookbook_helpers.py b/tests/test_cookbook_helpers.py index 5935115..5124a0c 100644 --- a/tests/test_cookbook_helpers.py +++ b/tests/test_cookbook_helpers.py @@ -12,6 +12,8 @@ from routes.cookbook_helpers import ( _local_tooling_path_export, _safe_env_prefix, _validate_gpus, + _validate_repo_id, + _validate_serve_model_id, _validate_ssh_port, ) @@ -52,6 +54,19 @@ def test_validate_gpus_accepts_indexes_only(): _validate_gpus("0; rm -rf /") +def test_validate_repo_id_stays_strict_for_hf_downloads(): + assert _validate_repo_id("Qwen/Qwen3-8B") == "Qwen/Qwen3-8B" + with pytest.raises(HTTPException): + _validate_repo_id("DeepSeek-R1-UD-IQ4_XS") + + +def test_validate_serve_model_id_accepts_cached_local_model_names(): + assert _validate_serve_model_id("Qwen/Qwen3-8B") == "Qwen/Qwen3-8B" + assert _validate_serve_model_id("DeepSeek-R1-UD-IQ4_XS") == "DeepSeek-R1-UD-IQ4_XS" + with pytest.raises(HTTPException): + _validate_serve_model_id("../escape") + + def test_local_tooling_path_export_prepends_interpreter_bin(): """The cookbook runners must see the venv's bin (where `hf`/`python` live) so tmux shells can find them without an activated venv."""