Fix: "Modelle finden" bevorzugt faehigste passende Modelle (kein Downgrade) + Rollen-Farben zentral

- discover.rank_runnable: rankt jetzt fit-level > params_b(desc) > downloads -> fuer die
  128GB-Box wird das faehigste passende (MoE-)Modell empfohlen statt kleiner Populaer-Modelle.
  Top-4-Anzeige nutzt dasselbe Ranking.
- maintenance.model_upgrades: schlaegt KEIN Downgrade mehr vor (rec.params_b >= installiert*0.95).
  Behebt: fast 35B-A3B -> 4B wurde faelschlich als Upgrade angeboten.
- Frontend: Rollen-Farb-Mapping zentralisiert in ModelBadges (ROLE_TONE/roleTone);
  Cockpit/RolesCard/ActiveModelsCard nutzen es statt eigener Duplikate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-27 02:35:21 +02:00
parent 38f0394166
commit 8bb4e11f31
11 changed files with 444 additions and 430 deletions
+12 -7
View File
@@ -47,12 +47,18 @@ def _fetch_author_models(author: str) -> list:
def rank_runnable(models: list[dict]) -> list[dict]:
"""EINE Quelle der Wahrheit fürs Ranking lauffähiger Modelle:
bestes Fit-Level zuerst (perfect < marginal), bei Gleichstand meistgeladen.
Zu große Modelle (too_tight) fliegen raus."""
"""EINE Quelle der Wahrheit fürs Ranking lauffähiger Modelle für DIESE Hardware:
1) nur was komfortabel passt (perfect vor marginal, too_tight fliegt raus),
2) das FÄHIGSTE zuerst — mehr Parameter = mehr Können (bei MoE bleibt es dank
aktiver-Param-Schätzung schnell),
3) bei Gleichstand das meistgeladene.
So bevorzugt die 128-GB-Box große (MoE-)Modelle statt kleiner Populär-Modelle —
und „Modelle finden" schlägt nie ein Downgrade vor (z.B. 35B-A3B → 4B)."""
return sorted(
[m for m in models if m["fit"]["level"] != "too_tight"],
key=lambda m: (_FIT_ORDER[m["fit"]["level"]], -int(m.get("downloads") or 0)),
key=lambda m: (_FIT_ORDER[m["fit"]["level"]],
-float(m.get("params_b") or 0.0),
-int(m.get("downloads") or 0)),
)
@@ -97,9 +103,8 @@ def refresh_discover(ram_gb: float) -> dict:
for c in CATEGORIES:
items = by_cat[c["role"]]
ranked = rank_runnable(items)
# Top 4 je Kategorie (für die Anzeige) — gerankt, dann nach Downloads aufgefüllt.
items.sort(key=lambda x: (x["fit"]["level"] != "too_tight", x["downloads"]), reverse=True)
top = items[:4]
# Top 4 je Kategorie = die best-gerankten (fähigstes-was-passt zuerst).
top = ranked[:4]
if top:
cats.append({
"role": c["role"], "title": c["title"], "icon": c["icon"],
+14
View File
@@ -164,6 +164,12 @@ def model_upgrades() -> list[dict]:
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))
cmds = " ".join(str(s.get("cmd", "")).lower()
for s in (llamaswap.read_config().get("models") or {}).values())
@@ -178,6 +184,14 @@ def model_upgrades() -> list[dict]:
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
base = rec.split("/")[-1].lower()
stem = base[:-5] if base.endswith("-gguf") else base
if base in cmds or (stem and stem in cmds):
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -7,8 +7,8 @@
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>Mission Control 2.0</title>
<script type="module" crossorigin src="/assets/index-CrPS-3Fn.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CjZVMvhL.css">
<script type="module" crossorigin src="/assets/index-oijhNb41.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Cx3ZKkHL.css">
</head>
<body>
<div id="root"></div>
@@ -3,15 +3,7 @@ import { Activity, Zap, Moon } from "lucide-react"
import { useModels, useTokenStats } from "@/lib/queries"
import { fmtSize } from "@/lib/format"
import { cn } from "@/lib/utils"
const ROLE_TONE: Record<string, string> = {
fast: "bg-cyan-500/15 text-cyan-400 border-cyan-500/25",
heavy: "bg-amber-500/15 text-amber-400 border-amber-500/25",
coder: "bg-violet-500/15 text-violet-400 border-violet-500/25",
vision: "bg-pink-500/15 text-pink-400 border-pink-500/25",
scout: "bg-teal-500/15 text-teal-400 border-teal-500/25",
hermes: "bg-indigo-500/15 text-indigo-400 border-indigo-500/25",
}
import { roleTone } from "@/components/models/ModelBadges"
export function ActiveModelsCard() {
const { data } = useModels(2_000)
@@ -65,7 +57,7 @@ export function ActiveModelsCard() {
<div className="flex items-center gap-1.5">
{m.role && (
<span className={cn("text-[8px] font-semibold uppercase px-1.5 py-0.5 rounded font-mono border tracking-wider shrink-0",
ROLE_TONE[m.role] || "bg-slate-500/15 text-slate-300 border-slate-500/25")}>
roleTone(m.role))}>
{m.role}
</span>
)}
@@ -1,8 +1,7 @@
import { Layers } from "lucide-react"
import { useModels } from "@/lib/queries"
import { cn } from "@/lib/utils"
const ROLES = ["fast", "heavy", "coder", "vision", "scout"]
import { ROLES, roleTone } from "@/components/models/ModelBadges"
export function RolesCard() {
const { data } = useModels(3_000)
@@ -37,12 +36,7 @@ export function RolesCard() {
<div className="min-w-0 flex-1 mr-2">
<div className="flex items-center gap-2 min-w-0">
<span className={cn("text-[9px] font-semibold uppercase px-1.5 py-0.5 rounded font-mono border tracking-wider shrink-0",
role === "fast" ? "bg-cyan-500/15 text-cyan-400 border-cyan-500/25" :
role === "heavy" ? "bg-amber-500/15 text-amber-400 border-amber-500/25" :
role === "coder" ? "bg-violet-500/15 text-violet-400 border-violet-500/25" :
role === "vision" ? "bg-pink-500/15 text-pink-400 border-pink-500/25" :
"bg-teal-500/15 text-teal-400 border-teal-500/25"
)}>
roleTone(role))}>
{role}
</span>
@@ -4,6 +4,20 @@ import { cn } from "@/lib/utils"
// Die 5 kanonischen Serving-Rollen (identisch zu Backend sources.py/llamaswap.ROLE_IDS).
export const ROLES = ["fast", "heavy", "coder", "vision", "scout"]
// Rolle → Farbton. EINE Quelle der Wahrheit für alle Rollen-Badges (Cockpit, RolesCard,
// ActiveModelsCard, …). `hermes` = Agent-Hirn (taucht als geladenes Modell auf).
export const ROLE_TONE: Record<string, string> = {
fast: "bg-cyan-500/15 text-cyan-400 border-cyan-500/25",
heavy: "bg-amber-500/15 text-amber-400 border-amber-500/25",
coder: "bg-violet-500/15 text-violet-400 border-violet-500/25",
vision: "bg-pink-500/15 text-pink-400 border-pink-500/25",
scout: "bg-teal-500/15 text-teal-400 border-teal-500/25",
hermes: "bg-indigo-500/15 text-indigo-400 border-indigo-500/25",
}
export const roleTone = (role?: string | null) =>
(role && ROLE_TONE[role]) || "bg-slate-500/15 text-slate-300 border-slate-500/25"
export function FitBadge({ fit }: { fit: Fit }) {
const tone = {
perfect: "bg-emerald-500/15 text-emerald-400 border border-emerald-500/20",
+2 -7
View File
@@ -6,7 +6,7 @@ import { useDialog } from "@/lib/useDialog"
import { CapsChips } from "@/components/CapsChips"
import { cn } from "@/lib/utils"
import { fmtSize, fmtCtx } from "@/lib/format"
import { getBrandInfo, ROLES } from "@/components/models/ModelBadges"
import { getBrandInfo, ROLES, roleTone } from "@/components/models/ModelBadges"
import { SpecDraftModal } from "@/components/models/SpecDraftModal"
export function Cockpit() {
@@ -611,12 +611,7 @@ export function Cockpit() {
>
<div className="flex items-center justify-between">
<span className={cn("text-[9px] font-bold uppercase tracking-wider font-mono px-1.5 py-0.5 rounded border",
role === "fast" ? "bg-cyan-500/10 text-cyan-400 border-cyan-500/20" :
role === "heavy" ? "bg-amber-500/10 text-amber-400 border-amber-500/20" :
role === "coder" ? "bg-violet-500/10 text-violet-400 border-violet-500/20" :
role === "vision" ? "bg-pink-500/10 text-pink-400 border-pink-500/20" :
"bg-teal-500/10 text-teal-400 border-teal-500/20"
)}>{role}</span>
roleTone(role))}>{role}</span>
{isWarm && <span className="h-1.5 w-1.5 rounded-full bg-emerald-400 animate-pulse" />}
</div>