Skip to content

[Frontend] OpenAI Live sessions (/v1/live/sessions) for MiniCPM-o 4.5 and PersonaPlex - #9220

Open
vraiti wants to merge 1 commit into
vllm-project:mainfrom
vraiti:feat/live-sessions
Open

vraiti wants to merge 1 commit into
vllm-project:mainfrom
vraiti:feat/live-sessions

Conversation

@vraiti

@vraiti vraiti commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

Adds a native OpenAI Live (/v1/live/sessions) WebSocket endpoint and serves MiniCPM-o 4.5 and PersonaPlex on it as full-duplex models.

Implements RFC #8085 for the native-VAD models. The RFC's external-VAD models, Qwen3-Omni and Nemotron VoiceChat, already have full OpenAI support through /v1/realtime, so /v1/live/sessions serves only models that decide for themselves when to listen and speak.

Contributor intent (required human answers)

1. What outcome do you want to achieve?

Support OpenAI-compliant /v1/live/sessions for serving native fullduplex models.

2. What is the highest-leverage variable for that outcome?

An OpenAI-compliant /v1/live/sessions, which is completely absent in upstream right now.

3. How will this work move that variable?

By adding the endpoint using an architecture that leverages existing vLLM-Omni features to constrain blast radius to the API server and model pipeline code.

4. Whose customer or user needs does this address, and what are those needs?

Anyone who wishes to interface with a vLLM-Omni native VAD model via standard OpenAI tooling.

Purpose

Before: native full-duplex speech models are reachable only through vLLM-Omni's own /v1/realtime?duplex=1 stack. OpenAI Live clients, such as LiveKit Agents' GPTLiveModel or the OpenAI SDK's openai.types.live events, cannot connect to them.

