Skip to content

fix(stt): stop per-call language leaking in openai/elevenlabs, forward deepgram options to open streams - #7828

Open
Atharvsinh-codez wants to merge 5 commits into
livekit:mainfrom
Atharvsinh-codez:fix/stt-language-and-option-propagation
Open

Atharvsinh-codez wants to merge 5 commits into
livekit:mainfrom
Atharvsinh-codez:fix/stt-language-and-option-propagation

Conversation

@Atharvsinh-codez

Copy link
Copy Markdown

Summary

Three small STT plugin fixes, plus a fix for the flaky test that is currently failing unit-tests on PRs. These were #7820, #7821 and #7822. I've put them in one PR so they're easier to review, and I'll close the other three.

openai: don't persist the per-call language passed to recognize()

openai.STT._recognize_impl wrote a per-call language back to self._opts. So one recognize(buffer, language="fr") changed the language for every later recognize() and every new stream() on that STT.

This also hits the framework path. StreamAdapter passes its stream's language to every recognize() call, so any STT built on this plugin (groq.STT and the OpenAI-compatible providers too) stays on whatever language was requested last. Two streams with different languages overwrite each other's setting.

The override now goes on a copy of the options for that one request. That's how stream(language=...) already handles it, and #6097 made the same fix for Clova.

s = openai.STT(model="whisper-1", language="en")
await s.recognize(buf, language="fr")  # sends fr
await s.recognize(buf)                 # before: sends fr, after: sends en

elevenlabs: same bug in Scribe batch recognition

elevenlabs.STT._recognize_impl wrote the per-call language to self._opts.language_code. After one recognize(buffer, language="fr"):

  • later batch requests without a language also sent fr
  • new stream() calls started in fr
  • transcripts with no language_code in the response were tagged with the last override

The override now applies only to the request it came with.

deepgram: forward enable_diarization and tags to open streams

deepgram.STT.update_options() saves enable_diarization and tags on the STT, but leaves both out of the call that updates open streams. Each stream keeps its own copy of the options, so it reconnects with the old values:

  • stt.update_options(enable_diarization=True) mid-session reconnects without diarize=true. No speaker IDs come back, and nothing reports an error.
  • new tags never reach Deepgram for the open stream.

SpeechStream.update_options() already accepts both, and stt_v2 already forwards tags. Only these two arguments were missing.

test: freeze the clock in test_skewed_provider_timestamp

test_skewed_provider_timestamp[stt] currently fails unit-tests on every PR I looked at, including unrelated ones like #7807 and fix/7805-credit-balance-exhausted-fatal. Each time the value is off by about 0.4 to 0.55s:

assert 1791991266.012734 == 1791991265.4618857 ± 0.05

The test captures now before setup. _process_stt_event then calls time.time() again for the clamp, so any pause in between on a busy runner pushes the result past the 0.05s tolerance. It passes locally because nothing stalls there. I pinned the clock the same way test_vad_anchor_survives_a_transcript_without_timestamps in the same file already does (#7398). The assertion and the behavior it checks are unchanged.

Test plan

  • openai: test_file_transcription_language_override_applies_to_one_call fails on main (the second request sends fr) and passes here. I checked groq.STT the same way.
  • elevenlabs: test_recognize_language_override_applies_to_one_call fails on main (['fr', 'fr']) and passes here (['fr', 'en']).
  • deepgram: test_stt_update_options_reaches_open_streams_diarization_and_tags fails on main (no diarize in the reconnect URL) and passes here.
  • flaky test: I added a 0.6s sleep before _process_stt_event to mimic a slow runner. With that delay the old test fails exactly like CI (only [stt]), and the new version passes.
  • uv run pytest tests/test_plugin_openai_stt_context.py tests/test_plugin_deepgram_stt.py tests/test_plugin_elevenlabs_stt.py: 200 passed.
  • uv run pytest --unit --audio_eot: 5279 passed (I skipped tests/test_room.py locally because it needs a local livekit-server binary).
  • ruff check, ruff format --check, and scripts/check_types.py are clean.

STT._recognize_impl wrote a `language` override back to self._opts, so a
single recognize(language="fr") changed the language of every later
recognize() and new stream. StreamAdapter passes its stream's language on
every call, so a non-streaming OpenAI or Groq STT wrapped for a session
ended up pinned to whatever language was last requested.

Apply the override to a copy of the options for that request only, the
same way stream() already does.
STT.update_options() stored enable_diarization and tags on the STT but
left them out of the call that updates the open streams. Each stream keeps
its own copy of the options, so it reconnected with the old values: turning
diarization on mid-session never sent diarize=true, and new tags never
reached Deepgram.
…ize()

STT._recognize_impl wrote a `language` override to self._opts.language_code,
so one recognize(language="fr") changed the language of every later batch
request, the default language of new streams, and the fallback language
reported on transcripts. StreamAdapter passes its stream's language on every
call, so a wrapped batch STT stayed on the last language it was asked for.

Use the override for that request only.
The stt branch asserts the anchor clamps to a 'now' captured before setup,
but _process_stt_event reads time.time() again later. Any stall in between
on a busy runner pushes the value past the 0.05s tolerance, which is what
has been failing the unit-tests job (off by ~0.4-0.55s). Pin the clock the
same way test_vad_anchor_survives_a_transcript_without_timestamps does.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-14T15:44:14.949800Z d98d5a1 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

devin-ai-integration[bot]

This comment was marked as resolved.

@CLAassistant

CLAassistant commented Oct 14, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

…tions

update_options(enable_diarization=...) changed the options used for new
and open streams but left capabilities.diarization at its constructor
value, so MultiSpeakerAdapter still rejected an STT that had diarization
turned on afterwards.

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.

2 participants