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 import psutil
from services import budget, llamaswap from services import budget, catalog, llamaswap
from services.fit import evaluate_fit from services.fit import evaluate_fit
@@ -17,53 +17,86 @@ def _ram_gb() -> float:
return psutil.virtual_memory().total / (1024 ** 3) return psutil.virtual_memory().total / (1024 ** 3)
def _suitability(role: str, caps: dict, params: float, name: str) -> float: def _capability_suit(role: str, caps: dict, name: str) -> float:
"""0..1 — wie gut passt die Capability eines Modells zur Rolle. Harte Anforderungen """0..1 — Capability-Eignung (HARTE Gates). 0 = grundsätzlich falsch für die Rolle.
(Vision braucht Vision) geben 0 bei Nichterfüllung; weiche Präferenzen skalieren.""" Größe/Tempo bewertet getrennt _pref(), damit z.B. 'fast' nicht das größte Modell zieht."""
role = (role or "").lower() role = (role or "").lower()
low = (name or "").lower() low = (name or "").lower()
vision = bool(caps.get("vision")) vision = bool(caps.get("vision"))
coder = bool(caps.get("coder")) coder = bool(caps.get("coder"))
tools = caps.get("tools") != "no" tools = caps.get("tools") != "no"
moe = bool(caps.get("moe"))
if role == "vision": 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": 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": 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: if "hermes" in low:
return 1.0 return 1.0
return 0.85 if tools else 0.15 return 0.6 if tools else 0.1
if role == "fast": if role == "fast":
# schnelles Alltags-Hirn: klein/MoE bevorzugt (niedrige aktive Params = Tempo). # Alltags-Hirn braucht zuverlässige Tools; Größe/Tempo macht _pref.
return 1.0 if (moe or params <= 40) else 0.5 return 1.0 if tools else 0.6
if role == "heavy": if role == "heavy":
# schweres Reasoning: Wissen = Gesamt-Params (groß bevorzugt). return 1.0
return min(params / 70.0, 1.0)
if role == "scout": if role == "scout":
# Multimodal-Allrounder: Vision ein Plus, sonst solide Basis.
return 0.9 if vision else 0.7 return 0.9 if vision else 0.7
return 0.5 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: if incomplete:
return "Download unvollständig" return "Download unvollständig"
if role == "vision" and not caps.get("vision"): if role == "vision" and not caps.get("vision"):
return "keine Vision-Fähigkeit" 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" return "kein natives Tool-Calling"
if not fits: if not fits:
return "passt nicht ins Budget (OOM)" return "passt nicht ins Budget (OOM)"
bits = [] bits = []
if cat_match:
bits.append("Katalog-Pick ✓")
if role == "vision": if role == "vision":
bits.append("Vision ✓") bits.append("Vision ✓")
if role == "coder" and caps.get("coder"): if role == "coder" and caps.get("coder"):
bits.append("Coder ✓") bits.append("Coder ✓")
if role == "hermes": 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"): if caps.get("moe"):
bits.append("MoE") bits.append("MoE")
bits.append(f"{fit['text']}, ~{fit['tps']:.0f} t/s") 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"]) fit = evaluate_fit(params, quant, ctx, ram, name=m["name"])
incomplete = bool(m.get("incomplete")) incomplete = bool(m.get("incomplete"))
fits = (fit["level"] != "too_tight") and not 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 suitable = suit >= 0.5 and fits
fit_term = {"perfect": 1.0, "marginal": 0.3}.get(fit["level"], -2.0) fit_term = {"perfect": 1.0, "marginal": 0.3}.get(fit["level"], -2.0)
score = (2.0 * suit) + fit_term \ # Eignung dominiert (×2), rollengerechte Größe/Tempo (_pref), Katalog-Anker, dann Fit.
+ min((fit["tps"] or 0) / 80.0, 1.0) * 0.5 \ score = (2.0 * suit) + _pref(role, params, tps) + (0.6 if cat_match else 0.0) + fit_term
+ min(params / 120.0, 1.0) * 0.5
if not fits: if not fits:
score -= 5.0 score -= 5.0
@@ -99,7 +133,7 @@ def recommend_for_role(role: str) -> dict:
"params_b": round(params, 1), "quant": quant, "params_b": round(params, 1), "quant": quant,
"fit": fit, "suitable": suitable, "incomplete": incomplete, "fit": fit, "suitable": suitable, "incomplete": incomplete,
"score": round(score, 3), "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"]) out.sort(key=lambda x: -x["score"])