144 lines
5.8 KiB
Python
144 lines
5.8 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|||
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||
|
|
|
||
|
|
"""``protocol.duplex`` is the only door a duplex consumer uses.
|
||
|
|
|
||
|
|
The layering is a chain::
|
||
|
|
|
||
|
|
protocol.realtime <- protocol.duplex <- engine / entrypoints / clients
|
||
|
|
|
||
|
|
The middle link only earns its keep if the last arrow is the *only* one. If the
|
||
|
|
engine may also reach past it straight into ``protocol.realtime``, then giving a
|
||
|
|
Tier 1 helper a duplex-specific version later means touching every call site
|
||
|
|
instead of one file --- which is exactly the change
|
||
|
|
``convert_input_audio_with_rate`` is expected to need (it resamples to
|
||
|
|
MiniCPM-o's 16 kHz, not the client's rate).
|
||
|
|
|
||
|
|
So this asserts two things: nobody skips the middle link, and the middle link is
|
||
|
|
complete enough that nobody has to.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import ast
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||
|
|
|
||
|
|
REPO_ROOT = Path(__file__).resolve().parents[3]
|
||
|
|
|
||
|
|
#: Every module that speaks the duplex protocol. These import
|
||
|
|
#: ``vllm_omni.protocol.duplex``; reaching into ``vllm_omni.protocol.realtime``
|
||
|
|
#: from here skips the extension point.
|
||
|
|
CONSUMER_PATHS = (
|
||
|
|
"vllm_omni/engine/duplex",
|
||
|
|
"vllm_omni/engine/duplex_omni_engine.py",
|
||
|
|
"vllm_omni/entrypoints/duplex",
|
||
|
|
"vllm_omni/entrypoints/duplex_omni.py",
|
||
|
|
"vllm_omni/clients/duplex.py",
|
||
|
|
"vllm_omni/clients/inline_duplex.py",
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def _consumer_modules() -> list[Path]:
|
||
|
|
found: list[Path] = []
|
||
|
|
for entry in CONSUMER_PATHS:
|
||
|
|
path = REPO_ROOT / entry
|
||
|
|
found.extend(sorted(path.rglob("*.py")) if path.is_dir() else [path])
|
||
|
|
assert found, "consumer list is stale"
|
||
|
|
return found
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.parametrize("module", _consumer_modules(), ids=lambda p: p.name)
|
||
|
|
def test_a_duplex_consumer_does_not_skip_the_duplex_protocol_package(module: Path) -> None:
|
||
|
|
# Source-level, so a lazy import inside a function body is caught too.
|
||
|
|
tree = ast.parse(module.read_text())
|
||
|
|
offenders = []
|
||
|
|
for node in ast.walk(tree):
|
||
|
|
if isinstance(node, ast.ImportFrom) and (node.module or "").startswith("vllm_omni.protocol.realtime"):
|
||
|
|
offenders.append(f"line {node.lineno}: from {node.module} import ...")
|
||
|
|
elif isinstance(node, ast.Import):
|
||
|
|
offenders.extend(
|
||
|
|
f"line {node.lineno}: import {alias.name}"
|
||
|
|
for alias in node.names
|
||
|
|
if alias.name.startswith("vllm_omni.protocol.realtime")
|
||
|
|
)
|
||
|
|
assert not offenders, (
|
||
|
|
f"{module.relative_to(REPO_ROOT)} imports the Tier 1 package directly; "
|
||
|
|
f"import from vllm_omni.protocol.duplex instead:\n " + "\n ".join(offenders)
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def test_the_duplex_package_carries_the_whole_command_vocabulary() -> None:
|
||
|
|
from vllm_omni.engine.duplex import commands as engine_commands
|
||
|
|
from vllm_omni.protocol.duplex import commands as duplex_wire
|
||
|
|
|
||
|
|
# Every command the engine handles is nameable through the one door.
|
||
|
|
engine_names = {
|
||
|
|
name
|
||
|
|
for name in engine_commands.__all__
|
||
|
|
if isinstance(getattr(engine_commands, name), type)
|
||
|
|
and issubclass(getattr(engine_commands, name), engine_commands.DuplexCommand)
|
||
|
|
and getattr(engine_commands, name) is not engine_commands.DuplexCommand
|
||
|
|
}
|
||
|
|
assert engine_names <= set(duplex_wire.__all__)
|
||
|
|
|
||
|
|
|
||
|
|
def test_the_duplex_package_carries_the_whole_event_vocabulary() -> None:
|
||
|
|
from vllm_omni.engine.duplex import events as engine_events
|
||
|
|
from vllm_omni.protocol.duplex import events as duplex_wire
|
||
|
|
|
||
|
|
engine_names = {
|
||
|
|
name
|
||
|
|
for name in engine_events.__all__
|
||
|
|
if isinstance(getattr(engine_events, name), type)
|
||
|
|
and issubclass(getattr(engine_events, name), engine_events.DuplexEvent)
|
||
|
|
}
|
||
|
|
assert engine_names <= set(duplex_wire.__all__)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.parametrize(
|
||
|
|
"module_pair",
|
||
|
|
[("events", "events"), ("commands", "commands")],
|
||
|
|
ids=["events", "commands"],
|
||
|
|
)
|
||
|
|
def test_a_reexported_tier1_name_is_the_tier1_object(module_pair: tuple[str, str]) -> None:
|
||
|
|
"""Re-export, not re-declaration: the Tier 1 class is one object."""
|
||
|
|
import importlib
|
||
|
|
|
||
|
|
duplex_mod = importlib.import_module(f"vllm_omni.protocol.duplex.{module_pair[0]}")
|
||
|
|
realtime_mod = importlib.import_module(f"vllm_omni.protocol.realtime.{module_pair[1]}")
|
||
|
|
|
||
|
|
shared = [n for n in duplex_mod.__all__ if hasattr(realtime_mod, n)]
|
||
|
|
assert shared, "nothing re-exported --- the facade is not doing its job"
|
||
|
|
for name in shared:
|
||
|
|
duplex_obj = getattr(duplex_mod, name)
|
||
|
|
realtime_obj = getattr(realtime_mod, name)
|
||
|
|
if duplex_obj is realtime_obj:
|
||
|
|
continue
|
||
|
|
# The only permitted divergence is a Tier 2 class extending its twin.
|
||
|
|
assert isinstance(duplex_obj, type) and issubclass(duplex_obj, realtime_obj), (
|
||
|
|
f"{name} in protocol.duplex is neither the Tier 1 object nor a subclass of it"
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def test_duplex_event_is_an_alias_but_duplex_command_is_not() -> None:
|
||
|
|
"""The asymmetry is deliberate, and a later reader should not 'fix' it.
|
||
|
|
|
||
|
|
An event has no engine-internal half --- what the session emits is what the
|
||
|
|
socket sends --- so ``DuplexEvent`` is simply ``RealtimeEvent``. A command
|
||
|
|
does: ``payload()`` renders the session runner's mailbox dictionary, and for
|
||
|
|
four of the seventeen the mailbox channel is not even the client event type.
|
||
|
|
"""
|
||
|
|
from vllm_omni.engine.duplex.commands import DuplexCommand
|
||
|
|
from vllm_omni.protocol.duplex.events import DuplexEvent
|
||
|
|
from vllm_omni.protocol.realtime.commands import RealtimeCommand
|
||
|
|
from vllm_omni.protocol.realtime.events import RealtimeEvent
|
||
|
|
|
||
|
|
assert DuplexEvent is RealtimeEvent
|
||
|
|
|
||
|
|
assert DuplexCommand is not RealtimeCommand
|
||
|
|
assert issubclass(DuplexCommand, RealtimeCommand)
|
||
|
|
assert hasattr(DuplexCommand, "payload") and not hasattr(RealtimeCommand, "payload")
|