Skip to content

[Core] Make protocol/duplex the single duplex vocabulary; drop the engine copies (follow-up to #7640) - #7861

Open
chickeyton wants to merge 12 commits into
vllm-project:mainfrom
chickeyton:duplex_protocol
Open

chickeyton wants to merge 12 commits into
vllm-project:mainfrom
chickeyton:duplex_protocol

Conversation

@chickeyton

@chickeyton chickeyton commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up to #7640 (RFC #6592 P0a).

Purpose

After #7640 there were three overlapping sets of Realtime/duplex command and event definitions: protocol/realtime, protocol/duplex, and commands.py / events.py / realtime_commands.py / realtime_events.py / audio.py in engine/duplex. This PR collapses them into two layers with one direction of dependency:

  • protocol/realtime — the OpenAI v1/realtime command and event classes (RealtimeCommand / RealtimeEvent subclasses) and their helpers. Unchanged apart from docstrings.
  • protocol/duplex — the whole v1/realtime?duplex=1 vocabulary. DuplexCommand and DuplexEvent are plain aliases of RealtimeCommand / RealtimeEvent; the Tier 2 extensions and Tier 3 duplex-only classes live here, together with the wire decoder moved in from the engine (decode_duplex_command, build_append_audio). Consumer capabilities are injected through RealtimeProtocolCapabilities instead of imported from the engine, and the decoder raises RealtimeProtocolError directly (DuplexCommandError is removed).

The engine keeps only its own representation, under names that no longer look like protocol modules:

  • engine/duplex/mailbox.py (new): DUPLEX_REALTIME_CAPABILITIES, command_from_realtime (the decoder bound to those capabilities), and mailbox_payload / mailbox_channel, which replace the payload() methods and mailbox type of the removed parallel command hierarchy.
  • engine/duplex/projection.py: renamed from realtime_events.py (the per-session projection state and resolve_* helpers).
  • DOMAIN_TERMINAL_EVENTS / MODEL_OUTPUT_EVENTS move into session/emitter.py and duplex_response_format into config.py, their only consumers.

Dependency direction after this PR (one chain, no mesh):

protocol.realtime        OpenAI v1/realtime: RealtimeCommand / RealtimeEvent and their
    ^                    subclasses, helpers (audio, formats, items, session, capabilities)
    |  imports
    |
protocol.duplex          v1/realtime?duplex=1: DuplexCommand = RealtimeCommand,
    ^                    DuplexEvent = RealtimeEvent; Tier 1 re-exported unchanged,
    |  imports           Tier 2 subclasses (AppendAudio, Commit, SessionCreated, ...),
    |                    Tier 3 duplex-only classes, decode_duplex_command, error codes
    |
duplex engine, entrypoints, client
                         engine.duplex.mailbox   -> capabilities, command_from_realtime,
                                                    mailbox_payload (runner-internal dict)
                         engine.duplex.projection -> RealtimeProjectionState, resolve_*
                         entrypoints/duplex, DuplexOmni, InlineDuplexClient
  • Only the bottom arrow is allowed to target protocol.duplex; nothing below it imports protocol.realtime directly (tests/protocol/duplex/test_duplex_protocol_facade.py).
  • protocol/** imports no runtime package (tests/protocol/realtime/test_protocol_import_boundary.py).
  • Every Tier 1 name reachable through protocol.duplex is the same object as in protocol.realtime, or a subclass that only adds fields (tests/protocol/realtime/test_realtime_codec_single_source.py).

The duplex engine, entrypoints and clients import vllm_omni.protocol.duplex only; nothing outside vllm_omni/protocol imports protocol.realtime. Tests and docs are repointed; the facade, single-source and import-boundary tests assert the new layout.

Also in this PR: RealtimeEvent.to_realtime() is renamed to to_wire(), completing the wire_type / optional_wire_fields / _wire_fields vocabulary of the class and reading the same from RealtimeEvent and its DuplexEvent alias. Mechanical rename, no behaviour change.

No wire-format or behaviour change is intended anywhere in this PR.

Test Plan

vLLM Version: 0.30.0

vLLM-Omni Commit: 455c077 (branch duplex_protocol with main c1e84ce merged in; the sweep and pre-commit below were run at this head; the 2-GPU websocket comparison at the pre-rebase head 4e2129b vs its base 9fbc36a)

Run on a 4x L20X host, one pytest session:

pytest tests/protocol/ tests/engine/duplex/ tests/entrypoints/duplex/ tests/clients/ \
  tests/engine/test_duplex_import_boundary.py tests/engine/test_duplex_omni_engine.py \
  tests/engine/test_duplex_orchestrator.py tests/engine/test_single_stage_mode.py \
  tests/engine/test_orchestrator.py tests/engine/test_orchestrator_stage_input_bridge.py \
  tests/engine/test_async_omni_engine_outputs.py tests/entrypoints/test_duplex_omni.py \
  tests/entrypoints/openai_api/test_duplex_capability.py tests/entrypoints/openai_api/test_duplex_api_server.py \
  tests/entrypoints/openai_api/test_qwen3_omni_realtime_websocket.py \
  tests/entrypoints/openai/test_duplex_session_attachment.py tests/worker/test_native_duplex_hooks.py \
  tests/model_executor/models/minicpmo_4_5/duplex/ tests/model_executor/models/minicpmo_4_5/test_pipeline.py \
  tests/model_executor/models/nemotron_voicechat/duplex/ tests/model_executor/models/personaplex/ \
  tests/model_executor/models/qwen3_omni/test_duplex_plugin.py tests/benchmarks/patch/test_patch.py \
  tests/e2e/features/fullduplex/

plus tests/entrypoints/openai_api/test_qwen3_omni_realtime_websocket.py on 2 GPUs, on this branch and on the base commit 9fbc36a for comparison, and pre-commit run --files <branch diff>.

Test Result

🤖 Generated with Claude Code

@vllm-omni-review-bot

Copy link
Copy Markdown

This PR appears to belong to: docs/design/module/observability.md, docs/design/module/entrypoints.md.

Module owners: @NickCao @alex-jw-brooks @linyueqian

Routing: @NickCao via module named in the PR description, CODEOWNERS; @alex-jw-brooks via module named in the PR description; @linyueqian via module named in the PR description

@chickeyton, please review your own changes and leave a short self-review comment describing what you checked. PRs without author self-review may not be assigned a reviewer.

Please take a look when you have a chance. If you would like an automated review, mention @vllm-omni-review-bot in a comment.

@vllm-omni-review-bot

vllm-omni-review-bot commented Sep 20, 2026 •

Copy link
Copy Markdown

Omni ReviewBot triage note

Automated triage of commit 455c077ce04e produced:

  • Priority: high. Prompt maintainer attention is suggested.

These are automated triage suggestions only — the final decision belongs to the maintainers.

chickeyton and others added 3 commits September 20, 2026 10:35
…gine copies

Follow-up to vllm-project#7640. Three overlapping sets of command/event definitions
(protocol/realtime, protocol/duplex, and commands.py / events.py /
realtime_commands.py / realtime_events.py / audio.py in engine/duplex)
collapse into two layers:

* protocol/realtime: the OpenAI v1/realtime command and event classes and
  their helpers, unchanged.
* protocol/duplex: DuplexCommand / DuplexEvent as aliases of RealtimeCommand /
  RealtimeEvent, the Tier 2 extensions, the Tier 3 duplex-only classes, and
  the wire decoder (decode_duplex_command, build_append_audio,
  DUPLEX_COMMAND_TYPES) moved in from the engine. Consumer capabilities are
  injected instead of imported, and the decoder raises RealtimeProtocolError;
  DuplexCommandError is removed.

The engine keeps only its own representation under non-protocol names:
engine/duplex/mailbox.py (DUPLEX_REALTIME_CAPABILITIES, command_from_realtime,
mailbox_payload / mailbox_channel replacing the payload() methods and mailbox
type on the removed parallel command classes) and
engine/duplex/projection.py (renamed realtime_events.py). The epoch-filter
sets move into session/emitter.py and duplex_response_format into config.py,
their only consumers.

The duplex engine, entrypoints and clients now import protocol.duplex only;
tests and docs are repointed and the facade / single-source / import-boundary
tests assert the new layout.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: chickeyton <ngton2014@gmail.com>
The method renders the socket payload, and the class already names that
concept "wire" (wire_type, optional_wire_fields, _wire_fields). to_wire()
completes the set and reads the same from RealtimeEvent and from its
DuplexEvent alias, whose Tier 3 events travel on the same Realtime wire.
Mechanical rename across code, tests and docs; no behaviour change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: chickeyton <ngton2014@gmail.com>
The check-forbidden-imports pre-commit hook rejects the stdlib base64 module.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: chickeyton <ngton2014@gmail.com>
@chickeyton chickeyton changed the title [Duplex] Make protocol/duplex the single duplex vocabulary; drop the engine copies (follow-up to #7640) [Core] Make protocol/duplex the single duplex vocabulary; drop the engine copies (follow-up to #7640) Sep 20, 2026
@chickeyton

chickeyton commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor Author

precheck-pr report (self-check, full mode)

Run with the repo's precheck-pr skill against duplex_protocol at 455c077c, diffed against main (c1e84ce9). Type: General (core refactor, no new model, no examples, no perf claims).

Updated 2026-10-05 after merging main through #8489 (177 commits, 15 conflicts, all import repoints of the removed engine modules); previously updated 2026-09-23 after the vLLM 0.30.0 rebase (#7820). The first version of this report ran at 2f887b94 against main fa422a3f; its two advisory warnings were fixed in 4c829b60 (see the follow-up comment below) and every dimension has been re-checked at the current head.

Pre-check report for duplex_protocol

  Mode: full
  Type: general

  Dimension          Result
  -----------------  ------
  PR title format    ✓  "[Core] ..." prefix; not draft
  Code quality       ✓  added lines introduce no **kwargs lookups, broad excepts, Any hints,
                        hot-path copies, or blocking calls in async code
  Examples policy    —  no files under examples/ touched
  Simplification     ✓  0 candidates (the earlier MRO-walk duplication and the
                        test-only DUPLEX_COMMAND_TYPES export were removed in 4c829b60)
  PR desc integrity  ✓  description matches the 55-file diff (1 added, 4 deleted, 1 renamed,
                        49 modified); test plan cites the exact command, head and versions
  Registry/config    —  not applicable
  Dead code          ✓  ruff F401 clean; every new public name has a production caller
                        (mailbox_channel is called by mailbox_payload and exported for the
                        facade tests that prove the mailbox/wire split)
  Test coverage      ✓  core paths changed; 20 test files updated, facade / single-source /
                        import-boundary tests assert the new layout; main's new #7714, #7791,
                        #7644 and #7695 duplex tests carried over and repointed; the removed
                        engine-mixin slots test (#7985) is replaced by one pinning the
                        single-slotted-base layout, verified on a real Python 3.10
  Unrelated changes  ✓  every file in the diff is a duplex/protocol repoint or the to_wire rename
  Local pre-commit   ✓  ruff, ruff-format, typos, markdownlint, SPDX, CI marks,
                        forbidden-imports, torch.cuda all pass on the 55-file diff;
                        mypy-3.10 reports 18 errors that are byte-identical when the same
                        hook is run on main c1e84ce9 for the same files (backlog)
  CI gates           ✓  DCO, pre-commit, build 3.11/3.12 green on 455c077c (see below)
  Rebased            ✓  main c1e84ce9 merged in, MERGEABLE

  Verdict: 0 blocking | 0 warnings

Test evidence at this head

  • Full duplex sweep (24 suites, one pytest session, L20X host, vLLM 0.30.0): 1562 passed, 0 failed, 56 skipped; every skip is the 2-GPU gate in tests/entrypoints/openai_api/test_qwen3_omni_realtime_websocket.py.
  • The sweep, pre-commit and the Python 3.10 import check ran at the current head 455c077c.
  • The 2-GPU websocket comparison (branch vs base, 3 host-specific failures identical on both) is unchanged from the PR description.

Notes

  • DUPLEX_COMMAND_TYPES and the duplicated MRO walk flagged in the first run are gone; the follow-up comment below records that change.
  • Deprecated audioop warning from protocol/realtime/audio.py is pre-existing (G.711 codec), not touched here.
  • No new dependencies, no examples/ changes, no ratchet files (allowed_files, MAX_MODEL_TYPE_BRANCHES) touched.

🤖 Generated with Claude Code

mailbox_channel and mailbox_payload walked a command's MRO separately; one
_for_class helper now serves both tables. DUPLEX_COMMAND_TYPES had no
production caller: the one test that read it now derives the canonical
wire types from the command classes themselves, so the vocabulary is not
listed twice.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: chickeyton <ngton2014@gmail.com>
@chickeyton

Copy link
Copy Markdown
Contributor Author

Follow-up to the precheck report above: both advisory warnings are now addressed in 4c829b60.

  • engine/duplex/mailbox.py: one _for_class helper serves both the channel and renderer tables; the duplicated MRO walk is gone.
  • DUPLEX_COMMAND_TYPES is removed from protocol/duplex. The only test that read it now derives the canonical wire types from the command classes, so the vocabulary is listed once and the coverage assertion is unchanged.

Re-verified at this head: full duplex sweep on the L20X host 1161 passed, 0 failed, 4 skipped (same four 2-GPU skips); DCO, pre-commit and build 3.11/3.12 green.

Updated verdict: 0 blocking | 0 warnings.

🤖 Generated with Claude Code

@hsliuustc0106 hsliuustc0106 added high priority high priority issue, needs to be done asap core related to core module: cache, scheduler, engine, worker, modelrunner labels Sep 20, 2026
chickeyton and others added 6 commits September 21, 2026 09:55
Brings in vllm-project#7791 (Qwen3-Omni realtime playback interruption fix), whose new
tests use the pre-refactor module paths and to_realtime(); resolved by
repointing them at vllm_omni.protocol.duplex / engine.duplex.projection and
to_wire(). Also fixes the stale DuplexCommand.payload() mention in
docs/design/module/entrypoints.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: chickeyton <ngton2014@gmail.com>
Resolves the modify/delete on engine/duplex/commands.py from vllm-project#7633 (AURA):
the empty-audio (frames-only) append rendering moves into
engine/duplex/mailbox.py, and the Tier 1 AppendAudio default main added
already lives in protocol/realtime. runner.py keeps main's audio+video
admission release with mailbox_payload(); helpers.py keeps main's new
contracts import. main's new tests (test_input_transcribed.py, the
engine-session additions) are repointed at protocol.duplex /
engine.duplex.projection and to_wire().

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: chickeyton <ngton2014@gmail.com>
Resolves the modify/delete on engine/duplex/events.py (removed on this
branch; main only touched its docstring), keeps main's TranscriptDelta
Tier 2 tests and the response-metrics projection tests with the to_wire()
spelling, and combines the metrics imports with the protocol TurnEvent
import in session/engine_session.py.

Signed-off-by: chickeyton <ngton2014@gmail.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@vllm-omni-review-bot

vllm-omni-review-bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Omni ReviewBot: superseded

The CI failure noted on ea9cff3dc87a refers to an earlier head; the pull request now points at 455c077ce04e.

@vllm-omni-review-bot

Copy link
Copy Markdown

Omni ReviewBot: no human activity for 11 days

@chickeyton this pull request has had no human commit, comment or review since 2026-09-20. Please confirm the current plan and next step. The author or a maintainer decides whether to change the PR state.

To keep it moving, any one of these is enough: push an update, reply to the open blocker, or post the current plan and timeline.

@hsliuustc0106

Copy link
Copy Markdown
Collaborator

@chickeyton this PR is labeled ready + high priority, but CI is failing on the latest commit:

Could you please take a look and push an update to get CI green? Once the checks pass we can proceed with review/merge. Thanks!

Brings in 177 upstream commits (through vllm-project#8489). Conflicts were all the
same shape: upstream imports from engine.duplex.events / .commands /
.realtime_events, which this branch removed, so the merged files import
the same names from protocol.duplex.events / protocol.duplex.commands /
engine.duplex.projection, and to_realtime() reads to_wire().

- engine/duplex/commands.py stays deleted. Upstream's vllm-project#7985 change there
  (an explicit empty __slots__ on the engine DuplexCommand mixin for
  Python 3.10) has no counterpart here because DuplexCommand is the wire
  base itself; test_duplex_commands.py pins the single-slotted-base layout
  instead of the removed mixin test.
- The new delivery.py (vllm-project#7644) and its tests are repointed the same way.
- test_realtime_codec_single_source.py takes upstream's parametrized
  error-type test and drops the engine-commands subclass test, whose
  subject no longer exists.
- docs/design/fullduplex.md keeps this branch's module tree plus the new
  delivery.py entry.

Signed-off-by: chickeyton <ngton2014@gmail.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@vllm-omni-review-bot

Copy link
Copy Markdown
Omni ReviewBot routing record

Assigned Strict on zcode (GLM-5.3-Flash) under experiment fleet-strict-cursor-grok46-zcode-glm53flash-5050-c5-z10-20261002.

@vllm-omni-review-bot

Copy link
Copy Markdown
Omni ReviewBot attempt record

Review attempt ended as failed (step 'review' (agent.review_diff): unhandled error: RuntimeError: zcode exited 1 without a result event: statusCode: undefined } Error: Turn execution failed (traceId: 662d4d9c-9676-4c00-852c-936bda8cbb24) — check zcode login and the CLI version; retrying strict/zcode/GLM-5.3-Flash in 120s (try ).

@vllm-omni-review-bot

Copy link
Copy Markdown
Omni ReviewBot attempt record

Review attempt ended as failed (step 'review' (agent.review_diff): unhandled error: RuntimeError: zcode exited 1 without a result event: statusCode: undefined } Error: Turn execution failed (traceId: e3c922a8-175e-419b-b2e1-f336849204f8) — check zcode login and the CLI version; retrying strict/zcode/GLM-5.3-Flash in 600s (try ).

@vllm-omni-review-bot

Copy link
Copy Markdown
Omni ReviewBot attempt record

Review attempt ended as failed (step 'review' (agent.review_diff): unhandled error: RuntimeError: zcode exited 1 without a result event: statusCode: undefined } Error: Turn execution failed (traceId: 431c3f25-9a3b-4ce6-835c-a8edd0637588) — check zcode login and the CLI version; falling back to direct/cursor/auto).

@vllm-omni-review-bot vllm-omni-review-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Omni ReviewBot review

1 actionable finding(s).

CI at 455c077ce04e (2026-10-10T11:31:23.233250+00:00): required check(s) blocking: buildkite/vllm-omni (missing).

Note: The assigned review arm strict/zcode/GLM-5.3-Flash could not complete this review, so it was produced by the fallback arm direct/cursor/auto. It is excluded from the routing experiment.

Full review analysis

PR description

This change makes protocol/duplex the only duplex command and event vocabulary. DuplexCommand and DuplexEvent are aliases of the OpenAI realtime bases, the wire decoder lives there as decode_duplex_command, and to_realtime() is renamed to to_wire().
The engine no longer keeps a parallel command hierarchy. engine/duplex/mailbox.py binds that decoder to the engine's turn-detection capabilities and renders the runner mailbox dictionary; projection state moves to engine/duplex/projection.py.
Websocket and inline clients still enter through command_from_realtime, and the mailbox dictionaries keep the same channels, including turn.signal for session and item updates. No wire-format change is intended.

Change flow

flowchart LR
    A["[EXISTING] OpenAI realtime classes"]:::existing --> B["[CHANGED] protocol.duplex decoder"]:::changed
    B --> C["[NEW] engine mailbox binding"]:::new
    D["[REMOVED] engine command copies"]:::removed --> C
    C --> E["[CHANGED] session runner and clients"]:::changed
    classDef existing fill:#e5e7eb,stroke:#6b7280,color:#111827
    classDef changed fill:#fef3c7,stroke:#d97706,color:#451a03,stroke-width:2px
    classDef new fill:#dcfce7,stroke:#16a34a,color:#052e16,stroke-width:2px
    classDef removed fill:#fee2e2,stroke:#dc2626,color:#450a0a,stroke-width:2px
Loading

See inline comments below.


🤖 This review was generated by InferMatrix Copilot, an open-source repo-maintenance agent for PR review, CI debugging and issue triage. Try it on your own repo, and ⭐ star it if it helped!

)


def command_from_realtime(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Reported tests skip the duplex e2e guard this diff selects

Evidence and suggested fix

command_from_realtime is the websocket and inline-client intake for every duplex command, including audio append and session update. Changing vllm_omni/protocol/ and vllm_omni/engine/duplex/ selects the ready-pipeline step "Omni · MiniCPM-o 4.5 Duplex Test" (source_file_dependencies: omni_minicpmo_4_5_duplex_function in .buildkite/cuda/test-ready.yml). That step runs pytest -s -v tests/e2e/online_serving/test_minicpmo_4_5_duplex.py tests/e2e/online_serving/test_duplex_client_live.py -m 'core_model and cuda' --run-level 'core_model'. The merge step "Omni · MiniCPM-o 4.5 Duplex Test" is the same dependency key with pytest -s -v tests/e2e/online_serving/test_minicpmo_4_5_duplex.py tests/e2e/online_serving/test_minicpmo_4_5_window.py -m 'advanced_model and cuda' --run-level 'advanced_model'.
The Test Plan reports a CPU sweep of tests/protocol/, tests/engine/duplex/, entrypoints, and tests/e2e/features/fullduplex/, plus a 2-GPU run of tests/entrypoints/openai_api/test_qwen3_omni_realtime_websocket.py. Those selectors do not include test_minicpmo_4_5_duplex.py or test_duplex_client_live.py, and they do not use the ready step's core_model and cuda / --run-level core_model pair. The body does not say that GPU duplex e2e was left to CI. Run that ready selector, or state the omitted target and marker in the Test Plan.

@vllm-omni-review-bot

Copy link
Copy Markdown

Omni ReviewBot: finding feedback

[p1] Reported tests skip the duplex e2e guard this diff selects — vllm_omni/engine/duplex/mailbox.py:84

See the review for details. If you are the PR author and disagree, react 👎 here; the maintainer will see your disagreement.

@vllm-omni-review-bot

Copy link
Copy Markdown

Omni ReviewBot: no human activity for 13 days

@chickeyton this pull request has had no human commit, comment or review since 2026-10-04. Please confirm the current plan and next step. The author or a maintainer decides whether to change the PR state.

To keep it moving, any one of these is enough: push an update, reply to the open blocker, or post the current plan and timeline.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core related to core module: cache, scheduler, engine, worker, modelrunner high priority high priority issue, needs to be done asap

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants