Files
vllm-omni/vllm_omni/experimental/fullduplex/openai/realtime_session.py
T
05b794f29b [Full Duplex] Feat: Support Full-Duplex realtime runtime & add MiniCPM-o 4.5 demo (#3907)
Signed-off-by: Sy03 <1370724210@qq.com>
Signed-off-by: lishunyang12 <lishunyang03@gmail.com>
Signed-off-by: Yueqian Lin <linyueqian@outlook.com>
Co-authored-by: lishunyang12 <lishunyang03@gmail.com>
Co-authored-by: Yueqian Lin <linyueqian@outlook.com>
Co-authored-by: StormMelody <22371184@zju.edu.cn>
Co-authored-by: Gao Han <hgaoaf@connect.ust.hk>
Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
2026-07-23 16:45:18 +08:00

140 lines
5.7 KiB
Python

from __future__ import annotations
import json
from typing import Any
from uuid import uuid4
from fastapi import WebSocket
from vllm_omni.experimental.fullduplex.openai import realtime_state as _state
from vllm_omni.experimental.fullduplex.openai.realtime_input import (
RealtimeInputTranslator,
)
from vllm_omni.experimental.fullduplex.openai.realtime_output import (
RealtimeOutputProjector,
)
REALTIME_ERROR_TYPES_BY_CODE = _state.REALTIME_ERROR_TYPES_BY_CODE
REALTIME_INPUT_AUDIO_FORMATS = _state.REALTIME_INPUT_AUDIO_FORMATS
REALTIME_OUTPUT_AUDIO_FORMATS = _state.REALTIME_OUTPUT_AUDIO_FORMATS
RealtimeSessionState = _state.RealtimeSessionState
_RealtimeResponseState = _state._RealtimeResponseState
class NativeRealtimeSessionProtocol(
RealtimeInputTranslator,
RealtimeOutputProjector,
_state.RealtimeStateOwner,
):
"""Compatibility facade for one native Realtime protocol session.
Input translation, output projection, and mutable state have separate
owners. The facade retains the established import path and private helper
surface used by serving and protocol contract tests.
"""
def __init__(self, query_params: Any) -> None:
self._state = RealtimeSessionState.from_query_params(query_params)
def bind_sender(self, send_realtime_json) -> None:
self._send_realtime_json = send_realtime_json
def _default_session_payload(self) -> dict[str, object]:
payload: dict[str, object] = {
"model": self._default_model,
"session_id": self._default_session_id,
}
if self._default_extra_body:
payload["extra_body"] = dict(self._default_extra_body)
return payload
async def receive_internal_event_text(self, websocket: WebSocket) -> str:
if not self._pending_outbound.empty():
return json.dumps(await self._pending_outbound.get())
if not self._opened and not self._resume_only and not self._autostarted_default_session and self._default_model:
self._opened = True
self._autostarted_default_session = True
return json.dumps(self._session_create_from_realtime(self._default_session_payload()))
while True:
if not self._pending_outbound.empty():
return json.dumps(await self._pending_outbound.get())
raw = await websocket.receive_text()
try:
event = json.loads(raw)
except json.JSONDecodeError:
return raw
if not isinstance(event, dict):
return raw
if not self._opened and event.get("type") == "session.resume":
self._opened = True
translated = await self._to_duplex_event(event)
if translated is None:
if not self._pending_outbound.empty():
return json.dumps(await self._pending_outbound.get())
continue
return json.dumps(translated)
if not self._opened and self._resume_only:
translated = await self._to_duplex_event(event)
if translated is None:
if not self._pending_outbound.empty():
return json.dumps(await self._pending_outbound.get())
continue
return json.dumps(translated)
if not self._opened and event.get("type") != "session.update":
self._opened = True
await self._pending_outbound.put(self._session_create_from_realtime(self._default_session_payload()))
translated = await self._to_duplex_event(event)
if translated is not None:
await self._send_realtime_input_ack(event)
await self._pending_outbound.put(translated)
return json.dumps(await self._pending_outbound.get())
translated = await self._to_duplex_event(event)
if translated is None:
if not self._pending_outbound.empty():
return json.dumps(await self._pending_outbound.get())
continue
await self._send_realtime_input_ack(event)
return json.dumps(translated)
def encode_outbound_event(self, data: dict[str, Any]) -> list[dict[str, object]]:
payloads = self._from_duplex_event(data)
for payload in payloads:
self._attach_event_id(payload)
return payloads
@staticmethod
def _attach_event_id(payload: dict[str, object]) -> None:
payload.setdefault("event_id", f"event_{uuid4().hex}")
async def _send_realtime_payload(self, payload: dict[str, object]) -> None:
self._attach_event_id(payload)
if (
self._hold_realtime_output_until_session_created
and payload.get("type") != "error"
and payload.get("type") != "session.created"
):
self._held_realtime_payloads.append(payload)
return
if self._send_realtime_json is None:
raise RuntimeError("Native Realtime sender is not bound")
await self._send_realtime_json(payload)
@staticmethod
def _realtime_error_payload(
code: str,
message: str,
*,
event_id: object | None = None,
param: object | None = None,
) -> dict[str, object]:
error: dict[str, object] = {
"type": REALTIME_ERROR_TYPES_BY_CODE.get(code, "invalid_request_error"),
"code": code,
"message": message,
}
if isinstance(event_id, str) and event_id:
error["event_id"] = event_id
if isinstance(param, str) and param:
error["param"] = param
return {"type": "error", "error": error}