feat: Phase 3 — streaming TTS WebSocket & self-hosted distribution credentials - #166
dg-coreylweathers wants to merge 12 commits into
Conversation
|
Addressed the review's BLOCKING finding: the TTS-WS worker busy-spun (and hung on current-thread runtimes) after input close because it kept selecting on the drained input channel. |
5a60203 to
570125f
Compare
GregHolmes
left a comment
There was a problem hiding this comment.
Review: deepgram-rust-sdk #166, feat: Phase 3 - streaming TTS WebSocket and self-hosted distribution credentials
Classification: mixed (code-led)
Verdict: request-changes
Intent
Add WebSocket streaming TTS and self-hosted distribution-credential management while keeping the SDK's existing async patterns. This PR is intentionally stacked on #165 and cannot merge until #164 and #165 are fixed, merged, and the stack is retargeted. Intent is based on the PR, issues #95, #147, and #148, and the author's live-verification comment; no launch or Slack context is linked.
Blocking
- [B1]
src/speak/websocket.rs:161, streaming TTS silently drops documented metadata.
Title: [B1] SpeakResponse::Metadata omits additional_model_uuids
Summary: The streaming TTS API's Metadata event includes the optional additional_model_uuids array, but neither TextEvent::Metadata nor the public SpeakResponse::Metadata carries it. Serde ignores the unknown field, so a successful request silently loses model provenance that a caller may need for auditing or billing.
Expected: All documented fields from a streaming TTS Metadata event are available in the typed response.
Observed: additional_model_uuids is discarded during deserialization and cannot be recovered by SDK users.
Recommended fix: Add additional_model_uuids: Option<Vec<Uuid>> to TextEvent::Metadata and SpeakResponse::Metadata, pass it through the conversion, and add a fixture that asserts the values survive parsing.
- [B2]
src/speak/websocket.rs:98, the streaming TTS builder does not enforce or expose the endpoint's option contract.
Title: [B2] Streaming TTS accepts invalid encodings and omits supported controls
Summary: The streaming endpoint supports only linear16, mulaw, and alaw, but SpeakStreamBuilder::encoding accepts the REST Encoding enum, including mp3, opus, flac, aac, and arbitrary custom values. Conversely, the endpoint documents speed and mip_opt_out, but the builder has no setters or query-parameter escape hatch, so callers cannot use them at all.
Expected: The streaming API rejects locally known-invalid encodings before connecting and exposes every documented streaming control.
Observed: Encoding::Mp3 is serialized into a request the server rejects, while speed and mip_opt_out cannot be sent through this client.
Recommended fix: Use a streaming-specific encoding enum or validate the existing enum in handle() with a clear DeepgramError::InvalidOptions; add speed and mip_opt_out to the builder, with the documented 0.7..=1.5 range for speed, and cover valid and invalid URLs in tests.
Should-fix
- [S1]
src/speak/websocket.rs:534, protocol tests do not cover every supported client and server message.
Title: [S1] Streaming TTS protocol coverage omits Clear, Cleared, and Warning paths
Summary: The unit tests assert Speak, Flush, Close, Metadata, Flushed, and Unknown behavior, but omit Clear transmission and Cleared and Warning parsing. These are public control paths; a future serde rename or refactor can break them without a test failure.
Expected: Each public protocol message has an exact serialization or parsing assertion.
Observed: The available tests do not exercise Clear, Cleared, or Warning.
Recommended fix: Extend the existing table-style tests with those three cases, including optional sequence_id, description, and code values.
Nits
- None.
Verified (evidence)
- The streaming TTS WebSocket path, ClientMessage variants, required metadata fields, Flush/Clear/Close messages, and supported encoding list align with the live API reference.
- The self-hosted list and create paths, request body/query placement, defaults, and response envelope align with the live Self-Hosted API reference.
- Local Rust 1.94.1: build, tests (79 unit plus 140 doc-tests), clippy, fmt, docs, and
cargo check --no-default-features --features speakpass. Therust:1.97image named in the PR contains neithercargonorrustup, so it cannot run the declared container gate. - The prior busy-spin report is fixed in the current worker: once the input closes, it stops selecting on the drained input channel and drains server messages only.
Needs human
- Greg: the stack depends on #164 and #165. Both have requested-changes reviews and #164 conflicts with
main, so this PR must stay unmerged until their fixes are merged and the stack is retargeted. The inherited SemVer failures are a release-process decision for the planned 0.11.0 aggregation.
Developer-facing messaging
- The changelog accurately says the
speakfeature now pulls WebSocket dependencies. It should not imply the typedMetadataevent is complete until [B1] preservesadditional_model_uuids. - Rejecting unsupported output encodings before opening the socket and exposing documented controls gives developers a clear local error instead of an avoidable server-side failure.
a5fa559 to
4c76c47
Compare
… validation, speed/mip_opt_out, protocol test coverage - B1: `SpeakResponse::Metadata` and the internal `TextEvent::Metadata` now carry `additional_model_uuids: Option<Vec<Uuid>>` (absent -> None), passed through the conversion, with a parsing fixture asserting the values survive (present, absent, and empty array). - B2: `SpeakStreamBuilder::handle()` validates options locally before connecting. REST-only encodings (`mp3`, `opus`, `flac`, `aac`) and a `speed` outside `0.7..=1.5` (or non-finite) are rejected with the new `DeepgramError::InvalidOptions(String)`; `linear16` / `mulaw` / `alaw` pass and `CustomEncoding` is left as an escape hatch. New `speed(f32)` and `mip_opt_out(bool)` setters serialize as `speed=` / `mip_opt_out=` query params. Tests cover valid/invalid URLs, each rejected encoding, the speed boundaries, and that `handle()` short-circuits without a network attempt. - S1: protocol tests now cover `Clear` client serialization and table-style parsing of `Cleared` (with/without `sequence_id`) and `Warning` (all combinations of `description` / `code`), matching the live API reference. - CHANGELOG: Unreleased entry mentions `speed`, `mip_opt_out`, local encoding validation via `DeepgramError::InvalidOptions`, and `additional_model_uuids`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
570125f to
8052666
Compare
…rward, and correct the streaming TTS docs Follow-ups from the #166 review: - `speak::options::Model` was missing `aura-2-perseo-it`, the 91st Aura-2 voice in the API specification. Add it in locale order and make the round-trip test assert the exact variant count (103) instead of a lower bound, so the next specification drift fails loudly. - Document that a hand-built `Model::CustomId` is not `==` to the named variant whose wire string it spells. - `src/tls.rs`, the `Deepgram::tls_config` rustdoc, and `README.md` each enumerated only three `wss://` surfaces; streaming text-to-speech goes through the same shared rustls connector, so name it. - AGENTS.md: mark streaming text-to-speech and self-hosted credentials as shipped with their real entry points (the documented self-hosted path was wrong too: it is `/self-hosted/distribution/credentials`, not `/onprem/`), add `src/speak/websocket.rs` and `self_hosted` to the repository map, and correct the default-features list. - The installable text-to-speech skill said the Aura `/v1/speak` WebSocket was not implemented. Correct every such claim and teach the real API. - `SpeakResponse::Metadata::additional_model_uuids` is now `Option<Vec<String>>`, matching `model_uuid`: one non-UUID element no longer downgrades a readable `Metadata` event to `Unknown`. - The worker's terminal write-error forward no longer waits for room on the response channel. With events undrained it parked the worker on a full channel while the caller parked on a full outbound channel, contradicting the rustdoc promise that an audio backlog never blocks `speak`. Same fix in the Flux text-to-speech worker, where it is a deadlock released in 0.10.1. - Tests: a wire assertion for `Encoding::CustomEncoding`, the `terminal_read_error_ends_worker_after_single_error` case the other two sockets already had, and a write-error-with-undrained-events regression test for both text-to-speech workers (each fails without the fix above). - `tests/connect_tls_config_local.rs` was gated `listen`-only, so its `speak`-only cases never compiled in CI's `speak` jobs. Relax the gate, per-test `cfg` the `listen` cases, and add a `speak`-only negative case.
…2 voice Applies the pass-4 review of #166. B1: the streaming text-to-speech worker forwarded a terminal write error with `try_send` on the shared response channel, so the error was silently discarded whenever the 256-deep channel was full at that instant. With `SpeakStreamHandle::split()` a consumer drains events from its own task and never parks on the outbound channel, so a full response channel at failure time is reachable with the consumer very much alive: it saw the audio stream end and its next `speak()` fail, with no error value. That violates the written contract in AGENTS.md ("the worker forwards a terminal transport error exactly once"). The worker now keeps a dedicated clone of the sender for exactly that error: a `futures` mpsc channel's capacity is `buffer + num_senders`, so the clone carries its own guaranteed slot and the forward neither blocks (which is what deadlocked a non-draining caller before) nor drops. Same shape applied to the Flux text-to-speech worker's write-failure and post-loop close paths. New test `write_error_reaches_a_slow_split_consumer` asserts the error is *received*, not merely that nothing hangs — the existing `write_error_with_undrained_events_does_not_stall_the_worker` passed either way, which is how this slipped through. Against the previous code it fails deterministically with 0 terminal errors after 297 drained audio events. B2: remove `Model::Aura2PerseoIt`. The API specification lists it as the 91st Aura-2 voice, but the specification is wrong: production answers `400 {"err_msg":"No such model/version combination found."}` for `aura-2-perseo-it`, the other nine Italian Aura-2 voices all return `200 audio/mpeg`, and `GET /v1/models?include_outdated=true` lists 90 Aura-2 canonical names with `aura-2-perseo-it` the only specification entry missing. The round-trip test keeps its exact-count assertion — that is the right mechanism — pinned to 102 (12 Aura-1 + the 90 Aura-2 voices the API serves) with the reason recorded in the comment, and gains an assertion that `aura-2-perseo-it` resolves to `CustomId`. Prose that claimed coverage of every voice "in the API specification" now says what is true: every voice the API serves. S1: the changelog bullet for the Flux text-to-speech fix says what a developer now observes and drops the "no public signature changed" clause; a signature fact is not evidence about behavior. S2: `with_base_url` / `with_base_url_and_api_key` no longer claim that all admin features ignore the base URL. They name what honors it (transcription, text-to-speech, Text Intelligence, model listing, self-hosted credentials) and what does not (billing, usage, keys, members, invitations, projects, scopes, and the token grant). Nits: the host-header capture now also reports the upgrade target, giving the streaming text-to-speech escape hatches (`query_params`, `Encoding::CustomEncoding`) a wire-level guard; `output.raw` and `flux-tts-batch.mp3` are gitignored; the `Url::join` trailing-slash requirement is documented on both `with_base_url` constructors; and the stray space inside the `scopes` URL literal is removed (the URL parser stripped it, so no behavior change). Gates, all exit 0 with `RUSTFLAGS=-D warnings RUSTDOCFLAGS=-D warnings`: `cargo fmt --check --all`; `cargo clippy --all-targets --all-features`; `cargo test --all --all-features` (204 unit + 150 doc tests, every `*_local.rs` suite, 0 failed); `cargo doc --workspace --all-features` plus the five single-feature doc builds; `cargo check --all-targets --no-default-features` with `speak` and with `speak,rustls-tls-native-roots`; `cargo test --no-default-features --features speak`.
…delivery Applies the three should-fix findings from the final verification review of #166. [S1] The README's `speak` feature row named only "Aura REST, Flux TTS REST and streaming WebSocket" — true on the parent branch, but this branch ships Aura streaming over `wss /v1/speak`, so the row understated the feature. It now covers all four surfaces, and the table's column width is back in alignment. The README named the new entry point nowhere, so a short pointer after the table gives `dg.text_to_speech().speak_stream()` and the runnable examples for both streaming sockets. [S2] The shipped management-API skill omitted the self-hosted credentials surface and stated a base-URL rule this branch breaks. It now lists the four `self_hosted()` methods (with the Uuid credentials ID and the return-once secret), `src/manage/self_hosted.rs`, and `examples/manage/self_hosted_credentials.rs`; gotcha 3 now matches the `with_base_url` rustdoc exactly — `models()` and `self_hosted()` honor the configured base URL, while billing, usage, keys, members, invitations, projects, scopes, and `/v1/auth/grant` stay on the hosted site. [S3] Nothing tested that the Flux TTS terminal write error is *delivered*: the only covering test asserted that `speak()` stops hanging, so it passed with the error dropped. The new test in `tests/flux_speak_backpressure_local.rs` fills the response channel, breaks the transport, and then drains `handle.receive()`, asserting that at least 256 audio events were queued (the channel was provably full at the failure), that exactly one `Err` arrives, that it is the last item, and that the stream then ends. Reverting the `error_tx` forward to `response_tx.try_send(..)` fails it with 0 errors delivered after 257 queued audio events, while the pre-existing deadlock test still passes.
4c76c47 to
472db5f
Compare
8052666 to
8c9b4b4
Compare
… validation, speed/mip_opt_out, protocol test coverage - B1: `SpeakResponse::Metadata` and the internal `TextEvent::Metadata` now carry `additional_model_uuids: Option<Vec<Uuid>>` (absent -> None), passed through the conversion, with a parsing fixture asserting the values survive (present, absent, and empty array). - B2: `SpeakStreamBuilder::handle()` validates options locally before connecting. REST-only encodings (`mp3`, `opus`, `flac`, `aac`) and a `speed` outside `0.7..=1.5` (or non-finite) are rejected with the new `DeepgramError::InvalidOptions(String)`; `linear16` / `mulaw` / `alaw` pass and `CustomEncoding` is left as an escape hatch. New `speed(f32)` and `mip_opt_out(bool)` setters serialize as `speed=` / `mip_opt_out=` query params. Tests cover valid/invalid URLs, each rejected encoding, the speed boundaries, and that `handle()` short-circuits without a network attempt. - S1: protocol tests now cover `Clear` client serialization and table-style parsing of `Cleared` (with/without `sequence_id`) and `Warning` (all combinations of `description` / `code`), matching the live API reference. - CHANGELOG: Unreleased entry mentions `speed`, `mip_opt_out`, local encoding validation via `DeepgramError::InvalidOptions`, and `additional_model_uuids`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…rward, and correct the streaming TTS docs Follow-ups from the #166 review: - `speak::options::Model` was missing `aura-2-perseo-it`, the 91st Aura-2 voice in the API specification. Add it in locale order and make the round-trip test assert the exact variant count (103) instead of a lower bound, so the next specification drift fails loudly. - Document that a hand-built `Model::CustomId` is not `==` to the named variant whose wire string it spells. - `src/tls.rs`, the `Deepgram::tls_config` rustdoc, and `README.md` each enumerated only three `wss://` surfaces; streaming text-to-speech goes through the same shared rustls connector, so name it. - AGENTS.md: mark streaming text-to-speech and self-hosted credentials as shipped with their real entry points (the documented self-hosted path was wrong too: it is `/self-hosted/distribution/credentials`, not `/onprem/`), add `src/speak/websocket.rs` and `self_hosted` to the repository map, and correct the default-features list. - The installable text-to-speech skill said the Aura `/v1/speak` WebSocket was not implemented. Correct every such claim and teach the real API. - `SpeakResponse::Metadata::additional_model_uuids` is now `Option<Vec<String>>`, matching `model_uuid`: one non-UUID element no longer downgrades a readable `Metadata` event to `Unknown`. - The worker's terminal write-error forward no longer waits for room on the response channel. With events undrained it parked the worker on a full channel while the caller parked on a full outbound channel, contradicting the rustdoc promise that an audio backlog never blocks `speak`. Same fix in the Flux text-to-speech worker, where it is a deadlock released in 0.10.1. - Tests: a wire assertion for `Encoding::CustomEncoding`, the `terminal_read_error_ends_worker_after_single_error` case the other two sockets already had, and a write-error-with-undrained-events regression test for both text-to-speech workers (each fails without the fix above). - `tests/connect_tls_config_local.rs` was gated `listen`-only, so its `speak`-only cases never compiled in CI's `speak` jobs. Relax the gate, per-test `cfg` the `listen` cases, and add a `speak`-only negative case.
…2 voice Applies the pass-4 review of #166. B1: the streaming text-to-speech worker forwarded a terminal write error with `try_send` on the shared response channel, so the error was silently discarded whenever the 256-deep channel was full at that instant. With `SpeakStreamHandle::split()` a consumer drains events from its own task and never parks on the outbound channel, so a full response channel at failure time is reachable with the consumer very much alive: it saw the audio stream end and its next `speak()` fail, with no error value. That violates the written contract in AGENTS.md ("the worker forwards a terminal transport error exactly once"). The worker now keeps a dedicated clone of the sender for exactly that error: a `futures` mpsc channel's capacity is `buffer + num_senders`, so the clone carries its own guaranteed slot and the forward neither blocks (which is what deadlocked a non-draining caller before) nor drops. Same shape applied to the Flux text-to-speech worker's write-failure and post-loop close paths. New test `write_error_reaches_a_slow_split_consumer` asserts the error is *received*, not merely that nothing hangs — the existing `write_error_with_undrained_events_does_not_stall_the_worker` passed either way, which is how this slipped through. Against the previous code it fails deterministically with 0 terminal errors after 297 drained audio events. B2: remove `Model::Aura2PerseoIt`. The API specification lists it as the 91st Aura-2 voice, but the specification is wrong: production answers `400 {"err_msg":"No such model/version combination found."}` for `aura-2-perseo-it`, the other nine Italian Aura-2 voices all return `200 audio/mpeg`, and `GET /v1/models?include_outdated=true` lists 90 Aura-2 canonical names with `aura-2-perseo-it` the only specification entry missing. The round-trip test keeps its exact-count assertion — that is the right mechanism — pinned to 102 (12 Aura-1 + the 90 Aura-2 voices the API serves) with the reason recorded in the comment, and gains an assertion that `aura-2-perseo-it` resolves to `CustomId`. Prose that claimed coverage of every voice "in the API specification" now says what is true: every voice the API serves. S1: the changelog bullet for the Flux text-to-speech fix says what a developer now observes and drops the "no public signature changed" clause; a signature fact is not evidence about behavior. S2: `with_base_url` / `with_base_url_and_api_key` no longer claim that all admin features ignore the base URL. They name what honors it (transcription, text-to-speech, Text Intelligence, model listing, self-hosted credentials) and what does not (billing, usage, keys, members, invitations, projects, scopes, and the token grant). Nits: the host-header capture now also reports the upgrade target, giving the streaming text-to-speech escape hatches (`query_params`, `Encoding::CustomEncoding`) a wire-level guard; `output.raw` and `flux-tts-batch.mp3` are gitignored; the `Url::join` trailing-slash requirement is documented on both `with_base_url` constructors; and the stray space inside the `scopes` URL literal is removed (the URL parser stripped it, so no behavior change). Gates, all exit 0 with `RUSTFLAGS=-D warnings RUSTDOCFLAGS=-D warnings`: `cargo fmt --check --all`; `cargo clippy --all-targets --all-features`; `cargo test --all --all-features` (204 unit + 150 doc tests, every `*_local.rs` suite, 0 failed); `cargo doc --workspace --all-features` plus the five single-feature doc builds; `cargo check --all-targets --no-default-features` with `speak` and with `speak,rustls-tls-native-roots`; `cargo test --no-default-features --features speak`.
…delivery Applies the three should-fix findings from the final verification review of #166. [S1] The README's `speak` feature row named only "Aura REST, Flux TTS REST and streaming WebSocket" — true on the parent branch, but this branch ships Aura streaming over `wss /v1/speak`, so the row understated the feature. It now covers all four surfaces, and the table's column width is back in alignment. The README named the new entry point nowhere, so a short pointer after the table gives `dg.text_to_speech().speak_stream()` and the runnable examples for both streaming sockets. [S2] The shipped management-API skill omitted the self-hosted credentials surface and stated a base-URL rule this branch breaks. It now lists the four `self_hosted()` methods (with the Uuid credentials ID and the return-once secret), `src/manage/self_hosted.rs`, and `examples/manage/self_hosted_credentials.rs`; gotcha 3 now matches the `with_base_url` rustdoc exactly — `models()` and `self_hosted()` honor the configured base URL, while billing, usage, keys, members, invitations, projects, scopes, and `/v1/auth/grant` stay on the hosted site. [S3] Nothing tested that the Flux TTS terminal write error is *delivered*: the only covering test asserted that `speak()` stops hanging, so it passed with the error dropped. The new test in `tests/flux_speak_backpressure_local.rs` fills the response channel, breaks the transport, and then drains `handle.receive()`, asserting that at least 256 audio events were queued (the channel was provably full at the failure), that exactly one `Err` arrives, that it is the last item, and that the stream then ends. Reverting the `error_tx` forward to `response_tx.try_send(..)` fails it with 0 errors delivered after 257 queued audio events, while the pre-existing deadlock test still passes.
Round 2 — pushed, and the self-hosted question is answeredRebased onto the updated #165. Your three items are fixed; I also probed the self-hosted contract live, because three claims in the code contradicted the published reference. The self-hosted scope concern turned out to be a docs bug, not an SDK bug The reference documents
So the server reads both from the body, the narrowed scope is received, and the SDK is correct. The reference page and the OpenAPI spec are wrong — routing that to @GregHolmes separately. No credential was created (403), so nothing was provisioned. What changed since your review
What I verified
Still open: the |
… validation, speed/mip_opt_out, protocol test coverage - B1: `SpeakResponse::Metadata` and the internal `TextEvent::Metadata` now carry `additional_model_uuids: Option<Vec<Uuid>>` (absent -> None), passed through the conversion, with a parsing fixture asserting the values survive (present, absent, and empty array). - B2: `SpeakStreamBuilder::handle()` validates options locally before connecting. REST-only encodings (`mp3`, `opus`, `flac`, `aac`) and a `speed` outside `0.7..=1.5` (or non-finite) are rejected with the new `DeepgramError::InvalidOptions(String)`; `linear16` / `mulaw` / `alaw` pass and `CustomEncoding` is left as an escape hatch. New `speed(f32)` and `mip_opt_out(bool)` setters serialize as `speed=` / `mip_opt_out=` query params. Tests cover valid/invalid URLs, each rejected encoding, the speed boundaries, and that `handle()` short-circuits without a network attempt. - S1: protocol tests now cover `Clear` client serialization and table-style parsing of `Cleared` (with/without `sequence_id`) and `Warning` (all combinations of `description` / `code`), matching the live API reference. - CHANGELOG: Unreleased entry mentions `speed`, `mip_opt_out`, local encoding validation via `DeepgramError::InvalidOptions`, and `additional_model_uuids`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…rward, and correct the streaming TTS docs Follow-ups from the #166 review: - `speak::options::Model` was missing `aura-2-perseo-it`, the 91st Aura-2 voice in the API specification. Add it in locale order and make the round-trip test assert the exact variant count (103) instead of a lower bound, so the next specification drift fails loudly. - Document that a hand-built `Model::CustomId` is not `==` to the named variant whose wire string it spells. - `src/tls.rs`, the `Deepgram::tls_config` rustdoc, and `README.md` each enumerated only three `wss://` surfaces; streaming text-to-speech goes through the same shared rustls connector, so name it. - AGENTS.md: mark streaming text-to-speech and self-hosted credentials as shipped with their real entry points (the documented self-hosted path was wrong too: it is `/self-hosted/distribution/credentials`, not `/onprem/`), add `src/speak/websocket.rs` and `self_hosted` to the repository map, and correct the default-features list. - The installable text-to-speech skill said the Aura `/v1/speak` WebSocket was not implemented. Correct every such claim and teach the real API. - `SpeakResponse::Metadata::additional_model_uuids` is now `Option<Vec<String>>`, matching `model_uuid`: one non-UUID element no longer downgrades a readable `Metadata` event to `Unknown`. - The worker's terminal write-error forward no longer waits for room on the response channel. With events undrained it parked the worker on a full channel while the caller parked on a full outbound channel, contradicting the rustdoc promise that an audio backlog never blocks `speak`. Same fix in the Flux text-to-speech worker, where it is a deadlock released in 0.10.1. - Tests: a wire assertion for `Encoding::CustomEncoding`, the `terminal_read_error_ends_worker_after_single_error` case the other two sockets already had, and a write-error-with-undrained-events regression test for both text-to-speech workers (each fails without the fix above). - `tests/connect_tls_config_local.rs` was gated `listen`-only, so its `speak`-only cases never compiled in CI's `speak` jobs. Relax the gate, per-test `cfg` the `listen` cases, and add a `speak`-only negative case.
…2 voice Applies the pass-4 review of #166. B1: the streaming text-to-speech worker forwarded a terminal write error with `try_send` on the shared response channel, so the error was silently discarded whenever the 256-deep channel was full at that instant. With `SpeakStreamHandle::split()` a consumer drains events from its own task and never parks on the outbound channel, so a full response channel at failure time is reachable with the consumer very much alive: it saw the audio stream end and its next `speak()` fail, with no error value. That violates the written contract in AGENTS.md ("the worker forwards a terminal transport error exactly once"). The worker now keeps a dedicated clone of the sender for exactly that error: a `futures` mpsc channel's capacity is `buffer + num_senders`, so the clone carries its own guaranteed slot and the forward neither blocks (which is what deadlocked a non-draining caller before) nor drops. Same shape applied to the Flux text-to-speech worker's write-failure and post-loop close paths. New test `write_error_reaches_a_slow_split_consumer` asserts the error is *received*, not merely that nothing hangs — the existing `write_error_with_undrained_events_does_not_stall_the_worker` passed either way, which is how this slipped through. Against the previous code it fails deterministically with 0 terminal errors after 297 drained audio events. B2: remove `Model::Aura2PerseoIt`. The API specification lists it as the 91st Aura-2 voice, but the specification is wrong: production answers `400 {"err_msg":"No such model/version combination found."}` for `aura-2-perseo-it`, the other nine Italian Aura-2 voices all return `200 audio/mpeg`, and `GET /v1/models?include_outdated=true` lists 90 Aura-2 canonical names with `aura-2-perseo-it` the only specification entry missing. The round-trip test keeps its exact-count assertion — that is the right mechanism — pinned to 102 (12 Aura-1 + the 90 Aura-2 voices the API serves) with the reason recorded in the comment, and gains an assertion that `aura-2-perseo-it` resolves to `CustomId`. Prose that claimed coverage of every voice "in the API specification" now says what is true: every voice the API serves. S1: the changelog bullet for the Flux text-to-speech fix says what a developer now observes and drops the "no public signature changed" clause; a signature fact is not evidence about behavior. S2: `with_base_url` / `with_base_url_and_api_key` no longer claim that all admin features ignore the base URL. They name what honors it (transcription, text-to-speech, Text Intelligence, model listing, self-hosted credentials) and what does not (billing, usage, keys, members, invitations, projects, scopes, and the token grant). Nits: the host-header capture now also reports the upgrade target, giving the streaming text-to-speech escape hatches (`query_params`, `Encoding::CustomEncoding`) a wire-level guard; `output.raw` and `flux-tts-batch.mp3` are gitignored; the `Url::join` trailing-slash requirement is documented on both `with_base_url` constructors; and the stray space inside the `scopes` URL literal is removed (the URL parser stripped it, so no behavior change). Gates, all exit 0 with `RUSTFLAGS=-D warnings RUSTDOCFLAGS=-D warnings`: `cargo fmt --check --all`; `cargo clippy --all-targets --all-features`; `cargo test --all --all-features` (204 unit + 150 doc tests, every `*_local.rs` suite, 0 failed); `cargo doc --workspace --all-features` plus the five single-feature doc builds; `cargo check --all-targets --no-default-features` with `speak` and with `speak,rustls-tls-native-roots`; `cargo test --no-default-features --features speak`.
…delivery Applies the three should-fix findings from the final verification review of #166. [S1] The README's `speak` feature row named only "Aura REST, Flux TTS REST and streaming WebSocket" — true on the parent branch, but this branch ships Aura streaming over `wss /v1/speak`, so the row understated the feature. It now covers all four surfaces, and the table's column width is back in alignment. The README named the new entry point nowhere, so a short pointer after the table gives `dg.text_to_speech().speak_stream()` and the runnable examples for both streaming sockets. [S2] The shipped management-API skill omitted the self-hosted credentials surface and stated a base-URL rule this branch breaks. It now lists the four `self_hosted()` methods (with the Uuid credentials ID and the return-once secret), `src/manage/self_hosted.rs`, and `examples/manage/self_hosted_credentials.rs`; gotcha 3 now matches the `with_base_url` rustdoc exactly — `models()` and `self_hosted()` honor the configured base URL, while billing, usage, keys, members, invitations, projects, scopes, and `/v1/auth/grant` stay on the hosted site. [S3] Nothing tested that the Flux TTS terminal write error is *delivered*: the only covering test asserted that `speak()` stops hanging, so it passed with the error dropped. The new test in `tests/flux_speak_backpressure_local.rs` fills the response channel, breaks the transport, and then drains `handle.receive()`, asserting that at least 256 audio events were queued (the channel was provably full at the failure), that exactly one `Err` arrives, that it is the last item, and that the stream then ends. Reverting the `error_tx` forward to `response_tx.try_send(..)` fails it with 0 errors delivered after 257 queued audio events, while the pre-existing deadlock test still passes.
472db5f to
0bd4173
Compare
… validation, speed/mip_opt_out, protocol test coverage - B1: `SpeakResponse::Metadata` and the internal `TextEvent::Metadata` now carry `additional_model_uuids: Option<Vec<Uuid>>` (absent -> None), passed through the conversion, with a parsing fixture asserting the values survive (present, absent, and empty array). - B2: `SpeakStreamBuilder::handle()` validates options locally before connecting. REST-only encodings (`mp3`, `opus`, `flac`, `aac`) and a `speed` outside `0.7..=1.5` (or non-finite) are rejected with the new `DeepgramError::InvalidOptions(String)`; `linear16` / `mulaw` / `alaw` pass and `CustomEncoding` is left as an escape hatch. New `speed(f32)` and `mip_opt_out(bool)` setters serialize as `speed=` / `mip_opt_out=` query params. Tests cover valid/invalid URLs, each rejected encoding, the speed boundaries, and that `handle()` short-circuits without a network attempt. - S1: protocol tests now cover `Clear` client serialization and table-style parsing of `Cleared` (with/without `sequence_id`) and `Warning` (all combinations of `description` / `code`), matching the live API reference. - CHANGELOG: Unreleased entry mentions `speed`, `mip_opt_out`, local encoding validation via `DeepgramError::InvalidOptions`, and `additional_model_uuids`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…rward, and correct the streaming TTS docs Follow-ups from the #166 review: - `speak::options::Model` was missing `aura-2-perseo-it`, the 91st Aura-2 voice in the API specification. Add it in locale order and make the round-trip test assert the exact variant count (103) instead of a lower bound, so the next specification drift fails loudly. - Document that a hand-built `Model::CustomId` is not `==` to the named variant whose wire string it spells. - `src/tls.rs`, the `Deepgram::tls_config` rustdoc, and `README.md` each enumerated only three `wss://` surfaces; streaming text-to-speech goes through the same shared rustls connector, so name it. - AGENTS.md: mark streaming text-to-speech and self-hosted credentials as shipped with their real entry points (the documented self-hosted path was wrong too: it is `/self-hosted/distribution/credentials`, not `/onprem/`), add `src/speak/websocket.rs` and `self_hosted` to the repository map, and correct the default-features list. - The installable text-to-speech skill said the Aura `/v1/speak` WebSocket was not implemented. Correct every such claim and teach the real API. - `SpeakResponse::Metadata::additional_model_uuids` is now `Option<Vec<String>>`, matching `model_uuid`: one non-UUID element no longer downgrades a readable `Metadata` event to `Unknown`. - The worker's terminal write-error forward no longer waits for room on the response channel. With events undrained it parked the worker on a full channel while the caller parked on a full outbound channel, contradicting the rustdoc promise that an audio backlog never blocks `speak`. Same fix in the Flux text-to-speech worker, where it is a deadlock released in 0.10.1. - Tests: a wire assertion for `Encoding::CustomEncoding`, the `terminal_read_error_ends_worker_after_single_error` case the other two sockets already had, and a write-error-with-undrained-events regression test for both text-to-speech workers (each fails without the fix above). - `tests/connect_tls_config_local.rs` was gated `listen`-only, so its `speak`-only cases never compiled in CI's `speak` jobs. Relax the gate, per-test `cfg` the `listen` cases, and add a `speak`-only negative case.
…2 voice Applies the pass-4 review of #166. B1: the streaming text-to-speech worker forwarded a terminal write error with `try_send` on the shared response channel, so the error was silently discarded whenever the 256-deep channel was full at that instant. With `SpeakStreamHandle::split()` a consumer drains events from its own task and never parks on the outbound channel, so a full response channel at failure time is reachable with the consumer very much alive: it saw the audio stream end and its next `speak()` fail, with no error value. That violates the written contract in AGENTS.md ("the worker forwards a terminal transport error exactly once"). The worker now keeps a dedicated clone of the sender for exactly that error: a `futures` mpsc channel's capacity is `buffer + num_senders`, so the clone carries its own guaranteed slot and the forward neither blocks (which is what deadlocked a non-draining caller before) nor drops. Same shape applied to the Flux text-to-speech worker's write-failure and post-loop close paths. New test `write_error_reaches_a_slow_split_consumer` asserts the error is *received*, not merely that nothing hangs — the existing `write_error_with_undrained_events_does_not_stall_the_worker` passed either way, which is how this slipped through. Against the previous code it fails deterministically with 0 terminal errors after 297 drained audio events. B2: remove `Model::Aura2PerseoIt`. The API specification lists it as the 91st Aura-2 voice, but the specification is wrong: production answers `400 {"err_msg":"No such model/version combination found."}` for `aura-2-perseo-it`, the other nine Italian Aura-2 voices all return `200 audio/mpeg`, and `GET /v1/models?include_outdated=true` lists 90 Aura-2 canonical names with `aura-2-perseo-it` the only specification entry missing. The round-trip test keeps its exact-count assertion — that is the right mechanism — pinned to 102 (12 Aura-1 + the 90 Aura-2 voices the API serves) with the reason recorded in the comment, and gains an assertion that `aura-2-perseo-it` resolves to `CustomId`. Prose that claimed coverage of every voice "in the API specification" now says what is true: every voice the API serves. S1: the changelog bullet for the Flux text-to-speech fix says what a developer now observes and drops the "no public signature changed" clause; a signature fact is not evidence about behavior. S2: `with_base_url` / `with_base_url_and_api_key` no longer claim that all admin features ignore the base URL. They name what honors it (transcription, text-to-speech, Text Intelligence, model listing, self-hosted credentials) and what does not (billing, usage, keys, members, invitations, projects, scopes, and the token grant). Nits: the host-header capture now also reports the upgrade target, giving the streaming text-to-speech escape hatches (`query_params`, `Encoding::CustomEncoding`) a wire-level guard; `output.raw` and `flux-tts-batch.mp3` are gitignored; the `Url::join` trailing-slash requirement is documented on both `with_base_url` constructors; and the stray space inside the `scopes` URL literal is removed (the URL parser stripped it, so no behavior change). Gates, all exit 0 with `RUSTFLAGS=-D warnings RUSTDOCFLAGS=-D warnings`: `cargo fmt --check --all`; `cargo clippy --all-targets --all-features`; `cargo test --all --all-features` (204 unit + 150 doc tests, every `*_local.rs` suite, 0 failed); `cargo doc --workspace --all-features` plus the five single-feature doc builds; `cargo check --all-targets --no-default-features` with `speak` and with `speak,rustls-tls-native-roots`; `cargo test --no-default-features --features speak`.
8c9b4b4 to
06ef75c
Compare
…delivery Applies the three should-fix findings from the final verification review of #166. [S1] The README's `speak` feature row named only "Aura REST, Flux TTS REST and streaming WebSocket" — true on the parent branch, but this branch ships Aura streaming over `wss /v1/speak`, so the row understated the feature. It now covers all four surfaces, and the table's column width is back in alignment. The README named the new entry point nowhere, so a short pointer after the table gives `dg.text_to_speech().speak_stream()` and the runnable examples for both streaming sockets. [S2] The shipped management-API skill omitted the self-hosted credentials surface and stated a base-URL rule this branch breaks. It now lists the four `self_hosted()` methods (with the Uuid credentials ID and the return-once secret), `src/manage/self_hosted.rs`, and `examples/manage/self_hosted_credentials.rs`; gotcha 3 now matches the `with_base_url` rustdoc exactly — `models()` and `self_hosted()` honor the configured base URL, while billing, usage, keys, members, invitations, projects, scopes, and `/v1/auth/grant` stay on the hosted site. [S3] Nothing tested that the Flux TTS terminal write error is *delivered*: the only covering test asserted that `speak()` stops hanging, so it passed with the error dropped. The new test in `tests/flux_speak_backpressure_local.rs` fills the response channel, breaks the transport, and then drains `handle.receive()`, asserting that at least 256 audio events were queued (the channel was provably full at the failure), that exactly one `Err` arrives, that it is the last item, and that the stream then ends. Reverting the `error_tx` forward to `response_tx.try_send(..)` fails it with 0 errors delivered after 257 queued audio events, while the pre-existing deadlock test still passes.
0bd4173 to
1f61ac3
Compare
… validation, speed/mip_opt_out, protocol test coverage - B1: `SpeakResponse::Metadata` and the internal `TextEvent::Metadata` now carry `additional_model_uuids: Option<Vec<Uuid>>` (absent -> None), passed through the conversion, with a parsing fixture asserting the values survive (present, absent, and empty array). - B2: `SpeakStreamBuilder::handle()` validates options locally before connecting. REST-only encodings (`mp3`, `opus`, `flac`, `aac`) and a `speed` outside `0.7..=1.5` (or non-finite) are rejected with the new `DeepgramError::InvalidOptions(String)`; `linear16` / `mulaw` / `alaw` pass and `CustomEncoding` is left as an escape hatch. New `speed(f32)` and `mip_opt_out(bool)` setters serialize as `speed=` / `mip_opt_out=` query params. Tests cover valid/invalid URLs, each rejected encoding, the speed boundaries, and that `handle()` short-circuits without a network attempt. - S1: protocol tests now cover `Clear` client serialization and table-style parsing of `Cleared` (with/without `sequence_id`) and `Warning` (all combinations of `description` / `code`), matching the live API reference. - CHANGELOG: Unreleased entry mentions `speed`, `mip_opt_out`, local encoding validation via `DeepgramError::InvalidOptions`, and `additional_model_uuids`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…rward, and correct the streaming TTS docs Follow-ups from the #166 review: - `speak::options::Model` was missing `aura-2-perseo-it`, the 91st Aura-2 voice in the API specification. Add it in locale order and make the round-trip test assert the exact variant count (103) instead of a lower bound, so the next specification drift fails loudly. - Document that a hand-built `Model::CustomId` is not `==` to the named variant whose wire string it spells. - `src/tls.rs`, the `Deepgram::tls_config` rustdoc, and `README.md` each enumerated only three `wss://` surfaces; streaming text-to-speech goes through the same shared rustls connector, so name it. - AGENTS.md: mark streaming text-to-speech and self-hosted credentials as shipped with their real entry points (the documented self-hosted path was wrong too: it is `/self-hosted/distribution/credentials`, not `/onprem/`), add `src/speak/websocket.rs` and `self_hosted` to the repository map, and correct the default-features list. - The installable text-to-speech skill said the Aura `/v1/speak` WebSocket was not implemented. Correct every such claim and teach the real API. - `SpeakResponse::Metadata::additional_model_uuids` is now `Option<Vec<String>>`, matching `model_uuid`: one non-UUID element no longer downgrades a readable `Metadata` event to `Unknown`. - The worker's terminal write-error forward no longer waits for room on the response channel. With events undrained it parked the worker on a full channel while the caller parked on a full outbound channel, contradicting the rustdoc promise that an audio backlog never blocks `speak`. Same fix in the Flux text-to-speech worker, where it is a deadlock released in 0.10.1. - Tests: a wire assertion for `Encoding::CustomEncoding`, the `terminal_read_error_ends_worker_after_single_error` case the other two sockets already had, and a write-error-with-undrained-events regression test for both text-to-speech workers (each fails without the fix above). - `tests/connect_tls_config_local.rs` was gated `listen`-only, so its `speak`-only cases never compiled in CI's `speak` jobs. Relax the gate, per-test `cfg` the `listen` cases, and add a `speak`-only negative case.
06ef75c to
7f415a5
Compare
…2 voice Applies the pass-4 review of #166. B1: the streaming text-to-speech worker forwarded a terminal write error with `try_send` on the shared response channel, so the error was silently discarded whenever the 256-deep channel was full at that instant. With `SpeakStreamHandle::split()` a consumer drains events from its own task and never parks on the outbound channel, so a full response channel at failure time is reachable with the consumer very much alive: it saw the audio stream end and its next `speak()` fail, with no error value. That violates the written contract in AGENTS.md ("the worker forwards a terminal transport error exactly once"). The worker now keeps a dedicated clone of the sender for exactly that error: a `futures` mpsc channel's capacity is `buffer + num_senders`, so the clone carries its own guaranteed slot and the forward neither blocks (which is what deadlocked a non-draining caller before) nor drops. Same shape applied to the Flux text-to-speech worker's write-failure and post-loop close paths. New test `write_error_reaches_a_slow_split_consumer` asserts the error is *received*, not merely that nothing hangs — the existing `write_error_with_undrained_events_does_not_stall_the_worker` passed either way, which is how this slipped through. Against the previous code it fails deterministically with 0 terminal errors after 297 drained audio events. B2: remove `Model::Aura2PerseoIt`. The API specification lists it as the 91st Aura-2 voice, but the specification is wrong: production answers `400 {"err_msg":"No such model/version combination found."}` for `aura-2-perseo-it`, the other nine Italian Aura-2 voices all return `200 audio/mpeg`, and `GET /v1/models?include_outdated=true` lists 90 Aura-2 canonical names with `aura-2-perseo-it` the only specification entry missing. The round-trip test keeps its exact-count assertion — that is the right mechanism — pinned to 102 (12 Aura-1 + the 90 Aura-2 voices the API serves) with the reason recorded in the comment, and gains an assertion that `aura-2-perseo-it` resolves to `CustomId`. Prose that claimed coverage of every voice "in the API specification" now says what is true: every voice the API serves. S1: the changelog bullet for the Flux text-to-speech fix says what a developer now observes and drops the "no public signature changed" clause; a signature fact is not evidence about behavior. S2: `with_base_url` / `with_base_url_and_api_key` no longer claim that all admin features ignore the base URL. They name what honors it (transcription, text-to-speech, Text Intelligence, model listing, self-hosted credentials) and what does not (billing, usage, keys, members, invitations, projects, scopes, and the token grant). Nits: the host-header capture now also reports the upgrade target, giving the streaming text-to-speech escape hatches (`query_params`, `Encoding::CustomEncoding`) a wire-level guard; `output.raw` and `flux-tts-batch.mp3` are gitignored; the `Url::join` trailing-slash requirement is documented on both `with_base_url` constructors; and the stray space inside the `scopes` URL literal is removed (the URL parser stripped it, so no behavior change). Gates, all exit 0 with `RUSTFLAGS=-D warnings RUSTDOCFLAGS=-D warnings`: `cargo fmt --check --all`; `cargo clippy --all-targets --all-features`; `cargo test --all --all-features` (204 unit + 150 doc tests, every `*_local.rs` suite, 0 failed); `cargo doc --workspace --all-features` plus the five single-feature doc builds; `cargo check --all-targets --no-default-features` with `speak` and with `speak,rustls-tls-native-roots`; `cargo test --no-default-features --features speak`.
…delivery Applies the three should-fix findings from the final verification review of #166. [S1] The README's `speak` feature row named only "Aura REST, Flux TTS REST and streaming WebSocket" — true on the parent branch, but this branch ships Aura streaming over `wss /v1/speak`, so the row understated the feature. It now covers all four surfaces, and the table's column width is back in alignment. The README named the new entry point nowhere, so a short pointer after the table gives `dg.text_to_speech().speak_stream()` and the runnable examples for both streaming sockets. [S2] The shipped management-API skill omitted the self-hosted credentials surface and stated a base-URL rule this branch breaks. It now lists the four `self_hosted()` methods (with the Uuid credentials ID and the return-once secret), `src/manage/self_hosted.rs`, and `examples/manage/self_hosted_credentials.rs`; gotcha 3 now matches the `with_base_url` rustdoc exactly — `models()` and `self_hosted()` honor the configured base URL, while billing, usage, keys, members, invitations, projects, scopes, and `/v1/auth/grant` stay on the hosted site. [S3] Nothing tested that the Flux TTS terminal write error is *delivered*: the only covering test asserted that `speak()` stops hanging, so it passed with the error dropped. The new test in `tests/flux_speak_backpressure_local.rs` fills the response channel, breaks the transport, and then drains `handle.receive()`, asserting that at least 256 audio events were queued (the channel was provably full at the failure), that exactly one `Err` arrives, that it is the last item, and that the stream then ends. Reverting the `error_tx` forward to `response_tx.try_send(..)` fails it with 0 errors delivered after 257 queued audio events, while the pre-existing deadlock test still passes.
1f61ac3 to
302f5ed
Compare
…credentials - TTS WebSocket (#148/#147/#95): Speak::speak_stream() -> SpeakStreamBuilder (model/encoding/sample_rate); handle() opens the connection and returns a SpeakStreamHandle for sending text (speak/flush/clear/close) and receiving audio + events. SpeakStreamHandle implements futures::Stream; SpeakResponse (Audio/Metadata/Flushed/Cleared/Warning/Unknown) is #[non_exhaustive] and unknown message types are preserved rather than breaking the stream. The speak feature now enables the WebSocket deps. New example text_to_speech_websocket. - Self-hosted distribution credentials: Deepgram::self_hosted() -> SelfHosted with list/get/create/delete_distribution_credentials, plus typed response models. New example self_hosted_credentials. All additive (cargo semver-checks: no new breaking changes beyond the Phase 1 #130 streaming change). Live-verified the TTS WebSocket against production; the self-hosted list path returns a correct 403 on accounts without the self-hosted scope. Closes #148, #147, #95 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… validation, speed/mip_opt_out, protocol test coverage - B1: `SpeakResponse::Metadata` and the internal `TextEvent::Metadata` now carry `additional_model_uuids: Option<Vec<Uuid>>` (absent -> None), passed through the conversion, with a parsing fixture asserting the values survive (present, absent, and empty array). - B2: `SpeakStreamBuilder::handle()` validates options locally before connecting. REST-only encodings (`mp3`, `opus`, `flac`, `aac`) and a `speed` outside `0.7..=1.5` (or non-finite) are rejected with the new `DeepgramError::InvalidOptions(String)`; `linear16` / `mulaw` / `alaw` pass and `CustomEncoding` is left as an escape hatch. New `speed(f32)` and `mip_opt_out(bool)` setters serialize as `speed=` / `mip_opt_out=` query params. Tests cover valid/invalid URLs, each rejected encoding, the speed boundaries, and that `handle()` short-circuits without a network attempt. - S1: protocol tests now cover `Clear` client serialization and table-style parsing of `Cleared` (with/without `sequence_id`) and `Warning` (all combinations of `description` / `code`), matching the live API reference. - CHANGELOG: Unreleased entry mentions `speed`, `mip_opt_out`, local encoding validation via `DeepgramError::InvalidOptions`, and `additional_model_uuids`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…shared TLS connector Route /v1/speak streaming through crate::tls like every other wss:// surface since 0.11.0, so rustls-tls-native-roots and Deepgram::tls_config apply to it and an untrusted certificate surfaces as UntrustedTlsCertificate. Adds the per-surface tls_config test alongside the existing live-transcription and Flux cases.
Uses the shared crate::websocket_host_header so a base URL on a non-default port is routed by host:port like every other WebSocket surface; the Host capture test covers the streaming TTS client.
…rward, and correct the streaming TTS docs Follow-ups from the #166 review: - `speak::options::Model` was missing `aura-2-perseo-it`, the 91st Aura-2 voice in the API specification. Add it in locale order and make the round-trip test assert the exact variant count (103) instead of a lower bound, so the next specification drift fails loudly. - Document that a hand-built `Model::CustomId` is not `==` to the named variant whose wire string it spells. - `src/tls.rs`, the `Deepgram::tls_config` rustdoc, and `README.md` each enumerated only three `wss://` surfaces; streaming text-to-speech goes through the same shared rustls connector, so name it. - AGENTS.md: mark streaming text-to-speech and self-hosted credentials as shipped with their real entry points (the documented self-hosted path was wrong too: it is `/self-hosted/distribution/credentials`, not `/onprem/`), add `src/speak/websocket.rs` and `self_hosted` to the repository map, and correct the default-features list. - The installable text-to-speech skill said the Aura `/v1/speak` WebSocket was not implemented. Correct every such claim and teach the real API. - `SpeakResponse::Metadata::additional_model_uuids` is now `Option<Vec<String>>`, matching `model_uuid`: one non-UUID element no longer downgrades a readable `Metadata` event to `Unknown`. - The worker's terminal write-error forward no longer waits for room on the response channel. With events undrained it parked the worker on a full channel while the caller parked on a full outbound channel, contradicting the rustdoc promise that an audio backlog never blocks `speak`. Same fix in the Flux text-to-speech worker, where it is a deadlock released in 0.10.1. - Tests: a wire assertion for `Encoding::CustomEncoding`, the `terminal_read_error_ends_worker_after_single_error` case the other two sockets already had, and a write-error-with-undrained-events regression test for both text-to-speech workers (each fails without the fix above). - `tests/connect_tls_config_local.rs` was gated `listen`-only, so its `speak`-only cases never compiled in CI's `speak` jobs. Relax the gate, per-test `cfg` the `listen` cases, and add a `speak`-only negative case.
…t the new public items Four follow-ups on the self-hosted and streaming-TTS work in this branch, all on API that is new here and not in 0.11.0: - `CreateDistributionCredentials::scopes` and the `scopes` on both response types are now `Vec<Scope>` instead of `Vec<String>`. `Scope` follows the SDK's open-enum convention: eight named variants plus `Scope::Unknown(String)`, which preserves an unrecognized wire value and re-serializes to it exactly, and is also how a caller sends a scope the SDK has not caught up with. `From<&str>` / `From<String>` keep the string form of the builder working. - All four `SelfHosted` methods build their URL against the client's configured base URL, the way `read::rest` does, so `Deepgram::with_base_url` is no longer ignored by the module self-hosted users are most likely to point at their own host. - `get_distribution_credentials` and `delete_distribution_credentials` take the `Uuid` the SDK hands back in `distribution_credentials_id` rather than `&str`, so the round trip through `.to_string()` in the example is gone. - The `#[allow(missing_docs)]` on the seven self-hosted response fields is replaced with real doc comments, and `speak_models!` now generates a doc comment per variant from the wire string and the language it is grouped under, so all 103 voices are documented from one place.
…2 voice Applies the pass-4 review of #166. B1: the streaming text-to-speech worker forwarded a terminal write error with `try_send` on the shared response channel, so the error was silently discarded whenever the 256-deep channel was full at that instant. With `SpeakStreamHandle::split()` a consumer drains events from its own task and never parks on the outbound channel, so a full response channel at failure time is reachable with the consumer very much alive: it saw the audio stream end and its next `speak()` fail, with no error value. That violates the written contract in AGENTS.md ("the worker forwards a terminal transport error exactly once"). The worker now keeps a dedicated clone of the sender for exactly that error: a `futures` mpsc channel's capacity is `buffer + num_senders`, so the clone carries its own guaranteed slot and the forward neither blocks (which is what deadlocked a non-draining caller before) nor drops. Same shape applied to the Flux text-to-speech worker's write-failure and post-loop close paths. New test `write_error_reaches_a_slow_split_consumer` asserts the error is *received*, not merely that nothing hangs — the existing `write_error_with_undrained_events_does_not_stall_the_worker` passed either way, which is how this slipped through. Against the previous code it fails deterministically with 0 terminal errors after 297 drained audio events. B2: remove `Model::Aura2PerseoIt`. The API specification lists it as the 91st Aura-2 voice, but the specification is wrong: production answers `400 {"err_msg":"No such model/version combination found."}` for `aura-2-perseo-it`, the other nine Italian Aura-2 voices all return `200 audio/mpeg`, and `GET /v1/models?include_outdated=true` lists 90 Aura-2 canonical names with `aura-2-perseo-it` the only specification entry missing. The round-trip test keeps its exact-count assertion — that is the right mechanism — pinned to 102 (12 Aura-1 + the 90 Aura-2 voices the API serves) with the reason recorded in the comment, and gains an assertion that `aura-2-perseo-it` resolves to `CustomId`. Prose that claimed coverage of every voice "in the API specification" now says what is true: every voice the API serves. S1: the changelog bullet for the Flux text-to-speech fix says what a developer now observes and drops the "no public signature changed" clause; a signature fact is not evidence about behavior. S2: `with_base_url` / `with_base_url_and_api_key` no longer claim that all admin features ignore the base URL. They name what honors it (transcription, text-to-speech, Text Intelligence, model listing, self-hosted credentials) and what does not (billing, usage, keys, members, invitations, projects, scopes, and the token grant). Nits: the host-header capture now also reports the upgrade target, giving the streaming text-to-speech escape hatches (`query_params`, `Encoding::CustomEncoding`) a wire-level guard; `output.raw` and `flux-tts-batch.mp3` are gitignored; the `Url::join` trailing-slash requirement is documented on both `with_base_url` constructors; and the stray space inside the `scopes` URL literal is removed (the URL parser stripped it, so no behavior change). Gates, all exit 0 with `RUSTFLAGS=-D warnings RUSTDOCFLAGS=-D warnings`: `cargo fmt --check --all`; `cargo clippy --all-targets --all-features`; `cargo test --all --all-features` (204 unit + 150 doc tests, every `*_local.rs` suite, 0 failed); `cargo doc --workspace --all-features` plus the five single-feature doc builds; `cargo check --all-targets --no-default-features` with `speak` and with `speak,rustls-tls-native-roots`; `cargo test --no-default-features --features speak`.
…delivery Applies the three should-fix findings from the final verification review of #166. [S1] The README's `speak` feature row named only "Aura REST, Flux TTS REST and streaming WebSocket" — true on the parent branch, but this branch ships Aura streaming over `wss /v1/speak`, so the row understated the feature. It now covers all four surfaces, and the table's column width is back in alignment. The README named the new entry point nowhere, so a short pointer after the table gives `dg.text_to_speech().speak_stream()` and the runnable examples for both streaming sockets. [S2] The shipped management-API skill omitted the self-hosted credentials surface and stated a base-URL rule this branch breaks. It now lists the four `self_hosted()` methods (with the Uuid credentials ID and the return-once secret), `src/manage/self_hosted.rs`, and `examples/manage/self_hosted_credentials.rs`; gotcha 3 now matches the `with_base_url` rustdoc exactly — `models()` and `self_hosted()` honor the configured base URL, while billing, usage, keys, members, invitations, projects, scopes, and `/v1/auth/grant` stay on the hosted site. [S3] Nothing tested that the Flux TTS terminal write error is *delivered*: the only covering test asserted that `speak()` stops hanging, so it passed with the error dropped. The new test in `tests/flux_speak_backpressure_local.rs` fills the response channel, breaks the transport, and then drains `handle.receive()`, asserting that at least 256 audio events were queued (the channel was provably full at the failure), that exactly one `Err` arrives, that it is the last item, and that the stream then ends. Reverting the `error_tx` forward to `response_tx.try_send(..)` fails it with 0 errors delivered after 257 queued audio events, while the pre-existing deadlock test still passes.
Review follow-ups on Phase 3.
The text-to-speech skill's dependency block lost its version key, which
leaves the snippet unparseable by cargo ("dependency (deepgram)
specified without providing a local path, Git repository, version, or
workspace dependency to use"). Pinned to the version this stack ships.
"covering every voice the API serves" and "names every Aura-1 and
Aura-2 voice the API serves" are unverifiable from the repo and false
the day a voice ships, so both are scoped to this release. The
exact-count test keeps enforcing the list, and its comment already
records the live probe behind the one exclusion (`aura-2-perseo-it`).
7f415a5 to
a8b8eaf
Compare
GregHolmes
left a comment
There was a problem hiding this comment.
[B1] src/manage/self_hosted.rs:293 — Successful credential creation can be reported as a client error
Summary: create_distribution_credentials deserializes a bare CreatedDistributionCredentials object with one-time username and secret. The published endpoint reference instead documents a { member, distribution_credentials } response, and the PR records that no scoped account was available to observe a successful server response. If the published response is what the server returns, creation succeeds remotely but the SDK returns a JSON error; a caller that retries may create duplicate credentials while never receiving the original result.
Expected: The SDK create-response type matches a verified successful server response and returns it without a deserialization failure.
Observed: The response model is author-asserted rather than exercised, and it conflicts with the published success schema.
Recommended fix: Run one create request against an account with the required self-hosted scope, capture the sanitized 200 response, and model that shape. If it is the documented wrapper, return DistributionCredentialsEntry; if it is the bare secret-bearing object, retain the current type and open a docs/spec correction with the captured evidence.
…ror (#179) ## Proposed changes The `/v1/listen` and `/v2/listen` streaming workers forwarded inbound responses and terminal errors with a blocking send, so a full response channel parked the worker mid-forward: it stopped writing the caller's audio, the caller filled the bounded command channel and parked in `send_data`, and the session hung. On `/v1/listen` this happened on a **perfectly healthy socket with no transport failure involved at all** — the inbound forward was never gated. Present on `/v1/listen` since 0.6.0 and on `/v2/listen` since 0.8.0. Three changes: - Inbound reads reserve capacity with `poll_ready` before forwarding, so backpressure reaches the socket instead of the worker — the behavior `/v2/listen` has had since 0.10.1. - Terminal errors forward through a `response_tx.clone()` that owns a guaranteed channel slot (a `futures` mpsc channel's capacity is `buffer + num_senders`), so the error neither blocks nor gets dropped. Plain `try_send` was measured dropping it: 257 queued responses, 0 errors delivered. - The first failed write ends the session, so one broken transport produces exactly one error instead of one per subsequent write plus one from cleanup. The Flux text-to-speech WebSocket (`/v2/speak`) has the same defect; its fix ships on the Phase 3 branch (#166), so this PR does not claim it. ## Types of changes - [x] Breaking change (fix or feature that would cause existing functionality to not work as expected) The worker no longer drains its command channel after the loop, so sends on an ended `/v1/listen` session return `Err` where 0.11.0 returned `Ok(())` indefinitely. Per `AGENTS.md`, a behavior change that makes working code fail until the consumer changes something is breaking in 0.x, so the release carrying this needs a minor bump and the `**BREAKING**` line is in the changelog. `cargo semver-checks` passes — no signature moved — which is exactly why it is not the evidence here. ## Checklist - [x] I have read the CONTRIBUTING.md doc - [x] I have added tests and/or examples that prove my fix is effective or that my feature works - [x] I have added necessary documentation (if appropriate) ## Further comments Each regression test was verified by reverting the fix and watching it fail, because a test that only asserts "does not hang" passes with the error dropped: | Revert | Result | | --- | --- | | Full source revert | both regression tests fail, `Elapsed(())` at 31.03 s total | | Gating kept, terminal forward back to blocking | write-error test fails, `Elapsed(())` at 16.02 s | | Gating kept, `try_send` instead of the sender clone | `left: 0 right: 1` — the error was dropped | | Command drain restored | send on an ended session succeeds forever instead of erroring | The two causes mask each other: with only the error-forward fix, the original reproduction still hangs. The delivery test is coordinated on two oneshots and asserts `responses == 257`, so a run that failed to fill the channel fails loudly rather than passing vacuously. **Known defect left for its own branch:** with `keep_alive` off (the default), the elapsed 3-second timer branch parks the worker in `pending::<()>()` permanently — a response arriving 5 s into an idle session never reaches the consumer. Pre-existing, different blast radius.
Replace the author-asserted note on the create-request body test with what the API server actually does. Probed against api.deepgram.com and api.staging.deepgram.com on 2026-09-23: `provider`, `comment`, and `scopes` are all required JSON body fields, omitting any one answers HTTP 400 with `Json deserialize error: missing field <name>` even when the same value is passed as a query parameter, and `provider` accepts only `quay`. The published reference documents `scopes` and `provider` as query parameters and `comment` as an optional body field; the server contradicts it. No behavior change: the request the SDK already builds matches the server.
|
I can't close B1 from this account, and I don't want to guess the shape — that's exactly what you flagged. Here's what I established and what I need. Blocked on scope. Our member is Tried all seven What would unblock it: a project on staging whose member holds any one of What I did verify — and the reference is wrong about it. The create request contract:
The reference documents Two more for the spec, separate from this PR:
A caution for whoever runs the scoped create. I nearly shipped a wrong fix here. |
… into feat/phase-3-tts-ws-selfhosted-r3 # Conflicts: # CHANGELOG.md
… into feat/phase-3-tts-ws-selfhosted-r3
GregHolmes
left a comment
There was a problem hiding this comment.
[B1] Self-hosted credential creation has no verified success-response contract
Summary: SelfHosted::create_distribution_credentials decodes only a bare CreatedDistributionCredentials object with one-time username and secret, while the published endpoint reference documents a { member, distribution_credentials } wrapper. The available account cannot obtain a successful create response, so the bare shape remains unverified. If production returns the documented wrapper, the credential is created but the SDK returns a deserialization error; a retry can create duplicates without exposing the original secret.
Expected: The return type and example are based on one sanitized HTTP 200 create response from an entitled project.
Observed: src/manage/self_hosted/response.rs and its fixtures encode the bare secret-bearing shape without a captured successful create response, despite the conflicting public schema.
Recommended fix: Run create, get, list, and delete against a project whose member has a self-hosted:product:* scope. Model the captured 200 response and add it as a fixture. If it is the documented wrapper, return DistributionCredentialsEntry; if it is the bare secret-bearing shape, retain the current type and file a deepgram-docs / OpenAPI correction with the evidence.
Phase 3 — Streaming TTS WebSocket + self-hosted credentials
Stacked on #165 (Phase 2). Base is
feat/phase-2-read-stream-models; GitHub will retarget tomainas the stack merges.What's included
Speak::speak_stream()→SpeakStreamBuilder(model / encoding / sample rate);handle()opens the connection →SpeakStreamHandlewithspeak/flush/clear/close, and receives audio + events.SpeakStreamHandle: futures::Stream<Item = Result<SpeakResponse>>.SpeakResponse(Audio/Metadata/Flushed/Cleared/Warning/Unknown) is#[non_exhaustive]; unknown message types are preserved rather than breaking the stream. Thespeakfeature now pulls the WebSocket deps. Modeled onsrc/listen/websocket.rs. Exampletext_to_speech_websocket.Deepgram::self_hosted()→SelfHostedwithlist/get/create/delete_distribution_credentials+ typed response models. Exampleself_hosted_credentials.Verification (Rust 1.97 container)
-D warnings/ fmt /cargo test --all --all-features(142 tests, incl. TTS-WS protocol + self-hosted deserialization tests) ✅cargo checkincl. speak-only (now pulls WS deps) ✅cargo semver-checks: no new breaking changes (only the inherited Phase 1 Extra metadata support #130 streaming breaks).Metadata+Flushed, and streamed 219 KB of audio. Self-hosted list returns a correct403 INSUFFICIENT_PERMISSIONSon accounts without the self-hosted scope (confirms the request path).Closes #148, #147, #95