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