Skip to content

feat(regen): listen v2 force-end-turn and listen v1 diarize metadata - #768

Open
GregHolmes wants to merge 8 commits into
mainfrom
gh/sdk-gen-2026-08-18
Open

feat(regen): listen v2 force-end-turn and listen v1 diarize metadata#768
GregHolmes wants to merge 8 commits into
mainfrom
gh/sdk-gen-2026-08-18

Conversation

@GregHolmes

@GregHolmes GregHolmes commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Fern SDK regeneration for 2026-08-18 (fern-python-sdk 5.27.1, CLI 5.95.1).

Status: reconciled & verified. mypy src/ clean (900 files) · mypy tests/typecheck clean · ruff check src/deepgram clean · 992 passed / 1 skipped · coverage 94.69% (≥ 90% gate). No breaking changes vs 7.7.0 — 0 removed public exports across every package, all renames aliased with identical values.

New in this regen

  • Listen V1 diarization detail — per-word speaker and speaker_confidence on the words item (pre-recorded only; speaker_confidence is not returned for streaming), plus ListenV1ResponseMetadataDiarizeInfo / ListenV1ResultsMetadataDiarizeInfo (model_uuid, arch) identifying which diarizer ran. Verified end-to-end against production: diarize=true returns diarize_info {model_uuid, arch: "v1"} and every word carries both fields; both are absent when diarization is off.
  • Listen V2 eot_threshold now accepts up to 1.0 (previously documented as 0.50.9). Boundary-verified against production: 0.49 and 1.01 are rejected with HTTP 400, 0.5 and 1.0 are accepted.
  • Listen V2 force-end-turn — requires server-side enablement, now live on staging. send_force_end_turn() + ListenV2ForceEndTurn (a no-payload control message), plus a new ListenV2TurnInfo.trigger field (model | manual | timeout, open enum) identifying what ended a turn. Verified end-to-end on a deployment that has the feature; still gated on production, where sending the message returns UNPARSABLE_CLIENT_MESSAGE"The ForceEndTurn message is not enabled on this deployment." — and closes the websocket. See Force-end-turn and the end-of-turn controls.
  • Provider/agent-history type renames — the generator dropped the *ThinkProvider* / *SpeakProvider* prefixes and renamed the agent conversation-history subtypes (see Backward compatibility). New canonical names: AnthropicModel, CartesiaModelId, CartesiaVoice, DeepgramModel, GoogleModel, GoogleVersion, GroqReasoningMode, ConversationHistoryMessageRole, FunctionCallHistoryMessageFunctionCallsItem.
  • SpeakV2SpeedValue — a dedicated float type for the mid-stream SpeakV2Configure.speed field.

Force-end-turn and the end-of-turn controls

Updated 2026-08-20. This supersedes the earlier finding that force-end-turn was rejected everywhere. The gate has since opened on staging, and the feature is now verified end-to-end through the SDK.

Check (5s speech + 14s trailing silence unless noted) Result
send_force_end_turn(), continuous speech EndOfTurn with trigger="manual", end_of_turn_confidence=0.0085, connection stays open and the turn index advances
eot_threshold=0.7 EndOfTurn with trigger="model"
eot_threshold=1.0 EndOfTurn with trigger="timeout"not "model"
eot_threshold=1.0 + eot_timeout_ms=60000 no EndOfTurn at all
same, then send_force_end_turn() EndOfTurn with trigger="manual"
Production, any of the above UNPARSABLE_CLIENT_MESSAGE, socket closed

The eot_threshold=1.0 row corrects an earlier reading. Counting events, 0.7 and 1.0 both produce exactly one EndOfTurn, which looks like 1.0 suppresses nothing. trigger shows that is a misattribution: at 1.0 Flux's confidence-based detection really is suppressed, and what closed the turn was eot_timeout_ms (default 5000), a separate mechanism. Nothing but trigger distinguishes those two cases — and trigger was not being emitted when this was last measured, which is why the distinction was invisible.

So the docstring's advice to "set to 1.0 to fully suppress natural end-of-turn detection and drive turn endings yourself" is incomplete rather than wrong: it needs eot_timeout_ms raised as well. The last two rows show the working combination. Worth reporting upstream as a spec-doc fix — the SDK type is generated and unfrozen, so patching the docstring here would mean freezing a file for a comment; better fixed at the source.

No SDK change was neededsend_force_end_turn() serialized correctly and trigger parsed all three values unmodified.