After: a deployment whose deploy config has a live_session_config block serves /v1/live/sessions, which speaks the OpenAI Live protocol. Each session drives one resumable engine request: audio arrives as session.input_audio.append, is cut into the model's native units and sent as streaming updates, and the model's audio (and MiniCPM-o's transcript) streams back as session.output_audio.delta / session.output_transcript.delta. The model decides when to listen and when to speak; there is no server-side VAD or turn detection.

  • Shared stack (vllm_omni/entrypoints/openai/live/):
    • the protocol layer, built on the openai.types.live models;
    • the session handler;
    • a resumable request driver;
    • audio format conversion (PCM16 at 16/24 kHz, G.711 μ-law and A-law);
    • a LiveSessionProcessor interface that each model implements.
  • Configuration:
    • a pipeline's live_session_config names its processor and an optional per-stage logits processor;
    • the deploy config's live_session_config enables the endpoint, with session_lifetime_s and an optional max_sessions cap. Over the cap, session.start gets session_limit_reached and the socket closes with 1013.
  • Context limit: on context_length_exceeded the session drops its oldest units so that what remains fills half of the context left after the prefix, then resubmits. MiniCPM-o carries the dropped units' text forward as "previous" context. PersonaPlex restarts from the newest frame.
  • Metrics: vllm_omni:live_active_sessions, live_rejected_sessions, live_audio_seconds{direction}, live_context_trims and live_errors{reason}.
  • MiniCPM-o 4.5:
    • a processor following the official duplex demo: 1 s units, streaming mel features, capped previous text;
    • a stage-0 logits processor that enforces the model's listen/speak grammar;
    • Live-aware hand-off from the text stage to TTS.
  • PersonaPlex: a processor that sends one 80 ms frame per streaming update. Its prompting helpers moved to personaplex/prompting.py, shared with the existing duplex path, and its Mimi codec pools are sized by max_sessions.
  • Deploy overlays: vllm_omni/deploy/minicpmo_4_5_live.yaml and personaplex_live.yaml.
  • Unchanged: existing chat, /v1/realtime and duplex deployments behave as before. The Live logits processor and session sizing apply only when the deploy config enables Live sessions, and the AR model runner is unchanged.
  • Shared code touched outside the API server and model pipelines:
    • an 8-line pass-through of the orchestrator's existing segment_finished flag to OmniRequestOutput (engine/messages.py, engine/orchestrator.py, entrypoints/omni_base.py, outputs/__init__.py). The flag defaults to False, and there's no scheduling or engine change;
    • Live config plumbing in config/stage_config.py and config/omni_config.py;
    • the new config/live_session.py and metrics/live.py;
    • openai>=3.18.0 in requirements/common.txt, for openai.types.live.

Docs: docs/serving/live_sessions_api.md. Example: examples/online_serving/live_sessions/livekit/agent.py, a LiveKit Agents voice agent on the endpoint.

Not supported by native-VAD models, and therefore narrower than the RFC's event list:

  • session.instructions.append, response.item.create and response.create, since these models have no mid-stream text slot and their output is clocked by input audio rather than requested;
  • tools and function calling;
  • store;
  • OpenAI built-in voice names;
  • input transcripts.

The events, tools and store are rejected with unsupported_* errors, and each session start lists them in an info event. A built-in voice name is rejected with invalid_value. Input transcripts are simply never sent.

Test Plan

vLLM Version: 0.32.0

vLLM-Omni Commit: abbd3d9 (on 1c11eef)

  • L1 (CPU): pytest tests/entrypoints/openai/live tests/model_executor/models/minicpmo_4_5/test_live_logits_processor.py tests/model_executor/stage_input_processors/test_minicpmo_4_5_live.py -m "core_model and cpu". Covers the session handler against a fake engine (session limit, protocol errors, context overflow and resubmission, close), context trimming, config validation, audio conversion, protocol events against the openai SDK's ServerEvent schema, and the MiniCPM-o logits processor and stage hand-off.

  • L3 (1× GPU): pytest tests/e2e/online_serving/test_minicpmo_4_5_live.py tests/e2e/online_serving/test_personaplex_live.py. Each model gets three raw-WebSocket tests:

    • conversation: spoken replies, plus transcripts for MiniCPM-o;
    • context trim: a 1024-token (MiniCPM-o) or 600-token (PersonaPlex) context overflowed in about 100 s, then a reply after the trim;
    • concurrent sessions: two sessions at once, and a third rejected with session_limit_reached / 1013.

    CI runs the MiniCPM-o file in the merge pipeline ("Omni · MiniCPM-o 4.5 Live Test"). PersonaPlex has no CI step, because its checkpoint (nvidia/personaplex-7b-v1) is gated.

  • Manual: LiveKit Agents GPTLiveModel (livekit-agents 1.8.3) through a local LiveKit server and the agent-starter-react frontend, talking to both models live.

Test Result

  • L1: 188 passed, and the handler tests passed in 5 consecutive runs. The full tests/entrypoints CPU suite passes (2,716 tests), including test_api_server_guards.py with /v1/live/sessions added to its route manifest.
  • L3: all 6 tests passed on 1× RTX PRO 6000 Blackwell (96 GB) in 9 min 31 s, on the branch before its rebase onto [Bugfix] MiniCPM-o 4.5 duplex: let the turn-end Talker condition finish speaking #8911's MiniCPM-o Talker changes.
  • Manual: several calls with each model; the agent connected, the models replied in speech, and the calls closed cleanly with no server errors.
  • Pre-commit: clean on all changed files, except 14 mypy errors in vllm_omni/engine/messages.py that were already there before this change.

… and PersonaPlex

Add a native /v1/live/sessions WebSocket endpoint implementing the OpenAI
Live protocol on top of one resumable request per session.

- entrypoints/openai/live: protocol, session handler, resumable request
  driver, audio format conversion and the per-model processor interface.
- live_session_config in the pipeline and deploy configs; optional
  max_sessions (over the limit: session_limit_reached, close 1013).
- On context_length_exceeded the session trims to half the context after
  the prefix and resubmits; vllm_omni:live_* metrics.
- MiniCPM-o 4.5: Live processor following the official duplex demo
  (per-unit transcripts, capped previous text, short-unit codec hold).
- PersonaPlex: Live processor and prompting helpers; Mimi pools sized by
  max_sessions.
- Deploy overlays, docs and a LiveKit example.
- L3 E2E tests for both models (conversation, context trim, concurrent
  sessions and the session limit) and a merge-pipeline job for MiniCPM-o.
- L1 CPU tests for the handler, trimming, config, audio, protocol and the
  MiniCPM-o logits processor and stage hand-off; the route manifest guard
  lists /v1/live/sessions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: vraiti <vraiti@redhat.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-21T13:05:43.165287Z
We couldn’t complete this review. Comment @codex review to try again.
abbd3d9 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@vllm-omni-review-bot

Copy link
Copy Markdown

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

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

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

@vraiti, 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.

@amy-why-3459 amy-why-3459 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for putting this together. Given the size, I focused on the core runtime paths: the Live handler/driver/session/processor under entrypoints/openai/live/, the config plumbing (live_session.py, stage_config.py, omni_config.py), the MiniCPM-o Live encoder, the llm2tts / tts2code2wav_async_chunk Live hand-off, and the Talker preprocess changes. I only skimmed the PersonaPlex processor/stage0, the protocol/audio helpers, docs and tests.

A few things inline. One more small one: segment_finished is plumbed through messages.py / orchestrator.py / omni_base.py / outputs, but the only reader I can find is the driver's debug log; the handler detects segment end from the stage-0 finish_reason. Either use it there, or drop the shared-code change from this PR.

processor.push_audio(model_samples, start_ms)
while (unit := processor.next_unit()) is not None:
self._pending.append(unit)
if len(self._pending) > max(BACKLOG_WARNING_UNITS, self._backlog_warned):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

_pending has no upper bound, only a warning. The server doesn't pace input: session_ms advances by the decoded audio duration, and expiry is wall-clock, so a client that sends faster than real time (or a slow/stalled stage 0) can grow this deque without limit. Each unit holds its raw audio, plus mel features for MiniCPM-o. Could we cap it (e.g. N units or N seconds) and send an error and close, or drop, once it's exceeded?

# Each session keeps a streaming audio-encoder cache of up to ~150 MB on the
# Stage 0 GPU that vLLM's memory profiling does not count. Set a bound when
# the Stage 0 headroom is tight.
max_sessions: null

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

With max_sessions: null, SessionSlots admits unlimited sessions, while the comment above says each one keeps ~150 MB of encoder cache on Stage 0 that profiling doesn't count. That makes the shipped overlay easy to OOM under load. Also, when this is null, streaming_session_capacity() falls back to 1 and duplex_max_sessions isn't propagated, so anything sized from it (e.g. the audio-encoder graph grid when duplex_audio_encoder_cuda_graph_batch_sizes_from_sessions is set) is sized for one session while the API server admits many. A finite default here (as in personaplex_live.yaml) seems safer.

available,
info.get("request_id"),
)
rows = rows[-available:] if available > 0 else rows[:0]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

After preemption, the recompute re-runs earlier units' rows, which then keep the placeholder token embedding, and only a warning is logged. The request carries on with KV that was computed from placeholders instead of audio, so the model silently loses those units. Since the handler keeps the full unit history and can already re-render it (render_resume), raising here so the session resubmits seems better than continuing with corrupted context. Note that the handler currently resubmits only on context_length_exceeded, so that path would need extending too.

f"prompt_len={info_dict.get('_omni_prompt_len')}"
)
if native_duplex:
if live_budget is not None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The Live TTS request spans the whole session and is re-prefilled for each unit, but only the native_duplex branch below drops _request_codec_device_states / _request_codec_device_inputs for the request. The batched GPU path is fine, because the new state dict has no _gpu_slot and CodecState.prepare reloads step/limit/min. But on the scalar sampler path (_codec_sample_setup), unit 2+ would resume the previous unit's device step counter and keep the first unit's cached min_tokens (0 at turn start), so the per-unit codec_min wouldn't apply. Should that cleanup be if native_duplex or live_budget is not None:?

@vllm-omni-review-bot

Copy link
Copy Markdown

Omni ReviewBot triage note

Automated triage of commit abbd3d91089b produced:

  • Priority: high. Prompt maintainer attention is suggested.

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

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants