"""Flat application settings schema loaded by Pydantic Settings.""" from __future__ import annotations from functools import lru_cache from typing import Any from pydantic import Field, field_validator, model_validator from pydantic_settings import BaseSettings, SettingsConfigDict from .constants import HTTP_CONNECT_TIMEOUT_DEFAULT from .env_files import ( ANTHROPIC_AUTH_TOKEN_ENV, env_file_override, settings_env_files, ) from .nim import NimSettings from .provider_catalog import BEDROCK_DEFAULT_BASE, SUPPORTED_PROVIDER_IDS from .reasoning import ReasoningPreference class Settings(BaseSettings): """Application settings loaded from environment variables.""" # ==================== Azure OpenAI ==================== azure_openai_api_key: str = Field( default="", validation_alias="AZURE_OPENAI_API_KEY" ) azure_openai_base_url: str = Field( default="", validation_alias="AZURE_OPENAI_BASE_URL" ) # ==================== OpenRouter Config ==================== open_router_api_key: str = Field(default="", validation_alias="OPENROUTER_API_KEY") # ==================== Mistral La Plateforme ==================== mistral_api_key: str = Field(default="", validation_alias="MISTRAL_API_KEY") # ==================== Mistral Codestral (codestral.mistral.ai) ==================== codestral_api_key: str = Field(default="", validation_alias="CODESTRAL_API_KEY") # ==================== DeepSeek Config ==================== deepseek_api_key: str = Field(default="", validation_alias="DEEPSEEK_API_KEY") # ==================== Kimi Config ==================== kimi_api_key: str = Field(default="", validation_alias="KIMI_API_KEY") # ==================== Kimi Code Subscription ==================== kimi_code_api_key: str = Field(default="", validation_alias="KIMI_CODE_API_KEY") # ==================== Wafer Config ==================== wafer_api_key: str = Field(default="", validation_alias="WAFER_API_KEY") # ==================== MiniMax Config ==================== minimax_api_key: str = Field(default="", validation_alias="MINIMAX_API_KEY") # ==================== OpenCode Zen / OpenCode Go ==================== # Same key from opencode.ai/auth; zen uses prefix ``opencode/``, Go uses ``opencode_go/``. opencode_api_key: str = Field(default="", validation_alias="OPENCODE_API_KEY") # ==================== Vercel AI Gateway ==================== vercel_ai_gateway_api_key: str = Field( default="", validation_alias="AI_GATEWAY_API_KEY" ) # ==================== Amazon Bedrock Mantle ==================== bedrock_api_key: str = Field( default="", validation_alias="AWS_BEARER_TOKEN_BEDROCK" ) bedrock_base_url: str = Field( default=BEDROCK_DEFAULT_BASE, validation_alias="BEDROCK_BASE_URL", ) # ==================== Hugging Face Inference Providers ==================== huggingface_api_key: str = Field(default="", validation_alias="HUGGINGFACE_API_KEY") # ==================== Cohere Compatibility API ==================== cohere_api_key: str = Field(default="", validation_alias="COHERE_API_KEY") # ==================== GitHub Models ==================== github_models_token: str = Field(default="", validation_alias="GITHUB_MODELS_TOKEN") # ==================== SambaNova Cloud ==================== sambanova_api_key: str = Field(default="", validation_alias="SAMBANOVA_API_KEY") # ==================== Kilo.ai Config ==================== kilo_api_key: str = Field(default="", validation_alias="KILO_API_KEY") # ==================== Z.ai Config ==================== zai_api_key: str = Field(default="", validation_alias="ZAI_API_KEY") # ==================== Fireworks AI Config ==================== fireworks_api_key: str = Field(default="", validation_alias="FIREWORKS_API_KEY") # ==================== Cloudflare Workers AI Config ==================== cloudflare_api_token: str = Field( default="", validation_alias="CLOUDFLARE_API_TOKEN" ) cloudflare_account_id: str = Field( default="", validation_alias="CLOUDFLARE_ACCOUNT_ID" ) # ==================== Google Gemini (Google AI Studio) ==================== gemini_api_key: str = Field(default="", validation_alias="GEMINI_API_KEY") # ==================== Google Vertex AI ==================== vertex_project_id: str = Field(default="", validation_alias="VERTEX_PROJECT_ID") vertex_location: str = Field(default="global", validation_alias="VERTEX_LOCATION") # ==================== Groq (OpenAI-compatible) ==================== groq_api_key: str = Field(default="", validation_alias="GROQ_API_KEY") # ==================== Cerebras Inference (OpenAI-compatible) ==================== cerebras_api_key: str = Field(default="", validation_alias="CEREBRAS_API_KEY") # ==================== Ollama Cloud ==================== ollama_api_key: str = Field(default="", validation_alias="OLLAMA_API_KEY") # ==================== Messaging Platform Selection ==================== # Valid: "telegram" | "discord" | "none" messaging_platform: str = Field( default="discord", validation_alias="MESSAGING_PLATFORM" ) messaging_rate_limit: int = Field( default=1, validation_alias="MESSAGING_RATE_LIMIT" ) messaging_rate_window: float = Field( default=1.0, validation_alias="MESSAGING_RATE_WINDOW" ) # ==================== 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", ) # ==================== Llama.cpp Config ==================== llamacpp_base_url: str = Field( default="http://localhost:8080/v1", validation_alias="LLAMACPP_BASE_URL", ) # ==================== Ollama Config ==================== ollama_base_url: str = Field( default="http://localhost:11434", validation_alias="OLLAMA_BASE_URL", ) # ==================== Model ==================== # All Claude model requests are mapped to this single model (fallback) # Format: provider_type/model/name model: str = "nvidia_nim/nvidia/nemotron-3-super-120b-a12b" # Per-model overrides (optional, falls back to MODEL) # Each can use a different provider model_fable: str | None = Field(default=None, validation_alias="MODEL_FABLE") model_opus: str | None = Field(default=None, validation_alias="MODEL_OPUS") model_sonnet: str | None = Field(default=None, validation_alias="MODEL_SONNET") model_haiku: str | None = Field(default=None, validation_alias="MODEL_HAIKU") # ==================== Per-Provider Proxy ==================== openai_proxy: str = Field(default="", validation_alias="OPENAI_PROXY") azure_openai_proxy: str = Field(default="", validation_alias="AZURE_OPENAI_PROXY") nvidia_nim_proxy: str = Field(default="", validation_alias="NVIDIA_NIM_PROXY") open_router_proxy: str = Field(default="", validation_alias="OPENROUTER_PROXY") mistral_proxy: str = Field(default="", validation_alias="MISTRAL_PROXY") codestral_proxy: str = Field(default="", validation_alias="CODESTRAL_PROXY") lmstudio_proxy: str = Field(default="", validation_alias="LMSTUDIO_PROXY") llamacpp_proxy: str = Field(default="", validation_alias="LLAMACPP_PROXY") kimi_proxy: str = Field(default="", validation_alias="KIMI_PROXY") kimi_code_proxy: str = Field(default="", validation_alias="KIMI_CODE_PROXY") wafer_proxy: str = Field(default="", validation_alias="WAFER_PROXY") minimax_proxy: str = Field(default="", validation_alias="MINIMAX_PROXY") opencode_proxy: str = Field(default="", validation_alias="OPENCODE_PROXY") opencode_go_proxy: str = Field(default="", validation_alias="OPENCODE_GO_PROXY") vercel_ai_gateway_proxy: str = Field( default="", validation_alias="VERCEL_AI_GATEWAY_PROXY" ) bedrock_proxy: str = Field(default="", validation_alias="BEDROCK_PROXY") huggingface_proxy: str = Field(default="", validation_alias="HUGGINGFACE_PROXY") cohere_proxy: str = Field(default="", validation_alias="COHERE_PROXY") github_models_proxy: str = Field(default="", validation_alias="GITHUB_MODELS_PROXY") sambanova_proxy: str = Field(default="", validation_alias="SAMBANOVA_PROXY") kilo_proxy: str = Field(default="", validation_alias="KILO_PROXY") zai_proxy: str = Field(default="", validation_alias="ZAI_PROXY") fireworks_proxy: str = Field(default="", validation_alias="FIREWORKS_PROXY") cloudflare_proxy: str = Field(default="", validation_alias="CLOUDFLARE_PROXY") gemini_proxy: str = Field(default="", validation_alias="GEMINI_PROXY") vertex_proxy: str = Field(default="", validation_alias="VERTEX_PROXY") groq_proxy: str = Field(default="", validation_alias="GROQ_PROXY") cerebras_proxy: str = Field(default="", validation_alias="CEREBRAS_PROXY") ollama_cloud_proxy: str = Field(default="", validation_alias="OLLAMA_CLOUD_PROXY") # ==================== 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" ) reasoning_policy: ReasoningPreference = Field( default=ReasoningPreference.CLIENT, validation_alias="REASONING_POLICY", ) reasoning_fable: ReasoningPreference = Field( default=ReasoningPreference.INHERIT, validation_alias="REASONING_FABLE", ) reasoning_opus: ReasoningPreference = Field( default=ReasoningPreference.INHERIT, validation_alias="REASONING_OPUS", ) reasoning_sonnet: ReasoningPreference = Field( default=ReasoningPreference.INHERIT, validation_alias="REASONING_SONNET", ) reasoning_haiku: ReasoningPreference = Field( default=ReasoningPreference.INHERIT, validation_alias="REASONING_HAIKU", ) # ==================== HTTP Client Timeouts ==================== http_read_timeout: float = Field( default=120.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=HTTP_CONNECT_TIMEOUT_DEFAULT, 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 # ==================== Local web server tools (web_search / web_fetch) ==================== # Off by default: these tools perform outbound HTTP from the proxy (SSRF risk). enable_web_server_tools: bool = Field( default=False, validation_alias="ENABLE_WEB_SERVER_TOOLS" ) # Comma-separated URL schemes allowed for web_fetch (default: http,https). web_fetch_allowed_schemes: str = Field( default="http,https", validation_alias="WEB_FETCH_ALLOWED_SCHEMES" ) # When true, skip private/loopback/link-local IP blocking for web_fetch (lab only). web_fetch_allow_private_networks: bool = Field( default=False, validation_alias="WEB_FETCH_ALLOW_PRIVATE_NETWORKS" ) # ==================== Debug / diagnostic logging (avoid sensitive content) ==================== # Minimum log level for the JSON file sink (DEBUG, INFO, WARNING, ERROR, CRITICAL). log_level: str = Field(default="INFO", validation_alias="LOG_LEVEL") # When false (default), API and SSE helpers log only metadata (counts, lengths, ids). log_raw_api_payloads: bool = Field( default=False, validation_alias="LOG_RAW_API_PAYLOADS" ) log_raw_sse_events: bool = Field( default=False, validation_alias="LOG_RAW_SSE_EVENTS" ) # When false (default), unhandled exceptions log only type + route metadata (no message/traceback). log_api_error_tracebacks: bool = Field( default=False, validation_alias="LOG_API_ERROR_TRACEBACKS" ) # When false (default), messaging logs omit text/transcription previews (metadata only). log_raw_messaging_content: bool = Field( default=False, validation_alias="LOG_RAW_MESSAGING_CONTENT" ) # When true, log full Claude CLI stderr, non-JSON lines, and parser error text. log_raw_cli_diagnostics: bool = Field( default=False, validation_alias="LOG_RAW_CLI_DIAGNOSTICS" ) # When true, log exception text / CLI error strings in messaging (may leak user content). log_messaging_error_details: bool = Field( default=False, validation_alias="LOG_MESSAGING_ERROR_DETAILS" ) debug_platform_edits: bool = Field( default=False, validation_alias="DEBUG_PLATFORM_EDITS" ) debug_subagent_stack: bool = Field( default=False, validation_alias="DEBUG_SUBAGENT_STACK" ) # ==================== 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") # ==================== Bot Wrapper Config ==================== telegram_bot_token: str | None = None allowed_telegram_user_id: str | None = None telegram_proxy_url: str = Field(default="", validation_alias="TELEGRAM_PROXY_URL") 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" ) allowed_dir: str = "" max_message_log_entries_per_chat: int | None = Field( default=None, validation_alias="MAX_MESSAGE_LOG_ENTRIES_PER_CHAT" ) # ==================== Server ==================== host: str = "0.0.0.0" port: int = 8082 open_admin_browser: bool = Field(default=True, validation_alias="FCC_OPEN_BROWSER") # Optional proxy bearer token protecting public API endpoints. # Set via env `ANTHROPIC_AUTH_TOKEN`. When empty, no auth is required. anthropic_auth_token: str = Field( default="", validation_alias="ANTHROPIC_AUTH_TOKEN" ) # Handle empty strings for optional string fields @field_validator( "telegram_bot_token", "allowed_telegram_user_id", "discord_bot_token", "allowed_discord_channels", "model_fable", "model_opus", "model_sonnet", "model_haiku", mode="before", ) @classmethod def parse_optional_str(cls, v: Any) -> Any: if v == "": return None return v @field_validator("max_message_log_entries_per_chat", mode="before") @classmethod def parse_optional_log_cap(cls, v: Any) -> Any: if v == "" or v is None: return None return v @field_validator("log_level") @classmethod def validate_log_level(cls, v: str) -> str: valid = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"} upper = v.upper() if upper not in valid: raise ValueError(f"LOG_LEVEL must be one of {sorted(valid)}, got {v!r}") return upper @field_validator("reasoning_policy") @classmethod def validate_root_reasoning_policy( cls, value: ReasoningPreference ) -> ReasoningPreference: if value is ReasoningPreference.INHERIT: raise ValueError("REASONING_POLICY cannot inherit") return value @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("messaging_platform") @classmethod def validate_messaging_platform(cls, v: str) -> str: if v not in ("telegram", "discord", "none"): raise ValueError( f"messaging_platform must be 'telegram', 'discord', or 'none', got {v!r}" ) return v @field_validator("messaging_rate_limit") @classmethod def validate_messaging_rate_limit(cls, v: int) -> int: if v <= 0: raise ValueError("messaging_rate_limit must be > 0") return v @field_validator("messaging_rate_window") @classmethod def validate_messaging_rate_window(cls, v: float) -> float: if v <= 0: raise ValueError("messaging_rate_window must be > 0") return float(v) @field_validator("web_fetch_allowed_schemes") @classmethod def validate_web_fetch_allowed_schemes(cls, v: str) -> str: schemes = [part.strip().lower() for part in v.split(",") if part.strip()] if not schemes: raise ValueError("web_fetch_allowed_schemes must list at least one scheme") for scheme in schemes: if not scheme.isascii() or not scheme.isalpha(): raise ValueError( f"Invalid URL scheme in web_fetch_allowed_schemes: {scheme!r}" ) return ",".join(schemes) @field_validator( "model", "model_fable", "model_opus", "model_sonnet", "model_haiku" ) @classmethod def validate_model_format(cls, v: str | None) -> str | None: if v is None: return None if "/" not in v: raise ValueError( f"Model must be prefixed with provider type. " f"Valid providers: {', '.join(SUPPORTED_PROVIDER_IDS)}. " f"Format: provider_type/model/name" ) provider = v.split("/", 1)[0] if provider not in SUPPORTED_PROVIDER_IDS: supported = ", ".join(f"'{p}'" for p in SUPPORTED_PROVIDER_IDS) raise ValueError(f"Invalid provider: '{provider}'. Supported: {supported}") 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 @model_validator(mode="after") def prefer_dotenv_anthropic_auth_token(self) -> Settings: """Let explicit .env auth config override stale shell/client tokens.""" dotenv_value = env_file_override(self.model_config, ANTHROPIC_AUTH_TOKEN_ENV) if dotenv_value is not None: self.anthropic_auth_token = dotenv_value return self model_config = SettingsConfigDict( env_file=settings_env_files(), env_file_encoding="utf-8", extra="ignore", ) @lru_cache def get_settings() -> Settings: """Get cached settings instance.""" return Settings()