[Bugfix] Fix Qwen3-Omni realtime playback interruption and conversation history (#7791)

Signed-off-by: psv666 <2693925048@qq.com>
This commit is contained in:
psv666 authored and GitHub committed 2026-09-20 14:42:17 +00:00
1 parent d3396fc5fb
commit 2d0b03f717
13 files changed
+1292 -68

No files matched your search

@@ -21,6 +21,7 @@ from vllm_omni.engine.duplex.config import (
from vllm_omni.engine.duplex.contracts import DuplexFence
from vllm_omni.engine.duplex.events import TurnEvent
from vllm_omni.engine.duplex.session.engine_session import DuplexEngineSession, DuplexFenceMismatchError
from vllm_omni.engine.duplex.session.playback_ledger import apply_playback_ack
from vllm_omni.model_executor.models.minicpmo_4_5.duplex.capabilities import (
minicpmo45_native_capabilities,
)
@@ -241,6 +242,83 @@ def test_history_commit_with_ack_only_policy_defers_unacknowledged_text():
assert f"item_{response_id}" in session.pending_history_item_ids
@pytest.mark.parametrize("audio_complete", [False, True])
def test_unaligned_response_keeps_empty_turn_before_later_user_input(audio_complete):
session = _session(config=DuplexSessionConfig(playback_commit_policy="ack_only"))
first = session.commit_audio_input(transcript="recite a poem")
response_id = session.begin_response()
session.reserve_history_item(f"item_{response_id}")
session.append_assistant_text("An answer the user has only partly heard")
session.mark_audio_sent(
duration_ms=10_000,
text_requires_complete_audio=True,
audio_complete=audio_complete,
)
assert session.end_response(commit_text=True) is None
session.barge_in()
session.clear_playback_cursor()
second = session.commit_audio_input(transcript="stop")
empty_answer = {"role": "assistant", "content": ""}
assert session.history == (first.message, empty_answer, second.message)
ack = apply_playback_ack(session, {"response_id": response_id, "played_ms": 6_981})
assert ack[0].to_realtime()["event"]["history_committed"] is False
assert session.history == (first.message, empty_answer, second.message)
if audio_complete:
ack = apply_playback_ack(session, {"response_id": response_id, "played_ms": 10_000})
assert ack[0].to_realtime()["event"]["history_committed"] is True
assert session.history == (
first.message,
{"role": "assistant", "content": "An answer the user has only partly heard"},
second.message,
)
@pytest.mark.parametrize(("reserve_slot", "assistant_text"), [(True, ""), (False, "unplayed answer")])
def test_unaligned_response_only_materializes_an_existing_answer_slot(reserve_slot, assistant_text):
session = _session(config=DuplexSessionConfig(playback_commit_policy="ack_only"))
first = session.commit_audio_input(transcript="first input")
response_id = session.begin_response()
if reserve_slot:
session.reserve_history_item(f"item_{response_id}")
session.append_assistant_text(assistant_text)
session.mark_audio_sent(duration_ms=10_000, text_requires_complete_audio=True, audio_complete=True)
assert session.end_response(commit_text=reserve_slot) is None
assert session.history == (first.message,)
@pytest.mark.parametrize("operation", ["delete", "truncate"])
def test_removing_one_empty_assistant_preserves_the_other_turn_boundary(operation):
session = _session(config=DuplexSessionConfig(playback_commit_policy="ack_only"))
user_inputs = []
response_ids = []
for transcript in ("first input", "second input"):
user_inputs.append(session.commit_audio_input(transcript=transcript).message)
response_id = session.begin_response()
response_ids.append(response_id)
session.reserve_history_item(f"item_{response_id}")
session.append_assistant_text("unplayed answer")
session.mark_audio_sent(duration_ms=10_000, text_requires_complete_audio=True, audio_complete=True)
session.end_response(commit_text=True)
item_id = f"item_{response_ids[1]}"
if operation == "delete":
assert session.delete_history_item(item_id) is True
else:
# A full ACK releases the pending snapshot. Truncation then edits the
# stored message into another empty dict before removing that item.
ack = apply_playback_ack(session, {"response_id": response_ids[1], "played_ms": 10_000})
assert ack[0].to_realtime()["event"]["history_committed"] is True
assert session.truncate_history_item(item_id, audio_end_ms=0, hard=True) is True
assert session.history == (user_inputs[0], {"role": "assistant", "content": ""}, user_inputs[1])
ack = apply_playback_ack(session, {"response_id": response_ids[1], "played_ms": 10_000})
assert ack[0].to_realtime()["error"]["code"] == "playback_item_not_found"
assert session.history == (user_inputs[0], {"role": "assistant", "content": ""}, user_inputs[1])
def test_truncate_history_item_uses_response_alignment_marks():
session = _session()
response_id = session.begin_response()