Files
ai-video-fullstack/backend/services/openai_api.py
T
Xin Wang 2649d64b55
Quality gate / backend (push) Canceled after 0s
Quality gate / frontend (push) Canceled after 0s
feat: harden public OpenAI API contracts
2026-09-07 14:25:48 +08:00

196 lines
5.6 KiB
Python

"""Shared HTTP primitives for the public OpenAI-compatible API."""
from __future__ import annotations
import re
from typing import Any, Literal
from uuid import uuid4
from fastapi.responses import JSONResponse
from fastapi.security import HTTPBearer
from pydantic import BaseModel, ConfigDict
from starlette.datastructures import Headers, MutableHeaders
from starlette.types import ASGIApp, Message, Receive, Scope, Send
PUBLIC_API_NAME = "OpenAI-compatible subset v1"
REQUEST_ID_HEADER = "X-Request-ID"
_VALID_REQUEST_ID = re.compile(r"^[A-Za-z0-9._:-]{1,128}$")
PUBLIC_API_BEARER = HTTPBearer(
auto_error=False,
scheme_name="PublicAPIKey",
description="Long-lived sk-rt-... API key or an endpoint-approved client secret.",
)
REQUEST_ID_RESPONSE_HEADERS = {
REQUEST_ID_HEADER: {
"description": "Request identifier generated by the server or supplied by the caller.",
"schema": {"type": "string"},
}
}
class OpenAIErrorDetail(BaseModel):
"""Error shape consumed by official OpenAI SDKs."""
model_config = ConfigDict(extra="forbid")
type: str = "invalid_request_error"
code: str
message: str
param: str | None = None
class OpenAIErrorResponse(BaseModel):
model_config = ConfigDict(extra="forbid")
error: OpenAIErrorDetail
class DeletedResource(BaseModel):
model_config = ConfigDict(extra="forbid")
id: str
object: Literal["response"] = "response"
deleted: Literal[True] = True
PUBLIC_ERROR_RESPONSES: dict[int | str, dict[str, Any]] = {
400: {
"model": OpenAIErrorResponse,
"description": "The request is unsupported or semantically invalid.",
},
401: {
"model": OpenAIErrorResponse,
"description": "The API credential is missing, invalid, or expired.",
},
404: {
"model": OpenAIErrorResponse,
"description": "The resource does not exist or belongs to another API key.",
},
409: {
"model": OpenAIErrorResponse,
"description": "The requested state transition conflicts with current state.",
},
422: {
"model": OpenAIErrorResponse,
"description": "The JSON body or query parameters do not match the schema.",
},
429: {
"model": OpenAIErrorResponse,
"description": "A request or concurrency limit was exceeded.",
},
503: {
"model": OpenAIErrorResponse,
"description": "The service is temporarily unavailable.",
},
500: {
"model": OpenAIErrorResponse,
"description": "The response failed because of an internal error.",
},
504: {
"model": OpenAIErrorResponse,
"description": "The response exceeded its generation timeout.",
},
}
_ERROR_EXAMPLES = {
400: ("unsupported_parameter", "The requested parameter is not supported."),
401: ("invalid_api_key", "Invalid or expired API key."),
404: ("response_not_found", "Response not found."),
409: ("response_not_ready", "The requested state transition conflicts."),
422: ("invalid_request_error", "Request does not match the API schema."),
429: ("rate_limit_exceeded", "The request limit was exceeded."),
500: ("internal_error", "The response failed unexpectedly."),
503: ("service_unavailable", "The service is temporarily unavailable."),
504: ("response_timeout", "Response timed out."),
}
def error_payload(
message: str,
*,
code: str,
param: str | None = None,
error_type: str = "invalid_request_error",
) -> dict[str, Any]:
return OpenAIErrorResponse(
error=OpenAIErrorDetail(
type=error_type,
code=code,
message=message,
param=param,
)
).model_dump()
for _status, _response in PUBLIC_ERROR_RESPONSES.items():
_code, _message = _ERROR_EXAMPLES[int(_status)]
_response["content"] = {
"application/json": {
"example": error_payload(
_message,
code=_code,
)
}
}
_response["headers"] = REQUEST_ID_RESPONSE_HEADERS
def openai_error_response(
message: str,
*,
code: str,
status_code: int,
param: str | None = None,
error_type: str = "invalid_request_error",
headers: dict[str, str] | None = None,
) -> JSONResponse:
return JSONResponse(
status_code=status_code,
content=error_payload(
message,
code=code,
param=param,
error_type=error_type,
),
headers=headers,
)
def normalize_request_id(value: str | None) -> str:
"""Reuse a safe caller ID or create one suitable for logs and support tickets."""
candidate = (value or "").strip()
if _VALID_REQUEST_ID.fullmatch(candidate):
return candidate
return f"req_{uuid4().hex}"
class RequestIDMiddleware:
"""Add a stable request ID without buffering SSE response bodies."""
def __init__(self, app: ASGIApp) -> None:
self.app = app
async def __call__(
self,
scope: Scope,
receive: Receive,
send: Send,
) -> None:
if scope["type"] != "http":
await self.app(scope, receive, send)
return
request_id = normalize_request_id(
Headers(scope=scope).get(REQUEST_ID_HEADER)
)
scope.setdefault("state", {})["request_id"] = request_id
async def send_with_request_id(message: Message) -> None:
if message["type"] == "http.response.start":
MutableHeaders(scope=message)[REQUEST_ID_HEADER] = request_id
await send(message)
await self.app(scope, receive, send_with_request_id)