Allow serving cached local llama.cpp models
Co-authored-by: Kevin <120500656+oooindefatigable@users.noreply.github.com>
This commit is contained in:
@@ -16,6 +16,11 @@ logger = logging.getLogger(__name__)
|
||||
# HuggingFace repo IDs are <org>/<name>, 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 <org>/<name> 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
|
||||
|
||||
@@ -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 (`<org>/<name>`) 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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user