Skip to content

fix(openai): stop realtime reconnect loop on fatal errors - #2031

Merged
toubatbrian merged 2 commits into
mainfrom
courses-clumping-morays
Jul 14, 2026
Merged

fix(openai): stop realtime reconnect loop on fatal errors#2031
toubatbrian merged 2 commits into
mainfrom
courses-clumping-morays

Conversation

@rosetta-livekit-bot

@rosetta-livekit-bot rosetta-livekit-bot Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Summary

  • classify OpenAI realtime server errors with fatal quota/auth/billing codes as non-retryable
  • propagate fatal realtime error and failed response.done events out of the websocket loop so the session stops reconnecting
  • close active generations and reject pending generateReply futures when reconnecting or when the realtime session exits
  • add OpenAI realtime unit coverage and a patch changeset for @livekit/agents-plugin-openai

Source diff coverage

Files from livekit/agents#6352
  • livekit-plugins/livekit-plugins-openai/livekit/plugins/openai/realtime/realtime_model.py: adapted to plugins/openai/src/realtime/realtime_model.ts. The fatal-code set and helper were translated to TypeScript; fatal server errors now create non-retryable APIErrors, websocket message handling propagates those errors to the main reconnect loop, and reconnect/session-exit cleanup closes active generations plus rejects pending response-created futures. This is adapted rather than literal because the JS implementation uses ws callbacks, Task, Future, and event-driven websocket close futures instead of Python async receive tasks.
  • tests/test_realtime/test_openai_realtime_model.py: ported to plugins/openai/src/realtime/realtime_model.test.ts. The source fatal classification tests and handler behavior tests were added to the existing Vitest realtime suite using the target’s RealtimeModel/RealtimeSession test setup.

Validation

  • pnpm prettier --write "plugins/openai/src/realtime/realtime_model.ts" "plugins/openai/src/realtime/realtime_model.test.ts" ".changeset/openai-realtime-fatal-errors.md"
  • pnpm build passed after the follow-up fixes
  • pnpm lint passed with existing warnings
  • pnpm test plugins/openai/src/realtime/realtime_model.test.ts passed: 41 tests
  • pnpm test plugins/openai passed 6 files / 109 tests (3 skipped); the package-wide selector remains blocked by an existing unrelated collection failure in plugins/openai/src/tts.test.ts because gpt-realtime-whisper is constructed without the now-required VAD

Ported from livekit/agents#6352

Original PR description

Addresses the production failure in #6205 . Rescoped per review (https://github.com/livekit/agents/pull/6352#issuecomment-4920965533)— see "Scoped out" below.

Motivation
On an OpenAI account whose quota ran out mid-service, the realtime session reconnect-loops forever while the caller hears dead air. From the production capture, every ~300ms cycle is:

error=APIError('OpenAI Realtime API returned an error',
body=RealtimeError(message='You exceeded your current quota, ...',
type='insufficient_quota', code='insufficient_quota'), retryable=True) recoverable=True
error=APIConnectionError('OpenAI S2S connection closed unexpectedly', body=None, retryable=True) recoverable=True
Two things combine to cause this, and neither is handled today:

The quota error is emitted with recoverable=True (the handlers optimistically mark everything recoverable), so nothing — including RealtimeModelFallbackAdapter, whose swap is triggered by recoverable=False — treats it as terminal.
The socket itself always reconnects fine (the key is valid; only generation fails, then the server closes). _main_task resets num_retries on every successful connect, so the give-up branch is unreachable and the loop never ends.
Meanwhile a pending generate_reply() sits out its full 10s timeout and fails with a generic "generate_reply timed out" that says nothing about quota.

Changes (OpenAI plugin only)
_FATAL_ERROR_CODES (insufficient_quota, invalid_api_key, account_deactivated, billing_hard_limit_reached) — codes that can never succeed on retry — classified where errors are parsed: _handle_error (server error events) and _handle_response_done_but_not_complete (failed response.done). These now emit recoverable=False instead of the hardcoded True; everything else keeps the optimistic default.
When a fatal code is seen, the session records it and _main_task stops reconnecting (raises the fatal error instead of retrying), ending the infinite loop. No retry-budget heuristics needed — the fatal error event arrives on every cycle, so classification alone is sufficient.
Pending generate_reply futures fail immediately with the actual code ("openai returned a fatal error: insufficient_quota") instead of waiting out the timeout.
With recoverable=False now emitted correctly, the existing recovery path works as designed: RealtimeModelFallbackAdapter swaps to the next model (and re-issues the reply via regenerate_on_swap), or a bare session fails fast and loud instead of spinning silently.

