fix(openai): forward session.update on RealtimeSession.updateOptions#1303
fix(openai): forward session.update on RealtimeSession.updateOptions#1303toubatbrian merged 4 commits intomainfrom
Conversation
Give each RealtimeSession its own shallow copy of RealtimeOptions so updateOptions can diff against the session's own state instead of the shared model-level state. This mirrors livekit/agents#5531. Ports: livekit/agents#5531
|
|
|
Requesting review from @toubatbrian and @livekit/agent-devs. Note: GitHub doesn't let the PR author be added as a formal "Reviewer" since @toubatbrian is the author of this automated PR — tagging here instead. Generated by Claude Code |
🦋 Changeset detectedLatest commit: d3e8847 The changes in this PR will be included in the next version bump. This PR includes changesets to release 26 packages
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 |
The disable comment used @typescript-eslint/no-require-imports (the newer rule name), but this repo's eslint config still uses the older no-var-requires rule, so the directive didn't suppress the error.
Summary
Ports livekit/agents#5531 ("fix(openai): forward session.update on RealtimeModel.update_options") from the Python repo.
Upstream bug
RealtimeSession.updateOptions()compared the incoming options againstRealtimeModel._optionsand then mutated that same object before forwarding the update. Because both sides of the diff read from the same mutable object, the diff always saw "no change" and nosession.updateevent was ever sent to OpenAI — so the realtime session never actually applied the new options.The shared-state problem is additionally harmful with multiple sessions attached to the same
RealtimeModel: one session'supdateOptionswould silently overwrite another session's options.Fix
Give each
RealtimeSessionits own shallow copy ofRealtimeOptions, populated from the model's options at construction time. All reads/writes insideRealtimeSessionnow go throughthis._optionsinstead ofthis.oaiRealtimeModel._options, so:updateOptionscompares against — and mutates — per-session state, so its diff is meaningful and thesession.updateevent is emitted.Ported changes
plugins/openai/src/realtime/realtime_model.tsRealtimeSession._options: RealtimeOptions.this._options = { ...realtimeModel._options }.this.oaiRealtimeModel._optionsreference insideRealtimeSessiontothis._options(connection headers, URL building, Azure branching, session-update construction, tool-update event,updateOptions, max-session-duration timeout, modalities resolution, etc.).RealtimeModelitself are untouched — onlyRealtimeSessionnow reads from a local copy..changeset/openai-realtime-session-opts-copy.md(patch bump for@livekit/agents-plugin-openai).Per-line
// Ref: python …comments are added at the two structural change sites (the new field and the constructor copy), matching the repo convention inCLAUDE.mdfor Python ports.Implementation nuances / notes on parity
dataclasses.replace(realtime_model._opts), which produces a new dataclass instance with the same field values (shallow). In TS,{ ...realtimeModel._options }is the direct equivalent — we copy the top-level field references. Nested objects (connOptions,turnDetection,inputAudioTranscription, etc.) are still shared by reference, which matches Python'sdataclasses.replacesemantics exactly. This is deliberate:updateOptionsreplaces whole option fields rather than mutating nested ones, so shared nested references don't reintroduce the bug.updateOptionsdiffers from Python. The JSupdateOptionscurrently only acceptstoolChoice(see the existingTODO(brian): add other options here), whereas the Python version diffs many more options (voice,turn_detection,input_audio_transcription,input_audio_noise_reduction,speed,tracing,truncation,max_response_output_tokens). We ported the root-cause structural fix (per-session_optionscopy), so when additional options are added to JS'supdateOptionsin the future, they will automatically inherit the correct diff semantics. Porting the additional option branches themselves is left out of this PR to keep the change minimal and 1:1 with the upstream fix's intent.RealtimeModel._optionsunchanged. The model-level options still reflect constructor inputs and are what new sessions copy from. This matches Python:RealtimeModel._optsis the template, eachRealtimeSessiongets an independent snapshot.session.updatefix. Callers who reuse a model across sessions get the additional correctness improvement of isolated per-session state.Test plan
pnpm build:agents— passes.pnpm --filter=@livekit/agents-plugin-openai build— passes (types + tsup).pnpm test -- plugins/openai— all 12realtime_model.test.tstests pass. (Unrelatedws/llm.test.tsfailure requiresOPENAI_API_KEYand predates this change.)pnpm --filter=@livekit/agents-plugin-openai lint— no new errors/warnings introduced by this change (remaining warnings are pre-existingno-explicit-any/ tsdoc items).session.updateOptions({ toolChoice: ... })mid-session and confirm asession.updateframe is emitted.cc @toubatbrian @livekit/agent-devs
🤖 This PR was generated by an automated Claude Code routine that ports merged
livekit/agentsplugin / core runtime PRs intolivekit/agents-js. Experimental — please review the ported logic and the nuances noted above carefully.Generated by Claude Code