feat: Modelle loeschen (Config austragen + Dateien entfernen)

- POST /api/delete_model: entlaedt das Modell, traegt es aus der config.yaml
  aus und loescht optional die GGUF-Dateien, um Speicher freizugeben
- Sicherheit: Loeschen STRENG auf MODELS_DIR begrenzt (Pfad-Resolve-Check);
  loescht den ganzen Repo-Unterordner inkl. .cache nur, wenn er ein direktes
  Kind von MODELS_DIR ist und kein anderes Modell eine Datei darin nutzt,
  sonst nur die einzelne GGUF-Datei. Meldet freigegebene GB zurueck.
- Modelle-Tab: roter "Loeschen"-Button je Zeile mit Klartext-Sicherheitsabfrage
  (unwiderruflich) via confirmModal(danger)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-21 18:31:56 +02:00
parent 3cf36d436b
commit f44da4b8e2
3 changed files with 95 additions and 0 deletions
+78
View File
@@ -21,6 +21,7 @@ from llamaswap import _swap_get, read_config, write_config
from hw_math import extract_params_b, max_ctx_for, estimate_memory_gb
import re
import os
import shutil
import psutil
router = APIRouter(prefix="/api", dependencies=[Depends(auth)])
@@ -53,6 +54,11 @@ class UpdateReq(BaseModel):
ctx: int
class DeleteReq(BaseModel):
alias: str
delete_files: bool = True
# ---------------------------------------------------------------------------
# Endpoints
# ---------------------------------------------------------------------------
@@ -186,6 +192,78 @@ def update_model(req: UpdateReq):
return {"ok": True}
def _dir_size(p: Path) -> int:
if p.is_file():
try:
return p.stat().st_size
except OSError:
return 0
total = 0
for root, _dirs, files in os.walk(p):
for f in files:
try:
total += os.path.getsize(os.path.join(root, f))
except OSError:
pass
return total
def _delete_model_files(model_path: str, cfg: dict) -> tuple[str | None, int]:
"""Loescht die Dateien eines Modells — STRENG auf MODELS_DIR begrenzt. Loescht den
ganzen Repo-Unterordner (inkl. .cache), wenn dieser ein direktes Kind von MODELS_DIR
ist und kein anderes (verbleibendes) Modell eine Datei darin nutzt; sonst nur die
GGUF-Datei selbst. Gibt (geloeschter_pfad|None, freigegebene_bytes) zurueck."""
root = MODELS_DIR.resolve()
p = Path(model_path).resolve()
if root not in p.parents: # Sicherheit: niemals ausserhalb des Modell-Ordners loeschen
return None, 0
parent = p.parent
others = " ".join(str(s.get("cmd", "")) for s in (cfg.get("models") or {}).values())
parent_shared = parent != root and str(parent) in others
target = parent if (parent != root and not parent_shared) else p
if not target.exists():
return None, 0
freed = _dir_size(target)
if target.is_dir():
shutil.rmtree(target)
else:
target.unlink()
return str(target), freed
@router.post("/delete_model")
def delete_model(req: DeleteReq):
"""Modell komplett entfernen: aus der config.yaml austragen (+ entladen) und optional
die GGUF-Dateien loeschen, um Speicher freizugeben. Unwiderruflich."""
cfg = read_config()
models = cfg.get("models", {})
if req.alias not in models:
raise HTTPException(404, "Modell nicht gefunden.")
cmd = str(models[req.alias].get("cmd", ""))
m = re.search(r'-(?:m|-model)\s+(\S+)', cmd)
model_path = m.group(1).strip('"\'') if m else None
# Erst entladen (falls geladen) — Fehler ignorieren, das Modell soll trotzdem weg.
try:
with httpx.Client(timeout=10.0) as c:
c.post(f"{LLAMA_SWAP_URL}/api/models/unload/{req.alias}")
except Exception: # noqa: BLE001
pass
del models[req.alias]
write_config(cfg) # cfg enthaelt das Modell jetzt nicht mehr -> _delete prueft die Restmenge
note, freed = "", 0
if req.delete_files and model_path:
try:
deleted, freed = _delete_model_files(model_path, cfg)
note = (f"Dateien gelöscht ({round(freed / 1024 ** 3, 1)} GB frei)."
if deleted else "Dateien liegen außerhalb des Modell-Ordners — nur ausgetragen.")
except Exception as exc: # noqa: BLE001
note = f"Aus der Konfiguration entfernt, aber Dateien konnten nicht gelöscht werden: {exc}"
return {"ok": True, "alias": req.alias, "freed_bytes": freed, "note": note or "Modell entfernt."}
@router.post("/unload")
def unload(model: str | None = None):
path = f"/api/models/unload/{model}" if model else "/api/models/unload"