Spaces:
Runtime error
Runtime error
| """ | |
| model_manager.py โ ูุธุงู ุฅุฏุงุฑุฉ ุงููู ุงุฐุฌ | |
| โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| ุงููุฑุงุฑุงุช ุงูู ุจููุฉ ุนูู debugging ุญูููู (ู ุญุฏููุซ ููููู 2026): | |
| โข Qwen3.5-9B (NEW!) โ ุงูุฃูุถู ูู ูุฆุฉ <10Bุ 262K contextุ 201 ูุบุฉุ ุนุฑุจู ุฃููู | |
| โข Qwen3.5 MoE (35B-A3B+) โ ู ุฑููุถ: KV cache reuse bug ู ุนุฑูู | |
| โข Qwen3-8B โ fallback ู ูุซูู ุฅุฐุง ูุดู ุชุญู ูู Qwen3.5-9B | |
| โข Q4_K_M ููุท โ Q8_0 ุนูุฏู garbage output bug ูู llama.cpp | |
| โข ูุง speculative decoding โ net-negative ุนูู CPU Q4 | |
| โข Thinking mode: OFF by default ูู Qwen3.5 (ุนูู ุนูุณ Qwen3) โ ุฃุญุณู ููุธุงู ูุง | |
| """ | |
| from __future__ import annotations | |
| import asyncio, json, logging, os, time | |
| from dataclasses import asdict, dataclass, field | |
| from pathlib import Path | |
| from typing import Any | |
| logger = logging.getLogger("mythical.models") | |
| MODEL_DIR = Path(os.getenv("MODEL_DIR", "/data/models")) | |
| CONFIG_PATH = Path(os.getenv("MODEL_CONFIG", "/data/model_config.json")) | |
| RAM_LIMIT_GB = float(os.getenv("RAM_LIMIT_GB","14.0")) | |
| class ModelDef: | |
| id: str | |
| name: str | |
| repo: str | |
| glob_primary: str | |
| glob_fallback: str | |
| size_gb: float # ุญุฌู ุงูุฃูุฒุงู ุชูุฑูุจุงู | |
| ctx_size: int # ุฃูุตู context ุขู ู | |
| quality: int # ุฌูุฏุฉ ูุณุจูุฉ 0-100 | |
| has_vision: bool | |
| has_thinking: bool | |
| mmproj_repo: str = "" | |
| mmproj_glob: str = "*mmproj*" | |
| extra_flags: str = "" | |
| temp: float = 0.6 | |
| top_k: int = 20 | |
| top_p: float = 0.95 | |
| min_p: float = 0.0 | |
| notes: str = "" | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # CATALOG โ ู ุญุฏููุซ ููููู 2026ุ ู ูุฎุชุจูุฑ ุนูู CPU Basic HF Spaces | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| CATALOG: list[ModelDef] = [ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # TIER 1 โ CHAMPION: Qwen3.5-9B Q4_K_M โ ุงูุฌุฏูุฏ ููููู 2026 | |
| # โข ุฃุญุณู ูู ูุฐุฌ <10B ูู ููููู 2026 ุจุฅุฌู ุงุน ุงููbenchmarks | |
| # โข 262K context window (vs 16K ููQwen3-8B) | |
| # โข 201 ูุบุฉ โ ุนุฑุจู ุฃููู ุจูุชูุฑ (MMLU-ProX + WMT24++ ุนูู 55 ูุบุฉ) | |
| # โข Thinking mode ู ุชุงุญ ููู OFF by default (ู ู ุชุงุฒ ููุธุงู ูุง!) | |
| # โข โ ๏ธ ุชุญุฐูุฑ: Qwen3.5 MoE (35B-A3B+) ุนูุฏู KV cache bug โ ุงูู9B dense ุชู ุงู | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| ModelDef( | |
| id = "qwen35-9b", | |
| name = "Qwen3.5 9B Q4_K_M (unsloth)", | |
| repo = "unsloth/Qwen3.5-9B-GGUF", | |
| glob_primary = "Qwen3.5-9B-Q4_K_M.gguf", | |
| glob_fallback= "Qwen3.5-9B-UD-Q4_K_XL.gguf", | |
| size_gb = 5.6, | |
| ctx_size = 32768, # ูุณุชุฎุฏู 32K ู ู ุฃุตู 262K โ ุขู ู ุนูู 16GB RAM | |
| quality = 95, | |
| has_vision = False, # vision ู ู ููุฉ ู ุณุชูุจูุงู ุจูmmproj ู ููุตู | |
| has_thinking = True, | |
| mmproj_repo = "", | |
| mmproj_glob = "", | |
| # presence_penalty=1.5 ู ูุตู ุจููุง ู ู unsloth ูุชูููู ุงูุชูุฑุงุฑ ูู Qwen3.5 | |
| extra_flags = "--jinja --reasoning-format deepseek --presence-penalty 1.5", | |
| temp=0.7, top_k=20, top_p=0.8, min_p=0.0, | |
| notes="CHAMPION Jun 2026. 262K ctx, 201 langs, thinking OFF by default. ~5.6GB.", | |
| ), | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # TIER 2 โ RELIABLE: Qwen3-8B Q4_K_M โ ุงูุงูุชุฑุงุถู ุงูุณุงุจูุ ู ุณุชูุฑ 100% | |
| # โข ู ูุฎุชุจูุฑ ูู productionุ ู ููุด bugs ู ุนุฑููุฉ | |
| # โข fallback ุขู ู ูู Qwen3.5-9B ุงุชุฃุฎุฑ ุชุญู ููู | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| ModelDef( | |
| id = "qwen3-8b", | |
| name = "Qwen3 8B Q4_K_M (unsloth)", | |
| repo = "unsloth/Qwen3-8B-GGUF", | |
| glob_primary = "Qwen3-8B-Q4_K_M.gguf", | |
| glob_fallback= "Qwen3-8B-Q4_K_S.gguf", | |
| size_gb = 5.1, | |
| ctx_size = 16384, | |
| quality = 88, | |
| has_vision = False, | |
| has_thinking = True, | |
| mmproj_repo = "", | |
| mmproj_glob = "", | |
| extra_flags = "--jinja --reasoning-format deepseek", | |
| temp=0.6, top_k=20, top_p=0.95, min_p=0.0, | |
| notes="Reliable fallback. Production-tested. 5.1GB. Thinking ON by default.", | |
| ), | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # TIER 3 โ FAST: Qwen3-4B Q4_K_M โ ููู ูุงู ุงูุจุณูุทุฉ ุงูุณุฑูุนุฉ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| ModelDef( | |
| id = "qwen3-4b", | |
| name = "Qwen3 4B Q4_K_M (unsloth)", | |
| repo = "unsloth/Qwen3-4B-GGUF", | |
| glob_primary = "Qwen3-4B-Q4_K_M.gguf", | |
| glob_fallback= "Qwen3-4B-Q4_K_S.gguf", | |
| size_gb = 2.5, | |
| ctx_size = 16384, | |
| quality = 76, | |
| has_vision = False, | |
| has_thinking = True, | |
| mmproj_repo = "", | |
| mmproj_glob = "", | |
| extra_flags = "--jinja --reasoning-format deepseek", | |
| temp=0.6, top_k=20, top_p=0.95, min_p=0.0, | |
| notes="Emergency fallback. 2.5GB. Fast on CPU. Simple tasks only.", | |
| ), | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # TIER 4 โ HEAVY: Qwen3-14B โ ููุฃุฌูุฒุฉ ุงููู ุนูุฏูุง >12GB ู ุชุงุญุฉ | |
| # ู ูุงุญุธุฉ: Qwen3.5-9B (quality=95) ุฃุญุณู ู ูู (94) ู ุน ูุต ุญุฌู ู! | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| ModelDef( | |
| id = "qwen3-14b", | |
| name = "Qwen3 14B Q4_K_M (unsloth)", | |
| repo = "unsloth/Qwen3-14B-GGUF", | |
| glob_primary = "Qwen3-14B-Q4_K_M.gguf", | |
| glob_fallback= "*Q4_K_M*", | |
| size_gb = 9.5, | |
| ctx_size = 12288, | |
| quality = 94, | |
| has_vision = False, | |
| has_thinking = True, | |
| mmproj_repo = "", | |
| mmproj_glob = "", | |
| extra_flags = "--jinja --reasoning-format deepseek", | |
| temp=0.6, top_k=20, top_p=0.95, min_p=0.0, | |
| notes="Heavy. 9.5GB. Slower than Qwen3.5-9B but higher param count.", | |
| ), | |
| ] | |
| CATALOG_BY_ID = {m.id: m for m in CATALOG} | |
| class ModelConfig: | |
| active_id: str | |
| active_path: str | |
| mmproj_path: str | |
| pending_id: str = "" | |
| pending_path: str = "" | |
| last_check: float = 0.0 | |
| version: int = 2 | |
| def save(self): | |
| CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True) | |
| CONFIG_PATH.write_text(json.dumps(asdict(self), indent=2)) | |
| def load(cls) -> "ModelConfig": | |
| if not CONFIG_PATH.exists(): | |
| cfg = cls(active_id="qwen35-9b", active_path="", mmproj_path="", | |
| last_check=time.time()) # prevents immediate upgrade check on fresh install | |
| cfg.save() | |
| return cfg | |
| try: | |
| d = json.loads(CONFIG_PATH.read_text()) | |
| # Migration: ุชูุธูู ุญููู ูุฏูู ุฉ | |
| valid = {k for k in cls.__dataclass_fields__} | |
| return cls(**{k: v for k, v in d.items() if k in valid}) | |
| except Exception as e: | |
| logger.warning(f"[models] Config load error ({e}), using default.") | |
| return cls(active_id="qwen35-9b", active_path="", mmproj_path="") | |
| class ModelManager: | |
| UPDATE_INTERVAL_H = float(os.getenv("UPDATE_CHECK_HOURS", "168")) | |
| def __init__(self): | |
| self.cfg = ModelConfig.load() | |
| self._downloading = False | |
| self._bg_task: asyncio.Task | None = None | |
| # โโ Selection โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def select_best(self, ram_gb: float | None = None) -> ModelDef: | |
| limit = ram_gb or RAM_LIMIT_GB | |
| candidates = sorted( | |
| [m for m in CATALOG if m.size_gb <= limit], | |
| key=lambda m: m.quality, | |
| reverse=True, | |
| ) | |
| if not candidates: | |
| return min(CATALOG, key=lambda m: m.size_gb) | |
| return candidates[0] | |
| # โโ Discovery โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def find_local(self, m: ModelDef) -> tuple[str, str]: | |
| """ูุฑุฌุน (model_path, mmproj_path) ู ู ุงูุฏูุณู.""" | |
| def _find(glob: str, min_mb: int) -> str: | |
| for p in sorted(MODEL_DIR.glob(glob), | |
| key=lambda x: x.stat().st_size, reverse=True): | |
| if p.stat().st_size > min_mb * 1024 * 1024: | |
| return str(p) | |
| return "" | |
| model = _find(m.glob_primary, 500) or _find(m.glob_fallback, 500) | |
| mmproj = _find(m.mmproj_glob, 50) if m.has_vision else "" | |
| return model, mmproj | |
| # โโ Download โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| async def download(self, m: ModelDef, force=False) -> tuple[str, str]: | |
| if not force: | |
| existing, existing_mm = self.find_local(m) | |
| if existing: | |
| return existing, existing_mm | |
| self._downloading = True | |
| logger.info(f"[models] Downloading {m.name} from {m.repo}...") | |
| loop = asyncio.get_running_loop() | |
| def _dl(repo, glob): | |
| import subprocess | |
| # Build env โ HF_TOKEN optional for public repos | |
| env = {**os.environ, "HF_HUB_ENABLE_HF_TRANSFER": "1"} | |
| token = os.getenv("HF_TOKEN", "").strip() | |
| if token: | |
| env["HUGGING_FACE_HUB_TOKEN"] = token | |
| env["HF_TOKEN"] = token | |
| for g in [glob, m.glob_fallback if glob == m.glob_primary else ""]: | |
| if not g: | |
| continue | |
| cmd = ["huggingface-cli", "download", repo, | |
| "--include", g, "--local-dir", str(MODEL_DIR)] | |
| if not token: | |
| # Public access โ verbose to see any auth errors | |
| cmd.append("--quiet") | |
| else: | |
| cmd.append("--quiet") | |
| logger.info(f"[models] Running: {' '.join(cmd[:5])}...") | |
| r = subprocess.run(cmd, env=env, capture_output=True, timeout=600) | |
| if r.returncode == 0: | |
| logger.info(f"[models] Download succeeded for glob: {g}") | |
| return "" | |
| else: | |
| err = r.stderr.decode("utf-8", errors="replace")[:300] | |
| logger.warning(f"[models] Download failed (glob={g}): {err}") | |
| return "" | |
| TIMEOUT = float(os.getenv("DOWNLOAD_TIMEOUT", "30.0")) * 20 # max 10 min | |
| try: | |
| # [CRIT-09 fix] timeout prevents infinite hang on HF outage | |
| await asyncio.wait_for( | |
| loop.run_in_executor(None, _dl, m.repo, m.glob_primary), | |
| timeout=TIMEOUT) | |
| if m.has_vision and m.mmproj_repo: | |
| await asyncio.wait_for( | |
| loop.run_in_executor(None, _dl, m.mmproj_repo, m.mmproj_glob), | |
| timeout=TIMEOUT) | |
| model, mmproj = self.find_local(m) | |
| logger.info(f"[models] Done: {model}") | |
| return model, mmproj | |
| except asyncio.TimeoutError: | |
| logger.error(f"[models] Download timed out after {TIMEOUT:.0f}s") | |
| return "", "" | |
| finally: | |
| self._downloading = False | |
| # โโ Apply pending upgrade โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def apply_pending(self) -> bool: | |
| if not self.cfg.pending_id or not self.cfg.pending_path: | |
| return False | |
| if not Path(self.cfg.pending_path).exists(): | |
| self.cfg.pending_id = self.cfg.pending_path = "" | |
| self.cfg.save() | |
| return False | |
| old = self.cfg.active_id | |
| self.cfg.active_id = self.cfg.pending_id | |
| self.cfg.active_path = self.cfg.pending_path | |
| self.cfg.pending_id = self.cfg.pending_path = "" | |
| self.cfg.save() | |
| logger.info(f"[models] Hot-swap: {old} โ {self.cfg.active_id}") | |
| return True | |
| # โโ Auto-update background loop โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def start_updater(self): | |
| self._bg_task = asyncio.create_task(self._update_loop(), name="model_updater") | |
| async def _update_loop(self): | |
| await asyncio.sleep(7200) # wait 2h after startup before first upgrade check | |
| while True: | |
| try: | |
| if (time.time() - self.cfg.last_check) / 3600 >= self.UPDATE_INTERVAL_H: | |
| await self._smart_upgrade() | |
| except Exception as e: | |
| logger.error(f"[models] Update loop error: {e}") | |
| await asyncio.sleep(3600) | |
| async def _smart_upgrade(self): | |
| current = CATALOG_BY_ID.get(self.cfg.active_id) | |
| if not current: | |
| return | |
| better = [ | |
| m for m in CATALOG | |
| if m.quality > current.quality | |
| and m.size_gb <= RAM_LIMIT_GB | |
| and m.id != current.id | |
| ] | |
| if not better: | |
| self.cfg.last_check = time.time() | |
| self.cfg.save() | |
| logger.info(f"[models] Already on best model for this hardware.") | |
| return | |
| best = max(better, key=lambda m: m.quality) | |
| logger.info(f"[models] Better model found: {best.name}. Downloading in background...") | |
| async def _bg(): | |
| path, mmproj = await self.download(best) | |
| if path: | |
| self.cfg.pending_id = best.id | |
| self.cfg.pending_path = path | |
| self.cfg.last_check = time.time() | |
| self.cfg.save() | |
| logger.info(f"[models] Upgrade ready: {best.name}. Activates on restart.") | |
| asyncio.create_task(_bg()) | |
| def stop(self): | |
| if self._bg_task: | |
| self._bg_task.cancel() | |
| # โโ Build llama-server command โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def build_cmd(self, host="127.0.0.1", port="8080", | |
| ctx_size: str | None = None) -> list[str]: | |
| m = CATALOG_BY_ID.get(self.cfg.active_id) | |
| if not m: | |
| raise ValueError(f"Unknown model id: {self.cfg.active_id}") | |
| ctx = ctx_size or str(m.ctx_size) | |
| cmd = [ | |
| "llama-server", | |
| "--model", self.cfg.active_path, | |
| "--host", host, | |
| "--port", port, | |
| "--ctx-size", ctx, | |
| "-t", "2", | |
| "-tb", "2", | |
| "-np", "2", | |
| "--cont-batching", | |
| "--poll", "100", | |
| "--prio", "3", | |
| "--prio-batch", "2", | |
| "--cpu-range", "0-1", | |
| "--cpu-strict", "1", | |
| "-b", "512", | |
| "-ub", "128", | |
| "-ctk", "q4_0", | |
| "-ctv", "q4_0", | |
| "-fa", "on", | |
| "--slot-save-path", "/data/slot_cache", | |
| "--mlock", | |
| "--no-mmap", | |
| "--timeout", "600", # must be > THINKING_TIMEOUT (300s) | |
| "--log-disable", | |
| # NO speculative decoding โ net-negative on CPU Q4 quantized models | |
| # Qwen3 sampling params (official recommended) | |
| "--temp", str(m.temp), | |
| "--top-k", str(m.top_k), | |
| "--top-p", str(m.top_p), | |
| "--min-p", str(m.min_p), | |
| ] | |
| # Vision | |
| if self.cfg.mmproj_path and m.has_vision: | |
| cmd += ["--mmproj", self.cfg.mmproj_path, "--no-mmproj-offload"] | |
| # Model-specific flags (jinja, reasoning-format, etc.) | |
| if m.extra_flags: | |
| import shlex | |
| cmd += shlex.split(m.extra_flags) | |
| return cmd | |
| # โโ Status & Catalog โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def status(self) -> dict: | |
| m = CATALOG_BY_ID.get(self.cfg.active_id, {}) | |
| pm = CATALOG_BY_ID.get(self.cfg.pending_id, {}) if self.cfg.pending_id else {} | |
| return { | |
| "active": { | |
| "id": self.cfg.active_id, | |
| "name": m.name if m else "unknown", | |
| "path": self.cfg.active_path, | |
| "mmproj": self.cfg.mmproj_path, | |
| "quality": m.quality if m else 0, | |
| "thinking": m.has_thinking if m else False, | |
| "vision": m.has_vision if m else False, | |
| "ctx_size": m.ctx_size if m else 0, | |
| "size_gb": m.size_gb if m else 0, | |
| }, | |
| "pending": {"id": self.cfg.pending_id, "name": pm.name if pm else None} if self.cfg.pending_id else None, | |
| "is_downloading":self._downloading, | |
| "last_check_h": round((time.time() - self.cfg.last_check)/3600, 1), | |
| "update_every_h":self.UPDATE_INTERVAL_H, | |
| } | |
| def catalog(self) -> list[dict]: | |
| result = [] | |
| for m in CATALOG: | |
| local, _ = self.find_local(m) | |
| result.append({ | |
| "id": m.id, | |
| "name": m.name, | |
| "quality": m.quality, | |
| "size_gb": m.size_gb, | |
| "thinking": m.has_thinking, | |
| "vision": m.has_vision, | |
| "on_disk": bool(local), | |
| "active": m.id == self.cfg.active_id, | |
| "pending": m.id == self.cfg.pending_id, | |
| "notes": m.notes, | |
| }) | |
| return result | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # CLI โ ูุณุชุฎุฏู ู startup.sh | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| if __name__ == "__main__": | |
| import sys | |
| logging.basicConfig(level=logging.INFO) | |
| mgr = ModelManager() | |
| cmd = sys.argv[1] if len(sys.argv) > 1 else "status" | |
| if cmd == "status": | |
| print(json.dumps(mgr.status(), indent=2)) | |
| elif cmd == "apply-pending": | |
| print("applied" if mgr.apply_pending() else "no-pending") | |
| elif cmd == "get-cmd": | |
| try: | |
| parts = mgr.build_cmd() | |
| print(" ".join(parts)) | |
| except ValueError as e: | |
| print(f"ERROR: {e}", file=sys.stderr); sys.exit(1) | |
| elif cmd == "ensure-downloaded": | |
| async def _run(): | |
| token = os.getenv("HF_TOKEN", "").strip() | |
| print(f"[model_manager] HF_TOKEN: {'SET (len=' + str(len(token)) + ')' if token else 'NOT SET โ using anonymous access'}", file=sys.stderr) | |
| mgr.apply_pending() | |
| m = CATALOG_BY_ID.get(mgr.cfg.active_id) or mgr.select_best() | |
| mgr.cfg.active_id = m.id | |
| print(f"[model_manager] Downloading: {m.name} ({m.size_gb}GB) from {m.repo}", file=sys.stderr) | |
| path, mmproj = await mgr.download(m) | |
| if not path: | |
| print(f"[model_manager] Primary download failed, trying smallest fallback...", file=sys.stderr) | |
| fb = min(CATALOG, key=lambda x: x.size_gb) | |
| print(f"[model_manager] Fallback: {fb.name}", file=sys.stderr) | |
| path, mmproj = await mgr.download(fb) | |
| if path: | |
| mgr.cfg.active_id = fb.id | |
| if not path: | |
| print(f"[model_manager] ALL downloads failed!", file=sys.stderr) | |
| else: | |
| print(f"[model_manager] SUCCESS: {path}", file=sys.stderr) | |
| mgr.cfg.active_path = path | |
| mgr.cfg.mmproj_path = mmproj | |
| mgr.cfg.save() | |
| print(json.dumps({ | |
| "model_id": mgr.cfg.active_id, | |
| "model_path": mgr.cfg.active_path, | |
| "mmproj_path":mgr.cfg.mmproj_path, | |
| "ok": bool(path), | |
| })) | |
| asyncio.run(_run()) | |
| else: | |
| print(f"Unknown: {cmd}", file=sys.stderr); sys.exit(1) |