feat(2.0): Phase 6a — Modell-Install (Download-Jobs + Split + UI)
jobengine portiert (Live-Log + %-Fortschritt aus .incomplete), services/hf.py (resolve_gguf inkl. Split -of-, Groessen), POST /api/models/install (hf download als Job + sofortige Registrierung, Split-fest), GET /api/jobs + cancel. huggingface_hub in requirements. Frontend: Install-Buttons auf Discover-Karten + Live-Download-Fortschrittsbalken (JobsBar). Verifiziert: resolve_gguf gegen echte Repos (Qwen3.6-35B-A3B 23GB single, Qwen3.5-122B-A10B 77GB 3-part split) + Frontend-Build. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -4,8 +4,9 @@ import psutil
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from services import discover, llamaswap
|
||||
from services.fit import evaluate_fit, max_ctx_for
|
||||
from config import HF_DOWNLOAD_ENV, MODELS_DIR
|
||||
from services import discover, hf, jobengine, llamaswap
|
||||
from services.fit import evaluate_fit, extract_params_b, max_ctx_for
|
||||
|
||||
router = APIRouter(prefix="/api")
|
||||
|
||||
@@ -60,6 +61,69 @@ def register(req: RegisterReq) -> dict:
|
||||
return {"ok": True, "model_id": model_id}
|
||||
|
||||
|
||||
class InstallReq(BaseModel):
|
||||
repo: str
|
||||
role: str | None = None
|
||||
quant: str = "Q4_K_M"
|
||||
ctx: int | None = None
|
||||
jinja: bool = False
|
||||
hf_token: str | None = None
|
||||
|
||||
|
||||
@router.post("/models/install")
|
||||
def install(req: InstallReq) -> dict:
|
||||
"""Lädt ein Modell von HuggingFace (Hintergrund-Job) UND trägt es sofort in
|
||||
llama-swap ein (cmd + Rolle-Alias). llama-swap (-watch-config) lädt es, sobald
|
||||
die Datei da ist. Split-GGUFs werden komplett geladen, registriert wird der
|
||||
erste Teil (-00001-of-…)."""
|
||||
info = hf.resolve_gguf(req.repo, req.quant)
|
||||
if not info["first"]:
|
||||
raise HTTPException(404, f"Keine GGUF-Datei für Quant '{req.quant}' in {req.repo} gefunden.")
|
||||
|
||||
subdir = req.repo.split("/")[-1]
|
||||
target = MODELS_DIR / subdir
|
||||
target.mkdir(parents=True, exist_ok=True)
|
||||
model_path = str(target / info["first"])
|
||||
mmproj_path = str(target / info["mmproj"]) if info["mmproj"] else None
|
||||
|
||||
ctx = req.ctx
|
||||
if ctx is None:
|
||||
ram = _ram_gb()
|
||||
ctx = max_ctx_for(extract_params_b(req.repo), req.quant, ram)
|
||||
|
||||
# Sofort registrieren (Datei kommt gleich) — robust gegen -watch-config.
|
||||
try:
|
||||
model_id = llamaswap.register_model(
|
||||
model_path, role=req.role, ctx=ctx, mmproj_path=mmproj_path, jinja=req.jinja)
|
||||
except PermissionError as exc:
|
||||
raise HTTPException(500, str(exc))
|
||||
|
||||
# Download-Job: alle GGUF-Teile (+ mmproj) per --include holen.
|
||||
args = [hf.hf_bin(), "download", req.repo]
|
||||
for f in info["files"]:
|
||||
args.append(f)
|
||||
if info["mmproj"]:
|
||||
args.append(info["mmproj"])
|
||||
args += ["--local-dir", str(target)]
|
||||
env = dict(HF_DOWNLOAD_ENV)
|
||||
if req.hf_token:
|
||||
env["HF_TOKEN"] = req.hf_token
|
||||
job_id = jobengine.start_job(args, f"download {req.repo}", env=env)
|
||||
jobengine.attach_download_progress(job_id, str(target), info["total_bytes"])
|
||||
return {"ok": True, "job_id": job_id, "model_id": model_id, "model_path": model_path,
|
||||
"total_bytes": info["total_bytes"], "files": len(info["files"])}
|
||||
|
||||
|
||||
@router.get("/jobs")
|
||||
def jobs() -> dict:
|
||||
return {"jobs": jobengine.public_jobs()}
|
||||
|
||||
|
||||
@router.post("/jobs/{job_id}/cancel")
|
||||
def cancel(job_id: str) -> dict:
|
||||
return {"ok": jobengine.cancel_job(job_id)}
|
||||
|
||||
|
||||
@router.get("/groups")
|
||||
def groups() -> dict:
|
||||
return {"groups": llamaswap.list_groups()}
|
||||
|
||||
Reference in New Issue
Block a user