Feat: kuratierter Modell-Katalog (Cookbook) - korrekte Metadaten statt Namens-Raterei

Inspiriert von Odysseus' Cookbook: backend/models_catalog.json mit echten Metadaten
(total/active params, moe, generation) je Rolle. services/catalog.py: Laden, Name-Match,
MoE-bewusstes Scoring (Wissen + Tempo via tps -> MoE-first auf der bandbreiten-Box), Fit.
- discover.py: Empfehlung jetzt KATALOG-FIRST (kuratiert, korrekt), HF-Dynamik als Ergaenzung/Fallback.
- maintenance.model_upgrades: Metadaten aus Katalog -> praezise Familie/Generation/Groesse + MoE-first
  (dense ersetzt MoE nur bei grossem Wissens-Sprung). Behebt Coder-Next=7B-Fehlschaetzung,
  Qwen2.5-VL-Generations-Downgrade, falsches dense-scout-Upgrade.
- fit.py: MXFP4/FP8/AWQ in der Quant-Tabelle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-27 13:04:21 +02:00
parent 123303240d
commit c074d977ce
5 changed files with 237 additions and 26 deletions
+41 -17
View File
@@ -16,7 +16,7 @@ import httpx
import psutil
from config import ANYTHINGLLM_REPO, ANYTHINGLLM_URL, HERMES_AGENT_REPO
from services import discover, jobengine, llamaswap, system
from services import catalog, discover, jobengine, llamaswap, system
# System-Dienste (root, via sudo -n NOPASSWD) vs. User-Dienste (systemctl --user).
SYSTEM_SERVICES = {"llama-swap"}
@@ -185,11 +185,27 @@ def _gen_key(name: str):
return None
def _meta(name: str, model_dict: dict | None = None, im: dict | None = None) -> dict:
"""Metadaten (family, gen, total, active, moe) — bevorzugt den kuratierten Katalog,
sonst die Felder eines Discover-/Modell-Dicts, sonst Namens-/Größen-Heuristik."""
cm = catalog.meta_for_name(name)
if cm:
return {"family": cm.get("family"), "gen": cm.get("generation"),
"total": float(cm.get("total_params_b") or 0),
"active": cm.get("active_params_b"), "moe": bool(cm.get("moe"))}
d = model_dict or {}
g = _gen_key(name)
total = float(d.get("params_b") or 0) or (_params_of(im) if im else 0.0)
return {"family": (d.get("family") or (g[0] if g else None)),
"gen": (d.get("generation") if d.get("generation") is not None else (g[1] if g else None)),
"total": total, "active": d.get("active_b"), "moe": bool(d.get("moe"))}
def model_upgrades() -> list[dict]:
"""Je Rolle ein ECHTES Upgrade-Vorschlag — nur wenn die Empfehlung wirklich besser ist:
gleiche Familie UND (neuere Generation ODER größer in gleicher Generation). Verhindert
Generations-Downgrades (Qwen2.5-VL über Qwen3-VL), Größen-Downgrades (Coder-Next → 30B)
und Fremd-Familien-Swaps (gpt-oss als „Upgrade" von Qwen)."""
"""Je Rolle ein ECHTES Upgrade — nur wenn die Empfehlung wirklich besser ist:
gleiche Familie UND (neuere Generation ODER deutlich größer) UND kein Tempo-Downgrade
(MoE-first für die bandbreiten-limitierte Box: dense ersetzt MoE nur bei großem Wissens-
Sprung). Metadaten kommen aus dem kuratierten Katalog → keine Namens-Raterei."""
disc = discover.safe_discover(_ram_gb())
if not disc:
return []
@@ -203,28 +219,36 @@ def model_upgrades() -> list[dict]:
for c in disc.get("categories", []):
role = c["role"]
im = inst_by_role.get(role)
if im is None: # nur Rollen mit bereits installiertem Modell
if im is None:
continue
rec = c.get("recommended")
if not rec:
continue
rec_model = next((x for x in c.get("models", []) if x.get("repo") == rec), None)
rec_params = float((rec_model or {}).get("params_b") or 0.0)
inst_params = _params_of(im)
ig, rg = _gen_key(im["name"]), _gen_key(rec)
# Upgrade nur bei GLEICHER, erkennbarer Familie:
if not (ig and rg) or ig[0] != rg[0]:
continue # Fremd-Familie / unbekannt → kein „Upgrade"
if rg[1] < ig[1] - 1e-6:
continue # ältere Generation → niemals
if abs(rg[1] - ig[1]) < 1e-6 and rec_params and rec_params < inst_params * 1.05:
continue # gleiche Gen, nicht (deutlich) größer
i = _meta(im["name"], im=im)
r = _meta(rec, model_dict=rec_model)
if not i["family"] or not r["family"] or i["family"] != r["family"]:
continue # andere/unbekannte Familie → kein Upgrade
if r["gen"] is not None and i["gen"] is not None and r["gen"] < i["gen"] - 1e-6:
continue # ältere Generation → niemals
same_gen = (r["gen"] is None or i["gen"] is None or abs(r["gen"] - i["gen"]) < 1e-6)
if same_gen:
if r["total"] and i["total"] and r["total"] < i["total"] * 1.05:
continue # gleiche Gen, nicht größer → kein Upgrade
# MoE-first: ein MoE durch dense ersetzen nur bei deutlichem Wissens-Sprung
if i["moe"] and not r["moe"] and r["total"] < i["total"] * 1.5:
continue
# Tempo nicht verschlechtern (aktive Params), außer großer Wissens-Gewinn
ia, ra = (i["active"] or i["total"]), (r["active"] or r["total"])
if ia and ra > ia * 1.3 and r["total"] < i["total"] * 1.3:
continue
base = rec.split("/")[-1].lower()
stem = base[:-5] if base.endswith("-gguf") else base
if base in cmds or (stem and stem in cmds):
continue # schon installiert
continue # schon installiert
out.append({"role": role, "title": c["title"], "repo": rec})
return out