mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-11-07 21:52:20 +01:00
Merge remote-tracking branch 'origin/dev' into fix/pr-6080-llama-recovery
This commit is contained in:
+133
-39
@@ -50,7 +50,7 @@ from routes.cookbook_helpers import (
|
||||
_SESSION_ID_RE, _validate_repo_id, _validate_serve_model_id, _validate_include, _validate_token,
|
||||
_validate_local_dir, _validate_gpus, _shell_path,
|
||||
_ps_squote, _bash_squote, _validate_serve_cmd, _parse_serve_phase, OLLAMA_MISSING_HINT,
|
||||
_safe_env_prefix, _local_tooling_path_export, _append_serve_preflight_exit_lines,
|
||||
_safe_env_prefix, _local_windows_bash_env_prefix, _local_tooling_path_export, _append_serve_preflight_exit_lines,
|
||||
_append_serve_exit_code_lines, _append_llama_cpp_linux_accel_build_lines, _cached_model_scan_script,
|
||||
load_stored_hf_token,
|
||||
_append_vllm_linux_preflight_lines, _ollama_bind_from_cmd, _pip_install_fallback_chain,
|
||||
@@ -116,6 +116,16 @@ def _append_mlx_image_server_script(runner_lines: list[str]) -> None:
|
||||
runner_lines.append('chmod +x scripts/mlx_image_server.py 2>/dev/null || true')
|
||||
|
||||
|
||||
def _normalize_runtime_adapter(value: str | None) -> str:
|
||||
"""Return a shell-safe explicit image adapter name."""
|
||||
value = (value or "auto").strip().lower()
|
||||
if not value:
|
||||
return "auto"
|
||||
if not re.fullmatch(r"[a-z0-9][a-z0-9_-]{0,39}", value):
|
||||
raise HTTPException(400, "Invalid runtime adapter")
|
||||
return value
|
||||
|
||||
|
||||
def _venv_root_from_serve_cmd(cmd: str) -> str:
|
||||
"""Best-effort venv root from an absolute venv python in a serve command."""
|
||||
try:
|
||||
@@ -397,7 +407,34 @@ def _append_local_ollama_download_command_lines(
|
||||
|
||||
|
||||
def setup_cookbook_routes() -> APIRouter:
|
||||
router = APIRouter(tags=["cookbook"])
|
||||
async def protect_native_control(request: Request):
|
||||
if request.method in {"GET", "HEAD"}:
|
||||
return
|
||||
# UI records/session strings are not process authority. Local tool
|
||||
# launches require a one-use capability from their admitted producer.
|
||||
path = request.url.path
|
||||
from routes.shell_routes import _require_admin
|
||||
if path in {"/api/cookbook/kill-pid", "/api/cookbook/state", "/api/cookbook/ssh-key"}:
|
||||
_require_admin(request)
|
||||
if path in {"/api/model/download", "/api/model/serve"}:
|
||||
payload = await request.json()
|
||||
if not payload.get("remote_host"):
|
||||
from src.agent_runtime.local_model_control import consume_model_control
|
||||
from src.agent_runtime.resources import ResourceIdentityError
|
||||
try:
|
||||
claimed = consume_model_control(request, payload)
|
||||
except (ResourceIdentityError, ValueError, TypeError):
|
||||
raise HTTPException(403, "Local model capability denied") from None
|
||||
if not claimed:
|
||||
_require_admin(request)
|
||||
router = APIRouter(tags=["cookbook"], dependencies=[Depends(protect_native_control)])
|
||||
|
||||
def protect_local_model_producer(request, remote_host):
|
||||
# Scoped wrappers can call endpoint functions directly, without FastAPI
|
||||
# dependencies. Enforce native control at the actual producer as well.
|
||||
if not remote_host and getattr(request.state, "local_model_authority", None) is None:
|
||||
from routes.shell_routes import _require_admin
|
||||
_require_admin(request)
|
||||
_cookbook_state_path = Path(COOKBOOK_STATE_FILE)
|
||||
_state_get_cache = {"ts": 0.0, "mtime": 0.0, "value": None}
|
||||
_tasks_status_cache = {"ts": 0.0, "value": None}
|
||||
@@ -666,13 +703,17 @@ def setup_cookbook_routes() -> APIRouter:
|
||||
return cmd
|
||||
|
||||
repo_id = "cyankiwi/MiniMax-M3-AWQ-INT4"
|
||||
snapshot = (
|
||||
"/home/pewds/.cache/huggingface/hub/"
|
||||
"models--cyankiwi--MiniMax-M3-AWQ-INT4/"
|
||||
"snapshots/4082acbbec1236d21828d55b6bb0fe02ade4ab5b"
|
||||
)
|
||||
if body[serve_i + 1] == repo_id:
|
||||
body[serve_i + 1] = snapshot
|
||||
hf_home = Path(os.environ.get("HF_HOME", str(Path.home() / ".cache" / "huggingface")))
|
||||
hf_cache = Path(os.environ.get("HUGGINGFACE_HUB_CACHE", str(hf_home / "hub")))
|
||||
snapshot_root = hf_cache / "models--cyankiwi--MiniMax-M3-AWQ-INT4" / "snapshots"
|
||||
if body[serve_i + 1] == repo_id and snapshot_root.is_dir():
|
||||
installed_snapshots = sorted(
|
||||
(p for p in snapshot_root.iterdir() if p.is_dir()),
|
||||
key=lambda p: p.stat().st_mtime,
|
||||
reverse=True,
|
||||
)
|
||||
if installed_snapshots:
|
||||
body[serve_i + 1] = str(installed_snapshots[0])
|
||||
|
||||
def add_env(key: str, value: str) -> None:
|
||||
if not any(p.startswith(f"{key}=") for p in env_parts):
|
||||
@@ -1068,6 +1109,7 @@ def setup_cookbook_routes() -> APIRouter:
|
||||
"""Download a HuggingFace model in a tmux session.
|
||||
Uses `hf download` CLI directly — runs in tmux via `script -qc`
|
||||
for real TTY progress, streams ANSI-stripped output via log file."""
|
||||
protect_local_model_producer(request, req.remote_host)
|
||||
require_admin(request)
|
||||
# Defence-in-depth: even though this endpoint is admin-gated, refuse
|
||||
# values that would land in shell contexts with metacharacters.
|
||||
@@ -1338,7 +1380,7 @@ def setup_cookbook_routes() -> APIRouter:
|
||||
# Local: run hf download in the background (tmux on POSIX, a detached
|
||||
# process + logfile on Windows where tmux doesn't exist).
|
||||
if req.env_prefix:
|
||||
lines.append(_safe_env_prefix(req.env_prefix))
|
||||
lines.append(_safe_env_prefix(_local_windows_bash_env_prefix(req.env_prefix) if local_windows else req.env_prefix))
|
||||
else:
|
||||
lines.append("deactivate 2>/dev/null; hash -r")
|
||||
# Show whether the HF token reached this run (masked) — tells a gated
|
||||
@@ -1413,7 +1455,6 @@ def setup_cookbook_routes() -> APIRouter:
|
||||
# unvalidated value (e.g. "x'; rm -rf ~ #") would be command injection.
|
||||
host = validate_remote_host(host)
|
||||
ssh_port = validate_ssh_port(ssh_port)
|
||||
TMUX_LOG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
model_dirs = []
|
||||
if model_dir:
|
||||
@@ -1425,20 +1466,17 @@ def setup_cookbook_routes() -> APIRouter:
|
||||
model_dirs.append(d)
|
||||
paths_code = _cached_model_scan_script(model_dirs)
|
||||
|
||||
scan_py = TMUX_LOG_DIR / "scan_cache.py"
|
||||
scan_py.write_text(paths_code, encoding="utf-8")
|
||||
|
||||
async def _run_cached_scan_once():
|
||||
# Each request owns its script bytes. A shared scan_cache.py races
|
||||
# when the tool scans several hosts/directories concurrently.
|
||||
if host:
|
||||
_ssh_opts = "-o BatchMode=yes -o ConnectTimeout=8 -o ServerAliveInterval=4 -o ServerAliveCountMax=1 "
|
||||
_pf = f"-p {ssh_port} " if ssh_port and ssh_port != "22" else ""
|
||||
if platform == "windows":
|
||||
# Windows: use 'python' and pipe via stdin with double-quote wrapping
|
||||
cmd = f'ssh {_ssh_opts}{_pf}{host} "python -" < \'{scan_py}\''
|
||||
else:
|
||||
cmd = f"ssh {_ssh_opts}{_pf}{host} 'python3 -' < '{scan_py}'"
|
||||
proc = await asyncio.create_subprocess_shell(
|
||||
cmd,
|
||||
ssh_args = ['ssh', '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=8',
|
||||
'-o', 'ServerAliveInterval=4', '-o', 'ServerAliveCountMax=1']
|
||||
if ssh_port and ssh_port != '22':
|
||||
ssh_args.extend(['-p', ssh_port])
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
*ssh_args, host, 'python -' if platform == 'windows' else 'python3 -',
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
cwd=str(Path.home()),
|
||||
@@ -1456,12 +1494,31 @@ def setup_cookbook_routes() -> APIRouter:
|
||||
or which_tool("py") or "python"
|
||||
)
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
local_py, str(scan_py),
|
||||
local_py, '-',
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
cwd=str(Path.home()),
|
||||
)
|
||||
return await asyncio.wait_for(proc.communicate(), timeout=60), proc.returncode
|
||||
try:
|
||||
output = await asyncio.wait_for(proc.communicate(paths_code.encode('utf-8')), timeout=60)
|
||||
return output, proc.returncode
|
||||
finally:
|
||||
# A timed-out/cancelled request must not abandon its scanner.
|
||||
# This handle belongs only to this request, never a model job.
|
||||
if proc.returncode is None:
|
||||
try:
|
||||
proc.terminate()
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
try:
|
||||
await asyncio.wait_for(proc.wait(), timeout=2)
|
||||
except asyncio.TimeoutError:
|
||||
try:
|
||||
proc.kill()
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
await asyncio.wait_for(proc.wait(), timeout=2)
|
||||
|
||||
(stdout_b, stderr_b), returncode = await _run_cached_scan_once()
|
||||
stderr_txt = stderr_b.decode(errors="replace").strip()
|
||||
@@ -1971,11 +2028,13 @@ def setup_cookbook_routes() -> APIRouter:
|
||||
keep strict validation, but serving local cached models must not require
|
||||
a fake org/name wrapper.
|
||||
"""
|
||||
protect_local_model_producer(request, req.remote_host)
|
||||
require_admin(request)
|
||||
# Defence-in-depth: reject values that could break out of shell contexts.
|
||||
validate_remote_host(req.remote_host)
|
||||
req.ssh_port = validate_ssh_port(req.ssh_port)
|
||||
req.gpus = _validate_gpus(req.gpus)
|
||||
req.runtime_adapter = _normalize_runtime_adapter(req.runtime_adapter)
|
||||
req.hf_token = req.hf_token or _load_stored_hf_token()
|
||||
_validate_token(req.hf_token)
|
||||
# Cookbook emits two fixed Docker exec forms for its Ollama sidecars.
|
||||
@@ -2168,7 +2227,7 @@ def setup_cookbook_routes() -> APIRouter:
|
||||
if req.gpus:
|
||||
runner_lines.append(f"export CUDA_VISIBLE_DEVICES='{req.gpus}'")
|
||||
if req.env_prefix:
|
||||
runner_lines.append(_safe_env_prefix(req.env_prefix))
|
||||
runner_lines.append(_safe_env_prefix(_local_windows_bash_env_prefix(req.env_prefix) if local_windows else req.env_prefix))
|
||||
else:
|
||||
runner_lines.append("deactivate 2>/dev/null; hash -r")
|
||||
_append_venv_nvidia_library_path_lines(runner_lines, cmd=req.cmd)
|
||||
@@ -2560,19 +2619,20 @@ def setup_cookbook_routes() -> APIRouter:
|
||||
runner_lines.append('print(model)')
|
||||
runner_lines.append('PY')
|
||||
runner_lines.append(')"')
|
||||
runner_lines.append('if printf "%s" "$ODYSSEUS_MLX_IMAGE_MODEL" | grep -qi hidream; then')
|
||||
runner_lines.append(f"export ODYSSEUS_MLX_IMAGE_ADAPTER='{_bash_squote(req.runtime_adapter or 'auto')}'")
|
||||
runner_lines.append('if [ "$ODYSSEUS_MLX_IMAGE_ADAPTER" = "hidream" ] || { [ "$ODYSSEUS_MLX_IMAGE_ADAPTER" = "auto" ] && printf "%s" "$ODYSSEUS_MLX_IMAGE_MODEL" | grep -qi hidream; }; then')
|
||||
runner_lines.append(' if ! "$ODYSSEUS_MLX_IMAGE_CMD_PY" -c "import mlx, mlx_vlm, transformers, huggingface_hub, safetensors, numpy, PIL" >/dev/null 2>&1; then')
|
||||
runner_lines.append(' echo "ERROR: HiDream MLX serving needs the model requirements in the launch Python: $ODYSSEUS_MLX_IMAGE_CMD_PY."')
|
||||
runner_lines.append(' echo "Install with: $ODYSSEUS_MLX_IMAGE_CMD_PY -m pip install -U fastapi uvicorn python-multipart mlx mlx-vlm \'transformers>=4.57.0,<6.0\' huggingface_hub safetensors numpy pillow tqdm sentencepiece hf_transfer"')
|
||||
runner_lines.append(' ODYSSEUS_PREFLIGHT_EXIT=127')
|
||||
runner_lines.append(' fi')
|
||||
runner_lines.append('elif printf "%s" "$ODYSSEUS_MLX_IMAGE_MODEL" | grep -qi boogu; then')
|
||||
runner_lines.append('elif [ "$ODYSSEUS_MLX_IMAGE_ADAPTER" = "boogu" ] || { [ "$ODYSSEUS_MLX_IMAGE_ADAPTER" = "auto" ] && printf "%s" "$ODYSSEUS_MLX_IMAGE_MODEL" | grep -qi boogu; }; then')
|
||||
runner_lines.append(' if ! "$ODYSSEUS_MLX_IMAGE_CMD_PY" -c "import boogu_image_mlx, mlx, huggingface_hub, safetensors, numpy, PIL" >/dev/null 2>&1; then')
|
||||
runner_lines.append(' echo "ERROR: Boogu MLX serving needs boogu-image-mlx in the launch Python: $ODYSSEUS_MLX_IMAGE_CMD_PY."')
|
||||
runner_lines.append(' echo "Install with: $ODYSSEUS_MLX_IMAGE_CMD_PY -m pip install -U git+https://github.com/xocialize/boogu-image-mlx.git fastapi uvicorn python-multipart pillow"')
|
||||
runner_lines.append(' ODYSSEUS_PREFLIGHT_EXIT=127')
|
||||
runner_lines.append(' fi')
|
||||
runner_lines.append('elif printf "%s" "$ODYSSEUS_MLX_IMAGE_MODEL" | grep -Eqi "ddcolor"; then')
|
||||
runner_lines.append('elif [ "$ODYSSEUS_MLX_IMAGE_ADAPTER" = "ddcolor" ] || { [ "$ODYSSEUS_MLX_IMAGE_ADAPTER" = "auto" ] && printf "%s" "$ODYSSEUS_MLX_IMAGE_MODEL" | grep -Eqi "ddcolor"; }; then')
|
||||
runner_lines.append(' if ! "$ODYSSEUS_MLX_IMAGE_CMD_PY" -c "import PIL" >/dev/null 2>&1; then')
|
||||
runner_lines.append(' echo "ERROR: DDColor MLX serving needs Pillow in the launch Python: $ODYSSEUS_MLX_IMAGE_CMD_PY."')
|
||||
runner_lines.append(' echo "Install with: $ODYSSEUS_MLX_IMAGE_CMD_PY -m pip install -U fastapi uvicorn python-multipart pillow huggingface_hub"')
|
||||
@@ -2592,7 +2652,7 @@ def setup_cookbook_routes() -> APIRouter:
|
||||
runner_lines.append(' ODYSSEUS_PREFLIGHT_EXIT=127')
|
||||
runner_lines.append(' fi')
|
||||
runner_lines.append(' fi')
|
||||
runner_lines.append('elif printf "%s" "$ODYSSEUS_MLX_IMAGE_MODEL" | grep -Eqi "mi-gan|migan|lama"; then')
|
||||
runner_lines.append('elif [ "$ODYSSEUS_MLX_IMAGE_ADAPTER" = "inpaint" ] || { [ "$ODYSSEUS_MLX_IMAGE_ADAPTER" = "auto" ] && printf "%s" "$ODYSSEUS_MLX_IMAGE_MODEL" | grep -Eqi "mi-gan|migan|lama"; }; then')
|
||||
runner_lines.append(' if ! "$ODYSSEUS_MLX_IMAGE_CMD_PY" -c "import PIL" >/dev/null 2>&1; then')
|
||||
runner_lines.append(' echo "ERROR: LaMa / MI-GAN MLX serving needs Pillow in the launch Python: $ODYSSEUS_MLX_IMAGE_CMD_PY."')
|
||||
runner_lines.append(' echo "Install with: $ODYSSEUS_MLX_IMAGE_CMD_PY -m pip install -U fastapi uvicorn python-multipart pillow huggingface_hub"')
|
||||
@@ -2612,10 +2672,12 @@ def setup_cookbook_routes() -> APIRouter:
|
||||
runner_lines.append(' ODYSSEUS_PREFLIGHT_EXIT=127')
|
||||
runner_lines.append(' fi')
|
||||
runner_lines.append(' fi')
|
||||
runner_lines.append('elif ! command -v mflux-generate >/dev/null 2>&1 && ! command -v mflux-generate-qwen >/dev/null 2>&1; then')
|
||||
runner_lines.append(' echo "ERROR: mflux-compatible MLX image serving requires mflux-generate or mflux-generate-qwen in PATH for launch Python: $ODYSSEUS_MLX_IMAGE_CMD_PY."')
|
||||
runner_lines.append(' echo "Install with: $ODYSSEUS_MLX_IMAGE_CMD_PY -m pip install -U mflux fastapi uvicorn python-multipart"')
|
||||
runner_lines.append(' ODYSSEUS_PREFLIGHT_EXIT=127')
|
||||
runner_lines.append('elif [ "$ODYSSEUS_MLX_IMAGE_ADAPTER" = "mflux" ] || [ "$ODYSSEUS_MLX_IMAGE_ADAPTER" = "auto" ]; then')
|
||||
runner_lines.append(' if ! command -v mflux-generate >/dev/null 2>&1 && ! command -v mflux-generate-qwen >/dev/null 2>&1; then')
|
||||
runner_lines.append(' echo "ERROR: mflux-compatible MLX image serving requires mflux-generate or mflux-generate-qwen in PATH for launch Python: $ODYSSEUS_MLX_IMAGE_CMD_PY."')
|
||||
runner_lines.append(' echo "Install with: $ODYSSEUS_MLX_IMAGE_CMD_PY -m pip install -U mflux fastapi uvicorn python-multipart"')
|
||||
runner_lines.append(' ODYSSEUS_PREFLIGHT_EXIT=127')
|
||||
runner_lines.append(' fi')
|
||||
runner_lines.append('fi')
|
||||
elif "scripts/diffusion_server.py" in req.cmd or ".diffusion_server.py" in req.cmd:
|
||||
runner_lines.append('export PATH="$HOME/.local/bin:$PATH"')
|
||||
@@ -3474,12 +3536,19 @@ def setup_cookbook_routes() -> APIRouter:
|
||||
return {"ok": False, "error": str(e)}
|
||||
|
||||
@router.get("/api/cookbook/hf-latest")
|
||||
async def hf_latest(vram_gb: float = 0, limit: int = 10, pipeline: str = "text-generation", owner: str = Depends(require_user)):
|
||||
async def hf_latest(
|
||||
vram_gb: float = 0,
|
||||
limit: int = 10,
|
||||
pipeline: str = "text-generation",
|
||||
official_only: bool = False,
|
||||
owner: str = Depends(require_user),
|
||||
):
|
||||
"""Fetch latest HuggingFace models, filtered by what fits in available VRAM.
|
||||
|
||||
vram_gb: total available VRAM in GB. 0 = no filter (return everything).
|
||||
limit: how many models to return (default 10).
|
||||
pipeline: HF pipeline_tag filter (text-generation, text-to-image, etc.).
|
||||
official_only: restrict results to recognized first-party provider namespaces.
|
||||
"""
|
||||
import re
|
||||
import httpx
|
||||
@@ -3545,6 +3614,20 @@ def setup_cookbook_routes() -> APIRouter:
|
||||
return True
|
||||
return False
|
||||
|
||||
# HF does not expose a universal "first-party" flag. Keep this as a
|
||||
# namespace policy rather than a model-name list, so newly published
|
||||
# provider models are included without recommending community forks.
|
||||
OFFICIAL_NAMESPACES = {
|
||||
"apple", "black-forest-labs", "deepseek-ai", "google", "lightricks",
|
||||
"meta-llama", "microsoft", "mistralai", "nvidia", "openai", "qwen",
|
||||
"stabilityai", "tencent", "runwayml",
|
||||
}
|
||||
|
||||
def _is_official(entry: dict, repo_id: str) -> bool:
|
||||
namespace = repo_id.split("/", 1)[0].strip().lower() if "/" in repo_id else ""
|
||||
author = str(entry.get("author") or "").strip().lower()
|
||||
return namespace in OFFICIAL_NAMESPACES and (not author or author == namespace)
|
||||
|
||||
out = []
|
||||
for entry in raw:
|
||||
repo_id = entry.get("modelId") or entry.get("id") or ""
|
||||
@@ -3559,6 +3642,8 @@ def setup_cookbook_routes() -> APIRouter:
|
||||
# Skip adapters, LoRAs, datasets, etc.
|
||||
if _is_excluded(repo_id, tags):
|
||||
continue
|
||||
if official_only and not _is_official(entry, repo_id):
|
||||
continue
|
||||
|
||||
est_fp16 = _est_vram_fp16(repo_id)
|
||||
quant_mult = _quant_factor(repo_id, tags)
|
||||
@@ -3572,7 +3657,11 @@ def setup_cookbook_routes() -> APIRouter:
|
||||
# if we cannot estimate size from the repo id/tags, do not
|
||||
# present it as runnable on this hardware.
|
||||
continue
|
||||
if needed_vram > vram_gb:
|
||||
# Leave allocator/runtime headroom instead of treating the
|
||||
# reported total as a safe load budget. This keeps the
|
||||
# official-only list honest on tight GPUs as well.
|
||||
usable_vram = vram_gb * 0.90
|
||||
if needed_vram > usable_vram:
|
||||
continue
|
||||
|
||||
out.append({
|
||||
@@ -4370,6 +4459,7 @@ def setup_cookbook_routes() -> APIRouter:
|
||||
|
||||
progress_text = ""
|
||||
full_snapshot = (task.get("output") or "")[-12000:] if task_type == "serve" else ""
|
||||
_persisted_terminal = False
|
||||
|
||||
if local_win_task:
|
||||
# File-based liveness + output for the detached-process model.
|
||||
@@ -4403,9 +4493,10 @@ def setup_cookbook_routes() -> APIRouter:
|
||||
and bool(full_snapshot)
|
||||
and _parse_serve_phase(full_snapshot, task_type).get("status") == "ready"
|
||||
)
|
||||
if _task_status in {"stopped", "done", "completed",
|
||||
_persisted_terminal = _task_status in {"stopped", "done", "completed",
|
||||
"crashed", "error", "failed",
|
||||
"ended", "killed"} and not _persisted_serve_ready:
|
||||
"ended", "killed"} and not _persisted_serve_ready
|
||||
if _persisted_terminal:
|
||||
is_alive = False
|
||||
# Keep the persisted output_tail for the UI — it's
|
||||
# what the agent uses to diagnose past failures.
|
||||
@@ -4444,7 +4535,9 @@ def setup_cookbook_routes() -> APIRouter:
|
||||
and (
|
||||
".incomplete" in full_snapshot
|
||||
or bool(re.search(r'model-\d+-of-\d+\.[A-Za-z0-9_.-]+:\s+(?:[0-9]|[1-8][0-9])%', full_snapshot))
|
||||
or _download_cache_incomplete(_payload.get("repo_id") or model, remote, str(_tport or ""), _payload.get("local_dir") or "")
|
||||
or (not _persisted_terminal and _download_cache_incomplete(
|
||||
_payload.get("repo_id") or model, remote, str(_tport or ""), _payload.get("local_dir") or ""
|
||||
))
|
||||
)
|
||||
)
|
||||
if is_alive or (local_win_task and full_snapshot):
|
||||
@@ -4496,6 +4589,7 @@ def setup_cookbook_routes() -> APIRouter:
|
||||
progress_text = "Download complete"
|
||||
elif (
|
||||
task_type == "download"
|
||||
and not _persisted_terminal
|
||||
and not download_has_incomplete_evidence
|
||||
and _download_cache_complete(_payload.get("repo_id") or model, remote, str(_tport or ""), _payload.get("local_dir") or "")
|
||||
):
|
||||
|
||||
Reference in New Issue
Block a user