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.
This commit is contained in:
Xin Wang
2026-08-11 10:05:55 +08:00
parent cf32ea605d
commit 86639692ba
28 changed files with 3286 additions and 128 deletions
@@ -41,13 +41,14 @@ from services.pipecat.realtime_tools import (
RealtimeToolDispatcher,
RealtimeToolSession,
)
from services.realtime.protocol import RealtimeProviderControlFrame
DEFAULT_QWEN_AUDIO_REALTIME_MODEL = "qwen-audio-3.0-realtime-flash"
DEFAULT_QWEN_AUDIO_REALTIME_VOICE = "longanqian"
QWEN_INPUT_SAMPLE_RATE = 16_000
QWEN_OUTPUT_SAMPLE_RATE = 24_000
SUPPORTED_TURN_DETECTION_MODES = frozenset({"server_vad", "smart_turn"})
SUPPORTED_TURN_DETECTION_MODES = frozenset({"none", "server_vad", "smart_turn"})
ExtraEventHandler = Callable[[dict[str, Any]], Awaitable[None] | None]
SpeechStartedHandler = Callable[[], Awaitable[None]]
@@ -161,6 +162,15 @@ class QwenAudioRealtimeService(AIService):
async def process_frame(self, frame: Frame, direction: FrameDirection) -> None:
await super().process_frame(frame, direction)
if isinstance(frame, RealtimeProviderControlFrame):
if frame.action == "commit_audio":
await self.commit_audio_buffer()
elif frame.action == "clear_audio":
await self.clear_audio_buffer()
else:
await self.request_response()
return
if isinstance(frame, InputAudioRawFrame):
if (
frame.sample_rate != self._input_sample_rate
@@ -234,6 +244,20 @@ class QwenAudioRealtimeService(AIService):
)
await self._send_event({"type": "response.create"})
async def commit_audio_buffer(self) -> None:
await self._send_event({"type": "input_audio_buffer.commit"})
async def clear_audio_buffer(self) -> None:
await self._send_event({"type": "input_audio_buffer.clear"})
async def update_turn_detection(self, config: dict[str, Any] | None) -> None:
"""Apply OpenAI PTT before provider startup; reject unsafe live changes."""
mode = "none" if config is None else "server_vad"
if self._session_ready.is_set():
raise ValueError("Qwen Realtime 不支持会话建立后切换轮次检测")
self._turn_detection_mode = mode
async def wait_for_response_boundary(self) -> None:
"""Wait until Qwen has finished or cancelled the current response."""
await self._response_done.wait()
@@ -355,7 +379,9 @@ class QwenAudioRealtimeService(AIService):
"tool_choice": "auto",
}
def _turn_detection_config(self) -> dict[str, Any]:
def _turn_detection_config(self) -> dict[str, Any] | None:
if self._turn_detection_mode == "none":
return None
if self._turn_detection_mode == "smart_turn":
return {"type": "smart_turn"}
return {