feat(v9): Phase 11 — selbst-aktualisierende Setups (Rollen-Slots × discover)

Setups sind nicht mehr hartkodiert: RECIPE_TEMPLATES definieren das KONZEPT
als Rollen-Slots, build_dynamic_recipes füllt sie live aus discover (Hardware-
Fit, MoE bevorzugt, dedupe, leere Slots ausgelassen). all_recipes liefert
Auto-Setups + eigene; statische RECIPES nur als Fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-24 18:27:19 +02:00
parent 30bae06c0c
commit 5d8babaede
2 changed files with 91 additions and 4 deletions
+31
View File
@@ -91,6 +91,37 @@ RECIPES = [
},
]
# ---------------------------------------------------------------------------
# Dynamische Setups: KONZEPT bleibt kuratiert (Rollen-Slots), die Modelle werden
# zur Laufzeit aus `discover` (live HF + Hardware-Fit) gefuellt → Setups
# aktualisieren sich von selbst und passen sich der Hardware an.
# slots = Rollen aus sources.CATEGORIES (vision/coder/reasoning/agent/scout).
# ---------------------------------------------------------------------------
RECIPE_TEMPLATES = [
{"id": "auto-elite", "title": "Elite-Lineup (für deine APU)", "icon": "bolt",
"desc": "Das Stärkste, das auf deiner Hardware flott läuft — bevorzugt MoE (viel Wissen, "
"wenige aktive Parameter). Ein Spitzenmodell je Rolle, automatisch aktuell gehalten.",
"slots": ["coder", "reasoning", "agent", "vision", "scout"]},
{"id": "auto-coding", "title": "Coden & Programmieren", "icon": "code",
"desc": "Bestes Coder-Modell für deine Hardware + ein Reasoning-Modell zum Gegenlesen.",
"slots": ["coder", "reasoning"]},
{"id": "auto-chat", "title": "Alltag & Chat", "icon": "compass",
"desc": "Vielseitiger Allrounder + Bildverständnis für den täglichen Gebrauch.",
"slots": ["scout", "vision"]},
{"id": "auto-agent", "title": "Agenten & Tools", "icon": "layers",
"desc": "Tool-fähige Modelle für autonome Aufgaben, ergänzt um einen Coder.",
"slots": ["agent", "coder"]},
]
# Kurzbegruendung je Rolle (ersetzt die handgeschriebenen „why"-Texte bei Auto-Setups).
ROLE_WHY = {
"coder": "Bestes lokales Coder-Modell für deine Hardware.",
"reasoning": "Reasoning-Modell für Mathe, Logik & knifflige Aufgaben.",
"agent": "Tool-fähig — plant, ruft Werkzeuge auf, arbeitet mehrschrittig.",
"vision": "Versteht Screenshots, Diagramme & Fehlerbilder.",
"scout": "Flinker Allrounder für schnelle Fragen & Chat.",
}
# Upgrade-Map: hat der Nutzer ein „altes" Modell, schlagen wir das aktuelle vor.
UPGRADES = [
{"match": ["qwen2.5-coder", "qwen2_5-coder"], "name": "Qwen3-Coder 30B-A3B",
+60 -4
View File
@@ -14,12 +14,12 @@ import psutil
from ruamel.yaml.scalarstring import LiteralScalarString
from auth import auth
from hw_math import evaluate_fit, max_ctx_for, extract_params_b
from hw_math import evaluate_fit, max_ctx_for, extract_params_b, extract_active_params_b
from config import (MODELS_DIR, CMD_TEMPLATE, DEFAULT_TTL, HF_DOWNLOAD_ENV, USER_RECIPES_PATH,
DISCOVER_CACHE_PATH, DISCOVER_TTL, hf_bin)
from llamaswap import read_config, write_config, model_id_from_path, set_role_alias
from jobengine import start_job, JOBS, attach_download_progress
from recipes import RECIPES, UPGRADES
from recipes import RECIPES, UPGRADES, RECIPE_TEMPLATES, ROLE_WHY
from sources import TRUSTED_AUTHORS, CATEGORIES, SKIP_TOKENS
router = APIRouter(prefix="/api/cookbook", dependencies=[Depends(auth)])
@@ -46,9 +46,65 @@ def save_user_recipes(items: list) -> None:
os.replace(tmp, USER_RECIPES_PATH)
def _is_moe(name: str) -> bool:
low = (name or "").lower()
return (extract_active_params_b(name) is not None
or "mixtral" in low or bool(re.search(r"\d+x\d+\.?\d*b", low)))
def _safe_discover(ram_gb: float) -> dict | None:
"""discover-Daten aus Cache (oder live), ohne zu werfen — None wenn nichts da."""
cached = _load_discover()
if cached and (time.time() - cached.get("updated", 0) < DISCOVER_TTL):
return cached
try:
return refresh_discover(ram_gb)
except Exception: # noqa: BLE001
return cached
def build_dynamic_recipes(disc: dict) -> list:
"""Fuellt die kuratierten Rollen-Slot-Templates LIVE aus discover.
Pro Slot das beste passende Modell (perfect>marginal, MoE bevorzugt fuer APU,
dann Beliebtheit); dedupe ueber Slots; leere Slots werden still ausgelassen."""
cats = {c["role"]: c for c in (disc.get("categories") or [])}
out = []
for tpl in RECIPE_TEMPLATES:
used: set[str] = set()
models = []
for role in tpl["slots"]:
cat = cats.get(role)
if not cat:
continue
cands = [m for m in cat["models"]
if m["fit"]["level"] != "too_tight" and m["repo"] not in used]
if not cands:
continue
cands.sort(key=lambda m: (_FIT_ORDER[m["fit"]["level"]],
0 if _is_moe(m["repo"]) else 1,
-int(m.get("downloads") or 0)))
pick = cands[0]
used.add(pick["repo"])
models.append({
"role": role, "name": pick["name"], "repo": pick["repo"],
"params_b": pick["params_b"], "quant": pick.get("quant", "Q4_K_M"),
"tags": pick.get("tags", []), "why": ROLE_WHY.get(role, ""),
})
if models:
out.append({"id": tpl["id"], "title": tpl["title"], "icon": tpl["icon"],
"desc": tpl["desc"], "models": models, "auto": True})
return out
def all_recipes() -> list:
"""Eingebaute + eigene Setups (eigene tragen 'user': True)."""
return list(RECIPES) + load_user_recipes()
"""Auto-komponierte Setups (live aus discover, Hardware-aware) + eigene Setups.
Fallback auf die statischen RECIPES, wenn discover gerade nichts liefert."""
ram_gb = psutil.virtual_memory().total / (1024 ** 3)
disc = _safe_discover(ram_gb)
base = build_dynamic_recipes(disc) if disc else []
if not base:
base = list(RECIPES)
return base + load_user_recipes()
class AnalyzeRequest(BaseModel):
repo_id: str