Skip to content

fix(stt): detect half-open websockets in OpenAI, ElevenLabs and Inworld STT - #7912

Open
ninglinLiu wants to merge 3 commits into
livekit:mainfrom
ninglinLiu:cursor/fix-halfopen-websocket-stt-193a
Open

ninglinLiu wants to merge 3 commits into
livekit:mainfrom
ninglinLiu:cursor/fix-halfopen-websocket-stt-193a

Conversation

@ninglinLiu

Copy link
Copy Markdown

Problem

If a streaming STT websocket goes half-open (the provider, or a NAT in between, disappears without a FIN/RST), the OpenAI realtime, ElevenLabs and Inworld STT streams wait on ws.receive() forever. Nothing raises, so the stream never reconnects and stt.FallbackAdapter never fails over; the agent just stops hearing the user (#7900).

Deepgram, Soniox, Telnyx, xAI and Simplismart were already fixed the same way (#7206, #7357, #7359, #7364, #7369).

Fix

Same pattern as those merged fixes, applied to the three remaining plugins:

  1. Pass heartbeat=30.0 to ws_connect in openai/stt.py, elevenlabs/stt.py and inworld/stt.py. aiohttp defaults heartbeat to None, so without it an unanswered socket is never noticed.
  2. Treat WSMsgType.ERROR in the receive loop as a retryable APIConnectionError, chained from ws.exception(). That is where aiohttp reports the missed pong. It already returns quietly when we are closing the socket ourselves. Before this change, OpenAI/ElevenLabs logged it as an "unexpected message type" and continued, and Inworld logged it and returned without triggering a reconnect.

I kept the change inline per plugin, the same way as the existing fixes, and kept the 30s interval they use. The issue asks whether a shared helper in livekit.agents.utils and/or a shorter interval would be better. I'm happy to rework this PR either way if you prefer.

Tests

New unit tests (one file per plugin, pytest.mark.unit) using a fake aiohttp session/socket:

  • tests/test_plugin_openai_stt_halfopen.py
  • tests/test_plugin_elevenlabs_stt_halfopen.py
  • tests/test_plugin_inworld_stt_halfopen.py

Each checks that the socket is opened with heartbeat=30.0, and that a heartbeat-timeout ERROR frame ends the receive loop and leads to a reconnect without spinning.

uv run pytest tests/test_plugin_openai_stt_halfopen.py tests/test_plugin_elevenlabs_stt_halfopen.py tests/test_plugin_inworld_stt_halfopen.py
ruff check . && ruff format --check .

Fixes #7900

cursoragent and others added 2 commits October 17, 2026 14:17
…rld STT plugins

Add heartbeat=30.0 to ws_connect and handle WSMsgType.ERROR by raising
retryable APIConnectionError in OpenAI realtime, ElevenLabs, and Inworld
STT plugins. Without heartbeat, a silently dropped socket (half-open TCP
connection, no FIN/RST) is never noticed: recv_task parks on ws.receive()
forever and the reconnect loop, which is exception-driven, never runs.

The heartbeat closes the socket when a ping goes unanswered, and that
surfaces as WSMsgType.ERROR. ws.exception() is the only place the reason
survives, so we chain it to the APIConnectionError.

Add regression tests for each plugin proving they notice a silent websocket
via heartbeat and raise retryable APIConnectionError within bounded time.

Fixes livekit#7900

Co-authored-by: NingLin Liu <x07514475479@gmail.com>
Run ruff format on 4 files per upstream CI requirements:
- livekit-plugins-elevenlabs/livekit/plugins/elevenlabs/stt.py
- tests/test_plugin_openai_stt_halfopen.py
- tests/test_plugin_elevenlabs_stt_halfopen.py
- tests/test_plugin_inworld_stt_halfopen.py

Restructured the Inworld test's _stream function to keep the
type: ignore[arg-type] comment on the http_session argument line
where mypy will apply it, while satisfying ruff format requirements.

Co-authored-by: NingLin Liu <x07514475479@gmail.com>

@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: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Devin Review

@DEVJHAWAR11

Copy link
Copy Markdown

Ran the probe from #7900 against this branch, and it works:

plugin main this PR
openai STT no error after 75s APIConnectionError after 47.5s
elevenlabs STT no error after 75s APIConnectionError after 45.9s
inworld STT no error after 75s APIConnectionError after 45.9s

So it now matches the other fixed plugins at about 45s. I'm happy to follow up with the shared helper and a shorter heartbeat across all the STT plugins if that's the direction you want, so it can land separately from this one.

This branch has not been deployed

No deployments
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.

Streaming STT: openai, elevenlabs and inworld never notice a silently dropped websocket, and the fixed plugins take ~45s

3 participants