Caveat: one API key per environment, so this shows the gate is open for that staging project; whether enablement is per-project, per-deployment, or global still needs the feature-flag config.

Backward compatibility (all preserved — non-breaking)

  • Renamed public types aliased — old import paths kept, and the aliases resolve to the new types with byte-identical literal values: AnthropicThinkProviderModelAnthropicModel, CartesiaSpeakProviderModelIdCartesiaModelId, CartesiaSpeakProviderVoiceCartesiaVoice, DeepgramSpeakProviderModelDeepgramModel, GoogleThinkProviderModelGoogleModel, GoogleThinkProviderVersionGoogleVersion, GroqThinkProviderReasoningModeGroqReasoningMode.
  • Agent-history names shimmed — the removed AgentV1HistoryContentRole, AgentV1HistoryFunctionCallsFunctionCallsItem (+Params), and the double-Context ...ContentRole / ...FunctionCallsFunctionCallsItem (+Params) are recreated as hand-written aliases to the new canonical types, so every old import path still resolves.
  • Repointed shims — the single-Context ...MessagesItemContentRole / ...FunctionCallsFunctionCallsItem shims whose underlying generated modules were deleted now re-export the new canonical types.
  • send_force_end_turn() takes an optional message (no-payload control), matching its siblings; listen/v2 send_configure still accepts a raw dict alongside the typed model; callable AgentV1SettingsAgent(...), legacy messages= / .messages, and language_hint= all continue to work.

SpeakV2Speed retype — patched (spec bug, raised with Fern)

