Skip to content

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
ben/tts-http-clientfrom
aleks/tts-http-client-voice-id
Open

aleks-mitov wants to merge 6 commits into
ben/tts-http-clientfrom
aleks/tts-http-client-voice-id

Conversation

@aleks-mitov

@aleks-mitov aleks-mitov commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

A small follow-up into ben/tts-http-client. Ben's branch, now at 8150b2c, carries everything earlier rounds of this PR added: the voice_id contract, the biometric codes, the 610 s synthesize() read timeout, the stream-cut handling, the pydantic detection fix with its tests, and the CLAUDE.md text on audio_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

  • Connection-release tests for a refusal whose body fails. A refused stream whose error body is cut while it is read (httpx.ReadError) still releases its connection, pinned for both api.stream_speech and async_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.
  • Billing of a cut. The billed_characters docstring adds the API reference's rules beyond a server failure: a session_expired cut bills the text before the sentence being spoken, and an audio_backlog cut bills every character.

Tests

  • Counts: 54 TTS tests (52 on 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.
  • Mutation check: leaving out the context manager exit after a refusal in the sync _open fails the sync test, and in the async _open fails the async test, in every one of those environments. Ben's tests alone stay green under both.
  • Lint and types: ruff 0.3.5 (format --check and check with ruff.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 on models.py, the one assemblyai/ file this PR changes, and stays clean with dependencies installed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KcyY6MAEUVjqBt5pMpetvD

…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
aleks-mitov and others added 2 commits October 21, 2026 22:19
…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
@aleks-mitov aleks-mitov changed the title feat(tts): name every voice by voice_id and follow the API's latest contract fix(tts): pydantic detection, connection-release test and doc corrections Oct 21, 2026
aleks-mitov and others added 2 commits October 21, 2026 22:39
…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
@aleks-mitov aleks-mitov changed the title fix(tts): pydantic detection, connection-release test and doc corrections fix(tts): release test for a refused stream whose body fails, and cut billing notes Oct 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants