1"""Application settings (environment variables, .env)."""23from __future__ import annotations45import os6from functools import lru_cache7from pathlib import Path89from pydantic import Field10from pydantic_settings import BaseSettings, SettingsConfigDict111213def _default_root() -> Path:14 return Path(os.environ.get("LLM_API_ROOT", str(Path.home() / "llm-api")))151617class Settings(BaseSettings):18 model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")1920 app_env: str = Field(default="development", alias="APP_ENV")21 domain: str = Field(default="www.llm-api.io", alias="DOMAIN")22 public_url: str = Field(default="https://www.llm-api.io", alias="PUBLIC_URL")2324 host: str = Field(default="127.0.0.1", alias="HOST")25 port: int = Field(default=8300, alias="PORT")26 web_url: str = Field(default="http://127.0.0.1:8301", alias="WEB_URL")2728 # Storage29 root: Path = Field(default_factory=_default_root, alias="LLM_API_ROOT")30 model_root: Path | None = Field(default=None, alias="MODEL_ROOT")31 data_dir: Path | None = Field(default=None, alias="DATA_DIR")32 log_dir: Path | None = Field(default=None, alias="LOG_DIR")33 database_path: Path | None = Field(default=None, alias="DATABASE_PATH")3435 # Memory policy (GB)36 max_model_memory_gb: float = Field(default=45.0, alias="MAX_MODEL_MEMORY_GB")37 absolute_max_memory_gb: float = Field(default=50.0, alias="ABSOLUTE_MAX_MEMORY_GB")38 macos_reserve_gb: float = Field(default=8.0, alias="MACOS_RESERVE_GB")39 min_free_disk_gb: float = Field(default=100.0, alias="MIN_FREE_DISK_GB")4041 max_simultaneous_models: int = Field(default=1, alias="MAX_SIMULTANEOUS_MODELS")42 model_idle_timeout_minutes: int = Field(default=30, alias="MODEL_IDLE_TIMEOUT_MINUTES")43 small_model_resident_gb: float = Field(default=3.0, alias="SMALL_MODEL_RESIDENT_GB")44 load_timeout_seconds: int = Field(default=900, alias="LOAD_TIMEOUT_SECONDS")45 generation_timeout_seconds: int = Field(default=1800, alias="GENERATION_TIMEOUT_SECONDS")46 preload_model: str = Field(default="none", alias="PRELOAD_MODEL")4748 enable_mlx: bool = Field(default=True, alias="ENABLE_MLX")49 enable_gguf: bool = Field(default=True, alias="ENABLE_GGUF")50 allow_downloads: bool = Field(default=True, alias="ALLOW_DOWNLOADS")51 log_prompts: bool = Field(default=False, alias="LOG_PROMPTS")5253 default_max_tokens: int = Field(default=2048, alias="DEFAULT_MAX_TOKENS")54 default_context: int = Field(default=16384, alias="DEFAULT_CONTEXT")5556 hf_token: str | None = Field(default=None, alias="HF_TOKEN")57 llama_server_bin: str = Field(default="llama-server", alias="LLAMA_SERVER_BIN")58 worker_python: str | None = Field(default=None, alias="WORKER_PYTHON")59 worker_port_start: int = Field(default=8310, alias="WORKER_PORT_START")60 worker_port_end: int = Field(default=8399, alias="WORKER_PORT_END")6162 secret_key: str = Field(default="", alias="SECRET_KEY")63 admin_email: str | None = Field(default=None, alias="ADMIN_EMAIL")64 admin_password: str | None = Field(default=None, alias="ADMIN_PASSWORD")65 session_hours: int = Field(default=24 * 14, alias="SESSION_HOURS")66 secure_cookies: bool = Field(default=False, alias="SECURE_COOKIES")6768 metrics_interval_seconds: int = Field(default=15, alias="METRICS_INTERVAL_SECONDS")69 metrics_retention_days: int = Field(default=30, alias="METRICS_RETENTION_DAYS")70 max_body_bytes: int = Field(default=20 * 1024 * 1024, alias="MAX_BODY_BYTES")7172 # ---- derived paths -------------------------------------------------73 @property74 def models_dir(self) -> Path:75 return (self.model_root or self.root / "models").expanduser()7677 @property78 def data_path(self) -> Path:79 return (self.data_dir or self.root / "data").expanduser()8081 @property82 def logs_path(self) -> Path:83 return (self.log_dir or self.root / "logs").expanduser()8485 @property86 def db_path(self) -> Path:87 return (self.database_path or self.data_path / "llm-api.db").expanduser()8889 @property90 def secret(self) -> str:91 if self.secret_key:92 return self.secret_key93 # Persist a generated secret so sessions survive restarts.94 p = self.data_path / ".secret"95 p.parent.mkdir(parents=True, exist_ok=True)96 if p.exists():97 return p.read_text().strip()98 import secrets99100 s = secrets.token_hex(32)101 p.write_text(s)102 os.chmod(p, 0o600)103 return s104105 def ensure_dirs(self) -> None:106 for d in (107 self.models_dir,108 self.models_dir / "mlx",109 self.models_dir / "gguf",110 self.models_dir / "embeddings",111 self.models_dir / "rerankers",112 self.models_dir / "vision",113 self.models_dir / "manifests",114 self.data_path,115 self.logs_path,116 ):117 d.mkdir(parents=True, exist_ok=True)118119120@lru_cache121def get_settings() -> Settings:122 return Settings()123