fix(phase-a): Kontext-Cap, Download-State, Recipe-Edit, OS-Badge, Swap-Flash
A1: 32768-Kontext-Cap entfernt (install-recipe, install-model, register) →
max_ctx_for() liefert nun bis zu 128k auf Strix Halo; behebt "context
size exceeded" bei externen Tools.
A2: Download-State jetzt im Status-Endpoint sichtbar: Modelle zeigen
"↓ Download X%" statt "bereit" während Job läuft (Backend + Frontend).
A3: PUT /api/cookbook/user-recipe/{id} + Edit-Button (✎) für eigene Setups.
Download-Modal setzt Kontext-Input automatisch auf optimal.
A4: /api/updates liefert apt_cache_age_h; Badge zeigt Tooltip + ⚠ wenn >24h.
A5: Swap-Flash: Topbar-Text pulst kurz teal wenn Modell den State wechselt.
A6: LLM-Engine-Update fragt jetzt per confirmModal nach (Konsistenz).
A7: Event-Delegation statt per-render addEventListener in models.js.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+32
-3
@@ -216,6 +216,35 @@ def create_user_recipe(req: UserRecipeReq):
|
||||
return {"ok": True, "id": rid}
|
||||
|
||||
|
||||
@router.put("/user-recipe/{recipe_id}")
|
||||
def update_user_recipe(recipe_id: str, req: UserRecipeReq):
|
||||
"""Bestehendes eigenes Setup aktualisieren (Titel, Beschreibung, Modelle)."""
|
||||
items = load_user_recipes()
|
||||
idx = next((i for i, r in enumerate(items) if r.get("id") == recipe_id), None)
|
||||
if idx is None:
|
||||
raise HTTPException(404, "Eigenes Setup nicht gefunden.")
|
||||
if not req.title.strip():
|
||||
raise HTTPException(400, "Bitte einen Titel angeben.")
|
||||
models = []
|
||||
for m in req.models:
|
||||
if not m.repo.strip():
|
||||
continue
|
||||
models.append({
|
||||
"role": (m.role or "modell").strip(),
|
||||
"name": (m.name or m.repo.split("/")[-1]).strip(),
|
||||
"repo": m.repo.strip(),
|
||||
"params_b": extract_params_b(m.repo),
|
||||
"quant": (m.quant or "Q4_K_M").strip(),
|
||||
"why": (m.why or "").strip(),
|
||||
})
|
||||
if not models:
|
||||
raise HTTPException(400, "Mindestens ein Modell (Repo) angeben.")
|
||||
items[idx] = {**items[idx], "title": req.title.strip(), "icon": (req.icon or "box").strip(),
|
||||
"desc": req.desc.strip(), "models": models}
|
||||
save_user_recipes(items)
|
||||
return {"ok": True, "id": recipe_id}
|
||||
|
||||
|
||||
@router.delete("/user-recipe/{recipe_id}")
|
||||
def delete_user_recipe(recipe_id: str):
|
||||
items = load_user_recipes()
|
||||
@@ -251,8 +280,8 @@ def install_recipe(req: InstallRecipeReq):
|
||||
JOBS[jid]["result_path"] = str(target / file)
|
||||
attach_download_progress(jid, str(target), hf_file_size(m["repo"], file))
|
||||
job_ids.append(jid)
|
||||
# Eintrag jetzt schon schreiben — optimaler Kontext, aber gedeckelt fuer schnellen Erststart.
|
||||
ctx = min(max_ctx_for(m["params_b"], m["quant"], ram_gb), 32768)
|
||||
# Eintrag jetzt schon schreiben — optimaler Kontext für die Hardware.
|
||||
ctx = max_ctx_for(m["params_b"], m["quant"], ram_gb)
|
||||
path = str(target / file)
|
||||
cmd = CMD_TEMPLATE.replace("{model}", path).replace("{ctx}", str(ctx))
|
||||
# Schluessel = sprechender Modellname; Rolle (aus dem Rezept) als Alias.
|
||||
@@ -490,7 +519,7 @@ def install_model(req: InstallModelReq):
|
||||
JOBS[jid]["result_path"] = str(target / file)
|
||||
attach_download_progress(jid, str(target), hf_file_size(req.repo, file))
|
||||
cfg = read_config()
|
||||
ctx = min(max_ctx_for(req.params_b, req.quant, ram_gb), 32768)
|
||||
ctx = max_ctx_for(req.params_b, req.quant, ram_gb)
|
||||
path = str(target / file)
|
||||
cmd = CMD_TEMPLATE.replace("{model}", path).replace("{ctx}", str(ctx))
|
||||
mid = model_id_from_path(path)
|
||||
|
||||
@@ -114,7 +114,14 @@ def updates():
|
||||
models = len(compute_upgrades(psutil.virtual_memory().total / (1024 ** 3)))
|
||||
except Exception: # noqa: BLE001
|
||||
models = 0
|
||||
return {"os": os_count, "models": models, "engine": 1 if _engine_update_available() else 0}
|
||||
apt_cache_age_h = None
|
||||
try:
|
||||
import time as _time
|
||||
apt_cache_age_h = round((_time.time() - os.path.getmtime("/var/lib/apt/lists")) / 3600, 1)
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
return {"os": os_count, "models": models, "engine": 1 if _engine_update_available() else 0,
|
||||
"apt_cache_age_h": apt_cache_age_h}
|
||||
|
||||
@router.websocket("/logs/{service}")
|
||||
async def stream_logs(websocket: WebSocket, service: str):
|
||||
|
||||
+21
-2
@@ -42,7 +42,7 @@ class RegisterReq(BaseModel):
|
||||
alias: str = "" # rueckwaertskompatibel: wird als Rolle interpretiert, wenn 'role' fehlt
|
||||
role: str | None = None # Rollen-Tag (vision/coder/scout/reviewer/manager o.ae.)
|
||||
model_path: str
|
||||
ctx: int = 8192
|
||||
ctx: int | None = None # None → optimal fuer die Hardware (max_ctx_for)
|
||||
ttl: int | None = None
|
||||
|
||||
|
||||
@@ -138,6 +138,20 @@ def status():
|
||||
"peak_ram_optimal_gb": (round(estimate_memory_gb(_pb, quant or "Q4_K_M", _oc), 1) if _oc else None),
|
||||
}
|
||||
}
|
||||
# Laufende Download-Jobs erkennnen: Modell bekommt state "downloading" + Fortschritt.
|
||||
for j in JOBS.values():
|
||||
if j.get("state") not in ("running", "queued"):
|
||||
continue
|
||||
rp = j.get("result_path", "")
|
||||
if not rp:
|
||||
continue
|
||||
for mconf in configured.values():
|
||||
m_p = re.search(r'-(?:m|-model)\s+(\S+)', mconf.get("cmd", ""))
|
||||
if m_p and m_p.group(1).strip("'\"") == rp:
|
||||
mconf["state"] = "downloading"
|
||||
mconf["download_progress"] = j.get("progress")
|
||||
break
|
||||
|
||||
swap_ok = True
|
||||
try:
|
||||
running = _swap_get("/running")
|
||||
@@ -200,7 +214,12 @@ def register(req: RegisterReq):
|
||||
# Bewusst KEIN exists()-Check: beim frischen Download läuft der hf-Job noch, die Datei kommt
|
||||
# erst gleich. Eintrag jetzt schon schreiben → llama-swap (-watch-config) lädt, sobald sie da ist.
|
||||
cfg = read_config()
|
||||
cmd = CMD_TEMPLATE.replace("{model}", req.model_path).replace("{ctx}", str(req.ctx))
|
||||
ctx = req.ctx
|
||||
if ctx is None:
|
||||
params_b = extract_params_b(req.model_path)
|
||||
ram_gb = psutil.virtual_memory().total / (1024 ** 3)
|
||||
ctx = max_ctx_for(params_b, "Q4_K_M", ram_gb)
|
||||
cmd = CMD_TEMPLATE.replace("{model}", req.model_path).replace("{ctx}", str(ctx))
|
||||
cmd = _augment_vision(cmd, req.model_path)
|
||||
# Neues Schema: Schluessel = sprechender Modellname (steht so in der Modell-Liste und ist
|
||||
# der API-Name), die Rolle kommt als llama-swap-Alias obendrauf (beide Namen funktionieren).
|
||||
|
||||
Reference in New Issue
Block a user