feat(2.0): W2+W3 — HF-Link/Suche + Modell-Verwaltungs-UX

W2: install akzeptiert HF-URL ODER org/repo (normalize_repo); GET /api/hf/
search + /api/hf/quants; Frontend AddModel-Panel (URL+Quant-Dropdown+freie
Suche) im Discover-Tab. W3: POST /api/models/{id}/role + /ctx; Installiert-
Tab mit Rollen-Select (fast/heavy/coder/...), ctx-Edit, Loeschen → LLM
tauschen per Klick.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-25 14:33:23 +02:00
parent 501ba36b89
commit c863f01a78
9 changed files with 295 additions and 57 deletions
+40 -6
View File
@@ -70,17 +70,29 @@ class InstallReq(BaseModel):
hf_token: str | None = None
@router.get("/hf/search")
def hf_search(q: str) -> dict:
return {"results": hf.search(q)}
@router.get("/hf/quants")
def hf_quants(repo: str) -> dict:
repo = hf.normalize_repo(repo)
return {"repo": repo, "quants": hf.list_quants(repo)}
@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)
erste Teil (-00001-of-…). Akzeptiert volle HF-URL ODER org/repo."""
repo = hf.normalize_repo(req.repo)
info = hf.resolve_gguf(repo, req.quant)
if not info["first"]:
raise HTTPException(404, f"Keine GGUF-Datei für Quant '{req.quant}' in {req.repo} gefunden.")
raise HTTPException(404, f"Keine GGUF-Datei für Quant '{req.quant}' in {repo} gefunden.")
subdir = req.repo.split("/")[-1]
subdir = repo.split("/")[-1]
target = MODELS_DIR / subdir
target.mkdir(parents=True, exist_ok=True)
model_path = str(target / info["first"])
@@ -89,7 +101,7 @@ def install(req: InstallReq) -> dict:
ctx = req.ctx
if ctx is None:
ram = _ram_gb()
ctx = max_ctx_for(extract_params_b(req.repo), req.quant, ram)
ctx = max_ctx_for(extract_params_b(repo), req.quant, ram)
# Sofort registrieren (Datei kommt gleich) — robust gegen -watch-config.
try:
@@ -99,7 +111,7 @@ def install(req: InstallReq) -> dict:
raise HTTPException(500, str(exc))
# Download-Job: alle GGUF-Teile (+ mmproj) per --include holen.
args = [hf.hf_bin(), "download", req.repo]
args = [hf.hf_bin(), "download", repo]
for f in info["files"]:
args.append(f)
if info["mmproj"]:
@@ -124,6 +136,28 @@ def cancel(job_id: str) -> dict:
return {"ok": jobengine.cancel_job(job_id)}
class RoleReq(BaseModel):
role: str | None = None
@router.post("/models/{model_id}/role")
def set_model_role(model_id: str, body: RoleReq) -> dict:
if not llamaswap.set_role(model_id, body.role):
raise HTTPException(404, "Modell nicht gefunden")
return {"ok": True}
class CtxReq(BaseModel):
ctx: int
@router.post("/models/{model_id}/ctx")
def set_model_ctx(model_id: str, body: CtxReq) -> dict:
if not llamaswap.set_ctx(model_id, body.ctx):
raise HTTPException(404, "Modell nicht gefunden")
return {"ok": True}
@router.delete("/models/{model_id}")
def delete(model_id: str) -> dict:
if not llamaswap.delete_model(model_id):