From d397a2f547a368643f10e2b0dd65ccc5bcf48edb Mon Sep 17 00:00:00 2001 From: wuli666 <421774554@qq.com> Date: Tue, 22 Sep 2026 00:24:42 +0800 Subject: [PATCH] [Bugfix] Refresh duplex client defaults after session updates (#7784) Signed-off-by: wuli666 <421774554@qq.com> --- tests/clients/test_duplex_client.py | 48 +++++++++++++++++++++++++++++ vllm_omni/clients/duplex.py | 14 +++++++++ 2 files changed, 62 insertions(+) diff --git a/tests/clients/test_duplex_client.py b/tests/clients/test_duplex_client.py index 75ef06e3f..7313828ce 100644 --- a/tests/clients/test_duplex_client.py +++ b/tests/clients/test_duplex_client.py @@ -13,6 +13,7 @@ from __future__ import annotations import asyncio import base64 import json +import struct import wave from collections.abc import Callable from typing import Any @@ -33,6 +34,7 @@ from vllm_omni.clients.duplex import ( ReconnectPolicy, SessionConfig, SessionResumed, + SessionUpdated, build_realtime_url, chunk_period_ms, duplex_unit_boundary_ms, @@ -797,6 +799,52 @@ async def test_inline_client_submits_typed_commands_in_order(): assert ack.played_ms == 120 and ack.response_id == "resp-1" +@pytest.mark.asyncio +@pytest.mark.parametrize("accepted", [True, False]) +@pytest.mark.parametrize( + "patch", + [ + {"input_audio_format": "pcm_f32le", "sample_rate_hz": 24_000}, + {"audio": {"input": {"format": "pcm_f32le", "sample_rate_hz": 24_000}}}, + ], +) +async def test_inline_input_defaults_change_only_after_session_updated(accepted, patch): + client, omni = make_inline_client() + patch = {**patch, "overlap_silence_rms": 0.1} + async with client: + handle = omni.handles[0] + previous_session = dict(client.session_info) + await client.send({"type": "session.update", "session": patch}) + # Sending a patch is not an acknowledgement of its acceptance. + assert client.session_info == previous_session + assert client._input_defaults().input_sample_rate_hz == 16_000 + updated_session = {**previous_session, **patch} + event: dict[str, object] = ( + {"type": "session.updated", "session": updated_session} + if accepted + else {"type": "error", "error": {"code": "invalid_session_update", "message": "rejected"}} + ) + handle.feed(event) + received = await client.wait_for("session.updated" if accepted else "error", timeout_s=2.0) + assert isinstance(received, SessionUpdated if accepted else ErrorEvent) + + pcm = struct.pack(" dict[str, object]: + value = self.raw.get("session") + return value if isinstance(value, dict) else {} + + class SessionClosed(DuplexEvent): pass @@ -456,6 +466,7 @@ class ErrorEvent(DuplexEvent): _EVENT_TYPES: dict[str, type[DuplexEvent]] = { "session.created": SessionCreated, "session.resumed": SessionResumed, + "session.updated": SessionUpdated, "session.closed": SessionClosed, "session.expired": SessionExpired, "response.created": ResponseCreated, @@ -1011,6 +1022,9 @@ class DuplexClientBase(ABC): self._adopt_session(event) elif isinstance(event, SessionCreated): self._adopt_session(event) + elif isinstance(event, SessionUpdated): + if event.session: + self.session_info = event.session elif isinstance(event, ResponseCreated): response_id = event.response_id if response_id and response_id not in self._responses: