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
+90 -18
View File
@@ -8,9 +8,15 @@ from typing import Any
from uuid import uuid4
from db.session import SessionLocal
from fastapi import APIRouter, Request, WebSocket
from fastapi import APIRouter, Depends, Request, WebSocket
from fastapi.responses import JSONResponse, Response
from loguru import logger
from services.openai_api import (
PUBLIC_API_BEARER,
PUBLIC_ERROR_RESPONSES,
REQUEST_ID_RESPONSE_HEADERS,
openai_error_response,
)
from starlette.websockets import WebSocketState
from services.openai_realtime.auth import (
@@ -27,6 +33,10 @@ from services.openai_realtime.events import (
assistant_id_from_model,
)
from services.openai_realtime.session import OpenAIRealtimeSession
from services.openai_realtime.schemas import (
RealtimeClientSecretBody,
RealtimeClientSecretResponse,
)
from services.openai_realtime.webrtc import OpenAIRealtimeWebRTCConnection
from services.openai_realtime.websocket import build_openai_websocket_transport
from services.pipecat.pipeline import run_pipeline
@@ -44,17 +54,18 @@ router = APIRouter(prefix="/v1/realtime", tags=["openai-realtime"])
_webrtc_peers: dict[str, OpenAIRealtimeWebRTCConnection] = {}
def _api_error(message: str, *, code: str, status_code: int) -> JSONResponse:
return JSONResponse(
def _api_error(
message: str,
*,
code: str,
status_code: int,
param: str | None = None,
) -> JSONResponse:
return openai_error_response(
message,
code=code,
status_code=status_code,
content={
"error": {
"type": "invalid_request_error",
"code": code,
"message": message,
"param": None,
}
},
param=param,
)
@@ -124,14 +135,25 @@ async def _build_session(
return state
@router.post("/client_secrets")
async def create_realtime_client_secret(request: Request):
@router.post(
"/client_secrets",
response_model=RealtimeClientSecretResponse,
responses={
200: {"headers": REQUEST_ID_RESPONSE_HEADERS},
**PUBLIC_ERROR_RESPONSES,
},
dependencies=[Depends(PUBLIC_API_BEARER)],
summary="Create a short-lived Realtime client secret",
)
async def create_realtime_client_secret(
body: RealtimeClientSecretBody,
request: Request,
):
try:
credential = await _credential(request)
if credential.ephemeral:
raise RealtimeAuthError("A long-lived API key is required")
body = await request.json()
session = _session_payload(body)
session = _session_payload(body.model_dump(exclude_unset=True))
_reject_locked_creation_fields(session)
assistant_id = assistant_id_from_model(session.get("model"))
# Resolve now so invalid models/configurations fail before a browser gets a token.
@@ -155,13 +177,62 @@ async def create_realtime_client_secret(request: Request):
except RealtimeAuthError as exc:
return _api_error(str(exc), code="invalid_api_key", status_code=401)
except RealtimeEventError as exc:
return _api_error(str(exc), code=exc.code, status_code=400)
return _api_error(
str(exc), code=exc.code, status_code=400, param=exc.param
)
except ValueError as exc:
code = "invalid_model" if "助手不存在" in str(exc) else "invalid_session"
return _api_error(str(exc), code=code, status_code=400)
@router.post("/calls")
@router.post(
"/calls",
response_class=Response,
responses={
200: {
"description": "WebRTC SDP answer.",
"headers": {
**REQUEST_ID_RESPONSE_HEADERS,
"Location": {
"description": "Relative URL of the created Realtime call.",
"schema": {"type": "string"},
},
},
"content": {
"application/sdp": {
"schema": {"type": "string", "format": "sdp"},
"example": "v=0\\r\\no=- ...",
}
},
},
**PUBLIC_ERROR_RESPONSES,
},
dependencies=[Depends(PUBLIC_API_BEARER)],
openapi_extra={
"requestBody": {
"required": True,
"content": {
"multipart/form-data": {
"schema": {
"type": "object",
"required": ["sdp", "session"],
"properties": {
"sdp": {"type": "string", "format": "sdp"},
"session": {
"type": "string",
"description": "JSON-encoded Realtime session.",
},
},
}
},
"application/sdp": {
"schema": {"type": "string", "format": "sdp"}
},
},
}
},
summary="Create a Realtime WebRTC call",
)
async def create_realtime_call(request: Request):
try:
credential = await _credential(request)
@@ -206,7 +277,8 @@ async def create_realtime_call(request: Request):
return _api_error(str(exc), code="invalid_api_key", status_code=401)
except (RealtimeEventError, json.JSONDecodeError, UnicodeDecodeError) as exc:
code = exc.code if isinstance(exc, RealtimeEventError) else "invalid_request_error"
return _api_error(str(exc), code=code, status_code=400)
param = exc.param if isinstance(exc, RealtimeEventError) else None
return _api_error(str(exc), code=code, status_code=400, param=param)
except ValueError as exc:
code = "invalid_model" if "助手不存在" in str(exc) else "invalid_session"
return _api_error(str(exc), code=code, status_code=400)