Skip to content

feat(regen): speak v2 barge-in and speed/expressivity controls, listen v2 redaction - #758

Merged
GregHolmes merged 12 commits into
mainfrom
gh/sdk-gen-2026-08-11
Aug 12, 2026
Merged

feat(regen): speak v2 barge-in and speed/expressivity controls, listen v2 redaction#758
GregHolmes merged 12 commits into
mainfrom
gh/sdk-gen-2026-08-11

Conversation

@GregHolmes

@GregHolmes GregHolmes commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Python SDK regeneration from the jherlihy/flux-tts-ga docs branch (fern-python-sdk 5.23.0).

What this adds

Speak v2

  • send_interrupt() — barge-in, optionally with a playback_offset ({type: "time_ms", value: N})
  • send_configure() — mid-stream speed changes, acknowledged by ConfigureSuccess / ConfigureFailure
  • SpeechInterrupted server message, with metadata incl. a new controls_applied.breaks_applied counter
  • speed and expressivity connect query parameters

Listen v2

  • redact connect parameter (ListenV2Redact: numbers, aggressive_numbers)
  • send_configure() is now properly typed (ListenV2Configure + ListenV2ConfigureSuccess in the response union), replacing the previous typing.Any shim

Other

  • GoogleThinkProviderVersion (adds ai-studio-v1beta, gemini-enterprise-agent-v1)
  • AgentV1UpdateListenListenProvider discriminated union (_V1/_V2, discriminant version)
  • deepgram speak provider version widened from Literal["v1"] to str
  • client_wrapper now derives its version from importlib.metadata instead of a hardcoded string

Manual patch reconciliation

Re-applied (generator still misses these): socket-client broad except (required for custom transports, which raise non-websockets errors) and the optional no-payload control params; agent-settings and language_hint back-compat validators; query_encoder bool coercion; the 10 __init__.py legacy 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_reportstt_latency is emitted natively again
  • core/client_wrapper — dynamic versioning supersedes the x-release-please-version annotations; its generic extra-file entry is also removed from release-please-config.json

Recovered: the agent_v1update_listen_listen.py provider-coercion validator was missing from .fernignore and was overwritten by the generator. Restored and now frozen — a regression test caught it.

listen/v2 send_configure keeps 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 (bare DeepgramListenProviderV2 -> required discriminated union). The restored validator coerces a legacy provider instance or a legacy bare dict into the new shape, verified at runtime — both produce version: "v2" on the wire.

Review follow-ups (b8b2e78)

  • AGENTS.md freeze ledger synced with .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.py 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 deliberately 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 a feat. That's expected: the generator patch-bumps pyproject.toml on every regen and release-please recomputes the correct minor at release time from the conventional-commit type. Verified across three cycles — e.g. #750 left 7.6.1 and release-please's PR proposed 7.7.0. This only holds because the squash-merge message is feat(...), which is why the PR title matters here.

Review follow-ups (round 2)

[B1] blocking — fixed. Passing provider as a plain dict to AgentV1UpdateListenListen skipped the language_hintlanguage_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 dict branch now routes through DeepgramListenProviderV1/V2 so 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_format are v1-only) rather than blanket-stamped v2.

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.list wire test (Fern derives the test from the spec example, which was simplified upstream), leaving their serialisation — including datetime → ISO-8601 Z — unverified while the client still forwarded them. Restored and frozen in .fernignore with the same rationale as the _keys.py entry.

[S2] reference.md updated. Added the five missing entries (redact, send_configure for listen v2; speed, expressivity, send_interrupt, send_configure for speak v2). The param entries spell out accepted values, since the spec's numeric enums are dropped in codegen.

