feat(openai): expose gpt-live-transcribe transcription context (keywords, languages, delay) - #6613
Conversation
`gpt-live-transcribe` and `gpt-transcribe` take recognition hints on `audio.input.transcription`: `keywords` for literal terms, `languages` for the expected input languages and `delay` for the latency/accuracy tradeoff. None of them were reachable from the plugin, and `language` (singular) was sent unconditionally even though `gpt-live-transcribe` rejects a session update carrying both `language` and `languages`. Adds `keywords`, `languages` and `delay` to the constructor, `with_azure` and `update_options`, sends the configured language as a single-entry `languages` list on models that only accept the plural field, and validates keywords against the characters the server rejects. Context changes are applied in-band on the live connection (the transcription config can be re-sent mid-session) so updating them doesn't cut the ongoing utterance. Keyword-capable models now report `STTCapabilities.keyterms`, so framework-managed keyterms merge with the user's own and reach the session too.
|
Hiroki Azuma seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account. You have signed the CLA already but the status is still pending? Let us recheck it. |
| def _update_session_keyterms(self, keyterms: list[str]) -> None: | ||
| if keyterms == self._session_keyterms: | ||
| return | ||
| self._session_keyterms = list(keyterms) | ||
| self._opts.keywords = self._merged_keywords() | ||
| self._apply_transcription_context() |
There was a problem hiding this comment.
π΄ Speech recognition can fail entirely when keyword hints are used with a model that doesn't accept them
Framework-supplied keyword hints are stored and sent to the transcription service (_update_session_keyterms at livekit-plugins/livekit-plugins-openai/livekit/plugins/openai/stt.py:502-507) even for models that reject those hints, so the service refuses the session setup and transcription stops working.
Impact: An agent configured with keyterms but a model that doesn't support them gets no transcripts at all instead of a harmless warning.
Missing capability guard in the keyterms override
The base implementation livekit-agents/livekit/agents/stt/stt.py:276-289 warns and skips when STTCapabilities.keyterms is false, and other plugins that only conditionally support keyterms defer to it (see livekit-agents/livekit/agents/inference/stt.py:729-737). The OpenAI override has no such guard.
The framework calls _update_session_keyterms unconditionally for static keyterms regardless of capability (livekit-agents/livekit/agents/voice/keyterm_detection.py:246-256 and :289). So with e.g. model="gpt-4o-mini-transcribe", use_realtime=True and keyterms=[...] configured on the session, self._opts.keywords gets set and _build_transcription_config (livekit-plugins/livekit-plugins-openai/livekit/plugins/openai/stt.py:523-524) adds keywords to audio.input.transcription, which the Realtime API rejects for that model β the error event then raises a non-retryable APIError in recv_task.
The same hole exists after update_options(model=...) switches to a model without keyword support while keywords are already stored.
| def _update_session_keyterms(self, keyterms: list[str]) -> None: | |
| if keyterms == self._session_keyterms: | |
| return | |
| self._session_keyterms = list(keyterms) | |
| self._opts.keywords = self._merged_keywords() | |
| self._apply_transcription_context() | |
| def _update_session_keyterms(self, keyterms: list[str]) -> None: | |
| if not self.capabilities.keyterms: | |
| super()._update_session_keyterms(keyterms) # warn-and-skip for unsupported models | |
| return | |
| if keyterms == self._session_keyterms: | |
| return | |
| self._session_keyterms = list(keyterms) | |
| self._opts.keywords = self._merged_keywords() | |
| self._apply_transcription_context() |
Was this helpful? React with π or π to provide feedback.
| self._session_keyterms = list(keyterms) | ||
| self._opts.keywords = self._merged_keywords() |
There was a problem hiding this comment.
π‘ Automatically collected recognition hints containing certain characters can break transcription
Hints coming from the framework are pushed to the service without the character check applied to user-provided ones (_merged_keywords() at livekit-plugins/livekit-plugins-openai/livekit/plugins/openai/stt.py:506), so a single hint containing <, > or a line break makes the service reject the whole configuration.
Impact: A stray character in an auto-detected hint can silently kill transcription for the rest of the call.
Validation applied only on the user path
STT.__init__ (livekit-plugins/livekit-plugins-openai/livekit/plugins/openai/stt.py:184-185) and update_options (:452-456) run _validate_keywords, which raises for the characters the server rejects (_KEYWORD_FORBIDDEN_CHARS, :85-95). Keyterms arriving through _update_session_keyterms (LLM-detected terms or session-level static keyterms, see livekit-agents/livekit/agents/voice/keyterm_detection.py:389) bypass that check and land in the merged keywords list sent in session.update. Since these terms are not authored inline by the developer, raising would also be wrong here β dropping/sanitizing the offending terms with a debug/warning log is the safer behavior.
Prompt for agents
In livekit-plugins/livekit-plugins-openai/livekit/plugins/openai/stt.py, keywords supplied by the user through __init__/update_options are validated with _validate_keywords (rejecting '<', '>', CR and LF, which the Realtime API refuses). Keyterms arriving via the framework hook _update_session_keyterms (static session keyterms plus LLM-detected terms from livekit-agents/livekit/agents/voice/keyterm_detection.py) are merged into the same keywords list without validation, so one bad term makes the server reject the whole session.update and transcription stops. Consider filtering out (rather than raising on) invalid framework-supplied keyterms in _update_session_keyterms/_merged_keywords, logging what was dropped.
Was this helpful? React with π or π to provide feedback.
| ws = self._ws | ||
| if ws is None or ws.closed: | ||
| # not connected; the next connection carries the latest options | ||
| return |
There was a problem hiding this comment.
π‘ Recognition hints changed while no audio stream is active may never take effect
Updated recognition hints are only pushed onto an already-open connection (_send_session_update_task at livekit-plugins/livekit-plugins-openai/livekit/plugins/openai/stt.py:705-708) and are dropped when nothing is connected, but a later stream can pick up a recycled connection that still carries the old hints.
Impact: Keyword/language/prompt changes made between utterances can be silently lost for up to the connection lifetime.
Pooled connections are not re-configured on reuse
_send_session_update_task returns early when self._ws is None or closed, with the comment "the next connection carries the latest options". That only holds for freshly created connections: _connect_ws (livekit-plugins/livekit-plugins-openai/livekit/plugins/openai/stt.py:570-594) is the sole place the session.update is sent, and ConnectionPool.get (livekit-agents/livekit/agents/utils/connection_pool.py:100-118) returns a cached websocket without invoking connect_cb when an available one exists (up to _max_session_duration = 10 min). So if update_options(keywords=...)/_update_session_keyterms runs while no stream holds a connection, and a new stream then reuses the pooled socket, the server keeps the previous transcription context.
One fix is to have _apply_transcription_context remember that the context is dirty and re-send (or self._pool.invalidate()) when no live connection accepted the update.
Was this helpful? React with π or π to provide feedback.
Motivation
OpenAI's new streaming transcription model
gpt-live-transcribe(andgpt-transcribe) acceptrecognition hints on
audio.input.transcriptionthat the plugin currently can't reach:keywordsβ literal terms (product names, acronyms, IDs)languagesβ expected input languages (plural)delayβ the latency/accuracy tradeoff (minimalβ¦xhigh)There is also a correctness problem:
_connect_wsalways sends the singularlanguage, butgpt-live-transcribeuseslanguagesinstead andthe API rejects a session update carrying both.
What this does
keywords,languagesanddelaytoSTT.__init__,STT.with_azureandSTT.update_options, sent underaudio.input.transcriptionlanguageas a single-entrylanguageslist on models that only acceptthe plural field, so
language="ja"keeps working ongpt-live-transcribeinstead of beingrejected
<,>, CR, LF)keywords/languageson models that don't support them, anddelayoutside realtime β same shape as the existing
temperature/turn_detectionguardsreconnecting. The transcription config can be re-sent mid-session, so context updates don't
cut the ongoing utterance. Sends are chained so they reach the server in order (same pattern as
the Deepgram Flux
Configurepath instt_v2.py)STTCapabilities.keytermsfor keyword-capable models and implements_update_session_keyterms, so framework-managed keyterms merge with the user's keywords andreach the session
Testing
New unit module
tests/test_plugin_openai_stt_context.py(14 tests) covering thesession.updatepayload, thelanguage/languagesmutual exclusion, keyword validation, thekeyterms capability and merge, and the in-band update path (delivery, ordering, no-op when
disconnected).
ruff format/ruff checkclean, andmypy -p livekit.plugins.openaireports nothing new.Also ran the neighbouring STT unit modules (
test_stt_base,test_stt_context,test_stt_fallback,test_inference_stt_*,test_plugin_openai_*,test_agent_update_options) β 130 passed.Notes / open questions
gpt-live-transcribein a follow-up if that's the direction you want.promptis OpenAI's "unstructured context" field and already existed, so it isn't new here βbut it's now updatable mid-session along with the rest of the context.
STTCapabilities.chat_context/_push_conversation_item: unlikeAssemblyAI's
agent_context, OpenAI has no separate carryover slot, so turn carryover wouldhave to compose into the user's
prompt(which has a server-side length cap). Glad to add it ina follow-up if you have a preferred shape.
there if you'd rather; this seemed close enough to the recent keyterm-parameter PRs ((cartesia stt): expose ink-2 turn-detection thresholds and keyterm paramsΒ #6594,
(xai stt): expose vad_threshold, smart_turn, smart_turn_timeout, and keyterm paramsΒ #6587) to send directly.
References: gpt-live-transcribe,
Realtime transcription guide