Feat: Brain-Rolle (hermes) als erstklassige, dauer-warme Hirn-Rolle

- ROLE_IDS (llamaswap, sources) + UI-Rollenlisten (ModelBadges, Discover,
  ModelBrowse, Cockpit-Slot-Grid) um `hermes` erweitert: gemma erscheint als
  „Hirn", nicht mehr als scout.
- Neuer set_ttl-Helper; set_agent_brain erzwingt ttl:0 (neu + idempotent beim
  Re-Setzen) und liefert eine weiche Budget-Warnung (kein Hard-Block) bei OOM.
- brain_status/brain_model_name: zeigen das echte Hirn (Rolle hermes / Hermes
  model.default) statt hart `fast` (Bugfix Health-/Ready-Check).
- POST /api/models/{id}/role delegiert die Rolle `hermes` an den warm-bewussten
  Flow (Alias + brains-Gruppe + ttl 0 + Hermes-Config + Gateway-Restart).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-30 17:01:26 +02:00
parent 087eb2259d
commit dd99401f3e
11 changed files with 360 additions and 284 deletions
+25 -1
View File
@@ -167,16 +167,40 @@ def set_agent_brain(model_id: str) -> dict:
return {"ok": False, "reason": "Modell nicht installiert — erst über Modelle-finden laden."}
old = next((m["name"] for m in models.values() if m.get("role") == "hermes"), None)
if model_id == old:
# Idempotent härten: auch wenn schon Hirn, warm (brains) + ttl 0 sicherstellen.
try:
from services.llamaswap import set_ttl
brains = (llamaswap.list_groups().get("brains") or {}).get("members") or []
if model_id not in brains:
llamaswap.set_group("brains", brains + [model_id], swap=False, persist=True)
set_ttl(model_id, 0)
except PermissionError as exc:
return {"ok": False, "reason": str(exc)}
return {"ok": True, "old": old, "new": model_id, "note": "ist bereits das Agent-Hirn"}
try:
llamaswap.set_role(model_id, "hermes") # 1) Alias
brains = (llamaswap.list_groups().get("brains") or {}).get("members") or []
new_members = [x for x in brains if x not in (old, model_id)] + [model_id]
llamaswap.set_group("brains", new_members, swap=False, persist=True) # 2) warm
# 2b) TTL härten: neues Hirn nie auto-entladen; altes Hirn auf Default entspannen.
from services.llamaswap import set_ttl, DEFAULT_TTL
set_ttl(model_id, 0)
if old:
set_ttl(old, DEFAULT_TTL)
except PermissionError as exc:
return {"ok": False, "reason": str(exc)}
update_brain_model("hermes") # 3) Config + Restart
return {"ok": True, "old": old, "new": model_id}
# Weiche Budget-Warnung (kein Hard-Block): passt Hirn + größtes on-demand zusammen ins GTT?
warning = None
try:
b = hermes_brain_info().get("budget") or {}
if b and not b.get("fits", True):
warning = (f"Speicher-Warnung: Hirn (~{b.get('brain_gb')} GB) + größtes on-demand-"
f"Modell (~{b.get('largest_ondemand_gb')} GB) übersteigen das GTT-Budget "
f"(~{b.get('gtt_gb')} GB) — heavy/coder würden das Hirn verdrängen.")
except Exception:
log.debug("set_agent_brain: Budget-Check fehlgeschlagen", exc_info=True)
return {"ok": True, "old": old, "new": model_id, "warning": warning}
def update_brain_model(new_model: str) -> bool:
+36 -9
View File
@@ -21,8 +21,9 @@ from config import (
log = logging.getLogger(__name__)
# Kanonische Serving-Rollen — EINE Quelle der Wahrheit (identisch zu sources.ROLE_IDS,
# maintenance, frontend ModelBadges.ROLES). Kein agent/reasoning mehr.
ROLE_IDS = {"fast", "heavy", "coder", "vision", "scout"}
# maintenance, frontend ModelBadges.ROLES). `hermes` = Lucys Agent-Hirn (warm + ko-resident
# in der `brains`-Gruppe); UI-Label „Hirn".
ROLE_IDS = {"fast", "heavy", "coder", "vision", "scout", "hermes"}
_CTX_RE = re.compile(r"-(?:c|-ctx-size)\s+(\d+)")
_PATH_RE = re.compile(r"-(?:m|-model)\s+([^\s]+)")
@@ -371,6 +372,18 @@ def set_ctx(model_id: str, ctx: int) -> bool:
return True
def set_ttl(model_id: str, ttl: int) -> bool:
"""Idle-TTL (Sekunden) eines bestehenden Modells setzen. ttl=0 → nie automatisch
entladen (für das Agent-Hirn, das dauerhaft warm bleiben muss)."""
cfg = read_config()
spec = (cfg.get("models") or {}).get(model_id)
if not isinstance(spec, dict):
return False
spec["ttl"] = int(ttl)
write_config(cfg)
return True
def delete_model(model_id: str) -> bool:
"""Entfernt einen Modell-Eintrag aus der config.yaml, löscht die zugehörigen
GGUF-Dateien (auch Splits) vom Datenträger und bereinigt leere Ordner.
@@ -433,23 +446,37 @@ def delete_model(model_id: str) -> bool:
def brain_model_name() -> str | None:
"""Modellname des Agent-Hirns = das Modell mit llama-swap-Alias/Rolle 'fast'."""
for m in list_models():
"""Modellname von Lucys Agent-Hirn. Bevorzugt das Modell mit dem 'hermes'-Alias/-Rolle;
fällt auf Hermes' aktives `model.default` zurück (deckt den Fall ab, dass die Config direkt
auf einen Modellnamen statt den Alias zeigt)."""
models = list_models()
for m in models:
names = {str(a).lower() for a in (m.get("aliases") or [])}
if m.get("role"):
names.add(str(m["role"]).lower())
if "fast" in names:
if "hermes" in names:
return m["name"]
# Fallback: das real von Hermes genutzte Hirn (model.default), per Alias/Name auflösen.
try:
from services.agent import _active_brain_name
brain = (_active_brain_name() or "").lower()
if brain and brain != "auto":
cur = next((m for m in models if (m.get("role") or "").lower() == brain), None) \
or next((m for m in models if brain in (m["name"] or "").lower()), None)
if cur:
return cur["name"]
except Exception:
log.debug("brain_model_name: Hermes-Fallback fehlgeschlagen", exc_info=True)
return None
def brain_status() -> dict:
"""Ist das Agent-Hirn ('fast') WIRKLICH geladen & bereit? Prüft /running — ein abgestürztes
Modell (z.B. OOM/Crash nach Engine-Update) erscheint dort NICHT als running. Fängt damit den
Fall 'Engine erreichbar, aber Hirn tot', den engine_reachable() nicht sieht (silent fail)."""
"""Ist Lucys Agent-Hirn (Rolle 'hermes') WIRKLICH geladen & bereit? Prüft /running — ein
abgestürztes Modell (z.B. OOM/Crash nach Engine-Update) erscheint dort NICHT als running.
Fängt damit den Fall 'Engine erreichbar, aber Hirn tot', den engine_reachable() nicht sieht."""
name = brain_model_name()
running = get_running_models()
return {"role": "fast", "model": name, "ready": bool(name and name in running)}
return {"role": "hermes", "model": name, "ready": bool(name and name in running)}
def get_running_models() -> list[str]:
+5 -2
View File
@@ -1,7 +1,7 @@
"""
Vertrauenswürdige Quellen + Kategorien für die automatische Modell-Entdeckung.
Rollen = die EINE Quelle der Wahrheit, identisch zu den llama-swap-Serving-Rollen
und der UI: fast · heavy · coder · vision · scout.
und der UI: fast · heavy · coder · vision · hermes (Agent-Hirn) · scout.
"""
# HF-Orgs, die zuverlässig aktuelle, hochwertige GGUF-Quants veröffentlichen.
@@ -9,7 +9,8 @@ TRUSTED_AUTHORS = ["unsloth", "bartowski", "ggml-org", "lmstudio-community"]
# Kanonische Rollen — eine Quelle der Wahrheit (deckt sich mit llamaswap.ROLE_IDS,
# maintenance.ROLE_MAP, frontend ModelBadges.ROLES + Discover.ROLE_METADATA).
ROLE_IDS = ["fast", "heavy", "coder", "vision", "scout"]
# `hermes` = Lucys Agent-Hirn (warm + ko-resident); UI-Label „Hirn".
ROLE_IDS = ["fast", "heavy", "coder", "vision", "hermes", "scout"]
# Kategorien (Reihenfolge = Anzeige + Zuordnungs-Priorität). Ein Modell wird der
# ERSTEN Kategorie zugeordnet, deren Stichwort im Repo-Namen vorkommt; sonst „scout".
@@ -19,6 +20,8 @@ CATEGORIES = [
"kw": ["-vl-", "-vl", "vision", "llava", "multimodal", "-mm-", "pixtral"]},
{"role": "coder", "title": "Coden & Programmieren", "icon": "code",
"kw": ["coder", "-code-", "code-", "codestral", "starcoder"]},
{"role": "hermes", "title": "Lucys Hirn (Agent)", "icon": "brain-circuit",
"kw": ["hermes"]}, # Agent-Hirn: Hermes-Familie am robustesten (natives Tool-Calling).
{"role": "heavy", "title": "Schweres Reasoning", "icon": "brain",
"kw": ["reasoning", "-think", "thinking", "gpt-oss", "deepseek-r", "-r1", "qwq",
"-70b", "-72b", "-120b", "-123b", "-235b", "-405b", "-a10b", "-a22b"]},