Files
vllm-omni/vllm_omni/experimental/fullduplex/openai/runtime_bridge.py
T
2026-07-31 03:17:25 -07:00

1312 lines
57 KiB
Python

from __future__ import annotations
import asyncio
import base64
import inspect
from collections.abc import Mapping
from dataclasses import asdict, is_dataclass
from enum import Enum
import numpy as np
from vllm.logger import init_logger
from vllm_omni.experimental.fullduplex.engine.duplex_control_client import DuplexControlRequestError
from vllm_omni.experimental.fullduplex.engine.duplex_runtime import duplex_data_plane_request_info
from vllm_omni.experimental.fullduplex.engine.messages import DuplexFence
from vllm_omni.experimental.fullduplex.openai.protocol import (
DuplexSession,
DuplexSessionState,
)
from vllm_omni.experimental.fullduplex.openai.runtime_adapter import (
coerce_int,
payload_turn_id,
)
logger = init_logger(__name__)
class NativeRuntimeBridgeMixin:
"""Bridge serving sessions to native runtime control and data-plane APIs."""
_NATIVE_RUNTIME_FENCE_CONTRACT = {
"open_duplex_session_async": ("fence",),
"append_duplex_input_async": ("fence",),
"signal_duplex_turn_async": ("fence", "next_fence"),
"close_duplex_session_async": ("fence",),
}
async def _open_runtime_session(self, session: DuplexSession, send_json) -> dict[str, object] | bool:
contract_error = self._native_runtime_contract_error(session)
if contract_error is not None:
await send_json(
{
"type": "error",
"code": "runtime_contract_invalid",
"error": contract_error,
}
)
return False
open_session = getattr(self._chat_service.engine_client, "open_duplex_session_async", None)
if not callable(open_session):
return True
try:
open_kwargs = {
"session_mode": "duplex",
"capabilities": session.capabilities.as_dict(),
"session_config": session.config.as_dict(),
"timeout": self._runtime_control_timeout_s(session),
}
if self._callable_accepts_keyword(open_session, "runtime_config"):
open_kwargs["runtime_config"] = dict(session.runtime_config)
if self._callable_accepts_keyword(open_session, "fence"):
open_kwargs["fence"] = DuplexFence(
session.session_id,
epoch=session.epoch,
turn_id=session.turn_id,
incarnation=session.incarnation,
)
result = await open_session(session.session_id, **open_kwargs)
except Exception as exc:
if isinstance(exc, DuplexControlRequestError) and exc.code == "resource_exhausted":
logger.info("Duplex runtime session admission rejected: %s", exc)
else:
logger.exception("Failed to open duplex runtime session: %s", exc)
await self._send_runtime_error(send_json, "runtime_open_failed", exc, session=session)
return False
if (
isinstance(result, dict)
and session.capabilities.implementation_level == "model_native_duplex"
and self._runtime_control_failed(result)
):
await send_json(
{
"type": "error",
"error": "Native duplex runtime is not available for this session",
"code": ("runtime_open_unsupported" if result.get("unsupported_count") else "runtime_open_failed"),
"runtime_control": self._redact_runtime_control_result(result),
}
)
return False
return result if isinstance(result, dict) else True
async def _append_runtime_input(
self,
session: DuplexSession,
payload: object,
*,
operation_id: str | None = None,
final: bool,
send_json,
mode: str = "append_tokens",
expected_epoch: int | None = None,
) -> tuple[bool, bool]:
if not session.capabilities.supports_input_append:
return True, False
append_input = getattr(self._chat_service.engine_client, "append_duplex_input_async", None)
if not callable(append_input):
return True, False
if expected_epoch is not None and session.epoch != expected_epoch:
return True, False
try:
append_kwargs = {
"mode": mode,
"payload": payload,
"final": final,
"timeout": self._runtime_control_timeout_s(session),
}
if operation_id is not None and self._callable_accepts_keyword(append_input, "operation_id"):
append_kwargs["operation_id"] = operation_id
if expected_epoch is not None and self._callable_accepts_keyword(append_input, "expected_epoch"):
append_kwargs["expected_epoch"] = expected_epoch
if self._callable_accepts_keyword(append_input, "fence"):
payload_turn = payload_turn_id(payload)
append_kwargs["fence"] = DuplexFence(
session.session_id,
epoch=session.epoch,
turn_id=(
payload_turn
if payload_turn is not None
else (
session.active_response_turn_id
if session.active_response_turn_id is not None
else session.turn_id
)
),
incarnation=session.incarnation,
)
if self._callable_accepts_keyword(append_input, "collect_outputs"):
append_kwargs["collect_outputs"] = False
result = await append_input(session.session_id, **append_kwargs)
except Exception as exc:
logger.exception("Failed to append duplex runtime input: %s", exc)
await self._send_runtime_error(send_json, "runtime_append_failed", exc, session=session)
return False, False
if isinstance(result, dict) and self._runtime_control_failed(result):
await self._send_runtime_control_error(
send_json,
"runtime_append_failed",
"Duplex runtime append failed",
result,
session=session,
)
return False, False
if expected_epoch is not None and session.epoch != expected_epoch:
return True, False
await self._send_runtime_control_if_needed(send_json, result, session=session)
request_id, _ = self._data_plane_request_info(result) if isinstance(result, dict) else (None, None)
if request_id is not None:
self._serving_runtime_adapter.data_plane.begin_request(request_id)
close_reason, emitted_response = await self._send_native_duplex_events(
send_json,
result,
session=session,
expected_epoch=expected_epoch,
)
# A resumable data-plane append remains active even when its persistent
# drain task already exists. Treating only a newly-created drain as
# activity clears active_request_id after later appends and prevents a
# terminal TTS segment from scheduling the next model decision.
emitted_response = emitted_response or request_id is not None
if close_reason is None and await self._start_native_data_plane_stream_task(
send_json,
result,
session=session,
expected_epoch=expected_epoch,
):
emitted_response = True
if close_reason is not None:
if not await self._close_runtime_session(session, reason=close_reason, send_json=send_json):
return False, emitted_response
session.close()
await send_json(
{
"type": "session.closed",
"session_id": session.session_id,
"reason": close_reason,
}
)
return False, emitted_response
return True, emitted_response
@staticmethod
def _callable_accepts_keyword(fn, name: str) -> bool:
try:
params = inspect.signature(fn).parameters.values()
except (TypeError, ValueError):
return True
return any(param.kind == inspect.Parameter.VAR_KEYWORD or param.name == name for param in params)
def _native_runtime_contract_error(self, session: DuplexSession) -> str | None:
if session.capabilities.implementation_level != "model_native_duplex":
return None
engine_client = self._chat_service.engine_client
invalid: list[str] = []
for method_name, required_keywords in self._NATIVE_RUNTIME_FENCE_CONTRACT.items():
method = getattr(engine_client, method_name, None)
if not callable(method):
invalid.append(f"{method_name} is missing")
continue
try:
parameters = tuple(inspect.signature(method).parameters.values())
except (TypeError, ValueError):
invalid.append(f"{method_name} signature is not inspectable")
continue
accepts_kwargs = any(parameter.kind == inspect.Parameter.VAR_KEYWORD for parameter in parameters)
declared = {parameter.name for parameter in parameters}
missing = [name for name in required_keywords if not accepts_kwargs and name not in declared]
if missing:
invalid.append(f"{method_name} is missing keyword(s): {', '.join(missing)}")
if not invalid:
return None
return "Native duplex runtime requires the fenced control contract: " + "; ".join(invalid)
async def _start_native_data_plane_stream_task(
self,
send_json,
result: object,
*,
session: DuplexSession,
expected_epoch: int | None = None,
) -> bool:
request_id, _ = self._data_plane_request_info(result)
if request_id is None or self._data_plane_outputs_finished(result):
return False
session.bind_request(request_id)
native = self._runtime_session_state(session)
old_task = native.data_plane_task
if old_task is not None and not old_task.done():
if self._session_auto_responds(session):
# One persistent drain per session, like the official worker's
# single synchronous loop: the resumable data-plane request id
# is stable, and cancel/restart on every append orphans any
# decision that lands in the swap window.
native.data_plane_restart_requested = True
return False
old_task.cancel()
try:
await asyncio.wait_for(asyncio.gather(old_task, return_exceptions=True), timeout=0.25)
except asyncio.TimeoutError:
pass
async def _run() -> None:
close_reason: str | None = None
try:
close_reason = await self._drain_native_data_plane_stream(
send_json,
result,
session=session,
expected_epoch=expected_epoch,
)
if close_reason is not None:
if await self._close_runtime_session(session, reason=close_reason, send_json=send_json):
session.close()
await send_json(
{
"type": "session.closed",
"session_id": session.session_id,
"reason": close_reason,
}
)
except asyncio.CancelledError:
raise
except Exception as exc:
logger.exception("Failed to drain duplex data-plane stream: %s", exc)
await self._send_runtime_error(send_json, "runtime_data_plane_stream_failed", exc, session=session)
if session.state != DuplexSessionState.CLOSED:
close_reason = "runtime_data_plane_stream_failed"
if await self._close_runtime_session(session, reason=close_reason, send_json=send_json):
session.close()
await send_json(
{
"type": "session.closed",
"session_id": session.session_id,
"reason": close_reason,
}
)
finally:
if native.data_plane_task is task:
native.data_plane_task = None
restart_requested = (
native.data_plane_restart_requested
and close_reason is None
and session.state != DuplexSessionState.CLOSED
and session.active_request_id == request_id
and (expected_epoch is None or session.epoch == expected_epoch)
)
native.data_plane_restart_requested = False
if close_reason is None and not self._session_auto_responds(session):
# Auto-respond sessions keep one resumable stage-1 stream
# whose audio accumulates across speak units; the offset
# must survive drain-task turnover or every unit re-sends
# the reply audio from the start.
self._serving_runtime_adapter.data_plane.close_stream(request_id)
if restart_requested:
await self._start_native_data_plane_stream_task(
send_json,
result,
session=session,
expected_epoch=expected_epoch,
)
elif close_reason is None:
await self._maybe_continue_native_response(send_json, session=session, expected_epoch=expected_epoch)
task = asyncio.create_task(_run())
native.data_plane_task = task
return True
# One model unit (1 s at 16 kHz) of pcm_f32le silence, matching the
# official full-duplex behavior where the microphone keeps streaming
# silence while the assistant speaks; replies span multiple units.
_NATIVE_SILENCE_UNIT_PAYLOAD_AUDIO = base64.b64encode(bytes(16000 * 4)).decode("ascii")
_NATIVE_RESPONSE_MAX_CONTINUATION_UNITS = 8
def _native_silence_unit_payload(self) -> dict[str, object]:
return {
"type": "audio",
"audio": self._NATIVE_SILENCE_UNIT_PAYLOAD_AUDIO,
"format": "pcm_f32le",
"sample_rate_hz": 16000,
}
def _native_response_continuations_remaining(self, session: DuplexSession, response_id: str) -> bool:
if self._session_auto_responds(session):
return True
native = self._runtime_session_state(session)
owner_id = f"response:{response_id}"
count = native.continuation_units if native.continuation_owner_id == owner_id else 0
return count < self._NATIVE_RESPONSE_MAX_CONTINUATION_UNITS
def _native_silence_continuation_is_stale(
self,
session: DuplexSession,
*,
request_id: str,
response_id: str | None,
response_owned: bool,
expected_epoch: int | None,
expected_incarnation: int,
expected_model_turn_id: int | None,
) -> bool:
stale_common_owner = (
session.state == DuplexSessionState.CLOSED
or session.incarnation != expected_incarnation
or session.active_request_id != request_id
or (expected_epoch is not None and session.epoch != expected_epoch)
)
stale_response_owner = response_owned and session.active_response_id != response_id
stale_model_turn_owner = not response_owned and (
session.active_response_id is not None or session.turn_id != expected_model_turn_id
)
return stale_common_owner or stale_response_owner or stale_model_turn_owner
async def _maybe_continue_native_response(
self,
send_json,
*,
session: DuplexSession,
expected_epoch: int | None,
expected_model_turn_id: int | None = None,
) -> None:
"""Give an active model turn another silence unit.
A model turn can need another unit before it has emitted visible
text/audio and therefore before a Realtime response exists. Bind that
continuation to the model-turn identity; once a response exists, its
response/turn ownership remains the continuation fence.
"""
response_id = session.active_response_id
native = self._runtime_session_state(session)
if session.state == DuplexSessionState.CLOSED:
native.clear_continuation()
return
request_id = session.active_request_id
if request_id is None:
native.clear_continuation()
return
if expected_epoch is not None and session.epoch != expected_epoch:
return
auto_response = self._session_auto_responds(session)
response_owned = response_id is not None
if response_owned:
owner_id = f"response:{response_id}"
payload_turn_id = (
session.active_response_turn_id if session.active_response_turn_id is not None else session.turn_id
)
else:
if not auto_response or expected_model_turn_id is None or session.turn_id != expected_model_turn_id:
native.clear_continuation()
return
owner_id = f"model-turn:{expected_model_turn_id}"
payload_turn_id = expected_model_turn_id
count = native.continuation_units if native.continuation_owner_id == owner_id else 0
if not auto_response and count >= self._NATIVE_RESPONSE_MAX_CONTINUATION_UNITS:
return
payload = self._native_silence_unit_payload()
payload["duplex_turn_id"] = payload_turn_id
scheduler = native.silence_continuation_scheduler
if scheduler is None:
return
try:
scheduled = await scheduler(
payload,
request_id=request_id,
owner_id=owner_id,
response_id=response_id,
response_owned=response_owned,
expected_epoch=expected_epoch,
expected_incarnation=session.incarnation,
expected_model_turn_id=expected_model_turn_id,
send_json=send_json,
)
except Exception as exc:
logger.exception("Failed to schedule duplex native response continuation: %s", exc)
scheduled = False
if scheduled:
native.continuation_owner_id = owner_id
native.continuation_units = count + 1
async def _cancel_native_data_plane_stream(self, session: DuplexSession) -> bool:
native = self._runtime_session_state(session)
task = native.data_plane_task
native.data_plane_task = None
native.data_plane_restart_requested = False
if task is None or task.done():
return False
task.cancel()
try:
await asyncio.wait_for(asyncio.gather(task, return_exceptions=True), timeout=0.25)
except asyncio.TimeoutError:
# Keep barge-in/cancel responsive. The task still carries the old
# expected_epoch and late model output is filtered by the writer.
pass
return True
async def _signal_runtime_session(
self,
session: DuplexSession,
event: str,
send_json=None,
*,
fence: DuplexFence | None = None,
next_fence: DuplexFence | None = None,
session_config: dict[str, object] | None = None,
runtime_config: dict[str, object] | None = None,
) -> bool:
signal_turn = getattr(self._chat_service.engine_client, "signal_duplex_turn_async", None)
if not callable(signal_turn):
return True
try:
signal_kwargs = {
"event": event,
"timeout": self._runtime_control_timeout_s(session),
}
if self._callable_accepts_keyword(signal_turn, "fence"):
signal_kwargs["fence"] = fence or DuplexFence(
session.session_id,
epoch=session.epoch,
turn_id=session.turn_id,
incarnation=session.incarnation,
)
if next_fence is not None and self._callable_accepts_keyword(signal_turn, "next_fence"):
signal_kwargs["next_fence"] = next_fence
if session_config is not None and self._callable_accepts_keyword(signal_turn, "session_config"):
signal_kwargs["session_config"] = session_config
if runtime_config is not None and self._callable_accepts_keyword(signal_turn, "runtime_config"):
signal_kwargs["runtime_config"] = runtime_config
result = await signal_turn(session.session_id, **signal_kwargs)
except Exception as exc:
logger.exception("Failed to signal duplex runtime session: %s", exc)
if send_json is not None:
await self._send_runtime_error(send_json, "runtime_signal_failed", exc, session=session)
return False
if isinstance(result, dict) and self._runtime_signal_failed(result):
logger.warning(
"Duplex runtime signal failed: session=%s event=%s result=%s",
session.session_id,
event,
self._redact_runtime_control_result(result),
)
if send_json is not None:
await self._send_runtime_control_error(
send_json,
"runtime_signal_failed",
"Duplex runtime signal failed",
result,
session=session,
)
return False
await self._send_runtime_control_if_needed(send_json, result, session=session)
return True
async def _close_runtime_session(self, session: DuplexSession, *, reason: str, send_json=None) -> bool:
close_session = getattr(self._chat_service.engine_client, "close_duplex_session_async", None)
if not callable(close_session):
return True
try:
close_kwargs = {
"reason": reason,
"timeout": self._runtime_control_timeout_s(session),
}
if self._callable_accepts_keyword(close_session, "fence"):
close_kwargs["fence"] = DuplexFence(
session.session_id,
epoch=session.epoch,
turn_id=session.turn_id,
incarnation=session.incarnation,
)
result = await close_session(session.session_id, **close_kwargs)
except Exception as exc:
logger.exception("Failed to close duplex runtime session: %s", exc)
if send_json is not None:
await self._send_runtime_error(send_json, "runtime_close_failed", exc, session=session)
return False
if isinstance(result, dict) and self._runtime_control_failed(result):
if send_json is not None:
await self._send_runtime_control_error(
send_json,
"runtime_close_failed",
"Duplex runtime close failed",
result,
session=session,
)
return False
await self._send_runtime_control_if_needed(send_json, result, session=session)
return True
@staticmethod
def _runtime_control_timeout_s(session: DuplexSession) -> float:
raw = session.config.extra_body.get("duplex_control_timeout_s") or session.config.extra_body.get(
"runtime_control_timeout_s"
)
if isinstance(raw, int | float) and raw > 0:
return float(raw)
if session.capabilities.implementation_level == "model_native_duplex":
return 60.0
return 10.0
async def _send_runtime_control_if_needed(
self,
send_json,
result: object,
*,
session: DuplexSession,
) -> None:
if send_json is None:
return
if not isinstance(result, dict):
return
if not result.get("unsupported_count") and not result.get("error_count"):
return
await send_json(
{
"type": "runtime.control",
"session_id": session.session_id,
"epoch": session.epoch,
"result": self._redact_runtime_control_result(result),
}
)
@staticmethod
def _runtime_control_failed(result: dict[str, object]) -> bool:
if result.get("ok") is False:
return True
for key in ("unsupported_count", "error_count"):
value = result.get(key)
if isinstance(value, int | float) and value > 0:
return True
return False
@classmethod
def _runtime_signal_failed(cls, result: dict[str, object]) -> bool:
error_count = result.get("error_count")
if isinstance(error_count, int | float) and error_count > 0:
return True
if result.get("ok") is not False:
return False
return not cls._runtime_signal_has_data_plane_ack(result)
@classmethod
def _runtime_signal_has_data_plane_ack(cls, value: object) -> bool:
if isinstance(value, dict):
if value.get("data_plane_signal") is True and value.get("supported") is True:
return True
return any(cls._runtime_signal_has_data_plane_ack(child) for child in value.values())
if isinstance(value, list | tuple):
return any(cls._runtime_signal_has_data_plane_ack(item) for item in value)
return False
@classmethod
def _redact_runtime_control_result(cls, value: object) -> object:
if isinstance(value, dict):
redacted = {
key: cls._redact_runtime_control_result(child)
for key, child in value.items()
if key
not in {
"stage_handoff",
"tts_handoff",
"omni_payload",
"tts_hidden_states",
"tts_token_ids",
"traceback",
}
}
native_result = redacted.get("native_result")
if isinstance(native_result, dict):
if native_result.get("requires_stage_handoff") is True:
native_result.pop("requires_stage_handoff", None)
if native_result.get("requires_tts_stage") is True:
native_result.pop("requires_tts_stage", None)
return redacted
if isinstance(value, list | tuple):
return [cls._redact_runtime_control_result(item) for item in value]
if is_dataclass(value) and not isinstance(value, type):
# Control results carry typed values straight from the engine:
# `fence` and `accepted_fence` are `DuplexFence` instances added by
# `DuplexControlClient.execute`. This redacted value is emitted on
# the wire (for example `session.created.runtime_control` in
# `session_runner`), where `WebSocket.send_json` would raise
# `TypeError: Object of type DuplexFence is not JSON serializable`
# and kill the session during the handshake.
return {key: cls._redact_runtime_control_result(child) for key, child in asdict(value).items()}
if isinstance(value, Enum):
return value.value
return value
async def _send_native_duplex_events(
self,
send_json,
result: object,
*,
session: DuplexSession,
expected_epoch: int | None = None,
) -> tuple[str | None, bool]:
if send_json is None:
return None, False
if expected_epoch is not None and session.epoch != expected_epoch:
return None, False
close_reason: str | None = None
emitted_response = False
request_id, _ = self._data_plane_request_info(result)
if self._serving_runtime_adapter.data_plane.is_terminal(request_id):
return None, False
if request_id is not None and session.active_request_id is None:
session.bind_request(request_id)
context = self._runtime_data_plane_context(session)
for native_result in self._serving_runtime_adapter.data_plane.project(result, context=context):
close_reason_for_result, did_emit = await self._send_one_native_duplex_event(
send_json,
native_result,
session=session,
expected_epoch=expected_epoch,
)
emitted_response = emitted_response or did_emit
close_reason = close_reason or close_reason_for_result
if expected_epoch is not None and session.epoch != expected_epoch:
return None, emitted_response
return close_reason, emitted_response
async def _drain_native_data_plane_stream(
self,
send_json,
result: object,
*,
session: DuplexSession,
expected_epoch: int | None = None,
) -> str | None:
request_id, response_stage_id = self._data_plane_request_info(result)
if request_id is None:
return None
if self._data_plane_outputs_finished(result):
return None
collect_outputs = getattr(
self._chat_service.engine_client,
"collect_duplex_data_plane_outputs_async",
None,
)
if not callable(collect_outputs):
return None
close_reason: str | None = None
empty_polls = 0
while close_reason is None:
if self._serving_runtime_adapter.data_plane.is_terminal(request_id):
return None
if self._session_auto_responds(session):
active_request_id = session.active_request_id
if active_request_id is not None and active_request_id != request_id:
return None
if expected_epoch is not None and session.epoch != expected_epoch:
return None
if session.state == DuplexSessionState.CLOSED:
return None
outputs = await collect_outputs(
request_id,
response_stage_id=response_stage_id,
timeout=self._runtime_control_timeout_s(session),
)
if expected_epoch is not None and session.epoch != expected_epoch:
return None
if not outputs:
# An empty poll means no output arrived within one control
# window, not that the stream is over. Exiting here orphans a
# decision that lands moments later: it would sit queued until
# the NEXT append starts a fresh drain task, adding one full
# chunk of latency to every model decision.
empty_polls += 1
if empty_polls >= 3:
return None
continue
empty_polls = 0
drain_result = {
"data_plane_outputs": outputs,
}
close_reason, emitted_response = await self._send_native_duplex_events(
send_json,
drain_result,
session=session,
expected_epoch=expected_epoch,
)
if (
self._data_plane_outputs_finished(drain_result)
and emitted_response
and not self._session_auto_responds(session)
):
# Auto-respond sessions are resumable: every segment ends
# with finished=True but the stream continues with the next
# audio chunk. Exiting here would make each decision wait for
# the next append to start a fresh drain task (one full chunk
# of added latency); keep draining until epoch change,
# session close, or cancellation by a newer drain task.
return close_reason
# A batch without a client-visible event (e.g. the generic
# stage-0 final-output message that precedes the listen/speak
# decision in the same segment) must not terminate the drain;
# the real decision is still in flight.
return close_reason
@staticmethod
def _data_plane_outputs_finished(result: object) -> bool:
if not isinstance(result, dict):
return False
outputs = result.get("data_plane_outputs")
if not isinstance(outputs, list) or not outputs:
return False
return bool(getattr(outputs[-1], "finished", False))
@staticmethod
def _data_plane_request_info(result: object) -> tuple[str | None, int | None]:
if not isinstance(result, dict):
return None, None
return duplex_data_plane_request_info(result)
def _runtime_data_plane_context(self, session: DuplexSession) -> object:
response_config = session.response_config
return self._serving_runtime_adapter.data_plane_context(
epoch=session.epoch,
turn_id=session.turn_id,
active_response_turn_id=session.active_response_turn_id,
active_response_id=session.active_response_id,
auto_responds=self._session_auto_responds(session),
response_format=response_config.response_format,
speed=response_config.speed,
modalities=tuple(response_config.modalities),
)
async def _send_one_native_duplex_event(
self,
send_json,
native_result: dict[str, object],
*,
session: DuplexSession,
expected_epoch: int | None = None,
) -> tuple[str | None, bool]:
close_reason: str | None = None
emitted_response = False
if expected_epoch is not None and session.epoch != expected_epoch:
return close_reason, emitted_response
data_plane_request_id = native_result.get("data_plane_request_id")
if isinstance(data_plane_request_id, str) and self._serving_runtime_adapter.data_plane.is_terminal(
data_plane_request_id
):
return close_reason, emitted_response
active_request_matches = session.active_request_id == data_plane_request_id or (
self._session_auto_responds(session) and session.active_request_id is None
)
if isinstance(data_plane_request_id, str) and not active_request_matches:
return close_reason, emitted_response
if isinstance(native_result.get("error_code"), str):
response_id = session.active_response_id
await send_json(
{
"type": "error",
"code": native_result.get("error_code"),
"session_id": session.session_id,
"epoch": session.epoch,
"error": str(native_result.get("error") or "Duplex native data-plane error"),
}
)
if response_id is not None:
session.end_response(commit_text=False)
await send_json(
{
"type": "response.done",
"session_id": session.session_id,
"response_id": response_id,
"epoch": session.epoch,
"committed": False,
"status": "failed",
"status_details": {
"type": "failed",
"reason": native_result.get("error_code"),
},
"playback": session.playback.as_dict(),
}
)
return close_reason, True
if native_result.get("requires_stage_handoff") is True or native_result.get("requires_tts_stage") is True:
# Stage0 announces a talker handoff before Stage1 has produced
# client-visible audio. A single client input stream may contain
# several model turns, including terminal-only turns. Reserve the
# protocol response only when Stage1 emits text/audio so empty
# model turns cannot create empty responses or steal later audio.
return close_reason, False
is_listen = native_result.get("is_listen")
model_turn_id = coerce_int(native_result.get("model_turn_id"))
if native_result.get("is_buffering") is True or native_result.get("prefill_success") is False:
if native_result.get("data_plane_request_id") == session.active_request_id:
session.clear_request()
payload = {
"type": "response.listen",
"session_id": session.session_id,
"epoch": session.epoch,
"reason": native_result.get("reason") or "buffering",
"model_listen": False,
"buffering": True,
}
self._attach_native_runtime_metadata(payload, native_result)
await send_json(payload)
return close_reason, emitted_response
if is_listen is True:
await self._end_active_response_before_future_model_turn(
send_json,
session=session,
model_turn_id=model_turn_id,
)
if (
session.active_response_id is not None
and model_turn_id is not None
and not session.active_response_accepts_model_turn(model_turn_id)
):
return close_reason, emitted_response
non_terminal_auto_listen = (
self._session_auto_responds(session)
and session.active_response_id is not None
and session.active_request_id is not None
and native_result.get("end_of_turn") is not True
)
if non_terminal_auto_listen:
await self._maybe_continue_native_response(
send_json,
session=session,
expected_epoch=expected_epoch,
)
return close_reason, emitted_response
auto_response = self._session_auto_responds(session)
if not auto_response and data_plane_request_id == session.active_request_id:
session.clear_request()
model_listen = native_result.get("model_listen")
if not isinstance(model_listen, bool):
model_listen = native_result.get("reason") in {None, "", "model_listen"}
response_id = session.active_response_id
if isinstance(data_plane_request_id, str) and not auto_response:
self._serving_runtime_adapter.data_plane.mark_terminal(data_plane_request_id)
emitted_response = True
payload = {
"type": "response.listen",
"session_id": session.session_id,
"epoch": session.epoch,
"reason": native_result.get("reason") or "model_listen",
"model_listen": model_listen,
}
self._attach_native_runtime_metadata(payload, native_result)
await send_json(payload)
if native_result.get("abort_data_plane_request") is True and isinstance(data_plane_request_id, str):
await self._abort_request_background(
session,
data_plane_request_id,
send_json,
notify=False,
)
if response_id is not None:
if not self._session_auto_responds(session) and self._native_response_continuations_remaining(
session, response_id
):
# The official model often listens for a silence beat or
# two before answering; keep the response open and give it
# the next silence unit as a decision point.
await self._maybe_continue_native_response(
send_json,
session=session,
expected_epoch=expected_epoch,
)
return close_reason, emitted_response
session.end_response(commit_text=False, preserve_request=auto_response)
await send_json(
{
"type": "response.done",
"session_id": session.session_id,
"response_id": response_id,
"epoch": session.epoch,
"committed": False,
"playback": session.playback.as_dict(),
}
)
return close_reason, emitted_response
text = native_result.get("text")
audio = native_result.get("audio_data", native_result.get("audio"))
end_of_turn = bool(native_result.get("end_of_turn", False))
has_text = isinstance(text, str) and bool(text)
has_audio = isinstance(audio, str) and bool(audio)
if not has_text and not has_audio and not end_of_turn:
tts_segment_ended = (
native_result.get("stage_role") == "tts" and native_result.get("abort_data_plane_request") is True
)
if (
tts_segment_ended
and self._session_auto_responds(session)
and (model_turn_id is not None or session.active_response_id is not None)
):
await self._maybe_continue_native_response(
send_json,
session=session,
expected_epoch=expected_epoch,
expected_model_turn_id=model_turn_id,
)
return close_reason, emitted_response
if end_of_turn and not has_text and not has_audio and session.active_response_id is None:
if isinstance(data_plane_request_id, str):
if not self._session_auto_responds(session) and data_plane_request_id == session.active_request_id:
session.clear_request()
if not self._session_auto_responds(session):
self._serving_runtime_adapter.data_plane.mark_terminal(data_plane_request_id)
if model_turn_id is not None:
session.complete_model_turn(model_turn_id)
if self._session_auto_responds(session):
self._runtime_session_state(session).clear_continuation()
emitted_response = True
payload = {
"type": "response.listen",
"session_id": session.session_id,
"epoch": session.epoch,
"reason": "model_turn_completed_without_output",
"model_listen": True,
}
self._attach_native_runtime_metadata(payload, native_result)
await send_json(payload)
return close_reason, emitted_response
if session.active_response_id is None and model_turn_id is not None and model_turn_id < session.turn_id:
# A continuation append can already be in flight when the prior
# response reaches turn EOS. Its late audio still carries the
# completed model turn, so it must not reserve a second Realtime
# response for that turn.
return close_reason, emitted_response
await self._end_active_response_before_future_model_turn(
send_json,
session=session,
model_turn_id=model_turn_id,
)
if (
session.active_response_id is not None
and model_turn_id is not None
and not session.active_response_accepts_model_turn(model_turn_id)
):
return close_reason, emitted_response
emitted_response = True
response_created = False
response_id = session.active_response_id
if response_id is None:
response_id = session.begin_response(turn_id=model_turn_id)
response_created = True
await send_json(
self._response_created_payload(
session,
response_id,
epoch=session.epoch,
)
)
response_stage_metrics = session.accumulate_response_stage_metrics(
native_result.get("stage_metrics") if isinstance(native_result.get("stage_metrics"), Mapping) else None
)
if response_created:
speak_payload = {
"type": "response.speak",
"session_id": session.session_id,
"response_id": response_id,
"epoch": session.epoch,
"text": text if isinstance(text, str) else "",
"end_of_turn": end_of_turn,
"model_speak": True,
}
self._attach_native_runtime_metadata(
speak_payload,
native_result,
stage_metrics=response_stage_metrics,
)
await send_json(speak_payload)
previous_sent_ms = session.playback.sent_ms
text_chars_before_append = len("".join(session.assistant_text_buffer))
if isinstance(text, str):
session.append_assistant_text(text)
duration_ms = native_result.get("audio_duration_ms")
text_chars = len("".join(session.assistant_text_buffer))
mark_duration_ms = None
mark_text_chars: int | None = text_chars
if native_result.get("audio_text_mark") is False:
mark_text_chars = None
if isinstance(duration_ms, int | float):
mark_duration_ms = int(duration_ms)
if native_result.get("audio_duration_is_cumulative") is not True:
mark_duration_ms += session.playback.sent_ms
audio_text_marks = native_result.get("audio_text_marks")
audio_text_marks = self._normalize_native_audio_text_marks(
audio_text_marks if isinstance(audio_text_marks, list) else None,
audio_offset_ms=(
0
if native_result.get("audio_text_marks_are_cumulative") is True
or native_result.get("audio_duration_is_cumulative") is True
else previous_sent_ms
),
text_offset_chars=(
0 if native_result.get("audio_text_marks_are_cumulative") is True else text_chars_before_append
),
)
session.mark_audio_sent(
mark_duration_ms,
text_chars=mark_text_chars if mark_duration_ms is not None else None,
audio_text_marks=audio_text_marks,
)
payload = {
"type": "response.output_audio.delta",
"session_id": session.session_id,
"response_id": response_id,
"epoch": session.epoch,
"text": text if isinstance(text, str) else "",
"audio": audio if isinstance(audio, str) else "",
"format": (
native_result.get("audio_format")
if isinstance(native_result.get("audio_format"), str)
else session.response_config.response_format
),
"end_of_turn": end_of_turn,
"model_speak": True,
}
if mark_duration_ms is not None:
payload["audio_duration_ms"] = mark_duration_ms
if audio_text_marks:
payload["audio_text_marks"] = audio_text_marks
elif mark_duration_ms is not None and mark_text_chars is not None:
payload["audio_text_marks"] = [
{
"text_chars": max(0, int(mark_text_chars)),
"audio_end_ms": max(0, int(mark_duration_ms)),
}
]
payload["playback"] = session.playback.as_dict()
sample_rate_hz = native_result.get("sample_rate_hz") or native_result.get("audio_sample_rate_hz")
if isinstance(sample_rate_hz, int | float) and int(sample_rate_hz) > 0:
payload["sample_rate_hz"] = int(sample_rate_hz)
self._attach_native_runtime_metadata(
payload,
native_result,
stage_metrics=response_stage_metrics,
)
await send_json(payload)
if (
not end_of_turn
and native_result.get("stage_role") == "tts"
and native_result.get("abort_data_plane_request") is True
and self._session_auto_responds(session)
):
await self._maybe_continue_native_response(
send_json,
session=session,
expected_epoch=expected_epoch,
)
if end_of_turn:
data_plane_request_id = native_result.get("data_plane_request_id")
if isinstance(data_plane_request_id, str) and not self._session_auto_responds(session):
self._serving_runtime_adapter.data_plane.close_stream(data_plane_request_id)
if isinstance(data_plane_request_id, str):
if not self._session_auto_responds(session) and data_plane_request_id == session.active_request_id:
session.clear_request()
if not self._session_auto_responds(session):
self._serving_runtime_adapter.data_plane.mark_terminal(data_plane_request_id)
should_commit = self._should_commit_response_to_history(session, response_id)
committed_message = session.end_response(
commit_text=should_commit,
preserve_request=self._session_auto_responds(session),
)
model_turn_id = coerce_int(native_result.get("model_turn_id"))
if model_turn_id is not None:
session.complete_model_turn(model_turn_id)
if should_commit:
session.register_history_item(f"item_{response_id}", committed_message)
await send_json(
{
"type": "response.done",
"session_id": session.session_id,
"response_id": response_id,
"epoch": session.epoch,
"committed": committed_message is not None,
"playback": session.playback.as_dict(),
}
)
return close_reason, emitted_response
async def _end_active_response_before_future_model_turn(
self,
send_json,
*,
session: DuplexSession,
model_turn_id: int | None,
) -> None:
if not self._session_auto_responds(session):
return
response_id = session.active_response_id
active_turn_id = session.active_response_turn_id
if response_id is None or model_turn_id is None or active_turn_id is None:
return
if int(model_turn_id) <= int(active_turn_id):
return
session.complete_model_turn(int(model_turn_id) - 1)
should_commit = self._should_commit_response_to_history(session, response_id)
committed_message = session.end_response(
commit_text=should_commit,
preserve_request=True,
)
if should_commit:
session.register_history_item(f"item_{response_id}", committed_message)
await send_json(
{
"type": "response.done",
"session_id": session.session_id,
"response_id": response_id,
"epoch": session.epoch,
"committed": committed_message is not None,
"playback": session.playback.as_dict(),
}
)
@staticmethod
def _normalize_native_audio_text_marks(
audio_text_marks: list[object] | None,
*,
audio_offset_ms: int,
text_offset_chars: int,
) -> list[dict[str, int]] | None:
if not audio_text_marks:
return None
normalized: list[dict[str, int]] = []
for raw_mark in audio_text_marks:
if not isinstance(raw_mark, dict):
continue
raw_text_chars = raw_mark.get("text_chars")
raw_audio_end_ms = raw_mark.get("audio_end_ms", raw_mark.get("audio_ms"))
if not isinstance(raw_text_chars, int | float) or not isinstance(raw_audio_end_ms, int | float):
continue
normalized.append(
{
"text_chars": max(0, int(raw_text_chars) + int(text_offset_chars)),
"audio_end_ms": max(0, int(raw_audio_end_ms) + int(audio_offset_ms)),
}
)
return normalized or None
def _cleanup_duplex_session_state(self, session: DuplexSession) -> None:
session_id = session.session_id
self._serving_runtime_adapter.remove_session_state(session_id)
self._serving_runtime_adapter.data_plane.close_session(
session_id,
active_request_id=session.active_request_id,
)
def _encode_native_data_plane_audio(
self,
audio_data: object,
sample_rate_hz: int,
response_format: str,
speed: float | None,
) -> str | None:
if audio_data is None:
return None
try:
import torch
from vllm_omni.entrypoints.openai.protocol.audio import CreateAudio
if isinstance(audio_data, torch.Tensor):
audio_tensor = audio_data.detach().cpu().float().numpy()
else:
audio_tensor = np.asarray(audio_data, dtype=np.float32)
if audio_tensor.ndim > 1:
audio_tensor = audio_tensor.reshape(-1)
audio_response = self._chat_service.create_audio(
CreateAudio(
audio_tensor=audio_tensor,
sample_rate=sample_rate_hz,
response_format=response_format,
speed=float(speed) if isinstance(speed, int | float) and speed > 0 else 1.0,
stream_format="audio",
base64_encode=True,
)
)
return str(audio_response.audio_data)
except Exception:
logger.exception("Failed to encode duplex data-plane audio output")
return None
@staticmethod
def _attach_native_runtime_metadata(
payload: dict[str, object],
native_result: dict[str, object],
*,
stage_metrics: Mapping[str, object] | None = None,
) -> None:
metadata: dict[str, object] = {}
runtime_impl = native_result.get("runtime_impl")
if isinstance(runtime_impl, str) and runtime_impl:
metadata["runtime_impl"] = runtime_impl
owned_runtime = native_result.get("owned_runtime")
if isinstance(owned_runtime, bool):
metadata["owned_runtime"] = owned_runtime
model_turn_id = coerce_int(native_result.get("model_turn_id"))
if model_turn_id is not None:
metadata["model_turn_id"] = model_turn_id
for name in (
"uses_model_runner_scheduler",
"runner_kv_backed",
):
value = native_result.get(name)
if isinstance(value, bool):
metadata[name] = value
effective_stage_metrics = stage_metrics if stage_metrics is not None else native_result.get("stage_metrics")
if isinstance(effective_stage_metrics, Mapping):
metadata["stage_metrics"] = {
str(stage_id): dict(values)
for stage_id, values in effective_stage_metrics.items()
if isinstance(values, Mapping)
}
if metadata:
payload["vllm_omni"] = metadata
async def _send_runtime_error(
self,
send_json,
code: str,
exc: Exception,
*,
session: DuplexSession | None = None,
) -> None:
retryable = False
message = str(exc)
if isinstance(exc, DuplexControlRequestError):
code = exc.code
retryable = exc.retryable
error_data = exc.result.get("error")
if isinstance(error_data, dict) and isinstance(error_data.get("message"), str):
message = error_data["message"]
payload: dict[str, object] = {
"type": "error",
"code": code,
"error": message,
"retryable": retryable,
}
if session is not None:
payload["session_id"] = session.session_id
payload["epoch"] = session.epoch
await send_json(payload)
async def _send_runtime_control_error(
self,
send_json,
code: str,
message: str,
result: dict[str, object],
*,
session: DuplexSession,
) -> None:
await send_json(
{
"type": "error",
"code": code,
"error": message,
"session_id": session.session_id,
"epoch": session.epoch,
"runtime_control": self._redact_runtime_control_result(result),
}
)