feat: harden public OpenAI API contracts
Quality gate / backend (push) Canceled after 0s
Quality gate / frontend (push) Canceled after 0s

This commit is contained in:
Xin Wang
2026-09-07 14:25:48 +08:00
parent 8025ed8aa0
commit 2649d64b55
36 changed files with 16643 additions and 236 deletions
+83 -2
View File
@@ -24,6 +24,12 @@ from contextlib import asynccontextmanager
import settings
import uvicorn
from db.session import sync_default_tools, sync_interface_definitions
from loguru import logger
from services.openai_api import (
PUBLIC_API_NAME,
RequestIDMiddleware,
openai_error_response,
)
from services.knowledge import recover_interrupted_documents
from services.openai_responses.state import recover_interrupted_responses
from services.post_call.worker import recover_interrupted_analyses, run_analysis_worker
@@ -32,8 +38,14 @@ from services.test_runs.orchestrator import (
recover_interrupted_test_runs,
test_run_orchestrator,
)
from fastapi import FastAPI
from fastapi import FastAPI, Request
from fastapi.exception_handlers import (
http_exception_handler,
request_validation_exception_handler,
)
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
from starlette.exceptions import HTTPException as StarletteHTTPException
from routes import (
assistants,
@@ -45,6 +57,7 @@ from routes import (
mcp_servers,
model_registry,
node_types,
openai_models,
openai_realtime,
openai_responses,
realtime_api_keys,
@@ -85,7 +98,12 @@ async def lifespan(_app: FastAPI):
await voice_webrtc.shutdown_active_sessions()
app = FastAPI(title="AI Video Assistant 平台 - 后端", lifespan=lifespan)
app = FastAPI(
title="AI Video Assistant 平台 - 后端",
description=f"Public protocol: {PUBLIC_API_NAME}.",
version="1.0.0",
lifespan=lifespan,
)
app.add_middleware(
CORSMiddleware,
@@ -93,7 +111,69 @@ app.add_middleware(
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
expose_headers=["X-Request-ID", "X-Idempotent-Replayed", "Location"],
)
app.add_middleware(RequestIDMiddleware)
def _validation_param(error: dict) -> str | None:
location = list(error.get("loc") or [])
if location and location[0] in {"body", "query", "path", "header"}:
location.pop(0)
if not location or location == ["json"]:
return None
value = ""
for part in location:
if isinstance(part, int):
value += f"[{part}]"
else:
value += ("." if value else "") + str(part)
return value or None
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(
request: Request,
exc: RequestValidationError,
):
if not request.url.path.startswith("/v1/"):
return await request_validation_exception_handler(request, exc)
first = (exc.errors() or [{}])[0]
return openai_error_response(
str(first.get("msg") or "Request does not match the API schema"),
code="invalid_request_error",
param=_validation_param(first),
status_code=422,
)
@app.exception_handler(StarletteHTTPException)
async def public_http_exception_handler(
request: Request,
exc: StarletteHTTPException,
):
if not request.url.path.startswith("/v1/"):
return await http_exception_handler(request, exc)
code = "not_found" if exc.status_code == 404 else "http_error"
return openai_error_response(
str(exc.detail),
code=code,
status_code=exc.status_code,
)
@app.exception_handler(Exception)
async def public_unhandled_exception_handler(request: Request, exc: Exception):
if not request.url.path.startswith("/v1/"):
raise exc
logger.exception(
f"Unhandled public API error: {type(exc).__name__}"
)
return openai_error_response(
"The server encountered an internal error",
code="internal_error",
status_code=500,
)
app.include_router(health.router)
app.include_router(auth.router)
@@ -104,6 +184,7 @@ app.include_router(knowledge_bases.router)
app.include_router(mcp_servers.router)
app.include_router(model_registry.router)
app.include_router(node_types.router)
app.include_router(openai_models.router)
app.include_router(openai_realtime.router)
app.include_router(openai_responses.router)
app.include_router(realtime_api_keys.router)