c4708d7d5d
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>
144 lines
5.8 KiB
Python
144 lines
5.8 KiB
Python
"""
|
||
Rollen-Empfehlung: welches INSTALLIERTE Modell passt am besten auf eine Serving-Rolle?
|
||
Capability-getrieben (Vision/Coder/Tools/MoE aus services.caps) + setup-bewusster Fit
|
||
(services.budget). Speist den 'Empfohlen'-Hinweis + Auto-Pick im Rollen-Zuweisungs-Modal.
|
||
|
||
EINE Quelle der Wahrheit mit der ctx-/Fit-Logik: nutzt budget.setup_aware_ctx_for_model
|
||
und fit.evaluate_fit — dieselbe Mathematik wie Install-Automatik und Auto-ctx-Button.
|
||
"""
|
||
|
||
import psutil
|
||
|
||
from services import budget, catalog, llamaswap
|
||
from services.fit import evaluate_fit
|
||
|
||
|
||
def _ram_gb() -> float:
|
||
return psutil.virtual_memory().total / (1024 ** 3)
|
||
|
||
|
||
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"
|
||
|
||
if role == "vision":
|
||
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.4 # Coder-Modell Pflicht für Empfehlung
|
||
if role == "hermes":
|
||
# Agent-Hirn: Hermes-Familie am robustesten; sonst natives Tool-Calling Pflicht.
|
||
if "hermes" in low:
|
||
return 1.0
|
||
return 0.6 if tools else 0.1
|
||
if role == "fast":
|
||
# Alltags-Hirn braucht zuverlässige Tools; Größe/Tempo macht _pref.
|
||
return 1.0 if tools else 0.6
|
||
if role == "heavy":
|
||
return 1.0
|
||
if role == "scout":
|
||
return 0.9 if vision else 0.7
|
||
return 0.5
|
||
|
||
|
||
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 == "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("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")
|
||
return " · ".join(bits)
|
||
|
||
|
||
def recommend_for_role(role: str) -> dict:
|
||
"""Rankt alle installierten Modelle für eine Rolle. Empfohlen = bester geeigneter,
|
||
passender Eintrag. Liefert pro Modell Fit/Eignung/Begründung fürs UI."""
|
||
role = (role or "").strip().lower()
|
||
ram = _ram_gb()
|
||
out = []
|
||
for m in llamaswap.list_models():
|
||
caps = m.get("capabilities") or {}
|
||
params = budget.params_of_model(m)
|
||
quant = m.get("quant") or "Q4_K_M"
|
||
ctx = budget.setup_aware_ctx_for_model(m)["ctx"]
|
||
fit = evaluate_fit(params, quant, ctx, ram, name=m["name"])
|
||
incomplete = bool(m.get("incomplete"))
|
||
fits = (fit["level"] != "too_tight") and not incomplete
|
||
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)
|
||
# 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
|
||
|
||
out.append({
|
||
"name": m["name"], "current_role": m.get("role"),
|
||
"params_b": round(params, 1), "quant": quant,
|
||
"fit": fit, "suitable": suitable, "incomplete": incomplete,
|
||
"score": round(score, 3),
|
||
"reason": _reason(role, caps, m["name"], fit, fits, incomplete, cat_match),
|
||
})
|
||
|
||
out.sort(key=lambda x: -x["score"])
|
||
rec = next((o["name"] for o in out if o["suitable"]), None)
|
||
for o in out:
|
||
o["recommended"] = (o["name"] == rec)
|
||
return {"role": role, "recommended": rec, "models": out}
|