The generator retyped SpeakV2Speed (the speak.v2.connect(speed=...) param) from float to Union[Literal["0.85"…"1.15"], Any] — a string-literal enum. That contradicts the actual API contract and is inconsistent with the new SpeakV2SpeedValue = float on the Configure message. Verified against production (wss://api.deepgram.com, flux-alexis-en):

  • speed: 1.05 (number) → ConfigureSuccess {applied.speed: 1.05}
  • speed: 2.0ConfigureFailure SPEED_OUT_OF_RANGE (value echoed back as the number 2.0)
  • speed: "1.05" (string) → Error MESSAGE-0000 "The message could not be parsed."

So the string form the generated type implies is rejected at the wire. Patched SpeakV2Speed back to float (frozen in .fernignore, guarded by a regression test). Raised upstream with Fern; unfreeze and drop the patch once the spec/generator emits a number.

Maintenance

Patches re-applied (generator still needs them): socket-client broad except + optional no-payload control params; agent _sanitize_numeric_types; query_encoder bool coercion; ApiError / ParsingError credential redaction; agent-settings and language_hintlanguage_hints back-compat validators; AgentV1UpdateListenListen legacy-provider coercion; the legacy alias re-exports merged into 10 __init__.py; both frozen wire tests.

New frozen files this cycle: 14 back-compat alias modules for the renames, plus types/speak_v2speed.py (the float patch). .fernignore and the AGENTS.md freeze ledger updated to match.

Added test coverage: send_force_end_turn (sync + async), listen/v2 send_configure typed-model + async paths, the provider/agent-history rename aliases, and the SpeakV2Speed-is-float regression guard. Plus live coverage in tests/manual/listen/v2/force_end_turn/ (sync + async, since the two socket clients have separate implementations) and examples/16-transcription-force-end-turn.py. All three report the gate and exit cleanly where the feature is not enabled, rather than dying on the connection teardown the rejection causes.

Verification

  • Unit: mypy + ruff clean, 992 passed / 1 skipped, coverage 94.69%.
  • Integration: 37/37 tests/manual scripts pass against the live API, plus the two new force_end_turn scripts — all four checks pass against a deployment with the feature, and both report SKIP against production.
  • Examples: 21/22 non-management examples pass; the only failure is 27-transcription-live-sagemaker.py, which needs the optional deepgram_sagemaker package (not a regression). New example 16 verified against both a feature-enabled deployment and production.

Version bump

The branch carries a patch bump in pyproject.toml, but this is a feat. That's expected — the generator patch-bumps on every regen and release-please recomputes the correct minor at release time from the conventional-commit type. This only holds because the squash-merge message is feat(regen): …, so the PR title matters here.

@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Code Coverage

Package Line Rate Branch Rate Complexity Health
src.deepgram 96% 92% 0
src.deepgram.agent 100% 100% 0
src.deepgram.agent.v1 98% 100% 0
src.deepgram.agent.v1.settings 100% 100% 0
src.deepgram.agent.v1.settings.think 100% 100% 0
src.deepgram.agent.v1.settings.think.models 97% 100% 0
src.deepgram.auth 100% 100% 0
src.deepgram.auth.v1 100% 100% 0
src.deepgram.auth.v1.tokens 97% 100% 0
src.deepgram.core 88% 81% 0
src.deepgram.errors 100% 100% 0
src.deepgram.helpers 100% 95% 0
src.deepgram.listen 100% 100% 0
src.deepgram.listen.v1 98% 93% 0
src.deepgram.listen.v1.media 97% 100% 0
src.deepgram.listen.v2 98% 93% 0
src.deepgram.manage 100% 100% 0
src.deepgram.manage.v1 100% 100% 0
src.deepgram.manage.v1.models 96% 100% 0
src.deepgram.manage.v1.projects 97% 100% 0
src.deepgram.manage.v1.projects.billing 100% 100% 0
src.deepgram.manage.v1.projects.billing.balances 96% 100% 0
src.deepgram.manage.v1.projects.billing.breakdown 97% 100% 0
src.deepgram.manage.v1.projects.billing.fields 97% 100% 0
src.deepgram.manage.v1.projects.billing.purchases 97% 100% 0
src.deepgram.manage.v1.projects.keys 96% 100% 0
src.deepgram.manage.v1.projects.members 97% 100% 0
src.deepgram.manage.v1.projects.members.invites 96% 100% 0
src.deepgram.manage.v1.projects.members.scopes 96% 100% 0
src.deepgram.manage.v1.projects.models 96% 100% 0
src.deepgram.manage.v1.projects.usage 98% 100% 0
src.deepgram.manage.v1.projects.usage.breakdown 97% 100% 0
src.deepgram.manage.v1.projects.usage.fields 97% 100% 0
src.deepgram.read 100% 100% 0
src.deepgram.read.v1 100% 100% 0
src.deepgram.read.v1.text 98% 100% 0
src.deepgram.self_hosted 100% 100% 0
src.deepgram.self_hosted.v1 100% 100% 0
src.deepgram.self_hosted.v1.distribution_credentials 96% 100% 0
src.deepgram.speak 100% 100% 0
src.deepgram.speak.v1 98% 97% 0
src.deepgram.speak.v1.audio 91% 80% 0
src.deepgram.speak.v2 98% 93% 0
src.deepgram.speak.v2.audio 100% 100% 0
src.deepgram.voice_agent 100% 100% 0
src.deepgram.voice_agent.configurations 95% 100% 0
src.deepgram.voice_agent.variables 95% 100% 0
Summary 95% (6486 / 6803) 91% (1411 / 1546) 0

Scope: hand-maintained SDK logic. Fern-generated data models (types/, requests/), package __init__.py files, version.py, and the unused core/http_sse/ scaffolding are excluded — see .coveragerc. Unscoped whole-package coverage is ~70%.

@GregHolmes GregHolmes changed the title chore: SDK regeneration 2026-08-18 feat(regen): listen v2 force-end-turn and listen v1 diarize metadata Aug 18, 2026
The ForceEndTurn gate has opened on staging, so the feature is now
verifiable end-to-end rather than only through a mocked websocket.

Add tests/manual/listen/v2/force_end_turn/ (sync + async, since the two
socket clients have separate send_force_end_turn implementations). The
sync script covers the whole end-of-turn control matrix via TurnInfo
.trigger: ForceEndTurn yields trigger="manual", eot_threshold=0.7 yields
"model", and eot_threshold=1.0 yields "timeout" rather than "model".

That last case corrects a misreading. eot_threshold=1.0 does suppress
Flux's confidence-based detection, but eot_timeout_ms is a separate
mechanism that still ends the turn, so counting EndOfTurn events makes
1.0 look like it changes nothing. Only trigger distinguishes them, and
it was not emitted when this was last measured. Full manual control
needs eot_threshold=1.0 together with a long eot_timeout_ms; with both,
no EndOfTurn arrives until ForceEndTurn is sent.

Add examples/16-transcription-force-end-turn.py demonstrating that
combination. Both the scripts and the example report the gate and exit
cleanly where the feature is not enabled, rather than dying on the
connection teardown the rejection causes.

Document the gate and the destructive failure mode on both
send_force_end_turn docstrings.
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.

1 participant