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]: def rank_runnable(models: list[dict]) -> list[dict]:
"""EINE Quelle der Wahrheit fürs Ranking lauffähiger Modelle: """EINE Quelle der Wahrheit fürs Ranking lauffähiger Modelle für DIESE Hardware:
bestes Fit-Level zuerst (perfect < marginal), bei Gleichstand meistgeladen. 1) nur was komfortabel passt (perfect vor marginal, too_tight fliegt raus),
Zu große Modelle (too_tight) fliegen 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( return sorted(
[m for m in models if m["fit"]["level"] != "too_tight"], [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: for c in CATEGORIES:
items = by_cat[c["role"]] items = by_cat[c["role"]]
ranked = rank_runnable(items) ranked = rank_runnable(items)
# Top 4 je Kategorie (für die Anzeige) — gerankt, dann nach Downloads aufgefüllt. # Top 4 je Kategorie = die best-gerankten (fähigstes-was-passt zuerst).
items.sort(key=lambda x: (x["fit"]["level"] != "too_tight", x["downloads"]), reverse=True) top = ranked[:4]
top = items[:4]
if top: if top:
cats.append({ cats.append({
"role": c["role"], "title": c["title"], "icon": c["icon"], "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() installed = llamaswap.list_models()
active_roles = {m["role"] for m in installed if m.get("role")} 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() cmds = " ".join(str(s.get("cmd", "")).lower()
for s in (llamaswap.read_config().get("models") or {}).values()) for s in (llamaswap.read_config().get("models") or {}).values())
@@ -178,6 +184,14 @@ def model_upgrades() -> list[dict]:
rec = c.get("recommended") rec = c.get("recommended")
if not rec: if not rec:
continue 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() base = rec.split("/")[-1].lower()
stem = base[:-5] if base.endswith("-gguf") else base stem = base[:-5] if base.endswith("-gguf") else base
if base in cmds or (stem and stem in cmds): 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="manifest" href="/manifest.webmanifest" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>Mission Control 2.0</title> <title>Mission Control 2.0</title>
<script type="module" crossorigin src="/assets/index-CrPS-3Fn.js"></script> <script type="module" crossorigin src="/assets/index-oijhNb41.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CjZVMvhL.css"> <link rel="stylesheet" crossorigin href="/assets/index-Cx3ZKkHL.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
@@ -3,15 +3,7 @@ import { Activity, Zap, Moon } from "lucide-react"
import { useModels, useTokenStats } from "@/lib/queries" import { useModels, useTokenStats } from "@/lib/queries"
import { fmtSize } from "@/lib/format" import { fmtSize } from "@/lib/format"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { roleTone } from "@/components/models/ModelBadges"
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 function ActiveModelsCard() { export function ActiveModelsCard() {
const { data } = useModels(2_000) const { data } = useModels(2_000)
@@ -65,7 +57,7 @@ export function ActiveModelsCard() {
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
{m.role && ( {m.role && (
<span className={cn("text-[8px] font-semibold uppercase px-1.5 py-0.5 rounded font-mono border tracking-wider shrink-0", <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} {m.role}
</span> </span>
)} )}
@@ -1,8 +1,7 @@
import { Layers } from "lucide-react" import { Layers } from "lucide-react"
import { useModels } from "@/lib/queries" import { useModels } from "@/lib/queries"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { ROLES, roleTone } from "@/components/models/ModelBadges"
const ROLES = ["fast", "heavy", "coder", "vision", "scout"]
export function RolesCard() { export function RolesCard() {
const { data } = useModels(3_000) const { data } = useModels(3_000)
@@ -37,12 +36,7 @@ export function RolesCard() {
<div className="min-w-0 flex-1 mr-2"> <div className="min-w-0 flex-1 mr-2">
<div className="flex items-center gap-2 min-w-0"> <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", <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" : roleTone(role))}>
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"
)}>
{role} {role}
</span> </span>
@@ -4,6 +4,20 @@ import { cn } from "@/lib/utils"
// Die 5 kanonischen Serving-Rollen (identisch zu Backend sources.py/llamaswap.ROLE_IDS). // Die 5 kanonischen Serving-Rollen (identisch zu Backend sources.py/llamaswap.ROLE_IDS).
export const ROLES = ["fast", "heavy", "coder", "vision", "scout"] 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 }) { export function FitBadge({ fit }: { fit: Fit }) {
const tone = { const tone = {
perfect: "bg-emerald-500/15 text-emerald-400 border border-emerald-500/20", 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 { CapsChips } from "@/components/CapsChips"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { fmtSize, fmtCtx } from "@/lib/format" 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" import { SpecDraftModal } from "@/components/models/SpecDraftModal"
export function Cockpit() { export function Cockpit() {
@@ -611,12 +611,7 @@ export function Cockpit() {
> >
<div className="flex items-center justify-between"> <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", <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" : roleTone(role))}>{role}</span>
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>
{isWarm && <span className="h-1.5 w-1.5 rounded-full bg-emerald-400 animate-pulse" />} {isWarm && <span className="h-1.5 w-1.5 rounded-full bg-emerald-400 animate-pulse" />}
</div> </div>