"""Application configuration from environment + runtime overrides.""" from functools import lru_cache from pathlib import Path from pydantic_settings import BaseSettings class Settings(BaseSettings): """App settings loaded from env.""" app_name: str = "Fund Tracer API" debug: bool = False # Database database_url: str = "sqlite+aiosqlite:///./fund_tracer.db" # Uploads upload_dir: Path = Path("./uploads") max_upload_size_mb: int = 20 allowed_extensions: set[str] = {"png", "jpg", "jpeg", "webp"} # --- OCR (vision) model --- ocr_provider: str = "openai" # openai | anthropic | deepseek | custom_openai ocr_model: str | None = None # if None, falls back to provider default # --- Inference (reasoning) model --- inference_provider: str = "openai" inference_model: str | None = None # --- Provider credentials (shared between OCR and inference) --- openai_api_key: str | None = None anthropic_api_key: str | None = None deepseek_api_key: str | None = None custom_openai_api_key: str | None = None custom_openai_base_url: str | None = None # Provider default model names (used when ocr_model / inference_model is None) openai_model: str = "gpt-4o" anthropic_model: str = "claude-3-5-sonnet-20241022" deepseek_model: str = "deepseek-chat" custom_openai_model: str = "gpt-4o-mini" # Legacy compat: llm_provider maps to ocr_provider on load llm_provider: str | None = None class Config: env_file = ".env" env_file_encoding = "utf-8" extra = "ignore" _runtime_overrides: dict[str, str | None] = {} _ALLOWED_RUNTIME_KEYS = { "ocr_provider", "ocr_model", "inference_provider", "inference_model", "openai_api_key", "anthropic_api_key", "deepseek_api_key", "custom_openai_api_key", "custom_openai_base_url", "custom_openai_model", } def _apply_overrides(settings: Settings) -> Settings: # Legacy: if llm_provider is set but ocr_provider is default, use it if settings.llm_provider and settings.ocr_provider == "openai": settings.ocr_provider = settings.llm_provider for key, value in _runtime_overrides.items(): if hasattr(settings, key): setattr(settings, key, value) return settings @lru_cache def get_settings() -> Settings: return _apply_overrides(Settings()) def _resolve_model(provider: str, explicit_model: str | None, settings: Settings) -> str: """Return the model name to use for a given provider.""" if explicit_model: return explicit_model defaults = { "openai": settings.openai_model, "anthropic": settings.anthropic_model, "deepseek": settings.deepseek_model, "custom_openai": settings.custom_openai_model, } return defaults.get(provider, settings.openai_model) def get_ocr_model() -> str: s = get_settings() return _resolve_model(s.ocr_provider, s.ocr_model, s) def get_inference_model() -> str: s = get_settings() return _resolve_model(s.inference_provider, s.inference_model, s) def update_runtime_settings(payload: dict[str, str | None]) -> Settings: for key, value in payload.items(): if key in _ALLOWED_RUNTIME_KEYS: _runtime_overrides[key] = value get_settings.cache_clear() return get_settings() def public_settings() -> dict: s = get_settings() return { "ocr_provider": s.ocr_provider, "ocr_model": get_ocr_model(), "inference_provider": s.inference_provider, "inference_model": get_inference_model(), "providers": ["openai", "anthropic", "deepseek", "custom_openai"], "provider_defaults": { "openai": s.openai_model, "anthropic": s.anthropic_model, "deepseek": s.deepseek_model, "custom_openai": s.custom_openai_model, }, "base_urls": { "custom_openai": s.custom_openai_base_url or "", }, "has_keys": { "openai": bool(s.openai_api_key), "anthropic": bool(s.anthropic_api_key), "deepseek": bool(s.deepseek_api_key), "custom_openai": bool(s.custom_openai_api_key), }, }