""" Modell-Capabilities (Phase 10) — EINE Quelle der Wahrheit fuer Modell-Eigenschaften. Statt Einzel-Label ("Code"/"Text") ein Satz unabhaengiger Tags, die ein Modell gleichzeitig tragen kann (MoE + Tools + Reasoning + Coder + Long-Context …). Quellen, geschichtet (sicher → Fallback): 1) GGUF-Metadaten der lokalen Datei (architecture, expert_count, context_length, parameter_count) — authoritativ & **offline** (kein Netz, passt zu 100%-lokal). 2) cmd-Flags der llama-swap-Config: `--jinja` = Tool-Template aktiv, `--mmproj` = Vision. 3) duenner Namens-/Familien-Fallback, wenn Metadaten fehlen. 4) optional `hf`-Block (HF-API: tags + gguf.chat_template) fuer die Profi-Suche (Schritt 2). Tool-Faehigkeit kommt NICHT aus dem Dateinamen, sondern aus `--jinja` (bestaetigt) / dem Chat-Template (HF) / der Familie (vermutet) → drei Stufen: yes | likely | no. """ import re import struct from hw_math import extract_params_b, extract_active_params_b # GGUF-Skalar-Typen → Bytebreite (fuer Skip) _GGUF_FIXED = {0: 1, 1: 1, 2: 2, 3: 2, 4: 4, 5: 4, 6: 4, 7: 1, 10: 8, 11: 8, 12: 8} def _read_gguf_meta(path: str) -> dict: """Liest nur den GGUF-Metadaten-Header (architecture/context_length/expert_count/ parameter_count). Bricht vor dem riesigen Tokenizer-Array ab → schnell, laedt NICHT das Modell. Robust: gibt {} bei jedem Fehler.""" out: dict = {} try: with open(path, "rb") as f: if f.read(4) != b"GGUF": return {} struct.unpack(" int: return struct.unpack(" int: return struct.unpack(" str: return f.read(ru64()).decode("utf-8", "replace") def rval(t: int): if t == 8: return rstr() if t == 0: return struct.unpack(" dict: """Capability-Tag-Set fuer ein Modell. Alle Quellen optional — nutzt, was da ist.""" low = (name or "").lower() cmdl = (cmd or "").lower() hf = hf or {} meta = _read_gguf_meta(gguf_path) if gguf_path else {} arch = str(meta.get("architecture") or hf.get("architecture") or "").lower() tags = [str(t).lower() for t in (hf.get("tags") or [])] chat_tpl = str(hf.get("chat_template") or "") # --- MoE --- expert_count = int(meta.get("expert_count") or 0) moe = ( expert_count > 1 or any(a in arch for a in _MOE_ARCH) or bool(re.search(r"\d+x\d+\.?\d*b", low)) # 8x7B or bool(re.search(r"a\d+\.?\d*b", low)) # 30B-A3B ) active_b = extract_active_params_b(name) # --- Params / Kontext --- pcount = int(meta.get("parameter_count") or 0) params_b = round(pcount / 1e9, 1) if pcount else extract_params_b(name) ctx = meta.get("context_length") if not ctx: m = re.search(r"-(?:c|-ctx-size)\s+(\d+)", cmdl) ctx = int(m.group(1)) if m else None # --- Tools: yes (bestaetigt) | likely (Familie) | no --- tool_confirmed = "--jinja" in cmdl or "tool_call" in chat_tpl or "" in chat_tpl tool_family = any(fam in low for fam in _TOOL_FAMILIES) or "function-calling" in tags tools = "yes" if tool_confirmed else ("likely" if tool_family else "no") vision = "--mmproj" in cmdl or "vl" in arch or "clip" in arch or any(k in low for k in _VISION_KW) coder = any(k in low for k in _CODE_KW) reasoning = any(k in low for k in _REASON_KW) or "reasoning" in tags embedding = "bert" in arch or any(k in low for k in _EMBED_KW) return { "moe": moe, "active_b": active_b, "tools": tools, # yes | likely | no "vision": vision, "coder": coder, "reasoning": reasoning, "embedding": embedding, "ctx": ctx, "params_b": params_b or None, "arch": arch or None, }