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:
+56
-22
@@ -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) # 7–24B 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"])
|
||||
|
||||
Reference in New Issue
Block a user