from __future__ import annotations from dataclasses import asdict, dataclass from pathlib import Path from typing import Any import yaml LOCAL_BACKEND_CONFIG_PATH = Path("data/local_backends.yaml") @dataclass(frozen=True) class LocalBackendConfig: """User-local backend settings stored outside git-tracked config.""" llama_cpp_server_url: str = "http://127.0.0.1:8080" llama_server_path: str = "" openai_compatible_base_url: str = "http://127.0.0.1:1234" openai_compatible_model_name: str = "" gguf_path: str = "" mmproj_path: str = "" n_ctx: int = 4096 n_gpu_layers: int = 0 def file_status_rows(self) -> list[list[str]]: return [ ["llama-server", self.llama_server_path, _local_file_status(self.llama_server_path)], ["GGUF model", self.gguf_path, _local_file_status(self.gguf_path)], ["mmproj", self.mmproj_path, _local_file_status(self.mmproj_path)], ] def load_local_backend_config( path: str | Path = LOCAL_BACKEND_CONFIG_PATH, ) -> LocalBackendConfig: config_path = Path(path) if not config_path.exists(): return LocalBackendConfig() raw = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {} llama_cpp = raw.get("llama_cpp", {}) openai_compatible = raw.get("openai_compatible", {}) return LocalBackendConfig( llama_cpp_server_url=str( llama_cpp.get("server_url", LocalBackendConfig.llama_cpp_server_url) ), llama_server_path=str(llama_cpp.get("llama_server_path", "")), openai_compatible_base_url=str( openai_compatible.get( "base_url", LocalBackendConfig.openai_compatible_base_url, ) ), openai_compatible_model_name=str(openai_compatible.get("model_name", "")), gguf_path=str(llama_cpp.get("gguf_path", "")), mmproj_path=str(llama_cpp.get("mmproj_path", "")), n_ctx=int(llama_cpp.get("n_ctx", LocalBackendConfig.n_ctx)), n_gpu_layers=int(llama_cpp.get("n_gpu_layers", LocalBackendConfig.n_gpu_layers)), ) def save_local_backend_config( config: LocalBackendConfig, path: str | Path = LOCAL_BACKEND_CONFIG_PATH, ) -> Path: config_path = Path(path) config_path.parent.mkdir(parents=True, exist_ok=True) data: dict[str, dict[str, Any]] = { "llama_cpp": { "server_url": config.llama_cpp_server_url, "llama_server_path": config.llama_server_path, "gguf_path": config.gguf_path, "mmproj_path": config.mmproj_path, "n_ctx": config.n_ctx, "n_gpu_layers": config.n_gpu_layers, }, "openai_compatible": { "base_url": config.openai_compatible_base_url, "model_name": config.openai_compatible_model_name, }, } config_path.write_text(yaml.safe_dump(data, sort_keys=False), encoding="utf-8") return config_path def build_llama_server_command(config: LocalBackendConfig) -> list[str]: if not config.gguf_path: return [] executable = config.llama_server_path or "llama-server" command = [executable, "-m", config.gguf_path] if config.mmproj_path: command.extend(["--mmproj", config.mmproj_path]) return command def local_backend_summary(config: LocalBackendConfig) -> dict[str, Any]: command = build_llama_server_command(config) return { **asdict(config), "llama_server_command": " ".join(command) if command else "", "files": config.file_status_rows(), "startup_downloads": False, "auto_model_load": False, } def _local_file_status(path: str) -> str: if not path: return "not configured" return "found" if Path(path).exists() else "missing"