""" Reads ai_service_config.json from the storage-service config bucket. 30-second TTL cache + env var overrides (dev credentials stay out of git). Env var overrides (all optional): AI_PROVIDER — "lmstudio" | "ollama" | "anthropic" LMSTUDIO_BASE_URL, LMSTUDIO_API_KEY, LMSTUDIO_MODEL OLLAMA_BASE_URL, OLLAMA_MODEL, OLLAMA_API_KEY ANTHROPIC_API_KEY, ANTHROPIC_MODEL """ import json import os import time from copy import deepcopy import httpx from app.core.config import settings _CONFIG_KEY = "ai_service_config.json" _DEFAULT_CONFIG: dict = { "provider": "lmstudio", "timeout_seconds": 60, "max_retries": 2, "anthropic": {"api_key": "", "model": "claude-haiku-4-5-20251001"}, "ollama": {"base_url": "http://host.docker.internal:11434/v1", "model": "llama3.2", "api_key": "ollama"}, "lmstudio": {"base_url": "http://host.docker.internal:1234/v1", "model": "gemma-4-e4b-it", "api_key": "lm-studio"}, } _cache: dict | None = None _cache_at: float = 0.0 _CACHE_TTL = 30.0 def _storage_url() -> str: return f"{settings.STORAGE_SERVICE_URL}/objects/config/{_CONFIG_KEY}" async def _fetch_config() -> dict: """Fetch config from storage-service. Returns defaults if not found.""" async with httpx.AsyncClient(timeout=10.0) as client: resp = await client.get(_storage_url()) if resp.status_code == 404: return deepcopy(_DEFAULT_CONFIG) resp.raise_for_status() return resp.json() def _apply_env_overrides(config: dict) -> dict: cfg = deepcopy(config) if v := os.environ.get("AI_PROVIDER"): cfg["provider"] = v lms = cfg.setdefault("lmstudio", {}) if v := os.environ.get("LMSTUDIO_BASE_URL"): lms["base_url"] = v if v := os.environ.get("LMSTUDIO_API_KEY"): lms["api_key"] = v if v := os.environ.get("LMSTUDIO_MODEL"): lms["model"] = v oll = cfg.setdefault("ollama", {}) if v := os.environ.get("OLLAMA_BASE_URL"): oll["base_url"] = v if v := os.environ.get("OLLAMA_MODEL"): oll["model"] = v if v := os.environ.get("OLLAMA_API_KEY"): oll["api_key"] = v ant = cfg.setdefault("anthropic", {}) if v := os.environ.get("ANTHROPIC_API_KEY"): ant["api_key"] = v if v := os.environ.get("ANTHROPIC_MODEL"): ant["model"] = v return cfg async def load_ai_config() -> dict: global _cache, _cache_at now = time.monotonic() if _cache is not None and (now - _cache_at) < _CACHE_TTL: return _cache raw = await _fetch_config() data = _apply_env_overrides(raw) _cache = data _cache_at = now return data