mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-16 17:55:26 -04:00
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 ._-
|
# HuggingFace repo IDs are <org>/<name>, both alphanumerics plus ._-
|
||||||
# Rejecting anything else up front closes off shell-interpolation vectors.
|
# 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._-]*$")
|
_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 pattern is a glob: allow typical safe glyphs only.
|
||||||
_INCLUDE_RE = re.compile(r"^[A-Za-z0-9._\-*?/\[\]]+$")
|
_INCLUDE_RE = re.compile(r"^[A-Za-z0-9._\-*?/\[\]]+$")
|
||||||
# Remote host: user@host (optionally with :port-free hostname parts).
|
# Remote host: user@host (optionally with :port-free hostname parts).
|
||||||
@@ -40,6 +45,14 @@ def _validate_repo_id(v: str | None) -> str:
|
|||||||
return v
|
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:
|
def _validate_include(v: str | None) -> str | None:
|
||||||
if v is None or v == "":
|
if v is None or v == "":
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
from routes.cookbook_helpers import (
|
from routes.cookbook_helpers import (
|
||||||
_SSH_PORT_RE, _REMOTE_HOST_RE, _SESSION_ID_RE,
|
_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,
|
_validate_local_dir, _validate_ssh_port, _validate_gpus, _shell_path,
|
||||||
_ps_squote, _bash_squote, _validate_serve_cmd, _parse_serve_phase,
|
_ps_squote, _bash_squote, _validate_serve_cmd, _parse_serve_phase,
|
||||||
_safe_env_prefix, _local_tooling_path_export, _append_serve_preflight_exit_lines,
|
_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).
|
"""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
|
`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
|
model-serve commands, a cached local-model id (the folder name reported
|
||||||
`python -m pip install …`. We only enforce the strict HF format on
|
by `/api/model/cached`) for models scanned from a custom model dir, OR a
|
||||||
the model paths.
|
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)
|
require_admin(request)
|
||||||
# Defence-in-depth: reject values that could break out of shell contexts.
|
# 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")
|
raise HTTPException(400, "Invalid pip package name")
|
||||||
else:
|
else:
|
||||||
_validate_repo_id(req.repo_id)
|
_validate_serve_model_id(req.repo_id)
|
||||||
TMUX_LOG_DIR.mkdir(parents=True, exist_ok=True)
|
TMUX_LOG_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
session_id = f"serve-{uuid.uuid4().hex[:8]}"
|
session_id = f"serve-{uuid.uuid4().hex[:8]}"
|
||||||
remote = req.remote_host
|
remote = req.remote_host
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ from routes.cookbook_helpers import (
|
|||||||
_local_tooling_path_export,
|
_local_tooling_path_export,
|
||||||
_safe_env_prefix,
|
_safe_env_prefix,
|
||||||
_validate_gpus,
|
_validate_gpus,
|
||||||
|
_validate_repo_id,
|
||||||
|
_validate_serve_model_id,
|
||||||
_validate_ssh_port,
|
_validate_ssh_port,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -52,6 +54,19 @@ def test_validate_gpus_accepts_indexes_only():
|
|||||||
_validate_gpus("0; rm -rf /")
|
_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():
|
def test_local_tooling_path_export_prepends_interpreter_bin():
|
||||||
"""The cookbook runners must see the venv's bin (where `hf`/`python` live)
|
"""The cookbook runners must see the venv's bin (where `hf`/`python` live)
|
||||||
so tmux shells can find them without an activated venv."""
|
so tmux shells can find them without an activated venv."""
|
||||||
|
|||||||
Reference in New Issue
Block a user