- 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.
116 lines
3.5 KiB
Python
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,
|
|
},
|
|
}
|