Skip to content

fix(google): drain realtime audio after tool calls#6285

Open
Anrahya wants to merge 4 commits into
livekit:mainfrom
Anrahya:fix/google-realtime-tool-audio-drain
Open

fix(google): drain realtime audio after tool calls#6285
Anrahya wants to merge 4 commits into
livekit:mainfrom
Anrahya:fix/google-realtime-tool-audio-drain

Conversation

@Anrahya

@Anrahya Anrahya commented Jul 1, 2026

Copy link
Copy Markdown

Closes #5742

Summary

  • keep Gemini realtime audio open after a tool call until trailing audio has drained
  • close the function stream immediately so tool execution can start without waiting for audio drain
  • add a regression test for audio -> toolCall -> trailing audio ordering

Why

Gemini Live exposes toolCall as a distinct server message from serverContent.turnComplete. The API reference defines turnComplete as the model-turn completion signal, while toolCall asks the client to execute function calls and return responses. The Live API capabilities guide also notes that Gemini 3.1 Live server events can carry multiple content parts and clients should process all parts.

Closing the generation at toolCall can therefore close audio_ch before trailing Gemini audio chunks are delivered. This makes RunContext.wait_for_playout() resolve too early for custom end-call tools that wait for playout before tearing down the room.

Tests

  • uv run pytest --plugin google tests/test_plugin_google_llm.py
  • uv run ruff check livekit-plugins/livekit-plugins-google/livekit/plugins/google/realtime/realtime_api.py tests/test_plugin_google_llm.py
  • uv run ruff format --check livekit-plugins/livekit-plugins-google/livekit/plugins/google/realtime/realtime_api.py tests/test_plugin_google_llm.py
  • uv run python scripts/check_types.py -- --no-sync

@CLAassistant

CLAassistant commented Jul 1, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@Anrahya Anrahya marked this pull request as ready for review July 1, 2026 16:22
@Anrahya Anrahya requested a review from a team as a code owner July 1, 2026 16:22

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

Open in Devin Review

Comment on lines +1466 to +1467
gen.function_ch.close()
self._schedule_generation_done_after_audio_drain(gen)

@devin-ai-integration devin-ai-integration Bot Jul 1, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚩 Behavioral change: text channel stays open during audio drain period

Previously, _mark_current_generation_done was called immediately on tool call, closing text_ch along with everything else. Now text_ch remains open during the audio drain window (up to 5 seconds). Any output transcription or text content arriving during this window will be delivered via push_text (livekit-plugins/livekit-plugins-google/livekit/plugins/google/realtime/realtime_api.py:1306) to the still-open text channel. This is likely desirable (capturing trailing transcription), but it's a semantic change that downstream consumers may not expect — text content could arrive after function calls have been signaled as complete.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 3 new potential issues.

Open in Devin Review

Comment on lines +1389 to +1410
async def _mark_generation_done_after_audio_drain(self, gen: _ResponseGeneration) -> None:
started_at = time.monotonic()
last_audio_frame_at = gen._last_audio_frame_at

while True:
if gen._done:
return

elapsed = time.monotonic() - started_at
if elapsed >= TOOL_CALL_AUDIO_DRAIN_TIMEOUT:
break

await asyncio.sleep(
min(TOOL_CALL_AUDIO_DRAIN_QUIESCENCE, TOOL_CALL_AUDIO_DRAIN_TIMEOUT - elapsed)
)

current_last_audio_frame_at = gen._last_audio_frame_at
if current_last_audio_frame_at == last_audio_frame_at:
break
last_audio_frame_at = current_last_audio_frame_at

self._mark_generation_done(gen)

@devin-ai-integration devin-ai-integration Bot Jul 1, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚩 Drain quiescence timer starts even when no audio has been received

When a tool call arrives for a generation that has no audio (_last_audio_frame_at is None), _schedule_generation_done_after_audio_drain still creates a drain task. The drain task sleeps for TOOL_CALL_AUDIO_DRAIN_QUIESCENCE (0.5s default), then breaks because None == None. This adds an unnecessary 0.5s delay before finalizing the generation. For text-only modalities or tool-call-only generations (like gen2 in a chained tool-call scenario), this delay is wasted. Consider checking gen._last_audio_frame_at is not None before scheduling the drain, or immediately finalizing if no audio was ever received.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

devin-ai-integration[bot]

This comment was marked as resolved.

@Anrahya Anrahya force-pushed the fix/google-realtime-tool-audio-drain branch from ce7b84b to 067f98a Compare July 1, 2026 19:37
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.

Gemini realtime: trailing audio truncated when a tool call ends the turn

2 participants