Scoped out (per review)
Earlier revisions of this PR added a framework-level reconnect state machine and an automatic generate_reply retry in the turn loop; both were dropped — speech retry belongs to RealtimeModelFallbackAdapter / the application. Two small follow-up PRs planned:

Make the pending-reply drop in _reconnect() observable via SpeechHandle.exception() (building on #6304.
Graceful handling of the update_chat_ctx timeout inside generate_reply (today an unhandled RealtimeError).
Testing
8 new unit tests in tests/test_realtime/test_openai_realtime_model.py: fatal-code matcher, classification at both parse sites (fatal + transient), fail-fast of pending reply futures, transient errors left untouched, Cancellation failed early-return.
ruff format --check, ruff check, full mypy (agents + all plugins), and pytest tests/test_realtime --unit all green.

Cue voice E2E

  • Strict voice-mode session: sid_3cffb55e639e at head 839dada2.
  • A deterministic local OpenAI-compatible WebSocket server produced insufficient_quota; the persisted QCLI event recorded retryable=false, recoverable=false, sessionClosed=true, and connectionCount=1 after a 3-second no-reconnect observation window.
  • The session persisted framework events plus full-session and per-command WAV recordings under ~/.cue-cli/sessions/sid_3cffb55e639e/.
  • Provider limitation: this deterministic fatal-path run used a local protocol-compatible server rather than the production OpenAI endpoint.

@rosetta-livekit-bot
rosetta-livekit-bot Bot requested a review from a team as a code owner July 14, 2026 08:00
@changeset-bot

changeset-bot Bot commented Jul 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 839dada

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 36 packages
Name Type
@livekit/agents-plugin-openai Patch
@livekit/agents-plugin-anam Patch
@livekit/agents-plugin-cartesia Patch
@livekit/agents-plugin-cerebras Patch
@livekit/agents-plugin-elevenlabs Patch
@livekit/agents-plugin-fishaudio Patch
@livekit/agents-plugin-google Patch
@livekit/agents-plugin-hume Patch
@livekit/agents-plugin-inworld Patch
@livekit/agents-plugin-neuphonic Patch
@livekit/agents-plugin-perplexity Patch
@livekit/agents-plugin-rime Patch
@livekit/agents-plugin-sarvam Patch
@livekit/agents-plugin-xai Patch
@livekit/agents Patch
@livekit/agents-plugin-anthropic Patch
@livekit/agents-plugin-assemblyai Patch
@livekit/agents-plugin-baseten Patch
@livekit/agents-plugin-bey Patch
@livekit/agents-plugin-deepgram Patch
@livekit/agents-plugin-did Patch
@livekit/agents-plugin-hedra Patch
@livekit/agents-plugin-lemonslice Patch
@livekit/agents-plugin-liveavatar Patch
@livekit/agents-plugin-livekit Patch
@livekit/agents-plugin-minimax Patch
@livekit/agents-plugin-mistral Patch
@livekit/agents-plugin-mistralai Patch
@livekit/agents-plugin-phonic Patch
@livekit/agents-plugin-resemble Patch
@livekit/agents-plugin-runway Patch
@livekit/agents-plugin-silero Patch
@livekit/agents-plugin-soniox Patch
@livekit/agents-plugin-tavus Patch
@livekit/agents-plugin-trugen Patch
@livekit/agents-plugins-test Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@rosetta-livekit-bot
rosetta-livekit-bot Bot requested a review from longcw July 14, 2026 08:00

@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 potential bugs to report.

View in Devin Review to see 1 additional finding.

Open in Devin Review

Preserve empty-code fallback and resolve discarded generations while verifying terminal websocket errors stop reconnecting.

Co-authored-by: Cursor <cursoragent@cursor.com>
@toubatbrian
toubatbrian merged commit 6a998f6 into main Jul 14, 2026
8 checks passed
@toubatbrian
toubatbrian deleted the courses-clumping-morays branch July 14, 2026 23:05
@github-actions github-actions Bot mentioned this pull request Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant