"""Centralized configuration using Pydantic Settings.""" from functools import lru_cache from dotenv import load_dotenv from pydantic import Field, field_validator, model_validator from pydantic_settings import BaseSettings, SettingsConfigDict from .nim import NimSettings load_dotenv() class Settings(BaseSettings): """Application settings loaded from environment variables.""" # ==================== OpenRouter Config ==================== open_router_api_key: str = Field(default="", validation_alias="OPENROUTER_API_KEY") # ==================== Messaging Platform Selection ==================== # Valid: "telegram" | "discord" messaging_platform: str = Field( default="discord", validation_alias="MESSAGING_PLATFORM" ) # ==================== NVIDIA NIM Config ==================== nvidia_nim_api_key: str = "" # ==================== LM Studio Config ==================== lm_studio_base_url: str = Field( default="http://localhost:1234/v1", validation_alias="LM_STUDIO_BASE_URL", ) # ==================== Model ==================== # All Claude model requests are mapped to this single model # Format: provider_type/model/name model: str = "nvidia_nim/meta/llama3-70b-instruct" # ==================== Provider Rate Limiting ==================== provider_rate_limit: int = Field(default=40, validation_alias="PROVIDER_RATE_LIMIT") provider_rate_window: int = Field( default=60, validation_alias="PROVIDER_RATE_WINDOW" ) provider_max_concurrency: int = Field( default=5, validation_alias="PROVIDER_MAX_CONCURRENCY" ) # ==================== HTTP Client Timeouts ==================== http_read_timeout: float = Field( default=300.0, validation_alias="HTTP_READ_TIMEOUT" ) http_write_timeout: float = Field( default=10.0, validation_alias="HTTP_WRITE_TIMEOUT" ) http_connect_timeout: float = Field( default=2.0, validation_alias="HTTP_CONNECT_TIMEOUT" ) # ==================== Fast Prefix Detection ==================== fast_prefix_detection: bool = True # ==================== Optimizations ==================== enable_network_probe_mock: bool = True enable_title_generation_skip: bool = True enable_suggestion_mode_skip: bool = True enable_filepath_extraction_mock: bool = True # ==================== NIM Settings ==================== nim: NimSettings = Field(default_factory=NimSettings) # ==================== Voice Note Transcription ==================== voice_note_enabled: bool = Field( default=True, validation_alias="VOICE_NOTE_ENABLED" ) # Device: "cpu" | "cuda" | "nvidia_nim" # - "cpu"/"cuda": local Whisper (requires voice_local extra: uv sync --extra voice_local) # - "nvidia_nim": NVIDIA NIM Whisper API (requires voice extra: uv sync --extra voice) whisper_device: str = Field(default="cpu", validation_alias="WHISPER_DEVICE") # Whisper model ID or short name (for local Whisper) or NVIDIA NIM model (for nvidia_nim) # Local Whisper: "tiny", "base", "small", "medium", "large-v2", "large-v3", "large-v3-turbo" # NVIDIA NIM: "nvidia/parakeet-ctc-1.1b-asr", "openai/whisper-large-v3", etc. whisper_model: str = Field(default="base", validation_alias="WHISPER_MODEL") # Hugging Face token for faster model downloads (optional, for local Whisper) hf_token: str = Field(default="", validation_alias="HF_TOKEN") # ==================== Bot Wrapper Config ==================== telegram_bot_token: str | None = None allowed_telegram_user_id: str | None = None discord_bot_token: str | None = Field( default=None, validation_alias="DISCORD_BOT_TOKEN" ) allowed_discord_channels: str | None = Field( default=None, validation_alias="ALLOWED_DISCORD_CHANNELS" ) claude_workspace: str = "./agent_workspace" allowed_dir: str = "" # ==================== Server ==================== host: str = "0.0.0.0" port: int = 8082 log_file: str = "server.log" # Handle empty strings for optional string fields @field_validator( "telegram_bot_token", "allowed_telegram_user_id", "discord_bot_token", "allowed_discord_channels", mode="before", ) @classmethod def parse_optional_str(cls, v): if v == "": return None return v @field_validator("whisper_device") @classmethod def validate_whisper_device(cls, v: str) -> str: if v not in ("cpu", "cuda", "nvidia_nim"): raise ValueError( f"whisper_device must be 'cpu', 'cuda', or 'nvidia_nim', got {v!r}" ) return v @field_validator("model") @classmethod def validate_model_format(cls, v: str) -> str: valid_providers = ("nvidia_nim", "open_router", "lmstudio") if "/" not in v: raise ValueError( f"Model must be prefixed with provider type. " f"Valid providers: {', '.join(valid_providers)}. " f"Format: provider_type/model/name" ) provider = v.split("/", 1)[0] if provider not in valid_providers: raise ValueError( f"Invalid provider: '{provider}'. " f"Supported: 'nvidia_nim', 'open_router', 'lmstudio'" ) return v @model_validator(mode="after") def check_nvidia_nim_api_key(self) -> Settings: if ( self.voice_note_enabled and self.whisper_device == "nvidia_nim" and not self.nvidia_nim_api_key.strip() ): raise ValueError( "NVIDIA_NIM_API_KEY is required when WHISPER_DEVICE is 'nvidia_nim'. " "Set it in your .env file." ) return self @property def provider_type(self) -> str: """Extract provider type from the model string.""" return self.model.split("/", 1)[0] @property def model_name(self) -> str: """Extract the actual model name from the model string.""" return self.model.split("/", 1)[1] model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", extra="ignore", ) @lru_cache def get_settings() -> Settings: """Get cached settings instance.""" return Settings()