From 5d8babaedebd21650f6336e96f9291bced8e5385 Mon Sep 17 00:00:00 2001 From: Hitonabi Date: Wed, 24 Jun 2026 18:27:19 +0200 Subject: [PATCH] =?UTF-8?q?feat(v9):=20Phase=2011=20=E2=80=94=20selbst-akt?= =?UTF-8?q?ualisierende=20Setups=20(Rollen-Slots=20=C3=97=20discover)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- recipes.py | 31 ++++++++++++++++++++++ routers/cookbook.py | 64 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 91 insertions(+), 4 deletions(-) diff --git a/recipes.py b/recipes.py index 8e9cb3f..8693a4d 100644 --- a/recipes.py +++ b/recipes.py @@ -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", diff --git a/routers/cookbook.py b/routers/cookbook.py index 9baee7f..f4d9853 100644 --- a/routers/cookbook.py +++ b/routers/cookbook.py @@ -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