"""Application settings (environment variables, .env).""" from __future__ import annotations import os from functools import lru_cache from pathlib import Path from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict def _default_root() -> Path: return Path(os.environ.get("LLM_API_ROOT", str(Path.home() / "llm-api"))) class Settings(BaseSettings): model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore") app_env: str = Field(default="development", alias="APP_ENV") domain: str = Field(default="www.llm-api.io", alias="DOMAIN") public_url: str = Field(default="https://www.llm-api.io", alias="PUBLIC_URL") host: str = Field(default="127.0.0.1", alias="HOST") port: int = Field(default=8300, alias="PORT") web_url: str = Field(default="http://127.0.0.1:8301", alias="WEB_URL") # Storage root: Path = Field(default_factory=_default_root, alias="LLM_API_ROOT") model_root: Path | None = Field(default=None, alias="MODEL_ROOT") data_dir: Path | None = Field(default=None, alias="DATA_DIR") log_dir: Path | None = Field(default=None, alias="LOG_DIR") database_path: Path | None = Field(default=None, alias="DATABASE_PATH") # Memory policy (GB) max_model_memory_gb: float = Field(default=45.0, alias="MAX_MODEL_MEMORY_GB") absolute_max_memory_gb: float = Field(default=50.0, alias="ABSOLUTE_MAX_MEMORY_GB") macos_reserve_gb: float = Field(default=8.0, alias="MACOS_RESERVE_GB") min_free_disk_gb: float = Field(default=100.0, alias="MIN_FREE_DISK_GB") max_simultaneous_models: int = Field(default=1, alias="MAX_SIMULTANEOUS_MODELS") model_idle_timeout_minutes: int = Field(default=30, alias="MODEL_IDLE_TIMEOUT_MINUTES") small_model_resident_gb: float = Field(default=3.0, alias="SMALL_MODEL_RESIDENT_GB") load_timeout_seconds: int = Field(default=900, alias="LOAD_TIMEOUT_SECONDS") generation_timeout_seconds: int = Field(default=1800, alias="GENERATION_TIMEOUT_SECONDS") preload_model: str = Field(default="none", alias="PRELOAD_MODEL") enable_mlx: bool = Field(default=True, alias="ENABLE_MLX") enable_gguf: bool = Field(default=True, alias="ENABLE_GGUF") allow_downloads: bool = Field(default=True, alias="ALLOW_DOWNLOADS") log_prompts: bool = Field(default=False, alias="LOG_PROMPTS") default_max_tokens: int = Field(default=2048, alias="DEFAULT_MAX_TOKENS") default_context: int = Field(default=16384, alias="DEFAULT_CONTEXT") hf_token: str | None = Field(default=None, alias="HF_TOKEN") llama_server_bin: str = Field(default="llama-server", alias="LLAMA_SERVER_BIN") worker_python: str | None = Field(default=None, alias="WORKER_PYTHON") worker_port_start: int = Field(default=8310, alias="WORKER_PORT_START") worker_port_end: int = Field(default=8399, alias="WORKER_PORT_END") secret_key: str = Field(default="", alias="SECRET_KEY") admin_email: str | None = Field(default=None, alias="ADMIN_EMAIL") admin_password: str | None = Field(default=None, alias="ADMIN_PASSWORD") session_hours: int = Field(default=24 * 14, alias="SESSION_HOURS") secure_cookies: bool = Field(default=False, alias="SECURE_COOKIES") metrics_interval_seconds: int = Field(default=15, alias="METRICS_INTERVAL_SECONDS") metrics_retention_days: int = Field(default=30, alias="METRICS_RETENTION_DAYS") max_body_bytes: int = Field(default=20 * 1024 * 1024, alias="MAX_BODY_BYTES") # ---- derived paths ------------------------------------------------- @property def models_dir(self) -> Path: return (self.model_root or self.root / "models").expanduser() @property def data_path(self) -> Path: return (self.data_dir or self.root / "data").expanduser() @property def logs_path(self) -> Path: return (self.log_dir or self.root / "logs").expanduser() @property def db_path(self) -> Path: return (self.database_path or self.data_path / "llm-api.db").expanduser() @property def secret(self) -> str: if self.secret_key: return self.secret_key # Persist a generated secret so sessions survive restarts. p = self.data_path / ".secret" p.parent.mkdir(parents=True, exist_ok=True) if p.exists(): return p.read_text().strip() import secrets s = secrets.token_hex(32) p.write_text(s) os.chmod(p, 0o600) return s def ensure_dirs(self) -> None: for d in ( self.models_dir, self.models_dir / "mlx", self.models_dir / "gguf", self.models_dir / "embeddings", self.models_dir / "rerankers", self.models_dir / "vision", self.models_dir / "manifests", self.data_path, self.logs_path, ): d.mkdir(parents=True, exist_ok=True) @lru_cache def get_settings() -> Settings: return Settings()