#!/usr/bin/env python3 """Odczyt llm/models.yml: wykrycie backendu i renderowanie zmiennych środowiskowych. Jedno miejsce decyduje, jaki backend jest właściwy dla platformy i jakie zmienne dostaje silnik - Taskfile tylko o to pyta, nie duplikuje wiedzy. Użycie: backend.py detect # nazwa backendu dla tej maszyny backend.py env [--backend NAME] # linie `export ...` do eval backend.py get serve.model [--backend NAME] backend.py list """ from __future__ import annotations import argparse import platform import shutil import subprocess import sys from pathlib import Path from typing import Any import yaml MODELS_FILE = Path(__file__).resolve().parent.parent / "models.yml" def load() -> dict[str, Any]: return yaml.safe_load(MODELS_FILE.read_text(encoding="utf-8")) or {} def _has_nvidia_gpu() -> bool: if not shutil.which("nvidia-smi"): return False try: return subprocess.run(["nvidia-smi", "-L"], capture_output=True, timeout=10).returncode == 0 except (OSError, subprocess.SubprocessError): return False def _has_docker() -> bool: if not shutil.which("docker"): return False try: return subprocess.run(["docker", "info"], capture_output=True, timeout=20).returncode == 0 except (OSError, subprocess.SubprocessError): return False def detect() -> str: """Kolejność jest świadoma: najpierw akceleracja sprzętowa, potem cokolwiek, co działa.""" system = platform.system().lower() if system == "darwin" and platform.machine() == "arm64": return "vllm-metal" if system == "linux" and _has_nvidia_gpu() and _has_docker(): return "vllm-gpu" if shutil.which("ollama") or _has_docker(): return "ollama" # Brak GPU, Metala i Ollamy: realnego modelu i tak nie ma sensu tu uruchamiać. return "mock" def resolve(name: str | None) -> tuple[str, dict[str, Any]]: data = load() backends = data.get("backends") or {} if not name or name == "auto": name = detect() if name not in backends: raise SystemExit(f"Nieznany backend '{name}'. Dostępne: {', '.join(sorted(backends))}") return name, backends[name] def _dotted(config: dict[str, Any], path: str) -> Any: value: Any = config for part in path.split("."): if not isinstance(value, dict) or part not in value: raise SystemExit(f"Brak klucza '{path}' w konfiguracji backendu") value = value[part] return value def cmd_env(args: argparse.Namespace) -> None: name, config = resolve(args.backend) models = config.get("models") or {} lines = [ f"export CODEMOD_LLM_BACKEND={name}", f"export CODEMOD_MODEL_PROVIDER={config.get('provider', 'openai_like')}", f"export CODEMOD_LLM_BASE_URL={config['base_url']}", f"export CODEMOD_LLM_API_KEY={config.get('api_key', 'not-required')}", ] lines += [f"export CODEMOD_MODEL_{role.upper()}={model}" for role, model in sorted(models.items())] print("\n".join(lines)) def cmd_detect(args: argparse.Namespace) -> None: requested = (args.requested or "auto").strip() print(detect() if requested in ("", "auto") else requested) def cmd_get(args: argparse.Namespace) -> None: _, config = resolve(args.backend) value = _dotted(config, args.key) print(" ".join(str(v) for v in value) if isinstance(value, list) else value) def cmd_list(_: argparse.Namespace) -> None: data = load() current = detect() for name, config in (data.get("backends") or {}).items(): marker = "*" if name == current else " " platforms = ", ".join(config.get("platforms", [])) print(f" {marker} {name:<12} {config['base_url']:<32} [{platforms}]") print("\n * = wykryty jako właściwy dla tej maszyny") def main() -> None: parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) sub = parser.add_subparsers(dest="command", required=True) det = sub.add_parser("detect") det.add_argument("--requested", default="auto", help="jawnie wskazany backend; 'auto' = wykrycie") det.set_defaults(func=cmd_detect) sub.add_parser("list").set_defaults(func=cmd_list) env = sub.add_parser("env") env.add_argument("--backend") env.set_defaults(func=cmd_env) get = sub.add_parser("get") get.add_argument("key") get.add_argument("--backend") get.set_defaults(func=cmd_get) args = parser.parse_args() args.func(args) if __name__ == "__main__": sys.exit(main())