InsuranceBot / backend /admin.py
rohitsar567's picture
Deploy: Stack A (NIM brain + Maverick judge + Sarvam voice). D-019.
13779e8 verified
Raw
History Blame
4.75 kB
"""Admin endpoints β€” IP+password gated. NEVER exposed to ordinary users.
The four endpoints:
GET /api/admin/health β€” full LLM health snapshot (every model)
POST /api/admin/probe β€” force a fresh probe of all models now
GET /api/admin/chain β€” current brain/fast/judge chain order
POST /api/admin/chain β€” reorder a chain (promote a model to primary)
Access gates (BOTH must pass):
1. Client IP must match ADMIN_IP_ALLOWLIST env (comma-separated)
2. Request must include `X-Admin-Password` header matching ADMIN_PASSWORD env
If either gate fails, returns 404 (not 401) β€” the endpoints DO NOT exist
for unauthorized callers. Even the existence of /api/admin/* is hidden.
Setup:
Add to Space secrets (Settings β†’ Variables and secrets):
ADMIN_PASSWORD = your-strong-password-here
ADMIN_IP_ALLOWLIST = your.public.ip.v4,your.other.ip
Find your IP at https://api.ipify.org/
"""
from __future__ import annotations
import asyncio
import json
import os
from pathlib import Path
from typing import Optional
from fastapi import APIRouter, HTTPException, Request, Header
from pydantic import BaseModel
from backend import llm_health
router = APIRouter()
def _ip_allowed(client_ip: str) -> bool:
"""Allowlist check. Empty/unset allowlist => deny everyone."""
allowed = os.environ.get("ADMIN_IP_ALLOWLIST", "").strip()
if not allowed:
return False
return client_ip in {ip.strip() for ip in allowed.split(",") if ip.strip()}
def _password_ok(supplied: Optional[str]) -> bool:
expected = os.environ.get("ADMIN_PASSWORD", "").strip()
if not expected:
return False # no password configured => deny
return supplied is not None and supplied == expected
def _check_admin(request: Request, password: Optional[str]) -> None:
"""Both gates must pass. Returns 404 (not 401) on failure to hide existence."""
# Honor X-Forwarded-For for HF Space proxy headers (HF Spaces sets this)
client_ip = request.headers.get("X-Forwarded-For", "").split(",")[0].strip()
if not client_ip:
client_ip = request.client.host if request.client else ""
if not _ip_allowed(client_ip) or not _password_ok(password):
raise HTTPException(status_code=404, detail="Not Found")
# --- Endpoints --------------------------------------------------------------
@router.get("/api/admin/health")
async def admin_health(
request: Request,
x_admin_password: Optional[str] = Header(default=None, alias="X-Admin-Password"),
):
_check_admin(request, x_admin_password)
return llm_health.status_summary()
@router.post("/api/admin/probe")
async def admin_probe(
request: Request,
x_admin_password: Optional[str] = Header(default=None, alias="X-Admin-Password"),
):
_check_admin(request, x_admin_password)
state = await llm_health.probe_all()
return {"probed": len(state), "summary": llm_health.status_summary()}
@router.get("/api/admin/chain")
async def admin_chain_get(
request: Request,
x_admin_password: Optional[str] = Header(default=None, alias="X-Admin-Password"),
):
_check_admin(request, x_admin_password)
from backend.providers.nvidia_nim_llm import (
BRAIN_CHAIN, FAST_BRAIN_CHAIN, JUDGE_CHAIN,
)
return {
"brain": BRAIN_CHAIN,
"fast_brain": FAST_BRAIN_CHAIN,
"judge": JUDGE_CHAIN,
}
class ChainReorderRequest(BaseModel):
role: str # 'brain' | 'fast_brain' | 'judge'
order: list[str] # new ordering of model ids
@router.post("/api/admin/chain")
async def admin_chain_set(
body: ChainReorderRequest,
request: Request,
x_admin_password: Optional[str] = Header(default=None, alias="X-Admin-Password"),
):
_check_admin(request, x_admin_password)
if body.role not in ("brain", "fast_brain", "judge"):
raise HTTPException(status_code=400, detail="role must be brain | fast_brain | judge")
# Mutate the in-memory chain
from backend.providers import nvidia_nim_llm as nim
name = {"brain": "BRAIN_CHAIN", "fast_brain": "FAST_BRAIN_CHAIN", "judge": "JUDGE_CHAIN"}[body.role]
setattr(nim, name, list(body.order))
# Persist for next process restart β€” write to data/admin_overrides.json
override_path = Path(__file__).resolve().parent.parent / "data" / "admin_overrides.json"
override_path.parent.mkdir(parents=True, exist_ok=True)
state = {}
if override_path.exists():
try:
state = json.loads(override_path.read_text())
except Exception:
state = {}
state[body.role] = body.order
override_path.write_text(json.dumps(state, indent=2) + "\n")
return {"ok": True, "role": body.role, "new_chain": body.order}