Skip to content

RealtimeModel.update_options() does not send session.update to OpenAI (race condition with _opts mutation) #5530

@strukturedkaos

Description

@strukturedkaos

Summary

RealtimeModel.update_options() mutates the model-level _opts before forwarding to active sessions, causing RealtimeSession.update_options() to see no diff and skip sending session.update to OpenAI.

Environment

  • livekit-agents: 1.5.x
  • livekit-plugins-openai: 1.5.x
  • Model: gpt-realtime-1.5

Steps to Reproduce

  1. Start an OpenAI Realtime session via LiveKit
  2. Call agentSession._llm.update_options(speed=1.5) (or any other option like voice, turn_detection, etc.)
  3. Observe that no session.update event is sent to OpenAI
  4. The change has no audible/observable effect

Root Cause

In realtime_model.py:

RealtimeModel.update_options() (around line 649):

# Mutates _opts FIRST
if is_given(speed):
    self._opts.speed = speed

# THEN forwards to sessions
for sess in self._sessions:
    sess.update_options(speed=speed, ...)

RealtimeSession.update_options() (around line 1207):

if is_given(speed):
    if self._realtime_model._opts.speed != speed:  # Always False! Already mutated above
        audio_output.speed = speed
        has_audio_config = True
    self._realtime_model._opts.speed = speed

The session compares against the model's _opts, which was already mutated by the model-level call. The diff always sees "no change" and skips sending the update.

Affected Options

All options using this pattern:

  • speed
  • voice
  • turn_detection
  • input_audio_transcription
  • input_audio_noise_reduction
  • tool_choice
  • tracing
  • max_response_output_tokens

Workaround

Call RealtimeSession.update_options() directly on active sessions:

for sess in llm._sessions:
    sess.update_options(speed=1.5)

This works because the session-level call hasn't had its comparison poisoned by prior mutation.

Suggested Fix

In RealtimeModel.update_options(), forward to sessions before mutating _opts:

def update_options(self, *, speed=NOT_GIVEN, ...):
    # Forward to sessions FIRST (while _opts still has old values)
    for sess in self._sessions:
        sess.update_options(speed=speed, ...)
    
    # THEN mutate model opts
    if is_given(speed):
        self._opts.speed = speed
    ...

Additional Context

  • The workaround requires accessing llm._sessions, which is a private attribute
  • There's no public API to enumerate active sessions from AgentSession
  • Initial session setup works correctly (only live updates are broken)

Related Issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions