Coverage for gco/services/inference_api.py: 85.71%
42 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-30 21:22 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-30 21:22 +0000
1"""Dedicated authenticated streaming proxy API for managed inference endpoints."""
3from __future__ import annotations
5import logging
6import os
7from typing import Any
9from fastapi import FastAPI, Request
10from fastapi.responses import JSONResponse
12from gco.services.auth_middleware import AuthenticationMiddleware
13from gco.services.request_size_middleware import (
14 DEFAULT_MAX_REQUEST_BODY_BYTES,
15 RequestSizeLimitMiddleware,
16)
17from gco.services.service_metrics import mount_metrics
19logging.basicConfig(
20 level=logging.INFO,
21 format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
22)
23logger = logging.getLogger(__name__)
25DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT_SECONDS = 900
27app = FastAPI(
28 title="GCO Inference Proxy API",
29 description="Authenticated streaming reverse proxy for managed GCO inference endpoints",
30 version="1.0.0",
31)
33# Starlette executes middleware in reverse registration order. The size limit
34# runs first so an oversized request is rejected before secret retrieval or
35# HMAC work; AuthenticationMiddleware still validates the exact cached body.
36app.add_middleware(AuthenticationMiddleware)
37_max_body_bytes = int(os.getenv("MAX_REQUEST_BODY_BYTES", str(DEFAULT_MAX_REQUEST_BODY_BYTES)))
38app.add_middleware(RequestSizeLimitMiddleware, max_body_bytes=_max_body_bytes)
40mount_metrics(app, "inference-proxy")
42from gco.services.api_routes.inference_proxy import router as inference_proxy_router # noqa: E402
44app.include_router(inference_proxy_router)
47@app.get("/", tags=["Info"])
48async def root() -> dict[str, Any]:
49 """Return a small authenticated service descriptor."""
50 return {
51 "service": "GCO Inference Proxy API",
52 "version": "1.0.0",
53 "status": "running",
54 "region": os.getenv("REGION", "unknown"),
55 }
58@app.get("/healthz", tags=["Health"])
59async def kubernetes_health_check() -> dict[str, str]:
60 """Kubernetes and ALB liveness probe."""
61 return {"status": "ok"}
64@app.get("/readyz", tags=["Health"])
65async def kubernetes_readiness_check() -> dict[str, str]:
66 """Readiness probe; external dependencies are resolved per request."""
67 return {"status": "ready"}
70@app.exception_handler(Exception)
71async def global_exception_handler(request: Request, exc: Exception) -> JSONResponse:
72 """Log unexpected failures without exposing internal routing details."""
73 logger.exception(
74 "Unhandled inference proxy exception for %s %s", request.method, request.url.path
75 )
76 return JSONResponse(status_code=500, content={"error": "Internal server error"})
79def create_app() -> FastAPI:
80 """Return the process-wide FastAPI application."""
81 return app
84def _run_server() -> None:
85 """Run Uvicorn with the same drain budget declared by the pod manifest."""
86 import uvicorn
88 host = os.getenv("HOST", "0.0.0.0") # nosec B104 — container listener
89 port = int(os.getenv("PORT", "8080"))
90 log_level = os.getenv("LOG_LEVEL", "info").lower()
91 graceful_shutdown_seconds = int(
92 os.getenv(
93 "GRACEFUL_SHUTDOWN_TIMEOUT_SECONDS",
94 str(DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT_SECONDS),
95 )
96 )
97 logger.info("Starting Inference Proxy API on %s:%d", host, port)
98 uvicorn.run(
99 "gco.services.inference_api:app",
100 host=host,
101 port=port,
102 log_level=log_level,
103 reload=False,
104 timeout_graceful_shutdown=graceful_shutdown_seconds,
105 )
108if __name__ == "__main__":
109 _run_server()