From a2e383a1a582c8d05170915ad63828046dd9f823 Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Sat, 12 Sep 2026 12:53:03 -0700 Subject: [PATCH] fix(realtime): ignore a late speechStart for a finished Muse turn A duplicate speechStart for a turn that already stopped used to make that closed turn active again, so the next turnless PUSH_TO_TALK transcript was routed to the finished item and dropped. --- litellm/llms/meta/realtime/transformation.py | 2 ++ .../test_meta_realtime_transformation.py | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/litellm/llms/meta/realtime/transformation.py b/litellm/llms/meta/realtime/transformation.py index c43e1897fbc..549db60be85 100644 --- a/litellm/llms/meta/realtime/transformation.py +++ b/litellm/llms/meta/realtime/transformation.py @@ -495,6 +495,8 @@ class MuseEventTransformer: def _speech_start(self, message: Mapping[str, JsonValue]) -> _TurnState: turn: Final = self._turn(_required_turn_id(message, "speechStart")) + if turn.stopped: + return turn turn.started = True self._active_turn_id = turn.item_id return turn diff --git a/tests/test_litellm/llms/meta/realtime/test_meta_realtime_transformation.py b/tests/test_litellm/llms/meta/realtime/test_meta_realtime_transformation.py index 8b9eaf12dc2..3a4c0591fb5 100644 --- a/tests/test_litellm/llms/meta/realtime/test_meta_realtime_transformation.py +++ b/tests/test_litellm/llms/meta/realtime/test_meta_realtime_transformation.py @@ -407,6 +407,24 @@ def test_finished_turn_ignores_late_duplicates(): ) +def test_late_duplicate_speech_start_does_not_capture_the_next_turnless_transcript(): + transformer = MuseEventTransformer() + transformer.configure(MuseSessionConfig(MUSE_MODEL, "PUSH_TO_TALK", 24_000, ())) + transformer.transform(json.loads(_event("speechStart", turnId="turn-1"))) + transformer.transform(json.loads(_event("speechComplete", turnId="turn-1", transcript="first"))) + + assert transformer.transform(json.loads(_event("speechStart", turnId="turn-1"))) == () + events = transformer.transform(json.loads(_event("transcript", transcript="second", final=True))) + + assert [event["type"] for event in events] == [ + "input_audio_buffer.speech_started", + "input_audio_buffer.speech_stopped", + "conversation.item.input_audio_transcription.completed", + ] + assert events[2]["transcript"] == "second" + assert events[2]["item_id"] != "turn-1" + + def test_turn_memory_is_bounded_by_turn_limit(): transformer = MuseEventTransformer(turn_limit=2)