Files
vllm-omni/vllm_omni/experimental/fullduplex/engine/contracts.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

277 lines
8.3 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Model-neutral contracts for the experimental duplex engine plugin.
This module contains only immutable data transfer objects and narrow protocols.
Duplex control algorithms, session implementations, model policy, and Realtime
serving remain in sibling experimental modules.
"""
from __future__ import annotations
import base64
from collections.abc import Iterable, Mapping
from dataclasses import dataclass, field
from enum import Enum
from types import MappingProxyType
from typing import Any, Protocol
from vllm_omni.engine.messages import EngineQueueMessage
from vllm_omni.experimental.fullduplex.engine.messages import DuplexFence
class SessionMode(str, Enum):
TURN = "turn"
DUPLEX = "duplex"
class DuplexInputMode(str, Enum):
APPEND_TOKENS = "append_tokens"
APPEND_AUDIO_CHUNK = "append_audio_chunk"
REPLACE_LATEST_CHUNK = "replace_latest_chunk"
REENCODE_CONTEXT = "reencode_context"
ROLLBACK_TO_CHECKPOINT = "rollback_to_checkpoint"
TURN_COMMIT_ONLY = "turn_commit_only"
class DuplexOutputAction(str, Enum):
DIRECT_RESPONSE = "direct_response"
@dataclass
class DuplexRuntimeCapabilities:
input_modes: set[DuplexInputMode] = field(default_factory=lambda: {DuplexInputMode.TURN_COMMIT_ONLY})
implementation_level: str = "serving_session_adapter"
@dataclass(frozen=True)
class DuplexAppendPlan:
prompt: dict[str, Any]
@dataclass(frozen=True)
class DuplexOutputDecision:
action: DuplexOutputAction
metadata: Mapping[str, Any] = field(default_factory=dict)
final_output_type: str = "text"
def __post_init__(self) -> None:
object.__setattr__(self, "metadata", MappingProxyType(dict(self.metadata)))
class DuplexRuntimeExtension(Protocol):
"""Pure model policy invoked by the experimental duplex control plane."""
def configure_sampling_params(
self,
*,
runtime_config: dict[str, Any],
defaults: tuple[object, ...],
) -> tuple[object, ...]: ...
def plan_append(
self,
*,
request_id: str,
fence: DuplexFence,
session_config: dict[str, Any],
runtime_config: dict[str, Any],
seq: int,
turn_seq: int,
mode: DuplexInputMode,
payload: object,
final: bool,
sampling_params: object,
) -> DuplexAppendPlan: ...
def decide_output(
self,
*,
stage_id: int,
final_stage_id: int,
segment_finished: bool,
segment_token_ids: tuple[int, ...],
segment_output_metadata: dict[str, Any],
output: object,
) -> DuplexOutputDecision | None: ...
@dataclass(frozen=True)
class DuplexRequestIdentity:
session_id: str
fence: DuplexFence
@dataclass(frozen=True)
class DuplexStageRequestContext:
request_id: str
session_id: str
fence: DuplexFence
stage_id: int
final_stage_id: int
config_generation: int
sampling_params: tuple[object, ...]
session_config: Mapping[str, Any] = field(default_factory=dict)
runtime_config: Mapping[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
object.__setattr__(self, "sampling_params", tuple(self.sampling_params))
object.__setattr__(self, "session_config", MappingProxyType(dict(self.session_config)))
object.__setattr__(self, "runtime_config", MappingProxyType(dict(self.runtime_config)))
@property
def stage_sampling_params(self) -> object:
return self.sampling_params[self.stage_id]
@dataclass(frozen=True)
class DuplexStageSubmission:
context: DuplexStageRequestContext
prompt: Mapping[str, Any]
already_submitted: bool
def __post_init__(self) -> None:
object.__setattr__(self, "prompt", MappingProxyType(dict(self.prompt)))
@dataclass(frozen=True)
class DuplexStageSubmissionResult:
request_id: str
stage_id: int
replica_id: int
@dataclass(frozen=True)
class DuplexOutputContext:
identity: DuplexRequestIdentity
final_stage_id: int
segment_finished: bool
segment_token_ids: tuple[int, ...] = ()
segment_output_metadata: Mapping[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
object.__setattr__(self, "segment_token_ids", tuple(self.segment_token_ids))
object.__setattr__(
self,
"segment_output_metadata",
MappingProxyType(dict(self.segment_output_metadata)),
)
class DuplexStagePort(Protocol):
@property
def stage_count(self) -> int: ...
def sampling_defaults(self) -> tuple[object, ...]: ...
def ensure_request(self, context: DuplexStageRequestContext) -> None: ...
async def submit(self, submission: DuplexStageSubmission) -> DuplexStageSubmissionResult: ...
async def cleanup(self, request_ids: list[str], *, abort: bool = False) -> None: ...
class DuplexControlPlanePort(Protocol):
@property
def sessions(self) -> object: ...
def accepts(self, message: object) -> bool: ...
def dispatch(self, message: object) -> None: ...
async def shutdown(self) -> None: ...
def close_sessions_for_request_ids(self, request_ids: list[str]) -> dict[str, list[str]]: ...
def finalize_closed_sessions(self, session_ids: Iterable[str]) -> None: ...
def session_for_identity(self, identity: DuplexRequestIdentity | None) -> object | None: ...
def decide_output(
self,
stage_id: int,
output: object,
context: DuplexOutputContext | None,
) -> DuplexOutputDecision | None: ...
class CorrelatedRpcTransport(Protocol):
def execute(
self,
key: tuple[str, str],
message: EngineQueueMessage,
*,
timeout: float | None,
timeout_message: str,
block_on_submit: bool = False,
) -> EngineQueueMessage: ...
def duplex_data_plane_request_info(result: dict[str, object]) -> tuple[str | None, int | None]:
stage_results = result.get("stage_results")
if not isinstance(stage_results, list):
return None, None
for item in stage_results:
if not isinstance(item, dict):
continue
inner = item.get("result")
if not isinstance(inner, dict) or inner.get("data_plane_append") is not True:
continue
request_id = inner.get("request_id")
if not isinstance(request_id, str) or not request_id:
continue
response_stage_id = inner.get("response_stage_id")
return request_id, response_stage_id if isinstance(response_stage_id, int) else None
return None, None
def duplex_resource_request_id(fence: DuplexFence, role: str) -> str:
if not role or any(
character not in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-" for character in role
):
raise ValueError(f"invalid duplex resource role: {role!r}")
encoded_session_id = base64.urlsafe_b64encode(fence.session_id.encode("utf-8")).decode("ascii").rstrip("=")
return f"duplex-s.{encoded_session_id}.i.{fence.incarnation}.e.{fence.epoch}.r.{role}"
def duplex_resource_request_belongs_to_session(request_id: str, session_id: str) -> bool:
"""Return whether a current-format resource request belongs to a session."""
parts = request_id.split(".")
if len(parts) != 8 or parts[0] != "duplex-s" or parts[2] != "i" or parts[4] != "e" or parts[6] != "r":
return False
try:
int(parts[3])
int(parts[5])
except ValueError:
return False
role = parts[7]
if not role or any(
character not in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-" for character in role
):
return False
encoded_session_id = base64.urlsafe_b64encode(session_id.encode("utf-8")).decode("ascii").rstrip("=")
return parts[1] == encoded_session_id
__all__ = [
"CorrelatedRpcTransport",
"DuplexAppendPlan",
"DuplexControlPlanePort",
"DuplexInputMode",
"DuplexOutputAction",
"DuplexOutputContext",
"DuplexOutputDecision",
"DuplexRequestIdentity",
"DuplexRuntimeCapabilities",
"DuplexRuntimeExtension",
"DuplexStagePort",
"DuplexStageRequestContext",
"DuplexStageSubmission",
"DuplexStageSubmissionResult",
"SessionMode",
"duplex_data_plane_request_info",
"duplex_resource_request_belongs_to_session",
"duplex_resource_request_id",
]