Repository navigation
fix(tts): release test for a refused stream whose body fails, and cut billing notes - #278
Open
aleks-mitov wants to merge 6 commits into
Open
aleks-mitov wants to merge 6 commits into
aleks-mitov wants to merge 6 commits into
Conversation
…ontract The Text-to-Speech API retires the `voice` parameter: every voice is named by `voice_id` (a library slug such as `aai-jane`, a voice UUID, or one of the account's voice slugs), and a request that sends `voice` is refused 422 invalid_parameter. `X-TTS-Voice` gives way to `X-TTS-Voice-Id`, which carries the voice used (`aai-jane` when the request named none). - TTSConfig has no `voice` field. Passing it is refused like any unknown field, with a message that points to `voice_id`. - TTSResponse and TTSStream.info expose `voice_id` from `X-TTS-Voice-Id` (None from a server that does not send it yet) and `region` from `X-TTS-Region`; `X-TTS-Voice` is no longer read. - The biometric terms check is documented where errors are: 403 biometric_terms_not_accepted and 503 auth_unavailable, in the TTSError and extend_biometric_consent docstrings, the README and CLAUDE.md. - A streamed body cut after the first audio raises a retryable TTSError (status_code 200, the request's request_id, the httpx error as its cause) instead of a bare httpx error. - synthesize() reads for up to 610 s, over the API's 600 s request limit, so the API's own 504 session_expired arrives as a TTSError; connecting and sending use tts_http_timeout. - The pydantic version is probed with `model_validator` (v2 only), as the dictation and streaming models do: the `types.py` flag is False on pydantic 2 without pydantic-settings, where the v1 branch defined a class-based Config on a v2 model. `protected_namespaces=()` stops the `model_id` warning on pydantic 2.0 to 2.9. - Docstrings: voice_id forms and zones, the language rule, the streamed values of voice_expires_at and billed_characters, audio_duration_ms on responses without audio, empty audio. `container` takes a string, as `encoding` does; `audio` is left out of the repr. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcyY6MAEUVjqBt5pMpetvD
bgotthold-aai
approved these changes
Oct 21, 2026
…correct cut billing and error docs - test_tts_async: a refused stream whose error body fails releases its connection (mirrors the sync test; fails without the aclose in async_api.stream_speech). - test_tts: TTSConfig is a native model of the installed pydantic (read from pydantic.VERSION), so reverting models.py to the types.pydantic_v2 flag fails on pydantic 2 without pydantic-settings. - CLAUDE.md: audio_duration_ms on a stream is None only with audio; a streamed request with nothing to speak gets it (0). - CLAUDE.md: httpx exceptions come from synthesize() and from stream() until it returns; ReadError, ReadTimeout and RemoteProtocolError while a stream is iterated become TTSError (status_code 200). - README, _cut_error and billed_characters docstrings: a cut bills 0 on a server failure, the text before the current sentence on session_expired, and every character on audio_backlog, per the API reference. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcyY6MAEUVjqBt5pMpetvD
…low-up Ben's review follow-ups and #278 changed the same things; his design wins and #278 keeps only what it adds on top. - api.py, async_api.py: Ben's lazy streams (`stream()` sends nothing until the stream is entered or first read; `async with tts.stream(...)`), his `_cut_error(info)` and `_CUT_MESSAGE`, and his cut set (only `RemoteProtocolError` becomes TTSError; ReadError and ReadTimeout stay httpx exceptions). #278's `_CUT_ERRORS` and `_cut_error(info, error)` are dropped. Ben's `_seconds` retry rounding is kept. #278 keeps voice_id from X-TTS-Voice-Id, region, and the httpx.Timeout synthesize timeout. - client.py, async_client.py: Ben's stream docstrings and sync `stream()` on the async client; #278's voice_id examples and synthesize_timeout. - models.py: #278's pydantic detection (probe `model_validator`) over the `types.pydantic_v2` flag; the audio docstring keeps #278's empty-audio note with Ben's repr wording. Field(repr=False) was identical. - CLAUDE.md, README.md: #278's text (voice_id, biometric codes, 610 s synthesize read timeout, billing rules) with the stream wording moved to Ben's lazy, RemoteProtocolError-only design. - Tests: Ben's new tests adapted to voice_id; #278's tests moved to the lazy API. #278's cut tests now pin what Ben's do not: the first audio arrives and the connection is released on a cut, and ReadError / ReadTimeout while streaming stay httpx errors. #278's repr assertions are left to Ben's repr test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcyY6MAEUVjqBt5pMpetvD
bgotthold-aai
added a commit
that referenced
this pull request
Oct 21, 2026
…ract From Aleks's review and #278, integrated onto the lazy streams: - `TTSConfig` has only `voice_id`; a `voice` key is refused with a message that points to `voice_id`. Docs and examples use `aai-` slugs. - Responses expose `voice_id` (from `X-TTS-Voice-Id`, derived from `X-TTS-Voice` until the API sends it) and `region`. - A stream cut by `RemoteProtocolError`, `ReadError` or `ReadTimeout` raises `TTSError(retryable=True)`. - `synthesize()` reads for up to 610 s (the API caps a request at 600 s, so its `504 session_expired` arrives as a `TTSError`); connect and send keep `tts_http_timeout`. - `protected_namespaces=()` so `model_id` does not warn on pydantic 2.0-2.9. - `container` takes a string like `encoding`; biometric-terms errors and consent renewal are documented. - Streams open with `client.stream()` instead of `build_request(timeout=)`, which the declared httpx floor (0.19) lacks. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…leftovers Ben's 0ac0598 integrates #278 onto his lazy streams, so his side is taken everywhere: voice_id with the X-TTS-Voice fallback, ReadError and ReadTimeout as TTSError, client.stream(), and his docs and tests. #278's code, tests and text that differ from his are dropped, including the "stay httpx errors" tests and sentences. What #278 still adds on top of Ben's branch: - models.py probes `model_validator` for the pydantic version instead of `types.pydantic_v2`, which is False on pydantic 2 without pydantic-settings (the default install), where TTSConfig became a v2 model with a v1-style `class Config`. A test pins it against the installed pydantic. - Sync and async tests that a refused stream whose error body fails still releases its connection. - CLAUDE.md and the audio_duration_ms docstring: `0` on a streamed request with nothing to speak, `None` only on a stream with audio. - CLAUDE.md says which failures are httpx exceptions and which become TTSError under the lazy streams. - billed_characters docstring: a session_expired cut bills the text before the sentence being spoken, an audio_backlog cut bills every character. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcyY6MAEUVjqBt5pMpetvD
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcyY6MAEUVjqBt5pMpetvD
…leftovers Ben's 8150b2c covers the pydantic detection fix and its tests, the CLAUDE.md audio_duration_ms, 610 s and cut-error text, and refused-stream release tests, so his side is taken everywhere. What #278 still adds on top of Ben's branch: - Sync and async tests that a refused stream releases its connection when its error body fails while it is read. Ben's tests read a whole error body, which httpx closes itself, so they pass without the stream's own release; these fail without it. - billed_characters docstring: a session_expired cut bills the text before the sentence being spoken, an audio_backlog cut bills every character. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcyY6MAEUVjqBt5pMpetvD
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A small follow-up into
ben/tts-http-client. Ben's branch, now at 8150b2c, carries everything earlier rounds of this PR added: thevoice_idcontract, the biometric codes, the 610 ssynthesize()read timeout, the stream-cut handling, the pydantic detection fix with its tests, and the CLAUDE.md text onaudio_duration_ms, the timeout and cut errors. This branch is merged with his head and takes his side everywhere; it adds only the two items below.What this adds
httpx.ReadError) still releases its connection, pinned for bothapi.stream_speechandasync_api.stream_speech. Ben's refused-stream tests read a whole error body, which httpx closes on its own, so they still pass when the stream's own release after a refusal is removed. These fail without it. The test keeps the error referenced, as a caller that logs it would, so garbage collection cannot release the body in the stream's place.billed_charactersdocstring adds the API reference's rules beyond a server failure: asession_expiredcut bills the text before the sentence being spoken, and anaudio_backlogcut bills every character.Tests
ben/tts-http-client). The full unit suite gives 795 passed in each of these environments: latest dependencies (pydantic 2.14.1, httpx 0.28.1), pydantic 1.10.26, pydantic 2.9.2, httpx 0.22.0, httpx 0.24.1 and websockets 11.0.3. All ran on Python 3.12._openfails the sync test, and in the async_openfails the async test, in every one of those environments. Ben's tests alone stay green under both.format --checkandcheckwithruff.toml) is clean. mypy 1.5.1 the way CI runs it (--follow-imports=silent --ignore-missing-imports, with and without--python-version 3.9) is clean onmodels.py, the oneassemblyai/file this PR changes, and stays clean with dependencies installed.🤖 Generated with Claude Code
https://claude.ai/code/session_01KcyY6MAEUVjqBt5pMpetvD