[S7] barge-in example addedexamples/26-text-to-speech-barge-in-v2.py, async (the sync client blocks in start_listening(), so mid-stream interrupt can't be shown). ⚠️ Not yet verifiable: speak.v2.connect(model="flux-alexis-en") returns HTTP 400 on api.deepgram.com today, 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 raises ApiError(headers=dict(headers), ...) with the full request headers, and both ApiError and ParsingError stringify that dict. A failed connect therefore printed:

headers: {'Authorization': 'Token <live key>', ...}, status_code: 400, ...

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.py already covers for the websockets DEBUG handshake logs; the exception path is simply the other route to it, and was uncovered. It also defeats the telemetry scrubber, which strips request.headers but 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-id is 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:

  1. RequestOptions gains timeout, and timeout_in_seconds becomes a deprecated alias — a new public option plus a deprecation.
  2. Behaviour change for custom httpx_client users. The SDK previously adopted your client's .timeout.read as the base timeout for every request; the base timeout is now None, resolving to httpx.USE_CLIENT_DEFAULT. This is an improvement — the old path flattened a granular httpx.Timeout(connect=5, read=30, …) down to a single 30, silently widening the connect timeout — but if you tuned around that flattening, expect connect timeouts to tighten to what you actually configured.
  3. get_keepalive_socket_options() is a new public helper in core/http_client.py that nothing in the SDK calls.
  4. stream_reconnection_enabled / max_stream_reconnection_attempts are added to both client constructors and to RequestOptions, documented as "Defaults to True" — but they are consumed only by core/http_sse/_api.py, and nothing wires http_sse to an endpoint, so they are inert today and do not apply to the WebSocket clients. Worth knowing, since DeepgramClient already has an unrelated hand-written reconnect flag. (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 description fields, so the durable fix is in deepgram-docs: deepgram-docs#1094, stacked on the Flux GA PR #1090 (SpeakV2Speed/SpeakV2Expressivity don't exist on docs main yet, 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:

  • [S4] Flux TTS "Early Access" wording removed from the Deepgram agent speak-provider descriptions (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.
  • [S5] accepted values stated for speed and expressivity. Both are enums in the spec — the problem is downstream: Fern drops numeric enums, so speed's "Only the multipliers listed here are accepted" pointed at a list that wasn't there and expressivity gave no range at all. Both now spell the values out, and speed names 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

  • Numeric spec enums lose their constraints in codegen (SpeakV2Speed → bare float, SpeakV2Expressivity → bare int; string enums keep their Literal unions), so out-of-range values are caught only by the server. Documented rather than hand-validated — the reference.md entries and the corrected docstrings now state the accepted values. Worth raising with Fern, since it affects every Deepgram SDK.
  • [N2] .fern/metadata.json records originGitCommitIsDirty: true — the spec input isn't reproducible from version control. Pre-existing practice (main carries the same flag) and unchanged here, since fixing it needs a spec commit plus a regen.
  • [S7] the new barge-in example is unverified against a live API. speak.v2.connect(model="flux-alexis-en") returns HTTP 400 on api.deepgram.com today, consistent with the merge gate below.

Note on the ApiError fix and the other SDKs

The credential leak above is Python-specific and does not need porting. It required two Python-only things to coincide: ApiError.__str__ printing self.headers, and the websocket connect() paths passing the request headers. Checked both siblings: the JS DeepgramError.buildMessage({message, statusCode, body}) takes no headers and none of its construction sites pass request headers; Java's DeepgramHttpException populates its header map from rawResponse.headers() (response headers) and has no toString that dumps them.

Verification

  • pytest375 passed, 1 skipped
  • ruff check src/ clean; mypy src/ (882 files) and mypy tests/typecheck/ clean
  • 18 new tests covering the added surface, each mutation-verified (deliberately broke the source and confirmed the test fails). Note these models are extra="allow", so asserting a field's value does not detect a dropped field declaration — the metadata tests assert the declared field set as well.
  • 16 examples run against the live API, including 25-text-to-speech-streaming-v2.py (Speak v2 streaming) and 30-voice-agent.py. Management examples were intentionally skipped; 5 others need a local audio file, a microphone, or the separate deepgram-sagemaker package.

Rebased onto main after #757 (the listen-v2 revert) merged, so the revert is no longer duplicated on this branch.

GregHolmes and others added 4 commits August 11, 2026 13:05
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.
@GregHolmes
GregHolmes force-pushed the gh/sdk-gen-2026-08-11 branch from b8b0d90 to 223f490 Compare August 11, 2026 12:06
@GregHolmes GregHolmes changed the title chore: SDK regeneration 2026-08-11 feat(regen): speak v2 barge-in and speed/expressivity controls, listen v2 redaction Aug 11, 2026
…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.
@GregHolmes
GregHolmes marked this pull request as ready for review August 11, 2026 20:01
@GregHolmes GregHolmes self-assigned this Aug 11, 2026
…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 dg-coreylweathers left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 dg-coreylweathers left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. 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__ is None; SpeakV2Speed.__doc__ is float's). A corrected source-reading version, verified passing against this tree, is in the review notes.

  2. 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's DeepgramError carries no headers field; Java's DeepgramHttpException carries only response headers and omits them from toString(). 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-protocol absent from its SENSITIVE_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 dg-coreylweathers left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. 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__ is None; SpeakV2Speed.__doc__ is float's). A corrected source-reading version, verified passing against this tree, is in the review notes.

  2. 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's DeepgramError carries no headers field; Java's DeepgramHttpException carries only response headers and omits them from toString(). 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-protocol absent from its SENSITIVE_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.

@GregHolmes
GregHolmes merged commit aab1eae into main Aug 12, 2026
11 checks passed
@GregHolmes
GregHolmes deleted the gh/sdk-gen-2026-08-11 branch August 12, 2026 12:34
This was referenced Aug 12, 2026
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