cohorta/backend/app/models/app_settings.py
Vadym Samoilenko d92a099ade feat(ai-config): wire admin UI to LLM service — endpoint/key/model from DB
- _get_runtime_config(): reads active provider endpoint, api_key, main/mini
  model from app_settings (60s cache), falls back to env vars
- get_azure_client() now async, accepts cfg dict
- All generate_* methods call _get_runtime_config() per invocation so DB
  changes take effect without restart
- app_settings: _seed_from_env() backfills empty endpoint/api_key from env
  vars on first load so the admin UI shows current values immediately

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 13:10:40 +01:00

104 lines
3.5 KiB
Python

"""
App-wide configuration stored in MongoDB.
Single document with _id='config'. Cached in-memory for 60 seconds.
"""
import time
import logging
from typing import Any
from app.db import get_db
logger = logging.getLogger(__name__)
_cache: dict[str, Any] = {}
_cache_ts: float = 0
_CACHE_TTL = 60 # seconds
DEFAULTS = {
"_id": "config",
"persona_cost": 2,
"run_cost": 40,
"trial_grant": 50,
"credit_packs": [
{"id": "starter", "name": "Starter", "price_usd": 49, "credits": 50},
{"id": "pro", "name": "Pro", "price_usd": 199, "credits": 220, "popular": True},
{"id": "scale", "name": "Scale", "price_usd": 499, "credits": 600},
],
"active_provider": "azure_openai",
"active_main_model": "gpt-5.4",
"active_mini_model": "gpt-5.4-mini",
"ai_providers": [
{
"id": "azure_openai",
"name": "Azure OpenAI",
"enabled": True,
"endpoint": "", # populated from env var on first load
"api_key": "", # populated from env var on first load
"models": [
{"id": "gpt-5.4", "display_name": "GPT-5.4", "role": "main", "enabled": True},
{"id": "gpt-5.4-mini", "display_name": "GPT-5.4 Mini", "role": "mini", "enabled": True},
],
},
],
}
def _seed_from_env(doc: dict) -> dict:
"""Backfill endpoint/api_key from env vars when DB fields are still empty."""
import os
changed = False
for p in doc.get("ai_providers", []):
if not p.get("endpoint"):
p["endpoint"] = os.environ.get("AZURE_AI_ENDPOINT", "")
changed = True
if not p.get("api_key"):
p["api_key"] = os.environ.get("AZURE_AI_API_KEY", "")
changed = True
if not doc.get("active_main_model"):
doc["active_main_model"] = os.environ.get("AZURE_AI_MODEL_MAIN", "gpt-5.4")
changed = True
if not doc.get("active_mini_model"):
doc["active_mini_model"] = os.environ.get("AZURE_AI_MODEL_MINI", "gpt-5.4-mini")
changed = True
return doc if changed else doc
async def get_settings() -> dict:
global _cache, _cache_ts
if _cache and (time.monotonic() - _cache_ts) < _CACHE_TTL:
return _cache
db = await get_db()
doc = await db.app_settings.find_one({"_id": "config"})
if not doc:
await db.app_settings.insert_one(DEFAULTS.copy())
doc = DEFAULTS.copy()
else:
# Fill in any keys added to DEFAULTS since the document was first created
missing = {k: v for k, v in DEFAULTS.items() if k not in doc}
if missing:
await db.app_settings.update_one({"_id": "config"}, {"$set": missing})
doc.update(missing)
# Backfill endpoint/api_key from env if still empty (first run after feature added)
before = {p['id']: (p.get('endpoint'), p.get('api_key')) for p in doc.get('ai_providers', [])}
_seed_from_env(doc)
after = {p['id']: (p.get('endpoint'), p.get('api_key')) for p in doc.get('ai_providers', [])}
if before != after:
await db.app_settings.update_one({"_id": "config"}, {"$set": {"ai_providers": doc["ai_providers"]}})
_cache = doc
_cache_ts = time.monotonic()
return doc
async def update_settings(fields: dict) -> dict:
global _cache, _cache_ts
db = await get_db()
await db.app_settings.update_one(
{"_id": "config"},
{"$set": fields},
upsert=True,
)
_cache = {} # invalidate cache
return await get_settings()