Fix: Modell-Upgrades nur bei ECHTEM Upgrade (gleiche Familie + neuere Gen/groesser)

model_upgrades verletzte das Prinzip: schlug Generations-Downgrades (Qwen2.5-VL ueber
Qwen3-VL), Groessen-Downgrades (Qwen3-Coder-Next ~84B -> 30B; Params aus Name als 7B
fehlgeschaetzt) und Fremd-Familien-Swaps (gpt-oss als Qwen-"Upgrade") vor.

- _params_of: groessen-bewusste Params (max aus Name + Dateigroesse).
- _gen_key: Familie+Subtyp+Generation aus dem Namen (qwen-vl 3.0 vs 2.5 etc.).
- Guard: Upgrade nur bei gleicher erkennbarer Familie UND (neuere Generation ODER
  deutlich groesser in gleicher Gen). Sonst keine "Bessere Version"-Anzeige.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-27 12:49:14 +02:00
parent 31d5e5d727
commit 123303240d
+49 -20
View File
@@ -154,48 +154,77 @@ def _components_cached() -> list[dict]:
return data
def _params_of(m: dict) -> float:
"""Größen-bewusste Parameterzahl eines installierten Modells: max aus Namens-Schätzung
und Dateigröße (fängt namenlose wie 'Qwen3-Coder-Next' UND Split-GGUFs ab)."""
from services.fit import QUANT_BYTES_PER_PARAM
caps = m.get("capabilities") or {}
bpp = QUANT_BYTES_PER_PARAM.get((m.get("quant") or "Q4_K_M").upper(), 0.55)
size_gb = (m.get("size_bytes") or 0) / (1024 ** 3)
pb_size = (size_gb / bpp) if size_gb > 1.0 else 0.0
return max(float(caps.get("params_b") or 0), pb_size, 0.0)
# Familien-Subtyp + Generations-Version aus dem Modellnamen (für „echtes Upgrade?").
_FAM_PATS = (("qwen", r"qwen(\d+(?:\.\d+)?)"), ("gemma", r"gemma[-_ ]?(\d+(?:\.\d+)?)"),
("llama", r"llama[-_ ]?(\d+(?:\.\d+)?)"), ("phi", r"phi[-_ ]?(\d+(?:\.\d+)?)"),
("mistral", r"mistral"), ("hermes", r"hermes[-_ ]?(\d+(?:\.\d+)?)"))
def _gen_key(name: str):
"""(Familie+Subtyp, Generations-Version) oder None. Z.B. 'Qwen3-VL-2B' → ('qwen-vl', 3.0),
'Qwen2.5-VL-7B' → ('qwen-vl', 2.5). Nur gleiche Familie ist sinnvoll vergleichbar."""
low = (name or "").lower()
sub = "-vl" if any(k in low for k in ("-vl", "vl-", "vision", "llava", "pixtral")) else \
"-coder" if ("coder" in low or "-code" in low) else ""
for fam, pat in _FAM_PATS:
m = re.search(pat, low)
if m:
ver = float(m.group(1)) if (m.groups() and m.group(1)) else 0.0
return (fam + sub, ver)
return None
def model_upgrades() -> list[dict]:
"""Dynamisch: je discover-Kategorie das empfohlene Modell, das NOCH NICHT installiert ist,
aber NUR wenn für diese Rolle bereits irgendein Modell konfiguriert ist.
→ Upgrade-Vorschlag für diese Rolle. Self-updating (kein Hardcode wie v1)."""
"""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)."""
disc = discover.safe_discover(_ram_gb())
if not disc:
return []
installed = llamaswap.list_models()
active_roles = {m["role"] for m in installed if m.get("role")}
# Größtes installiertes Modell je Rolle (params_b) — Basis für „kein Downgrade".
inst_params: dict[str, float] = {}
for m in installed:
r, pb = m.get("role"), (m.get("capabilities") or {}).get("params_b")
if r and pb:
inst_params[r] = max(inst_params.get(r, 0.0), float(pb))
inst_by_role = {m["role"]: m for m in installed if m.get("role")}
cmds = " ".join(str(s.get("cmd", "")).lower()
for s in (llamaswap.read_config().get("models") or {}).values())
out = []
# Discover-Rollen == Serving-Rollen (fast/heavy/coder/vision/scout) → kein Mapping mehr.
for c in disc.get("categories", []):
role = c["role"]
if role not in active_roles: # nur Rollen, die bereits ein Modell haben
im = inst_by_role.get(role)
if im is None: # nur Rollen mit bereits installiertem Modell
continue
rec = c.get("recommended")
if not rec:
continue
# KEIN Downgrade: Empfehlung nur, wenn sie mind. so „groß"/fähig ist wie das
# installierte Modell der Rolle (verhindert z.B. fast 35B-A3B → 4B).
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)
cur = inst_params.get(role, 0.0)
if cur and rec_params and rec_params < cur * 0.95:
continue
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
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
continue # schon installiert
out.append({"role": role, "title": c["title"], "repo": rec})
return out