HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
1"""FastAPI application — `/api/v1` (API 1.1). Loopback-only in production (Next.js proxies `/api/v1/*`); docs at `/api/v1/docs`."""2from __future__ import annotations34import asyncio5import logging6from contextlib import asynccontextmanager7from datetime import UTC, datetime8from typing import Any910from fastapi import FastAPI, Request11from fastapi.exceptions import RequestValidationError12from fastapi.middleware.cors import CORSMiddleware13from fastapi.middleware.gzip import GZipMiddleware14from starlette.exceptions import HTTPException as StarletteHTTPException1516import aiatlas17from aiatlas.api.common import AtlasJSONResponse18from aiatlas.api.etag import ETagMiddleware19from aiatlas.api.routers import (20 admin,21 admin_quality,22 benchmarks,23 changes,24 claims,25 companies,26 compare,27 deployments,28 diff,29 entities,30 explore,31 families,32 graph,33 hardware,34 intelligence,35 misc,36 models,37 papers,38 prices,39 providers,40 search,41 sources,42 stats,43 timeline,44)45from aiatlas.config import settings46from aiatlas.db import connection, dispose, fetch_val47from aiatlas.logging import setup_logging48from aiatlas.services import cache49from aiatlas.services.llm import gateway5051log = logging.getLogger("aiatlas.api")52API_VERSION = "1.1"535455@asynccontextmanager56async def lifespan(app: FastAPI): # type: ignore[no-untyped-def]57 setup_logging(service="aia-api")58 settings.ensure_dirs()59 log.info("api started", extra={"version": aiatlas.__version__, "api": API_VERSION, "env": settings.app_env, "port": settings.api_port})60 yield61 await cache.close()62 await dispose()636465app = FastAPI(title="AI Atlas API", version=aiatlas.__version__, lifespan=lifespan, default_response_class=AtlasJSONResponse,66 docs_url="/api/v1/docs", redoc_url=None, openapi_url="/api/v1/openapi.json",67 description="The global intelligence layer for artificial intelligence: models, companies, papers, providers, prices, benchmarks, hardware — "68 "with provenance and history on every fact. Contract: docs/API.md (API 1.1, additive over v1).")6970# Middleware order: the LAST added is the OUTERMOST. ETag must be inside GZip (hash of the uncompressed body), so it is added first.71app.add_middleware(ETagMiddleware)72app.add_middleware(GZipMiddleware, minimum_size=1024)73_origins = {settings.site_url, "https://www.ai-atlas.co", "https://ai-atlas.co", "http://localhost:8320", "http://127.0.0.1:8320", "http://localhost:8330", "http://127.0.0.1:8330"}74app.add_middleware(CORSMiddleware, allow_origins=sorted(o for o in _origins if o), allow_methods=["GET", "POST", "PATCH", "OPTIONS"],75 allow_headers=["*"], expose_headers=["etag", "cache-control", "x-api-version"], max_age=600)767778@app.middleware("http")79async def security_headers(request: Request, call_next): # type: ignore[no-untyped-def]80 try:81 response = await call_next(request)82 except Exception:83 log.exception("unhandled error", extra={"route": request.url.path})84 return AtlasJSONResponse({"detail": "internal server error"}, status_code=500)85 response.headers["x-content-type-options"] = "nosniff"86 response.headers["referrer-policy"] = "strict-origin-when-cross-origin"87 response.headers["x-api-version"] = API_VERSION88 return response899091@app.exception_handler(StarletteHTTPException)92async def http_error(request: Request, exc: StarletteHTTPException): # type: ignore[no-untyped-def]93 detail = exc.detail if isinstance(exc.detail, str) else str(exc.detail)94 return AtlasJSONResponse({"detail": detail}, status_code=exc.status_code, headers=dict(exc.headers or {}))959697@app.exception_handler(RequestValidationError)98async def validation_error(request: Request, exc: RequestValidationError): # type: ignore[no-untyped-def]99 errs = exc.errors()[:5]100 msg = "; ".join(f"{'.'.join(str(x) for x in e.get('loc', []) if x not in ('query', 'body', 'path'))}: {e.get('msg')}" for e in errs) or "invalid request"101 return AtlasJSONResponse({"detail": msg, "errors": [{"loc": e.get("loc"), "msg": e.get("msg"), "type": e.get("type")} for e in errs]}, status_code=422)102103104@app.exception_handler(Exception)105async def unhandled(request: Request, exc: Exception): # type: ignore[no-untyped-def]106 log.exception("unhandled error", extra={"route": request.url.path})107 return AtlasJSONResponse({"detail": "internal server error"}, status_code=500)108109110async def _health() -> dict[str, Any]:111 db_ok = redis_ok = False112 try:113 async with connection() as conn:114 db_ok = (await asyncio.wait_for(fetch_val(conn, "select 1"), timeout=3)) == 1115 except Exception: # noqa: BLE001116 db_ok = False117 try:118 redis_ok = bool(await asyncio.wait_for(cache.redis().ping(), timeout=2))119 except Exception: # noqa: BLE001120 redis_ok = False121 llm: dict[str, Any] = {"available": gateway.available}122 if gateway.available:123 cached = await cache.cache_get("health:llm")124 if cached is None:125 try:126 cached = {"reachable": bool(await asyncio.wait_for(gateway.engine.health(), timeout=2.5))}127 except Exception: # noqa: BLE001128 cached = {"reachable": False}129 await cache.cache_set("health:llm", cached, 300)130 llm.update(cached)131 return {"status": "ok" if db_ok else "degraded", "version": aiatlas.__version__, "api_version": API_VERSION, "db": db_ok, "redis": redis_ok, "llm": llm, "time": datetime.now(UTC)}132133134@app.get("/health", tags=["health"])135async def health_root() -> dict[str, Any]:136 return await _health()137138139@app.get("/api/v1/health", tags=["health"])140async def health_v1() -> dict[str, Any]:141 return await _health()142143144# Order matters only where a literal path and a `{param}` path share a prefix — literal routes live in the same router and are declared first.145# `intelligence`, `families`, `graph`, `claims`, `deployments` and `misc` mount literal paths under /api/v1 and must precede `entities` (/{slug_or_id}).146for r in (stats, search, models, companies, papers, providers, prices, deployments, benchmarks, hardware, explore, changes, timeline, compare, diff, sources,147 intelligence, families, graph, claims, misc, entities, admin, admin_quality):148 app.include_router(r.router)149150__all__ = ["API_VERSION", "app"]151