Fix: Rollen-Scorer prinzipien-konform (fast!=groesstes, Hirn=Hermes/moderat)

Erster Wurf empfahl fast->122B und hermes->122B: ein globaler Wissen=Groesse-Term
ueberstimmte die Rollen-Absicht. Jetzt getrennt:
- _capability_suit: harte Gates (Vision braucht Vision; Coder-Modell Pflicht; Hirn
  bevorzugt Hermes-Familie/Tools; dedizierte VL > Omni).
- _pref: rollengerechte Groessen-/Tempo-Praeferenz (fast=Tempo&klein, heavy=gross,
  hermes=7-24B, vision=klein, scout=moderat).
- _catalog_role_match: Cookbook-Eintrag fuer die Rolle = starker Anker-Bonus.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-27 14:40:19 +02:00
parent 763e634dfd
commit c4708d7d5d
+56 -22
View File
@@ -9,7 +9,7 @@ und fit.evaluate_fit — dieselbe Mathematik wie Install-Automatik und Auto-ctx-
import psutil
from services import budget, llamaswap
from services import budget, catalog, llamaswap
from services.fit import evaluate_fit
@@ -17,53 +17,86 @@ def _ram_gb() -> float:
return psutil.virtual_memory().total / (1024 ** 3)
def _suitability(role: str, caps: dict, params: float, name: str) -> float:
"""0..1 — wie gut passt die Capability eines Modells zur Rolle. Harte Anforderungen
(Vision braucht Vision) geben 0 bei Nichterfüllung; weiche Präferenzen skalieren."""
def _capability_suit(role: str, caps: dict, name: str) -> float:
"""0..1 — Capability-Eignung (HARTE Gates). 0 = grundsätzlich falsch für die Rolle.
Größe/Tempo bewertet getrennt _pref(), damit z.B. 'fast' nicht das größte Modell zieht."""
role = (role or "").lower()
low = (name or "").lower()
vision = bool(caps.get("vision"))
coder = bool(caps.get("coder"))
tools = caps.get("tools") != "no"
moe = bool(caps.get("moe"))
if role == "vision":
return 1.0 if vision else 0.0 # harte Anforderung
if not vision:
return 0.0 # harte Anforderung
return 1.0 if ("vl" in low or "llava" in low or "pixtral" in low) else 0.7 # dediziert > omni
if role == "coder":
return 1.0 if coder else 0.45 # Coder bevorzugt, andere notfalls
return 1.0 if coder else 0.4 # Coder-Modell Pflicht für Empfehlung
if role == "hermes":
# Agent-Hirn: natives Tool-Calling Pflicht; Hermes-Familie am robustesten.
# Agent-Hirn: Hermes-Familie am robustesten; sonst natives Tool-Calling Pflicht.
if "hermes" in low:
return 1.0
return 0.85 if tools else 0.15
return 0.6 if tools else 0.1
if role == "fast":
# schnelles Alltags-Hirn: klein/MoE bevorzugt (niedrige aktive Params = Tempo).
return 1.0 if (moe or params <= 40) else 0.5
# Alltags-Hirn braucht zuverlässige Tools; Größe/Tempo macht _pref.
return 1.0 if tools else 0.6
if role == "heavy":
# schweres Reasoning: Wissen = Gesamt-Params (groß bevorzugt).
return min(params / 70.0, 1.0)
return 1.0
if role == "scout":
# Multimodal-Allrounder: Vision ein Plus, sonst solide Basis.
return 0.9 if vision else 0.7
return 0.5
def _reason(role: str, caps: dict, fit: dict, suit: float, fits: bool, incomplete: bool) -> str:
def _pref(role: str, params: float, tps: float) -> float:
"""0..1 — rollengerechte GRÖSSEN-/TEMPO-Präferenz. 'fast' belohnt Tempo & Kleinheit,
'heavy' Größe (Wissen), 'hermes' moderate Größe (muss warm + ko-resident bleiben)."""
role = (role or "").lower()
if role == "fast":
speed = min(tps / 25.0, 1.0)
size_ok = 1.0 if params <= 50 else 50.0 / params
return speed * size_ok
if role == "heavy":
return min(params / 120.0, 1.0)
if role == "hermes":
return 1.0 if params <= 24 else max(0.15, 24.0 / params) # 724B ideal als Hirn
if role == "vision":
return 1.0 if params <= 12 else 0.7 # klein/günstig bevorzugt
if role == "coder":
return 0.5 + 0.5 * min(params / 80.0, 1.0)
if role == "scout":
return 1.0 if params <= 40 else 0.5
return 0.5
def _catalog_role_match(role: str, name: str) -> bool:
"""Ist dieses Modell im kuratierten Katalog (Cookbook) genau für DIESE Rolle gelistet?
Dann ist es der prinzipien-konforme Pick → starker Bonus."""
meta = catalog.meta_for_name(name)
return bool(meta and (meta.get("role") or "").lower() == (role or "").lower())
def _reason(role: str, caps: dict, name: str, fit: dict, fits: bool,
incomplete: bool, cat_match: bool) -> str:
if incomplete:
return "Download unvollständig"
if role == "vision" and not caps.get("vision"):
return "keine Vision-Fähigkeit"
if role == "hermes" and caps.get("tools") == "no":
if role == "coder" and not caps.get("coder"):
return "kein Coder-Modell"
if role == "hermes" and "hermes" not in (name or "").lower() and caps.get("tools") == "no":
return "kein natives Tool-Calling"
if not fits:
return "passt nicht ins Budget (OOM)"
bits = []
if cat_match:
bits.append("Katalog-Pick ✓")
if role == "vision":
bits.append("Vision ✓")
if role == "coder" and caps.get("coder"):
bits.append("Coder ✓")
if role == "hermes":
bits.append("Tools ✓" if caps.get("tools") != "no" else "ohne Tools")
bits.append("Hermes" if "hermes" in (name or "").lower()
else ("Tools ✓" if caps.get("tools") != "no" else "ohne Tools"))
if caps.get("moe"):
bits.append("MoE")
bits.append(f"{fit['text']}, ~{fit['tps']:.0f} t/s")
@@ -84,13 +117,14 @@ def recommend_for_role(role: str) -> dict:
fit = evaluate_fit(params, quant, ctx, ram, name=m["name"])
incomplete = bool(m.get("incomplete"))
fits = (fit["level"] != "too_tight") and not incomplete
suit = _suitability(role, caps, params, m["name"])
tps = fit["tps"] or 0
suit = _capability_suit(role, caps, m["name"])
cat_match = _catalog_role_match(role, m["name"])
suitable = suit >= 0.5 and fits
fit_term = {"perfect": 1.0, "marginal": 0.3}.get(fit["level"], -2.0)
score = (2.0 * suit) + fit_term \
+ min((fit["tps"] or 0) / 80.0, 1.0) * 0.5 \
+ min(params / 120.0, 1.0) * 0.5
# Eignung dominiert (×2), rollengerechte Größe/Tempo (_pref), Katalog-Anker, dann Fit.
score = (2.0 * suit) + _pref(role, params, tps) + (0.6 if cat_match else 0.0) + fit_term
if not fits:
score -= 5.0
@@ -99,7 +133,7 @@ def recommend_for_role(role: str) -> dict:
"params_b": round(params, 1), "quant": quant,
"fit": fit, "suitable": suitable, "incomplete": incomplete,
"score": round(score, 3),
"reason": _reason(role, caps, fit, suit, fits, incomplete),
"reason": _reason(role, caps, m["name"], fit, fits, incomplete, cat_match),
})
out.sort(key=lambda x: -x["score"])