Files
ai-video-fullstack/backend/services/openai_realtime/events.py
Xin Wang 86639692ba feat: implement OpenAI-compatible Realtime API with authentication and management features
- Added support for public Realtime API, including new routes for managing API keys and handling WebRTC connections.
- Introduced RealtimeApiKey model and associated CRUD operations for admin management of API keys.
- Implemented authentication mechanisms for API keys and client secrets.
- Enhanced environment configuration with new secrets for Realtime API.
- Created OpenAIRealtime session management and event processing for real-time interactions.
- Updated schemas and settings to accommodate new features and ensure compatibility with existing systems.
2026-08-11 10:05:55 +08:00

116 lines
3.5 KiB
Python

"""Small, explicit validation helpers for the supported Realtime event subset."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from uuid import uuid4
SUPPORTED_CLIENT_EVENTS = {
"session.update",
"conversation.item.create",
"conversation.item.truncate",
"input_audio_buffer.append",
"input_audio_buffer.commit",
"input_audio_buffer.clear",
"output_audio_buffer.clear",
"response.create",
"response.cancel",
"x.interactive_media.capabilities.update",
"x.interactive_media.session.variables.update",
}
SUPPORTED_CAPABILITIES = {
"dynamic_variables",
"workflow_events",
"handoff",
"video_track",
}
@dataclass
class RealtimeEventError(ValueError):
message: str
code: str = "invalid_request_error"
param: str | None = None
event_id: str | None = None
def __str__(self) -> str:
return self.message
def require_event(message: object) -> dict[str, Any]:
if not isinstance(message, dict):
raise RealtimeEventError("Event must be a JSON object")
event_type = str(message.get("type") or "")
if event_type not in SUPPORTED_CLIENT_EVENTS:
raise RealtimeEventError(
f"Unsupported client event: {event_type or '<missing>'}",
code="invalid_event",
param="type",
event_id=str(message.get("event_id") or "") or None,
)
return message
def assistant_id_from_model(model: object) -> str:
value = str(model or "").strip()
if not value.startswith("assistant:") or not value.removeprefix("assistant:"):
raise RealtimeEventError(
"model must use assistant:asst_xxx",
code="invalid_model",
param="session.model",
)
return value.removeprefix("assistant:")
def normalize_turn_detection(value: object) -> dict[str, Any] | None:
if value is None:
return None
if not isinstance(value, dict) or value.get("type") != "server_vad":
raise RealtimeEventError(
"turn_detection must be server_vad or null",
param="session.audio.input.turn_detection",
)
try:
threshold = float(value.get("threshold", 0.7))
prefix_ms = int(value.get("prefix_padding_ms", 200))
silence_ms = int(value.get("silence_duration_ms", 600))
except (TypeError, ValueError) as exc:
raise RealtimeEventError(
"Invalid server_vad threshold, padding, or silence duration",
param="session.audio.input.turn_detection",
) from exc
if (
not 0 <= threshold <= 1
or not 0 <= prefix_ms <= 5000
or not 100 <= silence_ms <= 10000
):
raise RealtimeEventError(
"Invalid server_vad threshold, padding, or silence duration",
param="session.audio.input.turn_detection",
)
return {
"type": "server_vad",
"threshold": threshold,
"prefix_padding_ms": prefix_ms,
"silence_duration_ms": silence_ms,
"create_response": bool(value.get("create_response", True)),
"interrupt_response": bool(value.get("interrupt_response", True)),
}
def error_event(error: RealtimeEventError) -> dict[str, Any]:
return {
"type": "error",
"event_id": f"event_{uuid4().hex}",
"error": {
"type": "invalid_request_error",
"code": error.code,
"message": error.message,
"param": error.param,
"event_id": error.event_id,
},
}