feat(regen): speak v2 barge-in and speed/expressivity controls, listen v2 redaction - #758
Conversation
Re-applied the still-needed manual patches over the fresh generator output: - socket clients (agent/listen v1+v2, speak v1+v2): broad except for custom transports, optional control-message params; listen v2 send_configure keeps the generator's typed model but retains raw-dict passthrough for back-compat; speak v2 keeps the new flux-GA send_interrupt/send_configure surface - agent settings + language_hint provider validators (back-compat model_validators) - core/query_encoder bool coercion - 10 __init__.py: generator's new flux-GA exports merged with legacy alias re-exports - tests/wire legacy create-key alias coverage - agent_v1update_listen_listen provider-coercion validator restored + re-frozen Dropped (generator now covers them): - agent_v1latency_report (stt_latency emitted natively) — unfrozen - core/client_wrapper (dynamic importlib.metadata version) — unfrozen, and its generic extra-file entry removed from release-please-config.json Verify: pytest 337 passed, ruff check src/ clean, mypy clean.
…vider union The 2026-08-11 regen added public surface with no automated coverage. Adds 18 tests, all mutation-verified (each fails when the behaviour it pins is removed from the source): - tests/custom/test_speak_v2_interrupt_configure.py (new, frozen): send_interrupt (bare + playback_offset), send_configure, and parsing of SpeechInterrupted / ConfigureSuccess / ConfigureFailure, plus the speed and expressivity connect query params. Sync and async paths. - test_agent_update_listen.py: pins the provider-coercion validator on agent_v1update_listen_listen.py -- legacy DeepgramListenProviderV1/V2 and a legacy bare dict must all gain the correct version discriminant -- and adds round-trips for the generator-native union members. - test_listen_v2_connect_wire.py: redact query-string serialization. Note: these models are extra="allow", so asserting a field's value alone does not detect a dropped field declaration (verified: that mutation initially slipped through). The metadata tests therefore assert against the declared field set as well. Verify: pytest 355 passed / 1 skipped, ruff clean, mypy clean.
b8b0d90 to
223f490
Compare
…upt arg Review follow-ups: - AGENTS.md freeze ledger had drifted from .fernignore. Removed the now-unfrozen client_wrapper.py entry, added the newly-frozen agent_v1update_listen_listen.py (with a note that this patch was silently lost once by a regen), added the three language_hint provider validators and the new frozen test file, and corrected the listen/v2 socket_client entry which still described the superseded typing.Any send_configure shim. Verified zero drift in both directions. - speak/v2 send_interrupt now takes an optional message, matching the sibling no-payload controls (send_flush/send_close). SpeakV2Interrupt has no required fields, so send_interrupt() is meaningful. send_configure keeps its required argument on purpose -- a Configure with no settings is meaningless. Verify: pytest 357 passed / 1 skipped, ruff clean on touched files, mypy clean. (The 4 remaining ruff findings in test_text_builder.py / test_transport.py are pre-existing on main.)
Blocking review finding [B1]: passing `provider` as a plain dict to
AgentV1UpdateListenListen skipped the language_hint -> language_hints remap, so
the deprecated singular key reached the wire. The API uses deny_unknown_fields,
so the whole UpdateListen was rejected with no client-side signal. The
model-instance path was correct (it calls provider.dict(), which runs the shim's
validator); only the dict branch was affected, and it stamped version blindly.
Confirmed by probe before fixing:
dict -> {"version":"v2", ..., "language_hint":"en"} (rejected)
model -> {"version":"v2", ..., "language_hints":["en"]} (correct)
The dict branch now routes through DeepgramListenProviderV1/V2 so their own
before-validators run -- one remap implementation instead of two, which is how
this drifted in the first place.
Also fixes [S1]: the version is inferred from the dict's own keys rather than
blanket-stamped "v2". `language`/`smart_format` exist only on the v1 provider
(verified against both union members), so a v1-shaped dict is now stamped "v1"
instead of declaring a Flux/v2 provider carrying v1-only fields.
Deviation from the suggested fix: constructing the shim model raises on a
malformed dict (e.g. missing the required `model`), which would have made the
compat shim a NEW source of exceptions. Wrapped in try/except with a
pass-through fallback, so behaviour is never worse than pre-regen. Verified the
downstream union rejection of a genuinely-invalid payload is pre-existing --
the old code produced an identical payload and it was rejected identically.
Adds 3 tests (the language_hint dict assertion whose absence let this through,
version inference, and the never-raises guard). Mutation-checked: reverting the
dict branch fails 2 of them.
[S8]: added an AGENTS.md section recording the residual concerns on the two
newly-unfrozen files -- notably that client_wrapper's version fallback literal
is refreshed only by regen.
Verify: pytest 360 passed / 1 skipped, ruff clean, mypy clean.
Every websocket connect() path raises ApiError(headers=dict(headers), ...) with
the FULL request headers, and both error types stringify that dict. A failed
connect therefore produced:
headers: {'Authorization': 'Token <live key>', ...}, status_code: 400, ...
so any application that logged the exception -- print(e), a traceback, a log
aggregator, an error tracker -- wrote the customer's API key in clear text. Found
while running the new barge-in example: the SDK printed a real production key.
This is the same threat _secure_logging.py already covers for the websockets
DEBUG handshake logs; the exception path is simply the other route to it, and was
not covered. It also defeats the telemetry scrubber: scrub_event strips
request.headers, but here the credential sits in the exception message, which
scrubbing does not touch -- so an armed Sentry client could have shipped it.
Fix: both error types mask credential values at construction via a new
_secure_logging.redact_sensitive_headers() helper, reusing the existing
_SENSITIVE_HEADERS set and scheme-preserving mask. At construction rather than
only in __str__, so repr() and attribute serialisation are covered too (error
trackers send both). Non-sensitive headers are preserved deliberately --
dg-request-id is the main reason to inspect headers at all.
Both files are generated, so they are frozen in .fernignore with the rationale,
and documented in the AGENTS.md ledger. Redaction is exception-safe: odd input
drops the mapping rather than raising from an error path.
Adds tests/custom/test_api_error_redaction.py (14 tests, frozen) covering str,
repr, the headers attribute, scheme preservation, header survival, None
pass-through, and the never-raises guard. Mutation-checked: un-redacting
ApiError fails 4 of them.
Pre-existing, not introduced by this regen; affects all five socket clients.
Verify: pytest 375 passed / 1 skipped, ruff clean, mypy clean.
reference.md is permanently frozen (the generator cannot update it), so the new surface was missing from the SDK's own API reference -- the third cycle running. Adds the five missing entries, each into the correct v2 block: - listen.v2.connect: `redact` param; `send_configure` send-method (which this regen promotes from a typing.Any shim to a typed model, noting that a raw dict is still accepted) - speak.v2.connect: `speed` and `expressivity` params; `send_interrupt` and `send_configure` send-methods The param entries spell out the accepted values, because the spec's numeric enums are dropped in codegen -- so the generated docstrings state no range and the reference was the only place a developer could learn them. The send_interrupt entry documents the two sequencing rules that are otherwise only in type stubs: the playback offset is cumulative from session start, and each interrupt must advance past the previous one. Anchors were verified by line before editing: `send_close_stream` and `numerals` each appear in multiple sections, so string-matching would have patched listen v1 or a REST block by mistake.
Hand-applies the spec copy fixes so tomorrow's launch does not ship stale text, without needing a generator re-run: - Removes the Flux TTS "Early Access" caveats from the Deepgram agent speak-provider descriptions (types/ + requests/). Flux is GA; the direct Speak V2 path had already been updated, so the Agent path was the only surface left telling developers Flux might change before GA. src/ now has zero "Early Access" occurrences. - SpeakV2Speed said "Only the multipliers listed here are accepted" and listed nothing; SpeakV2Expressivity never stated its bounds. Both are enums in the spec, but Fern drops numeric enums, so the generated text pointed at a list that was not there. Both now spell out the accepted values, and speed names the two failure codes a developer would otherwise only meet at runtime. All four are frozen in .fernignore, since they are generated and the patch would otherwise be lost. Marked TEMPORARY and documented in the AGENTS.md ledger with an explicit instruction to unfreeze them at the next regen -- once #1094 merges the generator emits this text itself and the freezes become dead weight. Verify: pytest 375 passed / 1 skipped, ruff clean, mypy clean, no .fernignore entry pointing at a missing path.
The hand-applied descriptions match deepgram-docs#1094 exactly, verified by parsing the spec YAML, normalising the folded scalars the way Fern renders them, and comparing against the generated docstrings -- all five descriptions (provider, version, model, speed, expressivity) match byte-for-byte. So the next regen emits identical text and the freezes protected nothing. Removed the four .fernignore entries, the comment block, and the AGENTS.md ledger bullet along with its TEMPORARY unfreeze instruction. Fern owns these files again. The patched text itself is unchanged: src/ still has zero "Early Access" occurrences and the speed/expressivity ranges are still stated. Verify: pytest 375 passed / 1 skipped, ruff clean, mypy clean, 95 .fernignore entries with no ghosts and no undocumented src/ freezes.
…and logs The browser/'custom headers not supported' auth pattern the connect() docstrings recommend carries the API key in the Sec-WebSocket-Protocol header (value 'token, <key>'). That header was not in _SENSITIVE_HEADERS, so redact_sensitive_headers (used by ApiError/ ParsingError) and the websockets debug-log filter left the key in clear text — the same leak class a8e8238 claimed to close. Add it to the set; _mask_value renders it as 'token, [REDACTED]'. Regression tests cover both the log-filter and error-header paths.
dg-coreylweathers
left a comment
There was a problem hiding this comment.
Blocking findings resolved and verified on 9929ca1.
Verified: Sec-WebSocket-Protocol credentials are masked on both the error-header path (ApiError/ParsingError: absent from str, repr, and .headers) and the websockets debug-log filter, with non-sensitive headers such as dg-request-id preserved. language_hint remap and version inference confirmed by probe on the UpdateListen raw-dict path. Zero 'Early Access' occurrences in src/. reference.md documents the new speak v2 and listen v2 surface. Gates: 378 passed / 1 skipped, ruff clean, mypy clean.
Approving the content. Merge remains gated on the API being in production, and note deepgram-docs #1090 is still open (the SDK is generated from jherlihy/flux-tts-ga, so main's spec still carries the Early Access wording until it lands).
Non-blocking follow-ups, fine to take after the launch: [S9] the malformed-dict test asserts on the private coercion helper while the public constructor still raises; [S10] add a freeze-independent test guarding the GA copy now that the four copy-fix files are unfrozen; [N4] a dict mixing a v1-only key with language_hint still ships the singular key.
Transcribed from the approved deepgram-docs#1090 spec (4170ce53), which defers both inline controls at GA. Two descriptions were not merely stale but inverted: the batch request text said batch "rejects the whole request with a 400" (the server now strips the control instead), and the streaming Speak text said a malformed control is fatal via DATA-0002 (nothing is fatal). Warning codes are now described as reserved and not currently emitted, and the controls-applied counts as always 0 at launch. These docstrings are a bridge, not a patch to preserve — once #1090 is on main a regen reproduces them from the spec and should overwrite freely, so no .fernignore entries were added. Do not regen before #1090 lands or the old wording returns.
dg-coreylweathers
left a comment
There was a problem hiding this comment.
Re-reviewed on 9929ca1 (unchanged since the previous approval). Content approved — hold merge until the Flux TTS API is confirmed in production.
[B2] verified fixed, by probe rather than attestation: sec-websocket-protocol is now in _SENSITIVE_HEADERS, and credentials mask correctly across all five header shapes — the documented "token, <key>" form (masks to token, [REDACTED], dg-request-id preserved), no-scheme values, lowercase header names, list values, and on ParsingError as well as ApiError. Both mutation paths are covered: removing the header from the frozenset trips test_secure_logging.py:83, removing the redact call trips test_api_error_redaction.py.
Also verified on this head: CI green on 3.10–3.13, base fresh against main (48c88fc), .fernignore at 95 entries with 0 ghosts and no .bak leftovers, zero "Early Access" in src/, and the speed/expressivity accepted-value copy present.
Two corrections to my round-2 review:
-
The
[S10]regression test I recommended does not work as written — it asserts on__doc__, but Fern emits those descriptions as PEP 224 variable docstrings, which Python discards at runtime (module__doc__isNone;SpeakV2Speed.__doc__isfloat's). A corrected source-reading version, verified passing against this tree, is in the review notes. -
My claim that
[B2]"applies to the JS SDK with more force" was wrong. Parity pass run against both sibling checkouts: the leak needed an error type that stringifies headers and connect paths passing outbound request headers into it. JS'sDeepgramErrorcarries no headers field; Java'sDeepgramHttpExceptioncarries only response headers and omits them fromtoString(). Greg's read in the PR description was correct — this is Python-specific and does not need porting. What remains for the JS repo is defense-in-depth only (sec-websocket-protocolabsent from itsSENSITIVE_HEADERS, latent today since that helper is wired only into the HTTP fetcher).
Gates cleared since the last round: release-please #751 is closed with no open release PR, and deepgram-docs #1090 is now approved and green — which downgrades [S10] from live exposure to residual hygiene.
Deferred to post-launch, unchanged: [S9] the malformed-dict test asserts on the private helper while the public constructor still raises; [S10] the freeze-independent GA-copy guard; [N4] a dict mixing a v1-only key with language_hint still ships the singular key; [N2] dirty spec tree; [N3] breaks_applied typed int but None at runtime.
One ask for the release notes: add the key-rotation line. The fix stops new leaks but does nothing about keys already written to a customer's error tracker or log archive — anyone with events from a failed connect should treat those keys as exposed and rotate.
dg-coreylweathers
left a comment
There was a problem hiding this comment.
Re-reviewed on 9929ca1 (unchanged since the previous approval). Content approved — hold merge until the Flux TTS API is confirmed in production.
[B2] verified fixed, by probe rather than attestation: sec-websocket-protocol is now in _SENSITIVE_HEADERS, and credentials mask correctly across all five header shapes — the documented "token, <key>" form (masks to token, [REDACTED], dg-request-id preserved), no-scheme values, lowercase header names, list values, and on ParsingError as well as ApiError. Both mutation paths are covered: removing the header from the frozenset trips test_secure_logging.py:83, removing the redact call trips test_api_error_redaction.py.
Also verified on this head: CI green on 3.10–3.13, base fresh against main (48c88fc), .fernignore at 95 entries with 0 ghosts and no .bak leftovers, zero "Early Access" in src/, and the speed/expressivity accepted-value copy present.
Two corrections to my round-2 review:
-
The
[S10]regression test I recommended does not work as written — it asserts on__doc__, but Fern emits those descriptions as PEP 224 variable docstrings, which Python discards at runtime (module__doc__isNone;SpeakV2Speed.__doc__isfloat's). A corrected source-reading version, verified passing against this tree, is in the review notes. -
My claim that
[B2]"applies to the JS SDK with more force" was wrong. Parity pass run against both sibling checkouts: the leak needed an error type that stringifies headers and connect paths passing outbound request headers into it. JS'sDeepgramErrorcarries no headers field; Java'sDeepgramHttpExceptioncarries only response headers and omits them fromtoString(). Greg's read in the PR description was correct — this is Python-specific and does not need porting. What remains for the JS repo is defense-in-depth only (sec-websocket-protocolabsent from itsSENSITIVE_HEADERS, latent today since that helper is wired only into the HTTP fetcher).
Gates cleared since the last round: release-please #751 is closed with no open release PR, and deepgram-docs #1090 is now approved and green — which downgrades [S10] from live exposure to residual hygiene.
Deferred to post-launch, unchanged: [S9] the malformed-dict test asserts on the private helper while the public constructor still raises; [S10] the freeze-independent GA-copy guard; [N4] a dict mixing a v1-only key with language_hint still ships the singular key; [N2] dirty spec tree; [N3] breaks_applied typed int but None at runtime.
One ask for the release notes: add the key-rotation line. The fix stops new leaks but does nothing about keys already written to a customer's error tracker or log archive — anyone with events from a failed connect should treat those keys as exposed and rotate.
Python SDK regeneration from the
jherlihy/flux-tts-gadocs branch (fern-python-sdk 5.23.0).What this adds
Speak v2
send_interrupt()— barge-in, optionally with aplayback_offset({type: "time_ms", value: N})send_configure()— mid-streamspeedchanges, acknowledged byConfigureSuccess/ConfigureFailureSpeechInterruptedserver message, with metadata incl. a newcontrols_applied.breaks_appliedcounterspeedandexpressivityconnect query parametersListen v2
redactconnect parameter (ListenV2Redact:numbers,aggressive_numbers)send_configure()is now properly typed (ListenV2Configure+ListenV2ConfigureSuccessin the response union), replacing the previoustyping.AnyshimOther
GoogleThinkProviderVersion(addsai-studio-v1beta,gemini-enterprise-agent-v1)AgentV1UpdateListenListenProviderdiscriminated union (_V1/_V2, discriminantversion)deepgramspeak providerversionwidened fromLiteral["v1"]tostrclient_wrappernow derives its version fromimportlib.metadatainstead of a hardcoded stringManual patch reconciliation
Re-applied (generator still misses these): socket-client broad
except(required for custom transports, which raise non-websocketserrors) and the optional no-payload control params; agent-settings andlanguage_hintback-compat validators;query_encoderbool coercion; the 10__init__.pylegacy alias re-exports (merged with the new generated exports); wire-test legacy create-key alias coverage.Dropped (generator now covers them, entries removed from
.fernignore):agent_v1latency_report—stt_latencyis emitted natively againcore/client_wrapper— dynamic versioning supersedes thex-release-please-versionannotations; itsgenericextra-file entry is also removed fromrelease-please-config.jsonRecovered: the
agent_v1update_listen_listen.pyprovider-coercion validator was missing from.fernignoreand was overwritten by the generator. Restored and now frozen — a regression test caught it.listen/v2 send_configurekeeps runtime tolerance for a raw dict (sent verbatim) for back-compat with pre-typed-model callers.Compatibility
No breaking changes against the last release (v7.6.0): 0 removed public exports, 0 deleted modules, all baseline socket-client signatures intact, and enum changes are widenings only. Compat alias shims still pass.
The one change that would have broken callers is
AgentV1UpdateListenListen.provider(bareDeepgramListenProviderV2-> required discriminated union). The restored validator coerces a legacy provider instance or a legacy bare dict into the new shape, verified at runtime — both produceversion: "v2"on the wire.Review follow-ups (b8b2e78)
.fernignore. Removed the now-unfrozenclient_wrapper.pyentry, added the newly-frozenagent_v1update_listen_listen.py(with a note that this patch was silently lost once by a regen), added the threelanguage_hintprovider validators and the new frozen test file, and corrected thelisten/v2/socket_client.pyentry which still described the supersededtyping.Anysend_configureshim. Verified zero drift in both directions.speak/v2 send_interruptnow takes an optional message, matching the sibling no-payload controls (send_flush/send_close) —SpeakV2Interrupthas no required fields, sosend_interrupt()is meaningful.send_configuredeliberately keeps its required argument (a Configure with no settings is meaningless). Covered by new sync + async tests.Version bump
The branch carries
7.6.1(a patch) but this is afeat. That's expected: the generator patch-bumpspyproject.tomlon every regen and release-please recomputes the correct minor at release time from the conventional-commit type. Verified across three cycles — e.g.#750left7.6.1and release-please's PR proposed7.7.0. This only holds because the squash-merge message isfeat(...), which is why the PR title matters here.Review follow-ups (round 2)
[B1] blocking — fixed. Passing
provideras a plain dict toAgentV1UpdateListenListenskipped thelanguage_hint→language_hintsremap, so the deprecated singular key reached the wire. The API usesdeny_unknown_fields, so the wholeUpdateListenwas rejected with no client-side signal. The dict branch now routes throughDeepgramListenProviderV1/V2so their own before-validators run — one remap implementation instead of two. [S1] fixed in the same edit: the version is inferred from the dict's own keys (language/smart_formatare v1-only) rather than blanket-stampedv2.One deviation from the suggested fix: constructing the shim model raises on a malformed dict (e.g. missing the required
model), which would have made the compat shim a new source of exceptions. Wrapped with a pass-through fallback, so behaviour is never worse than pre-regen. The downstream union rejection of a genuinely-invalid payload was confirmed pre-existing (the old code produced an identical payload, rejected identically).[S3] restored wire coverage. The regen dropped all ten optional query params from the
manage.v1.projects.requests.listwire test (Fern derives the test from the spec example, which was simplified upstream), leaving their serialisation — includingdatetime→ ISO-8601Z— unverified while the client still forwarded them. Restored and frozen in.fernignorewith the same rationale as the_keys.pyentry.[S2] reference.md updated. Added the five missing entries (
redact,send_configurefor listen v2;speed,expressivity,send_interrupt,send_configurefor speak v2). The param entries spell out accepted values, since the spec's numeric enums are dropped in codegen.[S7] barge-in example added —⚠️ Not yet verifiable:
examples/26-text-to-speech-barge-in-v2.py, async (the sync client blocks instart_listening(), so mid-stream interrupt can't be shown).speak.v2.connect(model="flux-alexis-en")returns HTTP 400 onapi.deepgram.comtoday, consistent with the API not being in production yet.[S8] documented — AGENTS.md now records the residual concerns on the two newly-unfrozen files, notably that
client_wrapper's version fallback literal is refreshed only by regen.Security fix (found while running the new example)
Every websocket
connect()path raisesApiError(headers=dict(headers), ...)with the full request headers, and bothApiErrorandParsingErrorstringify that dict. A failed connect therefore printed:so any application logging the exception —
print(e), a traceback, a log aggregator, an error tracker — wrote the customer's API key in clear text. This is the same threat_secure_logging.pyalready covers for thewebsocketsDEBUG handshake logs; the exception path is simply the other route to it, and was uncovered. It also defeats the telemetry scrubber, which stripsrequest.headersbut not the exception message.Both error types now mask credential values at construction (so
repr()and attribute serialisation are covered too, not just__str__), reusing_secure_logging's existing sensitive-header set and scheme-preserving mask. Non-sensitive headers are kept —dg-request-idis the main reason to inspect them. Both files are generated, so they're frozen with the rationale and documented in the ledger. 14 new tests, mutation-checked.Pre-existing, not introduced by this regen, and it affects all five socket clients.
Core / generator upgrade (5.14.18 → 5.23.0)
Nine generator minors also changed core HTTP behaviour that the feature summary above doesn't cover:
RequestOptionsgainstimeout, andtimeout_in_secondsbecomes a deprecated alias — a new public option plus a deprecation.httpx_clientusers. The SDK previously adopted your client's.timeout.readas the base timeout for every request; the base timeout is nowNone, resolving tohttpx.USE_CLIENT_DEFAULT. This is an improvement — the old path flattened a granularhttpx.Timeout(connect=5, read=30, …)down to a single30, silently widening the connect timeout — but if you tuned around that flattening, expect connect timeouts to tighten to what you actually configured.get_keepalive_socket_options()is a new public helper incore/http_client.pythat nothing in the SDK calls.stream_reconnection_enabled/max_stream_reconnection_attemptsare added to both client constructors and toRequestOptions, documented as "Defaults to True" — but they are consumed only bycore/http_sse/_api.py, and nothing wireshttp_sseto an endpoint, so they are inert today and do not apply to the WebSocket clients. Worth knowing, sinceDeepgramClientalready has an unrelated hand-writtenreconnectflag. (Not patched in the docstring: those files are generated and now unfrozen, so a comment there would be lost next regen.)Spec copy fixes — [S4] / [S5] (done, not deferred)
Both are spec
descriptionfields, so the durable fix is indeepgram-docs: deepgram-docs#1094, stacked on the Flux GA PR #1090 (SpeakV2Speed/SpeakV2Expressivitydon't exist on docsmainyet, so there was nothing to edit there).To avoid this release shipping stale copy while waiting on a regen, the same text is applied by hand here:
types/deepgram.py+requests/deepgram.py). Flux is GA; the direct Speak V2 path had already been updated, so the Agent path was the only surface still telling developers Flux might change before GA.src/now has zero "Early Access" occurrences. No "GA" wording added in its place.speedandexpressivity. Both are enums in the spec — the problem is downstream: Fern drops numeric enums, sospeed's "Only the multipliers listed here are accepted" pointed at a list that wasn't there andexpressivitygave no range at all. Both now spell the values out, andspeednames the two failure codes (SPEED_OUT_OF_RANGE,SPEED_INCREMENT_INVALID).These four files are not frozen. The hand-applied text was verified to match #1094 byte-for-byte — the spec YAML was parsed, folded scalars normalised the way Fern renders them, and all five descriptions (provider, version, model, speed, expressivity) compared equal — so the next regen emits identical output and a freeze would protect nothing. Fern keeps ownership; no maintenance debt once #1094 merges.
Known issues NOT fixed here
SpeakV2Speed→ barefloat,SpeakV2Expressivity→ bareint; string enums keep theirLiteralunions), so out-of-range values are caught only by the server. Documented rather than hand-validated — thereference.mdentries and the corrected docstrings now state the accepted values. Worth raising with Fern, since it affects every Deepgram SDK..fern/metadata.jsonrecordsoriginGitCommitIsDirty: true— the spec input isn't reproducible from version control. Pre-existing practice (maincarries the same flag) and unchanged here, since fixing it needs a spec commit plus a regen.speak.v2.connect(model="flux-alexis-en")returns HTTP 400 onapi.deepgram.comtoday, consistent with the merge gate below.Note on the
ApiErrorfix and the other SDKsThe credential leak above is Python-specific and does not need porting. It required two Python-only things to coincide:
ApiError.__str__printingself.headers, and the websocketconnect()paths passing the request headers. Checked both siblings: the JSDeepgramError.buildMessage({message, statusCode, body})takes no headers and none of its construction sites pass request headers; Java'sDeepgramHttpExceptionpopulates its header map fromrawResponse.headers()(response headers) and has notoStringthat dumps them.Verification
pytest— 375 passed, 1 skippedruff check src/clean;mypy src/(882 files) andmypy tests/typecheck/cleanextra="allow", so asserting a field's value does not detect a dropped field declaration — the metadata tests assert the declared field set as well.25-text-to-speech-streaming-v2.py(Speak v2 streaming) and30-voice-agent.py. Management examples were intentionally skipped; 5 others need a local audio file, a microphone, or the separatedeepgram-sagemakerpackage.Rebased onto
mainafter #757 (the listen-v2 revert) merged, so the revert is no longer duplicated on this branch.