2026-03-09 14:46:56 +08:00
|
|
|
"""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"}
|
|
|
|
|
|
2026-03-10 14:25:21 +08:00
|
|
|
# --- 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) ---
|
2026-03-09 14:46:56 +08:00
|
|
|
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
|
2026-03-10 14:25:21 +08:00
|
|
|
|
|
|
|
|
# Provider default model names (used when ocr_model / inference_model is None)
|
2026-03-09 14:46:56 +08:00
|
|
|
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"
|
|
|
|
|
|
2026-03-10 14:25:21 +08:00
|
|
|
# Legacy compat: llm_provider maps to ocr_provider on load
|
|
|
|
|
llm_provider: str | None = None
|
|
|
|
|
|
2026-03-09 14:46:56 +08:00
|
|
|
class Config:
|
|
|
|
|
env_file = ".env"
|
|
|
|
|
env_file_encoding = "utf-8"
|
|
|
|
|
extra = "ignore"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_runtime_overrides: dict[str, str | None] = {}
|
|
|
|
|
|
2026-03-10 14:25:21 +08:00
|
|
|
_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",
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-09 14:46:56 +08:00
|
|
|
|
|
|
|
|
def _apply_overrides(settings: Settings) -> Settings:
|
2026-03-10 14:25:21 +08:00
|
|
|
# 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
|
2026-03-09 14:46:56 +08:00
|
|
|
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())
|
|
|
|
|
|
|
|
|
|
|
2026-03-10 14:25:21 +08:00
|
|
|
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,
|
2026-03-09 14:46:56 +08:00
|
|
|
}
|
2026-03-10 14:25:21 +08:00
|
|
|
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:
|
2026-03-09 14:46:56 +08:00
|
|
|
for key, value in payload.items():
|
2026-03-10 14:25:21 +08:00
|
|
|
if key in _ALLOWED_RUNTIME_KEYS:
|
2026-03-09 14:46:56 +08:00
|
|
|
_runtime_overrides[key] = value
|
|
|
|
|
get_settings.cache_clear()
|
|
|
|
|
return get_settings()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def public_settings() -> dict:
|
|
|
|
|
s = get_settings()
|
|
|
|
|
return {
|
2026-03-10 14:25:21 +08:00
|
|
|
"ocr_provider": s.ocr_provider,
|
|
|
|
|
"ocr_model": get_ocr_model(),
|
|
|
|
|
"inference_provider": s.inference_provider,
|
|
|
|
|
"inference_model": get_inference_model(),
|
2026-03-09 14:46:56 +08:00
|
|
|
"providers": ["openai", "anthropic", "deepseek", "custom_openai"],
|
2026-03-10 14:25:21 +08:00
|
|
|
"provider_defaults": {
|
2026-03-09 14:46:56 +08:00
|
|
|
"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),
|
|
|
|
|
},
|
|
|
|
|
}
|