phase1b: Backend entruempelt und robuster (35 tote Routen raus, Sperren, ehrliche Update-Pruefung)
Ampel / ampel (push) Successful in 26s

Ballast raus:
- 35 Routen ohne Nutzer entfernt (agent/*, fit, roles, ctx, drafts, groups, routing/policy,
  system/history, system/self-update, maintenance/reboot, zeitmaschine/inhalt, zeitplan,
  voice/health|metrics|trace|voices|reference|tts). Von 95 auf 60.
- Tote Module geloescht: agent-Router, roles, agent_aktivitaet, metrics_history (samt
  10-s-Sampler), voice_metrics, migrate_config, parse_mc2_timeout, scripts/.
- Unbenutzte Funktionen und Konstanten entfernt (Modell-Upgrade-Empfehlung, Draft-/Kontext-
  Setzer, Konsole, PC-Ausfuehrer-Probe, Routing-Policy-Editor ...).

Robuster:
- Jobs in eigener Prozessgruppe (Abbrechen beendet wirklich alles), Zeitlimit je Job-Art,
  start_job_exklusiv: zwei Klicks starten kein doppeltes Update mehr; alte Jobs raeumen sich auf.
- Update-Pruefung meldet Fehler (pruef_fehler, Lampe "Pruefung unklar") statt "aktuell".
- Nach jedem Update sofort neu pruefen (update_stand) statt 10 Minuten alten Stand zeigen.
- llama-swap-Config: Sperre (RLock + flock) fuer UI, Radar, Aufraeumen und Hirn-Umstellung.
- Hermes-Config: bei Lesefehler nichts schreiben, atomar, mit Sicherung.
- Live-Strom und Gateway-Warnung blockieren den Event-Loop nicht mehr (Lucy, OpenChamber).
- Gateway antwortet bei Engine-Ausfall im OpenAI-Fehlerformat (502) statt nacktem 500.
- Abgestuerzte Waechter-Pruefung wird ein gelber Hinweis statt still zu verschwinden.
- Download laedt nur den gewuenschten Quant (vorher bei Fehlen alle Teile aller Varianten),
  Download-Jobs in Gruppe "download"; HF-Suche kodiert den Suchbegriff.
- Herkunftspruefung: schreibende /api-Aufrufe fremder Webseiten werden abgelehnt (keine
  Anmeldung, User-Entscheid); Skripte, Desktop-Lucy und /v1 unveraendert.
- Modellpfade: Eintragen und Loeschen nur innerhalb von MODELS_DIR.
- Dienste-Liste fragt keine abgebauten Dienste mehr ab (PC-Ausfuehrer haette 3 s gekostet).
- SSE-Fehlerzeilen von /api/voice/chat als gueltiges JSON.
- mission-control-2.service: --timeout-graceful-shutdown 3 (Neustart ohne 10-s-Haenger).

Tests: 92 gruen (neu: Herkunft, Quant-Auswahl, abgestuerzte Pruefung).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-24 15:08:44 +02:00
co-authored by Claude Opus 5.5
parent c8718fcde0
commit e9f488b56c
42 changed files with 799 additions and 2197 deletions
+10 -102
View File
@@ -1,11 +1,12 @@
"""Modelle-Endpoints: Liste (mit Caps), Discover, Fit, Register, Groups."""
"""Modelle-Endpoints: Liste, Discover, Suche und Installation bei Hugging Face, Jobs, Rollen,
Laden/Entladen/Löschen. Fit-, Kontext-, Draft- und Gruppen-Routen sind am 24.09.2026 entfallen
(ohne Nutzer seit dem Box-Wart-Umbau)."""
import psutil
from config import HF_DOWNLOAD_ENV, MODELS_DIR
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from services import budget, discover, geheimnisse, hf, jobengine, llamaswap
from services.fit import evaluate_fit, max_ctx_for
router = APIRouter(prefix="/api")
@@ -29,27 +30,6 @@ def discover_models(force: bool = False) -> dict:
return {**data, "sys_ram_gb": round(ram, 1)}
@router.get("/fit")
def fit(params_b: float = 0, quant: str = "Q4_K_M", ctx: int = 8192,
name: str = "", role: str = "") -> dict:
"""Hardware-Fit-Vorschau. params_b<=0 → aus KATALOG (echte Metadaten, MoE-bewusst)
oder sonst aus dem Namen geschätzt. assigned_ctx = der ctx, der TATSÄCHLICH vergeben
würde: SETUP-BEWUSST (neben Hirn/warmem Set), nicht nur gegen den Gesamt-RAM.
So sieht die 'Erweiterte Ansicht' vor dem Download Ampel + echten ctx."""
ram = _ram_gb()
pb = params_b if params_b > 0 else budget.params_b_for(name)
saw = budget.setup_aware_ctx(pb, quant, role=role or None)
return {
"params_b": round(pb, 1),
"fit": evaluate_fit(pb, quant, ctx, ram, name=name),
"optimal_ctx": max_ctx_for(pb, quant, ram), # Roh-Obergrenze (Modell allein)
"assigned_ctx": saw["ctx"], # setup-bewusst vergeben
"budget": {"gtt_gb": saw["gtt_gb"], "reserved_gb": saw["reserved_gb"],
"budget_gb": saw["budget_gb"], "mode": saw["mode"]},
"sys_ram_gb": round(ram, 1),
}
class RegisterReq(BaseModel):
model_path: str
role: str | None = None
@@ -61,6 +41,10 @@ class RegisterReq(BaseModel):
@router.post("/models/register")
def register(req: RegisterReq) -> dict:
# Nur Dateien aus dem Modellordner (24.09.2026): der Pfad landet im Startbefehl der Engine.
for pfad in (req.model_path, req.mmproj_path):
if pfad and not llamaswap.im_modellordner(pfad):
raise HTTPException(400, f"Pfad liegt nicht im Modellordner ({MODELS_DIR}): {pfad}")
try:
model_id = llamaswap.register_model(
req.model_path, role=req.role, ctx=req.ctx, ttl=req.ttl,
@@ -148,8 +132,9 @@ def install(req: InstallReq) -> dict:
env = dict(HF_DOWNLOAD_ENV)
if token := geheimnisse.hf_token():
env["HF_TOKEN"] = token
job_id = jobengine.start_job(args, f"download {req.repo}", env=env,
on_done=_apply_role if role else None)
# Gruppe „download“: so taucht der Download in der Oberfläche mit Fortschritt und Abbrechen auf.
job_id = jobengine.start_job(args, f"Download {repo}", env=env, group="download",
on_done=_apply_role if role else None, zeitlimit_s=6 * 3600)
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"])}
@@ -169,14 +154,6 @@ class RoleReq(BaseModel):
role: str | None = None
@router.get("/roles/{role}/recommend")
def recommend_role(role: str) -> dict:
"""Welches installierte Modell passt am besten auf diese Rolle? (Capability + setup-
bewusster Fit). Basis für 'Empfohlen'-Hinweis + Auto-Pick im Rollen-Zuweisungs-Modal."""
from services import roles
return roles.recommend_for_role(role)
@router.post("/models/{model_id}/role")
def set_model_role(model_id: str, body: RoleReq) -> dict:
new_role = (body.role or "").strip().lower()
@@ -201,54 +178,6 @@ def set_model_role(model_id: str, body: RoleReq) -> dict:
return {"ok": True}
class CtxReq(BaseModel):
ctx: int
@router.get("/models/{model_id}/ctx/auto")
def auto_ctx(model_id: str) -> dict:
"""Setup-bewusster Optimal-ctx für ein bestehendes Modell (Rolle/Params/Quant +
aktuelles Setup). Basis für den 'Auto'-Button an der Modellkarte."""
m = next((x for x in llamaswap.list_models() if x["name"] == model_id), None)
if not m:
raise HTTPException(404, "Modell nicht gefunden")
saw = budget.setup_aware_ctx_for_model(m)
return {"model_id": model_id, "current_ctx": m.get("ctx"),
"params_b": round(budget.params_of_model(m), 1), "quant": m.get("quant"),
"role": m.get("role"), **saw}
@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.get("/models/drafts")
def list_drafts(target: str = "") -> dict:
"""Verfügbare Draft-Modelle + ihre Vocab-Kompatibilität zum Ziel-Modell
(target = GGUF-Pfad). Basis für die idiotensichere Spec-Draft-Auswahl im UI."""
return llamaswap.drafts_for(target)
class DraftReq(BaseModel):
draft_path: str | None = None
@router.post("/models/{model_id}/draft")
def set_model_draft(model_id: str, body: DraftReq) -> dict:
"""Setzt/entfernt den Speculative-Decoding-Draft eines Modells. Inkompatible
(oder nicht prüfbare) Drafts werden serverseitig abgelehnt."""
try:
res = llamaswap.set_spec_draft(model_id, body.draft_path)
except PermissionError as exc:
raise HTTPException(500, str(exc))
if not res["ok"]:
raise HTTPException(400 if "kompatib" in res["reason"].lower() else 404, res["reason"])
return res
@router.post("/models/unload")
def unload_all_models() -> dict:
import httpx
@@ -297,24 +226,3 @@ def delete(model_id: str) -> dict:
if not llamaswap.delete_model(model_id):
raise HTTPException(404, "Modell nicht gefunden")
return {"ok": True}
@router.get("/groups")
def groups() -> dict:
return {"groups": llamaswap.list_groups()}
class GroupReq(BaseModel):
group: str
members: list[str]
swap: bool = False
persist: bool = False
@router.put("/groups")
def set_group(req: GroupReq) -> dict:
try:
llamaswap.set_group(req.group, req.members, swap=req.swap, persist=req.persist)
except PermissionError as exc:
raise HTTPException(500, str(exc))
return {"ok": True}