Fix: Modell-Manager Rollen-Taxonomie vereinheitlicht (5 Rollen) + echter Vocab-Check
- Rollen ueberall = fast/heavy/coder/vision/scout (eine Quelle der Wahrheit): sources.py CATEGORIES (agent/reasoning raus, fast/heavy rein), llamaswap.ROLE_IDS, maintenance ROLE_MAP entfernt (Discover-Rollen == Serving-Rollen), Discover.tsx ROLE_METADATA, ModelBadges.ROLES, ActiveModelsCard (stale reasoning-Farbe raus). Behebt: Discover zeigte "Reasoning"/"agent"; aus Discover installierte Modelle landeten in keinem Cockpit-Slot. - gguf_meta: Vocab-Check jetzt ECHT - sha256 ueber die vollstaendige Token-Liste statt nur Metadaten. Familienunabhaengig (Qwen/Llama/Mistral/...). Verifiziert: Coder + Qwen3-0.6B byte-identisch (kompatibel), Qwen3.6 abweichend (inkompatibel), 0.11s/Scan. - RolesCard SPEC-Badge -> spec_active (Konsistenz mit Cockpit). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ Fälle zu unterscheiden (Qwen2.5 vs Qwen3 vs Qwen3.6 etc.). Die llama.cpp-Prüfu
|
|||||||
beim Laden bleibt der letzte Schiedsrichter.
|
beim Laden bleibt der letzte Schiedsrichter.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
import struct
|
import struct
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
|
|
||||||
@@ -79,16 +80,26 @@ def _read_fingerprint(path: str) -> dict | None:
|
|||||||
r.u32() # version
|
r.u32() # version
|
||||||
r.u64() # tensor_count
|
r.u64() # tensor_count
|
||||||
kv_count = r.u64()
|
kv_count = r.u64()
|
||||||
fp: dict = {"model": None, "pre": None, "arch": None, "n_vocab": None}
|
fp: dict = {"model": None, "pre": None, "arch": None, "n_vocab": None,
|
||||||
|
"tokens_sha": None}
|
||||||
for _ in range(kv_count):
|
for _ in range(kv_count):
|
||||||
key = r.gstr()
|
key = r.gstr()
|
||||||
vtype = r.u32()
|
vtype = r.u32()
|
||||||
if key == "tokenizer.ggml.tokens" and vtype == _T_ARRAY:
|
if key == "tokenizer.ggml.tokens" and vtype == _T_ARRAY:
|
||||||
etype = r.u32()
|
etype = r.u32()
|
||||||
fp["n_vocab"] = r.u64()
|
count = r.u64()
|
||||||
# Wir haben alles (model/pre kommen vor tokens) → abbrechen.
|
fp["n_vocab"] = count
|
||||||
if etype != _T_STRING:
|
if etype != _T_STRING:
|
||||||
return None
|
return None
|
||||||
|
# ECHTE Vocab-Identität: sha256 über die tatsächliche Token-Liste
|
||||||
|
# (familienunabhängig — funktioniert für Qwen, Llama, Mistral, …).
|
||||||
|
h = hashlib.sha256()
|
||||||
|
h.update(count.to_bytes(8, "little"))
|
||||||
|
for _ in range(count):
|
||||||
|
n = r.u64()
|
||||||
|
h.update(r.read(n))
|
||||||
|
fp["tokens_sha"] = h.hexdigest()
|
||||||
|
# model/pre kommen vor tokens → wir haben alles. Abbrechen.
|
||||||
break
|
break
|
||||||
if key in _WANT_STRINGS and vtype == _T_STRING:
|
if key in _WANT_STRINGS and vtype == _T_STRING:
|
||||||
val = r.gstr()
|
val = r.gstr()
|
||||||
@@ -112,12 +123,12 @@ def _cached(path: str, mtime: float, size: int) -> tuple | None:
|
|||||||
fp = _read_fingerprint(path)
|
fp = _read_fingerprint(path)
|
||||||
if fp is None:
|
if fp is None:
|
||||||
return None
|
return None
|
||||||
return (fp.get("model"), fp.get("pre"), fp.get("n_vocab"), fp.get("arch"))
|
return (fp.get("model"), fp.get("pre"), fp.get("n_vocab"), fp.get("arch"), fp.get("tokens_sha"))
|
||||||
|
|
||||||
|
|
||||||
def fingerprint(path: str) -> dict | None:
|
def fingerprint(path: str) -> dict | None:
|
||||||
"""Tokenizer-Fingerprint eines GGUF (gecacht nach Pfad+mtime+size).
|
"""Tokenizer-Fingerprint eines GGUF (gecacht nach Pfad+mtime+size).
|
||||||
Returns dict(model, pre, n_vocab, arch) oder None wenn nicht lesbar."""
|
Returns dict(model, pre, n_vocab, arch, tokens_sha) oder None wenn nicht lesbar."""
|
||||||
import os
|
import os
|
||||||
try:
|
try:
|
||||||
st = os.stat(path)
|
st = os.stat(path)
|
||||||
@@ -126,16 +137,18 @@ def fingerprint(path: str) -> dict | None:
|
|||||||
t = _cached(path, st.st_mtime, st.st_size)
|
t = _cached(path, st.st_mtime, st.st_size)
|
||||||
if t is None:
|
if t is None:
|
||||||
return None
|
return None
|
||||||
return {"model": t[0], "pre": t[1], "n_vocab": t[2], "arch": t[3]}
|
return {"model": t[0], "pre": t[1], "n_vocab": t[2], "arch": t[3], "tokens_sha": t[4]}
|
||||||
|
|
||||||
|
|
||||||
def vocab_key(path: str) -> tuple | None:
|
def vocab_key(path: str) -> tuple | None:
|
||||||
"""Vergleichsschlüssel für Vocab-Kompatibilität: (model, pre, n_vocab).
|
"""ECHTER Vergleichsschlüssel für Vocab-Kompatibilität: (model, pre, n_vocab, sha256
|
||||||
Genau diese Identität verlangt llama.cpp für Speculative Decoding."""
|
der vollständigen Token-Liste). Vergleicht den TATSÄCHLICHEN Vokabular-Inhalt, nicht
|
||||||
|
nur Metadaten — familienunabhängig (Qwen, Llama, Mistral, …). Genau diese Identität
|
||||||
|
verlangt llama.cpp für Speculative Decoding."""
|
||||||
fp = fingerprint(path)
|
fp = fingerprint(path)
|
||||||
if not fp or fp["n_vocab"] is None:
|
if not fp or fp["n_vocab"] is None or not fp.get("tokens_sha"):
|
||||||
return None
|
return None
|
||||||
return (fp["model"], fp["pre"], fp["n_vocab"])
|
return (fp["model"], fp["pre"], fp["n_vocab"], fp["tokens_sha"])
|
||||||
|
|
||||||
|
|
||||||
def compatible(target_path: str, draft_path: str) -> bool | None:
|
def compatible(target_path: str, draft_path: str) -> bool | None:
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ from config import (
|
|||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Kanonische Rollen (vereinheitlicht ggü. v1: kein manager/reviewer mehr).
|
# Kanonische Serving-Rollen — EINE Quelle der Wahrheit (identisch zu sources.ROLE_IDS,
|
||||||
ROLE_IDS = {"vision", "coder", "agent", "scout"}
|
# maintenance, frontend ModelBadges.ROLES). Kein agent/reasoning mehr.
|
||||||
|
ROLE_IDS = {"fast", "heavy", "coder", "vision", "scout"}
|
||||||
|
|
||||||
_CTX_RE = re.compile(r"-(?:c|-ctx-size)\s+(\d+)")
|
_CTX_RE = re.compile(r"-(?:c|-ctx-size)\s+(\d+)")
|
||||||
_PATH_RE = re.compile(r"-(?:m|-model)\s+([^\s]+)")
|
_PATH_RE = re.compile(r"-(?:m|-model)\s+([^\s]+)")
|
||||||
|
|||||||
@@ -169,18 +169,10 @@ def model_upgrades() -> list[dict]:
|
|||||||
for s in (llamaswap.read_config().get("models") or {}).values())
|
for s in (llamaswap.read_config().get("models") or {}).values())
|
||||||
out = []
|
out = []
|
||||||
|
|
||||||
ROLE_MAP = {
|
# Discover-Rollen == Serving-Rollen (fast/heavy/coder/vision/scout) → kein Mapping mehr.
|
||||||
"agent": "fast",
|
|
||||||
"scout": "fast",
|
|
||||||
"coder": "coder",
|
|
||||||
"vision": "vision"
|
|
||||||
}
|
|
||||||
|
|
||||||
for c in disc.get("categories", []):
|
for c in disc.get("categories", []):
|
||||||
disc_role = c["role"]
|
role = c["role"]
|
||||||
mapped_role = ROLE_MAP.get(disc_role, disc_role)
|
if role not in active_roles: # nur Rollen, die bereits ein Modell haben
|
||||||
|
|
||||||
if mapped_role not in active_roles:
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
rec = c.get("recommended")
|
rec = c.get("recommended")
|
||||||
@@ -190,7 +182,7 @@ def model_upgrades() -> list[dict]:
|
|||||||
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):
|
||||||
continue
|
continue
|
||||||
out.append({"role": mapped_role, "title": c["title"], "repo": rec})
|
out.append({"role": role, "title": c["title"], "repo": rec})
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,31 @@
|
|||||||
"""
|
"""
|
||||||
Vertrauenswürdige Quellen + Kategorien für die automatische Modell-Entdeckung.
|
Vertrauenswürdige Quellen + Kategorien für die automatische Modell-Entdeckung.
|
||||||
Portiert aus Mission Control v1 (sources.py). Rollen sind die EINE Quelle der
|
Rollen = die EINE Quelle der Wahrheit, identisch zu den llama-swap-Serving-Rollen
|
||||||
Wahrheit (vereinheitlicht): vision · coder · agent · scout.
|
und der UI: fast · heavy · coder · vision · scout.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# HF-Orgs, die zuverlässig aktuelle, hochwertige GGUF-Quants veröffentlichen.
|
# HF-Orgs, die zuverlässig aktuelle, hochwertige GGUF-Quants veröffentlichen.
|
||||||
TRUSTED_AUTHORS = ["unsloth", "bartowski", "ggml-org", "lmstudio-community"]
|
TRUSTED_AUTHORS = ["unsloth", "bartowski", "ggml-org", "lmstudio-community"]
|
||||||
|
|
||||||
|
# Kanonische Rollen — eine Quelle der Wahrheit (deckt sich mit llamaswap.ROLE_IDS,
|
||||||
|
# maintenance.ROLE_MAP, frontend ModelBadges.ROLES + Discover.ROLE_METADATA).
|
||||||
|
ROLE_IDS = ["fast", "heavy", "coder", "vision", "scout"]
|
||||||
|
|
||||||
# Kategorien (Reihenfolge = Anzeige + Zuordnungs-Priorität). Ein Modell wird der
|
# Kategorien (Reihenfolge = Anzeige + Zuordnungs-Priorität). Ein Modell wird der
|
||||||
# ERSTEN Kategorie zugeordnet, deren Stichwort im Repo-Namen vorkommt; sonst „scout".
|
# ERSTEN Kategorie zugeordnet, deren Stichwort im Repo-Namen vorkommt; sonst „scout".
|
||||||
# Die `role` ist zugleich der Alias-Vorschlag und gehört zu ROLE_IDS.
|
# Die `role` ist zugleich der Alias-Vorschlag und EINE der 5 kanonischen Rollen.
|
||||||
CATEGORIES = [
|
CATEGORIES = [
|
||||||
{"role": "vision", "title": "Bilder verstehen", "icon": "eye",
|
{"role": "vision", "title": "Bilder verstehen", "icon": "eye",
|
||||||
"kw": ["-vl-", "-vl", "vision", "llava", "multimodal", "-mm-", "pixtral"]},
|
"kw": ["-vl-", "-vl", "vision", "llava", "multimodal", "-mm-", "pixtral"]},
|
||||||
{"role": "coder", "title": "Coden & Programmieren", "icon": "code",
|
{"role": "coder", "title": "Coden & Programmieren", "icon": "code",
|
||||||
"kw": ["coder", "-code-", "code-", "codestral", "starcoder"]},
|
"kw": ["coder", "-code-", "code-", "codestral", "starcoder"]},
|
||||||
{"role": "agent", "title": "Agenten & Tool-Use", "icon": "layers",
|
{"role": "heavy", "title": "Schweres Reasoning", "icon": "brain",
|
||||||
"kw": ["hermes", "-tool", "command-r", "watt", "-fc-", "function"]},
|
"kw": ["reasoning", "-think", "thinking", "gpt-oss", "deepseek-r", "-r1", "qwq",
|
||||||
{"role": "scout", "title": "Allrounder & Chat", "icon": "compass",
|
"-70b", "-72b", "-120b", "-123b", "-235b", "-405b", "-a10b", "-a22b"]},
|
||||||
"kw": []}, # Fallback: instruct/chat-Modelle
|
{"role": "fast", "title": "Schnelles Alltags-Hirn", "icon": "zap",
|
||||||
|
"kw": ["-a3b", "-a1", "-a2", "-30b", "-32b", "-14b", "-8b", "-7b", "-4b", "-moe"]},
|
||||||
|
{"role": "scout", "title": "Multimodal-Allrounder", "icon": "compass",
|
||||||
|
"kw": []}, # Fallback: instruct/chat-Modelle, die in keine Spezialrolle fallen
|
||||||
]
|
]
|
||||||
|
|
||||||
# Repo-Namensteile, die bei der Entdeckung übersprungen werden (Roh-/Spezialformate).
|
# Repo-Namensteile, die bei der Entdeckung übersprungen werden (Roh-/Spezialformate).
|
||||||
|
|||||||
+11
-11
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
<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-D9tRIUA4.js"></script>
|
<script type="module" crossorigin src="/assets/index-CrPS-3Fn.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-CjZVMvhL.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-CjZVMvhL.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ const ROLE_TONE: Record<string, string> = {
|
|||||||
fast: "bg-cyan-500/15 text-cyan-400 border-cyan-500/25",
|
fast: "bg-cyan-500/15 text-cyan-400 border-cyan-500/25",
|
||||||
heavy: "bg-amber-500/15 text-amber-400 border-amber-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",
|
coder: "bg-violet-500/15 text-violet-400 border-violet-500/25",
|
||||||
reasoning: "bg-emerald-500/15 text-emerald-400 border-emerald-500/25",
|
|
||||||
vision: "bg-pink-500/15 text-pink-400 border-pink-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",
|
scout: "bg-teal-500/15 text-teal-400 border-teal-500/25",
|
||||||
hermes: "bg-indigo-500/15 text-indigo-400 border-indigo-500/25",
|
hermes: "bg-indigo-500/15 text-indigo-400 border-indigo-500/25",
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export function RolesCard() {
|
|||||||
PC
|
PC
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{m.spec_draft_model && (
|
{m.spec_active && (
|
||||||
<span className="text-[7px] leading-none font-mono font-bold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 px-1 py-0.5 rounded" title={`Speculative Decoding aktiv (Draft: ${m.spec_draft_model})`}>
|
<span className="text-[7px] leading-none font-mono font-bold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 px-1 py-0.5 rounded" title={`Speculative Decoding aktiv (Draft: ${m.spec_draft_model})`}>
|
||||||
SPEC
|
SPEC
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { type Fit } from "@/lib/api"
|
import { type Fit } from "@/lib/api"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
export const ROLES = ["fast", "heavy", "coder", "agent", "vision", "scout"]
|
// Die 5 kanonischen Serving-Rollen (identisch zu Backend sources.py/llamaswap.ROLE_IDS).
|
||||||
|
export const ROLES = ["fast", "heavy", "coder", "vision", "scout"]
|
||||||
|
|
||||||
export function FitBadge({ fit }: { fit: Fit }) {
|
export function FitBadge({ fit }: { fit: Fit }) {
|
||||||
const tone = {
|
const tone = {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { Download, Search, Star, Layers, Check, Bot, Eye, Code, Brain, Compass, ChevronDown, ChevronUp } from "lucide-react"
|
import { Download, Search, Star, Layers, Check, Zap, Eye, Code, Brain, Compass, ChevronDown, ChevronUp } from "lucide-react"
|
||||||
import { api } from "@/lib/api"
|
import { api } from "@/lib/api"
|
||||||
import { useModels, useUpdates, useDiscover } from "@/lib/queries"
|
import { useModels, useUpdates, useDiscover } from "@/lib/queries"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
@@ -7,30 +7,31 @@ import { fmtBytes } from "@/lib/format"
|
|||||||
import { FitBadge } from "@/components/models/ModelBadges"
|
import { FitBadge } from "@/components/models/ModelBadges"
|
||||||
import { AddModel } from "./AddModel"
|
import { AddModel } from "./AddModel"
|
||||||
|
|
||||||
|
// Die 5 kanonischen Rollen (identisch zu sources.py / ModelBadges.ROLES).
|
||||||
const ROLE_METADATA: Record<string, { title: string; desc: string; icon: any }> = {
|
const ROLE_METADATA: Record<string, { title: string; desc: string; icon: any }> = {
|
||||||
vision: {
|
fast: {
|
||||||
title: "Bilder & Vision",
|
title: "Schnelles Alltags-Hirn",
|
||||||
desc: "Verarbeitet Bilder, Screenshots und visuelle Diagramme in multimodalen Chats.",
|
desc: "Schnelle MoE-Antworten für Chat, Zusammenfassungen und alltägliche Aufgaben.",
|
||||||
icon: Eye
|
icon: Zap
|
||||||
|
},
|
||||||
|
heavy: {
|
||||||
|
title: "Schweres Reasoning",
|
||||||
|
desc: "Große Modelle für komplexe Logik, Mathematik und tiefgründiges Planen.",
|
||||||
|
icon: Brain
|
||||||
},
|
},
|
||||||
coder: {
|
coder: {
|
||||||
title: "Coden & Entwicklung",
|
title: "Coden & Entwicklung",
|
||||||
desc: "Autovervollständigung, Refactoring und Codegenerierung direkt in deiner IDE.",
|
desc: "Autovervollständigung, Refactoring und Codegenerierung direkt in deiner IDE.",
|
||||||
icon: Code
|
icon: Code
|
||||||
},
|
},
|
||||||
reasoning: {
|
vision: {
|
||||||
title: "Logik & Nachdenken",
|
title: "Bilder & Vision",
|
||||||
desc: "Komplexe logische Gedankengänge, Mathematik und tiefgründiges Planen (Reasoning).",
|
desc: "Verarbeitet Bilder, Screenshots und visuelle Diagramme in multimodalen Chats.",
|
||||||
icon: Brain
|
icon: Eye
|
||||||
},
|
|
||||||
agent: {
|
|
||||||
title: "Autonomer Agent (Hermes)",
|
|
||||||
desc: "Führt selbstständig Terminalbefehle aus und interagiert mit deinem Homelab.",
|
|
||||||
icon: Bot
|
|
||||||
},
|
},
|
||||||
scout: {
|
scout: {
|
||||||
title: "Allrounder & Chat",
|
title: "Multimodal-Allrounder",
|
||||||
desc: "Schnelle Antworten, Zusammenfassungen, Übersetzungen und alltägliche Fragen.",
|
desc: "Multimodaler Allrounder für schnelle Antworten, Übersetzungen und Alltag.",
|
||||||
icon: Compass
|
icon: Compass
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user