feat: harden public OpenAI API contracts
This commit is contained in:
+83
-2
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user