Add missing realtime speech-to-text options - #830
Merged
Conversation
Ports the TypeScript SDK fix (elevenlabs-js#436) to Python. The hand-written realtime wrapper only exposed a subset of the parameters the websocket endpoint accepts. asyncapi.yml is the contract used throughout. New options, accepted by both connection modes: - secondary_languages, sent as repeated secondary_languages params - include_language_detection - entity_detection, accepting a single category/type or a list - filter_background_audio - enable_logging - token, a single-use token that authenticates the session on its own, so the xi-api-key header is omitted when no key is configured The two option TypedDicts duplicated every shared field, so they now inherit from a shared base instead of restating it, and the connect methods build their query kwargs through one helper rather than unpacking each option twice. New server messages, previously dropped by the dispatcher: - final_transcript and final_transcript_with_timestamps - committed_transcript_entities Also fixes unaccepted_terms: the enum only carried "unaccepted_terms_error", which the server never sends, so RealtimeEvents(message_type) raised and the message was swallowed. The correct literal is added and both event names are emitted so existing subscribers keep firing. Behaviour change: connecting with neither an api_key nor a token now raises ValueError instead of sending an empty xi-api-key header and waiting for the server to reject it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 9250190. Configure here.
The URL tests asserted one substring each, which only proved a value passed in came back out. Replaced with assertions on the exhaustive parameter set, so a renamed, dropped or duplicated parameter fails, plus cases for the behaviour that can actually regress: explicitly false booleans surviving serialization, list parameters repeating rather than joining, and both keyterm limits being inclusive at the boundary. Also drops the "new params" framing, which described the diff rather than the endpoint. Verified by mutation: comma-joining a list, switching enable_logging to a truthiness check, appending audio_format twice, and always sending the api key header each fail at least two tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two fixes from bugbot review. The api key was still sent whenever one was configured, even when the caller passed a token. The server tries the single-use token first and closes the session if it fails rather than falling back, so the key could never authenticate that connection - it was just a long-lived credential on the wire for no reason. A token now suppresses the header outright, which is also what the option's docstring already claimed. RealtimeEntityDetection was hand-added to src/elevenlabs/__init__.py, which Fern generates. The realtime root exports come from additional_init_exports in the API definition's generators.yml, so the next regeneration would have dropped this one while leaving the others, breaking `from elevenlabs import RealtimeEntityDetection`. Reverted that file to generated state; the type is exported from elevenlabs.realtime. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kraenhansen
reviewed
Jul 31, 2026
Per review: replicating server-side invariants client-side gives no type safety, and the server reports these anyway. The real cost is drift - if the constraint is relaxed server-side, callers cannot adopt the change until the SDK is updated and shipped, which is a failure mode nobody is watching for. Drops the filter_background_audio/include_timestamps conflict check and the keyterms count and length checks, along with their tests. The constraints stay in the docstrings, mirroring the API reference, since a stale doc misleads far less than a hard failure. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Now declared in asyncapi.yml (elevenlabs/xi#44830), so the SDK can pick it up from the spec. The server sends this when it rejects the connection parameters: it accepts the websocket, sends the error, then closes. The dispatcher resolves events through RealtimeEvents(message_type) and ignores unknown types, so the message was dropped and a bad parameter surfaced as a connection close with no error event at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two of the tests added on this branch asserted nothing about the SDK. test_committed_transcript_entities_carries_entities checked that a payload the SDK never transforms survived an emitter, and the list-parameter test duplicated the exhaustive serialization assertion, which already fails on a joined list. Strengthens the connect() option test instead: it is the only cover for the _shared_url_kwargs mapping, where a typo drops an option silently, so it now asserts the full parameter set rather than a few substrings. Verified by mutation: comma-joining a list, enable_logging truthiness, a mapping typo, sending the api key alongside a token, and removing either the INVALID_REQUEST or FINAL_TRANSCRIPT enum member each still fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kraenhansen
approved these changes
Aug 11, 2026
Minor: adds realtime speech-to-text options and message types. Matches the JS SDK, which the two are released in lockstep with. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kraenhansen
added a commit
that referenced
this pull request
Aug 11, 2026
* [fern-generated] Update SDK Generated by Fern CLI Version: unknown Generators: - fernapi/fern-python-sdk: 4.64.1 * [fern-replay] Applied customizations Patches with unresolved conflicts (1): - patch-560b8934: fix: send list-of-primitive multipart fields as repeated form fields (#819) (#825) Run `fern-replay resolve` to apply these customizations. Patches absorbed by generator (1): - patch-e1cf8db9: Add missing realtime speech-to-text options (#830) The generator now produces these customizations natively. * fix: preserve repeated-form-field encoding for keyterms/webhook_ids Fern regeneration reverted the #825 fix and dropped the json import, breaking mypy and re-introducing the original multipart bug. --------- Co-authored-by: fern-api[bot] <115122769+fern-api[bot]@users.noreply.github.com> Co-authored-by: Kræn Hansen <kraen@elevenlabs.io>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Python companion to elevenlabs/elevenlabs-js#436, which fixes elevenlabs/elevenlabs-js#434. The hand-written realtime STT wrapper in
src/elevenlabs/realtime/only exposed a subset of the parameters the websocket endpoint accepts.asyncapi.ymlis used as the contract throughout.New options
Accepted by both
RealtimeAudioOptionsandRealtimeUrlOptions:secondary_languagessecondary_languages(repeated)include_language_detectioninclude_language_detectionentity_detectionentity_detection(single value or repeated)filter_background_audiofilter_background_audioenable_loggingenable_loggingtokentokentokenis the single-use token param. When supplied, thexi-api-keyheader is not sent. The token takes precedence server-side (the endpoint tries it first and closes the session rather than falling back to the key), so sending the key alongside it would put a long-lived credential on the wire that could not authenticate the connection anyway.Server messages that were being silently dropped
The dispatcher resolves events with
RealtimeEvents(message_type)inside atry/except ValueErrorthat ignores unknown types, so any message type missing from the enum vanished:final_transcriptandfinal_transcript_with_timestampscommitted_transcript_entitiesinvalid_request, sent when the server rejects the connection parameters. It accepts the socket, sends the error, then closes — so without an enum member, a bad parameter surfaced as a connection close with no error event at all. This was missing from the spec too; added in elevenlabs/xi#44830 and now synced.unaccepted_terms was unreachable
The enum only carried
UNACCEPTED_TERMS_ERROR = "unaccepted_terms_error", which the server never sends — the spec declaresunaccepted_terms. SoRealtimeEvents(message_type)raisedValueErrorand the message was swallowed, meaning neither the specific event nor the genericERRORevent ever fired for it.UNACCEPTED_TERMSis added with the correct literal. Both event names are emitted, so existing subscribers on the old name keep working; the old member is marked deprecated in a comment rather than removed.Tidying the option types
RealtimeAudioOptionsandRealtimeUrlOptionsrestated every shared field, which is what let them drift apart. They now inherit from a shared_RealtimeSharedOptionsbase and only declare what's specific to each mode (audio_format/sample_ratevsurl). Similarly,_connect_audioand_connect_urleach unpacked all the shared options by hand before passing them along; that's now one_shared_url_kwargshelper, so adding a parameter touches one place instead of three.Behaviour change
Connecting with neither an
api_keynor atokennow raisesValueErrorinstead of sending an emptyxi-api-keyheader and waiting for the server to reject it. Note this path was already non-functional:ScribeRealtimeonly ever receives the extractedxi-api-keyvalue, never any custom headers passed to the client, so custom-header auth was never supported for realtime.Parameter constraints are left to the API
An earlier revision validated the
filter_background_audio/include_timestampsconflict and thekeytermslimits client-side. Removed per @kraenhansen's review: replicating server-side invariants gives no type safety, and the cost is drift — if a constraint is relaxed server-side, callers cannot adopt the change until the SDK ships an update. The constraints remain in the docstrings, mirroring the API reference, since a stale doc misleads far less than a hard failure.Testing
tests/test_stt_realtime.pygoes from 17 to 33 tests.URL assertions are exhaustive rather than one-substring-per-parameter, so a renamed, dropped or duplicated parameter fails rather than passing unnoticed. Beyond that: explicitly
Falsebooleans surviving serialization (a truthiness check would silently revertenable_logging=False), list parameters repeating rather than joining, options threading throughconnect(), and API key vs token vs neither asserted on the headers the socket is opened with.Verified by mutation. Each of these fails at least two tests: comma-joining a list parameter, switching
enable_loggingto a truthiness check, appendingaudio_formattwice, and always sending the api key header.The dispatch tests use a real async-iterable fake websocket. The existing tests set
__aiter__ = MagicMock(return_value=iter([])), which isn't an async iterator —async forraisesTypeError, the handler's broadexcept Exceptionswallows it, and the test still passes because it only asserts on the URL. That pattern silently cannot exercise message handling, so I didn't build on it. I confirmed the new dispatch tests fail against unpatched source (AttributeError: type object 'RealtimeEvents' has no attribute 'COMMITTED_TRANSCRIPT_ENTITIES', plus empty handler results).Full suite: 183 passed, 16 failed. The same 16 fail on a clean tree (they call the live API and need credentials) — baseline is 166 passed, 16 failed, so no new failures.
ruffclean on the changed files;mypyreports only 3 pre-existing errors in generatedcore/files, none inrealtime/.Root export
RealtimeEntityDetectionis exported fromelevenlabs.realtime, not the package root. An earlier revision hand-added it tosrc/elevenlabs/__init__.py, which Fern generates — the realtime root exports come fromadditional_init_exportsin the API definition'sgenerators.yml, so the next regeneration would have dropped this one while leaving the others. That file is back to generated state. Adding the root export properly needs agenerators.ymlchange in the API definition repo; happy to open that separately if wanted.🤖 Generated with Claude Code
Note
Medium Risk
Changes realtime STT connection auth and event behavior (including a breaking-style early
ValueErrorwithout credentials), but scope is limited to the realtime module with strong test coverage.Overview
Bumps the SDK to 2.63.0 and brings the hand-written realtime Scribe wrapper in line with the WebSocket API.
Connection options —
RealtimeAudioOptionsandRealtimeUrlOptionsnow share a_RealtimeSharedOptionsbase and forward additional query params:secondary_languages,include_language_detection,entity_detection,filter_background_audio,enable_logging, andtoken. Shared mapping lives in_shared_url_kwargs;RealtimeEntityDetectionis exported fromelevenlabs.realtime.Authentication — A
tokenauthenticates via the query string and omitsxi-api-key. Connecting with neitherapi_keynortokennow raisesValueErrorinstead of sending an empty header.Event dispatch —
RealtimeEventsgains handlers forfinal_transcript,final_transcript_with_timestamps,committed_transcript_entities,invalid_request, andunaccepted_terms(the server literal; the oldunaccepted_terms_errorname is still emitted for compatibility).invalid_requestandunaccepted_termsalso surface on the genericERRORevent.Tests —
tests/test_stt_realtime.pyadds exhaustive URL/auth coverage and async message-dispatch tests.Reviewed by Cursor Bugbot for commit e49fc09. Bugbot is set up for automated code reviews on this repo. Configure here.