Skip to content

fix(tts): refine provider timing for second-stage adapters - #2372

Open
YiminW wants to merge 17 commits into
mainfrom
fix/tts-ttfb-phase-two
Open

YiminW wants to merge 17 commits into
mainfrom
fix/tts-ttfb-phase-two

Conversation

@YiminW

@YiminW YiminW commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Several TTS adapters measured TTFB outside the provider boundary, including connection setup, buffering, conversion, or queueing time. The second-stage adapters need provider-boundary timing checks while keeping the existing 0.7 dependency range.

Goal

Update the second-stage timing implementation for Google, OpenAI, Mistral, EZAI, Hume, and Cosy. Each changed extension receives one patch-version increment. The shared timing implementation is provided by ten_ai_base 0.7.63 after publication.

Design rationale

HTTPX transport hooks record the request-body send boundary, and adapter-level observers confirm the first real audio payload for structured responses. SDK hooks use public calls, lazy input consumption, and first nonempty audio callbacks. Opaque SDK-internal socket timing is not inferred. Protocol decoding is deferred away from the provider I/O callback.

Validation

The guarder uses the same entry point as the first stage:

task tts-guarder-test EXTENSION=openai_tts2_python -- \\
  -k test_sequential_requests_ttfb -m api --strict-markers -v --tb=short

The selected case sends two sequential request IDs, with a single final text in the second request. It checks actual audio, completion, one TTFB metric per request, the tester send-to-first-audio bound, and agreement with the independent provider-boundary reference within 1 ms.

Deterministic validation passed in the container:

  • The remaining provider-hook fixture cases.
  • HTTP two-request TEN mock cases for the providers retained in this phase.
  • First-stage ElevenLabs live regression after rebase.
  • Black and fatal Ruff checks.

Live credentialed verification for this phase currently has these results:

  • Passed: Google, OpenAI, Mistral, EZAI, Hume, and Cosy. Cosy passed with model qwen-audio-3.1-tts-flash and voice qwen-audio-3.1-tts-flash-miaomiao-ecf86958296b421da34abe84a88f67e7.
  • Failed and intentionally left unchanged in this phase: Sarvam returned 403 invalid credentials; Polly returned that the configured neural engine is unsupported in the selected region.
  • No credentials and intentionally left unchanged in this phase: Smallest, Typecast, Oracle, Murf, and StepFun.

The two failed providers and five providers without credentials are outside this PR. Their production code, extension tests, and guarder hooks were reverted to the main branch. No live pass is claimed for them.

Release gate

Draft until shared base 0.7.63 and the updated extension versions are published. No package was published. Install and registry compatibility must be checked after publication; focused tests used local prepared packages.

Package version policy

All thirteen changed TTS versions increment the remote-main patch version exactly once. Cosy is 0.4.3 -> 0.4.4. Each extension keeps its original ten_ai_base 0.7 minor range, and extension pyproject versions match manifests. This PR does not pin extensions to 0.7.63.

Rebase status

Rebased onto main after the first-stage PR merged. The shared first-stage tester and 1 ms assertions now cover the second-stage provider hooks through the normal task tts-guarder-test entry point. Details and limitations are documented in tts_guarder/tests/PROVIDER_TTFB.md.

CI follow-up

Fixed the Google TTS Pylint W0640 by binding each retry's request generator when the timing wrapper is created. The fix is in commit a7f0b3d77. Google source Pylint now passes in the container. The local extension test collection requires the prepared TEN runtime package and was not run from the uninstalled worktree.

@github-actions

Copy link
Copy Markdown

[P2] Confirm audio samples before latching Typecast's receive time

ai_agents/agents/ten_packages/extension/typecast_tts_python/typecast_tts.py:98

When the SDK yields a WAV header separately from its PCM payload, this records the header arrival as first audio. StreamingWavToPcm16.feed() can return empty bytes for that chunk, but the shared timing state has already latched its receive timestamp and ignores the later audio arrival. The existing test_typecast_tts_extension_success already supplies a header-only first chunk; delaying its next chunk would expose an understated TTFB. Preserve the arrival timestamp as a candidate and confirm it only when the chunk contains audio samples, with a delayed split-header regression case.

[P2] Update Cosy's existing tests for the changed client contract

ai_agents/agents/ten_packages/extension/cosy_tts_python/cosy_tts.py:243

Making request_id mandatory breaks the unchanged calls in tests/test_client.py:30–31, which still pass two arguments. Conversely, the extension now passes three arguments to the two-argument test doubles in test_basic.py, test_state_machine.py, and test_robustness.py, causing TypeError instead of exercising synthesis, flush, and recovery. Update those callers and doubles, including their new request_ttfb_ms() behavior, and validate the complete local suite alongside the new focused case.

[P2] Update Typecast's constructor expectation for the supplied session

ai_agents/agents/ten_packages/extension/typecast_tts_python/typecast_tts.py:48

tests/test_typecast_tts.py:258–261 still asserts that AsyncTypecast is constructed with exactly host and api_key. The added session keyword makes that existing success test fail even when synthesis succeeds. Update the expectation to include the supplied session and verify that the extension closes the session it now owns.

[P2] Adapt Oracle's existing response fixture to the streaming read path

ai_agents/agents/ten_packages/extension/oracle_tts_python/oracle_tts.py:199

test_get_first_chunk_has_ttfb in tests/test_tts_client.py supplies an unrestricted MagicMock with only data.content configured. It also appears to have iter_content, whose default mock iterator yields nothing, so this new branch never calls record() and returns ttfb_ms=None. The test's assertion that the first response has an integer TTFB therefore fails. Configure a concrete streaming body or a suitably restricted mock, and retain coverage for both streaming and content-only responses.

[P2] Add the mandatory audio-end accounting checks to the touched suites

ai_agents/agents/ten_packages/extension/cosy_tts_python/tests/test_ttfb_boundary.py:92 (the same gap applies across all 13 touched extension suites)

The repository's TTS review test matrix makes exact audio-end accounting a merge requirement for each touched extension. None of these local suites asserts request_event_interval_ms; the new boundary testers stop after first audio and TTFB, before checking completion. Add local adapter tests with controlled time and known emitted PCM that assert the interval from tts_audio_start to its matching end, exact emitted-byte duration, and at most one end on success, partial-audio error, flush/cancellation, and no-audio termination. Existing approximate duration checks and the separate HTTP integration tests do not satisfy this gate.

Static review of ce3b74b; no PR code was executed or dependencies installed.

@YiminW
YiminW marked this pull request as ready for review October 27, 2026 08:13
@github-actions

Copy link
Copy Markdown

TTS extension review

Reviewed the extension-local changes under ai_agents/agents/ten_packages/extension/* using .agents/skills/ten-tts-review. integration_tests/tts_http_timing and ten_ai_base are out of scope for this pass.

[P2] Update existing mocked-client TTFB tests for the new timing accessors
ai_agents/agents/ten_packages/extension/cosy_tts_python/extension.py:549
ai_agents/agents/ten_packages/extension/stepfun_tts_python/extension.py:401
The current `tests/test_metrics.py` in cosy and stepfun patches the whole client class with a plain `MagicMock`. So `self.client.request_ttfb_ms(...)` now returns a `MagicMock` instead of `None`/int. That gets past the new `is not None` guard and goes into `send_tts_ttfb_metrics(ttfb_ms=<MagicMock>)`, which breaks the existing `ttfb_value >= 200` test. The same thing happens in ezai_tw, sarvam_http and humeai once their manifests require ten_ai_base 0.7.63. Their `test_metrics.py` also mocks the client wholesale, and the base now uses `self.client.get_ttfb_ms()` whenever it isn't `None`. The PR validation lists only the 16 new cases, so these existing suites were apparently not run. Fix: give the mocks real return values (or `autospec`) for `request_ttfb_ms` / `get_ttfb_ms`, or rewrite the tests to drive the real adapters.
[P2] Add extension-local TTFB tests for ezai_tw, mistral, sarvam_http and smallest
ai_agents/agents/ten_packages/extension/ezai_tw_tts_python/ezai_tts.py:62
ai_agents/agents/ten_packages/extension/mistral_tts_python/mistral_tts.py:133
ai_agents/agents/ten_packages/extension/sarvam_http_tts/sarvam_tts.py:65
ai_agents/agents/ten_packages/extension/smallest_tts_python/smallest_tts.py:126
These four extensions now measure TTFB through `observe_http_ttfb` and an adapter-side `record_audio_received()` (JSON/SSE envelopes). The only coverage is the shared `integration_tests/tts_http_timing` suite, which doesn't replace extension-local tests. Each extension's own suite should run its real adapter over a fake HTTPX transport and check three things: TTFB runs from the request-body send to the first non-empty decoded audio, it is emitted once per request, and empty/non-audio events (SSE keepalives, JSON without `audio`) don't set it. Their existing `test_metrics.py` mocks the client, so it can't catch a regression here.
[P3] Cosy text-sent timestamp still includes the SDK's connection handshake
ai_agents/agents/ten_packages/extension/cosy_tts_python/cosy_tts.py:265
`text_sent()` is stamped right before `synthesizer.streaming_call(text)`. On the first call for a fresh synthesizer, dashscope's `streaming_call` opens the WebSocket, sends run-task and waits for task-started before it submits the text. So Cosy TTFB still includes connection and handshake time for every request (`complete()` drops the synthesizer after each one). The new test only models setup cost in `SpeechSynthesizer.__init__`, so its name "excludes_sdk_setup" overstates what is proven. Either document that Cosy's boundary includes the SDK handshake, or stamp the time after the first `streaming_call` returns and model the handshake in the test's `streaming_call`.
[P3] Move the stepfun import below the license header
ai_agents/agents/ten_packages/extension/stepfun_tts_python/stepfun_tts.py:1
`from ten_ai_base.tts_timing import TTSRequestTiming` sits above the file's license header. Put it with the other `ten_ai_base` imports (around line 19).

@github-actions

Copy link
Copy Markdown

[P2] Wait for a complete WAV signature before recording Oracle audio

ai_agents/agents/ten_packages/extension/oracle_tts_python/oracle_tts.py:194

A valid chunked WAV response can initially yield only b"RI". That prefix does not start with b"RIFF", so this branch permanently records the header arrival as first audio, even if PCM arrives much later. iter_content(chunk_size=4096) does not guarantee a minimum chunk size. Buffer a possible partial RIFF signature and confirm actual audio before latching the timestamp. Add an adapter-level regression with a fragmented header and delayed PCM; the new hook's AudioDetector handles this case, but its fixture does not exercise this production detector.

[P2] Decode HTTP content encodings before recognizing reference audio

ai_agents/agents/integration_tests/tts_guarder/tests/provider_ttfb_hooks/http.py:90

This wrapper observes response.stream before HTTPX applies content decoding. For a normal application/json response with Content-Encoding: gzip, the production adapter successfully reads decoded JSON, while the reference feeds compressed bytes into Decoder.feed(). JSON recognition never succeeds, so elapsed_ms() returns None and the new guarder rejects a successful request. HTTPX advertises compression support by default. Preserve the captured arrival times while applying content decoding during deferred recognition, and cover compressed JSON/NDJSON or SSE split across chunks.

[P2] Complete the mandatory audio-end accounting tests in the touched suites

ai_agents/agents/ten_packages/extension/cosy_tts_python/tests/test_ttfb_boundary.py:92

This remains a merge-gate gap across all 13 touched extension suites: none asserts request_event_interval_ms, and the new local boundary testers stop after first audio and TTFB without checking completion. The repository's required local-test matrix requires controlled-clock assertions from tts_audio_start to its matching end, exact duration from emitted PCM bytes, and at most one end event on success, partial-audio error, flush/cancellation, and no-audio termination. Add these checks within each extension; shared integration tests and approximate duration checks do not satisfy that requirement.

The previously reported Typecast header timing, Cosy caller/test-double signatures, Typecast constructor expectation, and Oracle response-fixture issues also remain unchanged at this HEAD; see the existing findings. The mocked timing-accessor failures remain as well.

Static review of a7eaf188; no PR code was executed or dependencies installed.

@github-actions

Copy link
Copy Markdown

TTS extension review

[P2] Existing mocked metrics tests will break, because the new TTFB accessors return MagicMock
ai_agents/agents/ten_packages/extension/cosy_tts_python/extension.py:549, ai_agents/agents/ten_packages/extension/stepfun_tts_python/extension.py:401
cosy_tts_python/tests/test_metrics.py patches cosy_tts_python.extension.CosyTTSClient, and stepfun_tts_python/tests/test_metrics.py patches StepFunTTSWebsocket. Both use plain MagicMock instances. Now that TTFB comes from self.client.request_ttfb_ms(...), that call returns a MagicMock, not an int or None. The is not None guard passes, and send_tts_ttfb_metrics(ttfb_ms=<MagicMock>) gets called. The metric then either fails to serialize or never reaches ttfb >= 200. Moving to base 0.7.63 does the same to the HTTP extensions whose suites mock the whole client class (ezai_tw_tts_python, sarvam_http_tts, humeai_tts_python test_metrics.py, and polly_tts/tests/test_state_machine.py). In each case the base calls self.client.get_ttfb_ms() on the mock and gets back a truthy non-None object. The new test_ttfb_boundary.py files avoid this by patching the SDK instead, but the old suites were not updated. Please set request_ttfb_ms / get_ttfb_ms (and begin_ttfb_request / record_audio_received) on these mocks, or use spec=, and run each touched extension's full tests/bin/start, not only the new focused cases.

[P2] Mistral, Smallest, EZAI and Sarvam have no extension-local tests for the new timing boundary
ai_agents/agents/ten_packages/extension/mistral_tts_python/mistral_tts.py:275, smallest_tts_python/smallest_tts.py:250, ezai_tw_tts_python/ezai_tts.py:133, sarvam_http_tts/sarvam_tts.py:148
These four adapters now call observe_http_ttfb(self.client) and record_audio_received() at adapter-specific points:

  • Mistral: SSE event
  • Smallest: after PCM conversion
  • EZAI/Sarvam: decoded JSON audio

Their only coverage is integration_tests/tts_http_timing and the guarder hooks. Shared tests outside the extension directory don't satisfy the extension-local test requirement. Each extension needs a deterministic local test that drives its own get() through a fake HTTPX transport. It should assert three things:

  1. TTFB is measured from the request-body-send trace event to the first audio-bearing chunk, not to envelope or status chunks.
  2. Error/non-200 responses don't record audio.
  3. A second request ID gets independent timing.

The other nine extensions in this PR did add such tests.

[P3] Import placed above the license header
ai_agents/agents/ten_packages/extension/stepfun_tts_python/stepfun_tts.py:1
from ten_ai_base.tts_timing import TTSRequestTiming was inserted before the license comment block. Please move it into the existing ten_ai_base import group (next to from ten_ai_base.struct import ...) so the file header stays first, as in every other extension.

@github-actions

Copy link
Copy Markdown

TTS extension review (ten-tts-review)

Scope: changes under ai_agents/agents/ten_packages/extension/<tts>/ only. tts_guarder, tts_http_timing and ten_ai_base are out of scope for this skill. The ten_ai_base 0.7.63 timing API (TTSRequestTiming, observe_http_ttfb, record_*) is not in this checkout, so the findings below only assume what the extension code shows.

[P2] Attribute StepFun first audio to the request that owns the audio, not the latest sent request
ai_agents/agents/ten_packages/extension/stepfun_tts_python/stepfun_tts.py:506-512
_handle_audio_delta records timing.audio_received(self.request_id, ...). But _send_loop updates self.request_id (lines 338-339) as soon as it dequeues the next TTSTextInput. Suppose request B's text is sent on the same session while request A's audio deltas are still arriving. A's next non-empty delta is then recorded as B's first audio. B's reported TTFB (request_ttfb_ms(self.current_request_id) in extension.py) comes out near zero, or is computed against A's audio instead of B's. Track the request that owns the audio on the receive side, for example the request whose response is currently streaming, separately from the send-side id. Add a two-request test where B's text is sent before A's last delta arrives.

[P2] Cover the new TTFB boundary in each changed HTTP extension's own test suite
mistral_tts_python/mistral_tts.py:272, smallest_tts_python/smallest_tts.py:250, ezai_tw_tts_python/ezai_tts.py:133, sarvam_http_tts/sarvam_tts.py:148
These four extensions change how TTFB is measured (observe_http_ttfb plus record_audio_received), but none of them adds an extension-local test. The only coverage is the shared integration_tests/tts_http_timing harness, which does not satisfy the extension-local test requirement. Each extension already has a tests/test_metrics.py. Extend it to assert three things:

  • TTFB is emitted exactly once per request.
  • It is measured from request send to the first non-empty decoded audio, with an empty or zero-length audio event sent first so it can't trigger TTFB.
  • A second sequential request does not reuse the first request's timestamp.

[P3] Don't mark deterministic mock tests as external API contract tests
New */tests/test_ttfb_boundary.py files (e.g. cosy_tts_python/tests/test_ttfb_boundary.py:299) and the api marker added to each pyproject.toml
Every new boundary test sets pytestmark = pytest.mark.api and registers that marker as "External TTS API contract". These tests use local fakes and patched SDKs and need no vendor credentials. Elsewhere in the repo (fish_audio_tts_python/tests/test_api_contracts.py), api marks live-vendor tests. Anyone who runs -m "not api" to skip live calls will also skip these regression tests. Drop the marker, or use a separate one such as timing.

[P3] Move the StepFun import below the license header
ai_agents/agents/ten_packages/extension/stepfun_tts_python/stepfun_tts.py:1-2
from ten_ai_base.tts_timing import TTSRequestTiming sits above the license comment block. Move it into the existing import group.

Release gate (already acknowledged in the PR): the manifests now pinned to ten_ai_base 0.7.63 (Cosy, EZAI, Hume, Mistral, OpenAI, Polly, Sarvam, Smallest, StepFun, Typecast) can't resolve until that version is published (TEN-framework/ten_ai_base#130). Keep this PR as a draft until then.

@github-actions

Copy link
Copy Markdown

[P1] Require a base version that provides the new timing APIs

ai_agents/agents/ten_packages/extension/cosy_tts_python/cosy_tts.py:13

The latest manifests retain ten_ai_base: "0.7", but Cosy and StepFun now unconditionally import tts_timing, and the HTTP adapters call methods introduced in base 0.7.63. An app locked to an earlier 0.7 release still satisfies these manifests; the package solver explicitly prefers locked versions (core/src/ten_manager/src/solver/solve.rs:684). Such an installation fails at import, initialization, or synthesis. Publishing 0.7.63 alone does not upgrade these apps. Declare the minimum compatible version in the affected manifests, or provide a compatibility path for older bases. A 0.7.63 requirement still permits compatible later 0.7 releases.

[P2] Confirm PCM before recording Typecast's first audio

ai_agents/agents/ten_packages/extension/typecast_tts_python/typecast_tts.py:98

A streaming WAV header can arrive separately from its PCM payload. This records that header as first audio even though converter.feed(chunk) returns empty bytes. The timing state then ignores the later PCM arrival, understating TTFB by the entire intervening delay. The new Typecast reference hook also accepts any nonempty chunk, so it can approve the same incorrect measurement. Confirm audio samples before latching the timestamp and add a delayed header-only first-chunk regression.

[P2] Buffer a possible partial RIFF signature in Oracle's detector

ai_agents/agents/ten_packages/extension/oracle_tts_python/oracle_tts.py:194

A valid chunked WAV response may initially yield only b"RI"; iter_content(chunk_size=4096) does not guarantee a minimum chunk size. That prefix does not start with b"RIFF", so this branch permanently records header arrival as first audio, even if PCM arrives much later. Retain possible partial signatures until the format and actual audio are identifiable. The hook's fragmented-header fixture exercises its own AudioDetector, not this production path; add an adapter-level regression.

[P2] Update Cosy's existing callers and test doubles for the new signature

ai_agents/agents/ten_packages/extension/cosy_tts_python/cosy_tts.py:243

Making request_id mandatory breaks the two-argument calls in tests/test_client.py:30–31. Conversely, the extension now supplies three arguments to the unchanged two-argument doubles in test_basic.py, test_state_machine.py, and test_robustness.py, producing TypeError instead of testing synthesis, flush, and recovery. Update these callers and doubles, including their timing accessor behavior, and validate the complete local suite alongside the focused new case.

[P2] Add the required completion and audio-end accounting assertions

ai_agents/agents/ten_packages/extension/cosy_tts_python/tests/test_ttfb_boundary.py:92

The new boundary testers stop after first audio and TTFB, before checking completion. Across all 13 touched extension suites, none asserts request_event_interval_ms. The repository's required local-test matrix makes this a merge gate: use controlled time and known emitted PCM to assert the matching start-to-end interval, exact emitted-byte duration, and at most one end on success, partial-audio error, flush/cancellation, and no-audio termination. Add these checks within the extensions; shared integration tests do not replace them.

Static review of e5d9063; no PR code executed or dependencies installed.

@github-actions

Copy link
Copy Markdown

TTS extension review (ten-tts-review)

Scope: the 13 changed TTS extensions under ai_agents/agents/ten_packages/extension/. Per the skill, tts_guarder and integration_tests/tts_http_timing are out of scope. None of these extensions is new, so the guarder evidence gate does not apply.

[P1] Raise the ten_ai_base constraint before these packages can be installed
cosy_tts_python/manifest.json:14 (same "version": "0.7" entry in all 13 manifests)
Every changed adapter now depends on APIs that exist only in the unpublished base 0.7.63:

  • ten_ai_base.tts_timing.TTSRequestTiming, imported by Cosy and StepFun.
  • observe_http_ttfb, record_text_sent, record_transport_text_sent and record_audio_received, used by the HTTP adapters.

The manifests still accept any 0.7.x. An app that resolves 0.7.62 or earlier will install these new extension versions without complaint. Then:

  • StepFun and Cosy fail at import.
  • OpenAI, Mistral, Smallest, EZAI, Sarvam and Hume raise AttributeError in the client __init__, which breaks every request.

The PR body lists this as a release gate, but the manifest is what enforces compatibility, and once the extension versions are published that gate is gone. Pin the minimum (e.g. >=0.7.63 <0.8.0, or whatever form the manifest resolver supports) in the same change that bumps the extension versions.

[P2] No extension-local tests for the new TTFB boundary in four HTTP adapters
ezai_tw_tts_python/ezai_tts.py:62, mistral_tts_python/mistral_tts.py:133, sarvam_http_tts/sarvam_tts.py:65, smallest_tts_python/smallest_tts.py:126
These adapters change where TTFB is measured: HTTPX transport hooks plus record_audio_received() on the first non-empty decoded audio. Their only coverage is the shared integration_tests/tts_http_timing suite, which does not count as extension-local coverage. Each one needs a local test that drives its own client through two sequential requests with a fake transport and checks three things:

  • Empty or leading non-audio chunks (Sarvam empty base64, Mistral SSE without audio_data, Smallest non-PCM events) do not trigger the first-audio mark.
  • TTFB is emitted once per request.
  • The second request does not reuse the first request timestamps.

Google, Hume, Typecast, Polly, Oracle, Murf, Cosy, StepFun and OpenAI got such tests in this PR. These four did not.

[P3] Oracle can mark first audio on WAV header bytes
oracle_tts_python/oracle_tts.py:194-197
record() sets first_audio_at as soon as the RIFF prefix is ≥44 bytes and _strip_wav_header(prefix) returns non-empty bytes. Suppose the response puts a LIST/fact/JUNK chunk before data, and the data chunk is not in the prefix yet. _strip_wav_header then reaches its fallback audio[44:] and returns metadata bytes as "PCM". TTFB is stamped before any audio arrives, and it comes out too low. Only mark first audio once a data chunk header has been parsed and at least one payload byte follows it. A test with a pre-data LIST chunk split across 4096-byte reads would cover it.

No ASR paths are touched, so the ASR checklist does not apply.

@github-actions

Copy link
Copy Markdown

[P2] Keep hook fixtures runnable in a single-extension guarder app

ai_agents/agents/integration_tests/tts_guarder/tests/test_provider_ttfb_hooks.py:54

The normal task tts-guarder-test EXTENSION=... installs only the selected extension through manifest-tmpl.json, then runs all of tests/. These new fixtures unconditionally import every provider's production module, regardless of --extension_name, so a clean single-provider installation fails on the other adapters with ModuleNotFoundError. The additional fixture also requires generic_tts_http_python, which is absent from this repository. Keep the fixtures independent of installed vendor packages, or select/skip cases according to available adapters and make the external-package cases explicitly opt-in.

[P2] Match the Gemini hook to the checked-in adapter's SDK

ai_agents/agents/integration_tests/tts_guarder/tests/provider_ttfb_hooks/gemini.py:9

The checked-in gemini_tts_python/gemini_tts.py imports google.genai and calls genai.Client(...).models.generate_content(...); it has no httpx binding. Registering this hook makes install_hook("gemini_tts_python", ...) fail at module.httpx.AsyncClient before synthesis starts. This affects both the new live case and test_additional_http_hooks even when the repository's Gemini extension is installed. Observe its supported SDK boundary, or explicitly detect and skip this unsupported implementation instead of assuming the different adapter supplied by the external prepared app.

[P2] Make the shared tester importable from the standard guarder entry point

ai_agents/agents/integration_tests/tts_guarder/tests/test_provider_ttfb.py:17

tests/bin/start changes into tts_guarder and adds that directory and installed package paths to PYTHONPATH; it does not add the parent ai_agents/agents directory needed to resolve integration_tests. Consequently, the newly discovered case fails with ModuleNotFoundError: No module named 'integration_tests' for registered providers under the standard task unless the caller manually supplies an extra path. Put the reusable tester in an importable shared location or update the normal entry point alongside this import. The custom prepared-app command's path instructions do not fix automatic discovery in the existing runner.

[P2] Complete the mandatory local audio-end accounting coverage

ai_agents/agents/ten_packages/extension/cosy_tts_python/tests/test_ttfb_boundary.py:92

This remains a merge blocker under the repository's required local-test matrix: none of the 13 touched extension suites asserts request_event_interval_ms, and these new testers stop after first audio and TTFB. Add controlled-clock assertions from the matching tts_audio_start to tts_audio_end, exact duration calculated from emitted PCM bytes, and at most one end on success, partial-audio error, flush/cancellation, and no-audio termination. Shared integration tests and the existing approximate duration assertions do not satisfy this gate.

[P2] Add local adapter timing regressions for the four HTTP extensions

ai_agents/agents/ten_packages/extension/smallest_tts_python/smallest_tts.py:250

Smallest, Mistral, EZAI and Sarvam change their transport observation and audio-confirmation boundaries without adding extension-local tests for those changes. Their existing mocked-client metrics tests bypass the real adapters, while integration_tests/tts_http_timing does not replace the local coverage required by the review guide. Exercise each real adapter over a deterministic transport and verify the send-to-first-audio boundary, exclusion of empty/control responses, exactly one metric, and independent timing for a second request ID.

The earlier Typecast/Oracle first-audio and test-contract findings, fragmented Oracle header and compressed HTTP reference findings, and mocked timing-accessor failures also remain applicable at this HEAD.

Static review of e5d90631; no PR code was executed or dependencies installed.

@YiminW
YiminW force-pushed the fix/tts-ttfb-phase-two branch from e5d9063 to dda41b8 Compare October 28, 2026 14:10
@github-actions

Copy link
Copy Markdown

[P2] Confirm PCM before latching Typecast's first-audio timestamp

ai_agents/agents/ten_packages/extension/typecast_tts_python/typecast_tts.py:98

A nonempty SDK chunk can contain only the WAV header, which converter.feed() discards. Recording audio before that check permanently latches the header's arrival, so a delayed PCM payload produces understated TTFB. The existing success fixture already splits the header. Retain the arrival timestamp as a candidate, confirm actual audio, and add a delayed header-only regression.

[P2] Wait for actual audio when Oracle's WAV header is fragmented

ai_agents/agents/ten_packages/extension/oracle_tts_python/oracle_tts.py:194

If iter_content() first yields b"RI", startswith(b"RIFF") is false and the header is immediately recorded as audio. A longer header containing a pre-data metadata chunk can also trigger _strip_wav_header()'s fallback before PCM arrives. Buffer a possible RIFF signature and parse through the data chunk before latching TTFB; exercise the production adapter with fragmented headers and delayed PCM.

[P2] Update Cosy's existing callers and test doubles for the new signature

ai_agents/agents/ten_packages/extension/cosy_tts_python/cosy_tts.py:243

request_id is now mandatory, but tests/test_client.py:30–31 still calls synthesize_audio() with two arguments. Conversely, five test doubles in test_basic.py, test_state_machine.py, and test_robustness.py accept only two arguments while the extension now supplies three. These raise TypeError instead of testing synthesis, flush, and recovery. Update the callers/doubles and give mocked timing accessors explicit integer/None results.

[P2] Keep hook fixtures runnable in a single-extension guarder installation

ai_agents/agents/integration_tests/tts_guarder/tests/test_provider_ttfb_hooks.py:54

The standard task tts-guarder-test EXTENSION=... installs one extension and runs all of tests/. These new unfiltered cases import every provider's production module, so a clean single-provider run fails on missing adapters/SDKs. The additional cases even require the externally supplied Generic HTTP package. Use dependency-binding fakes, or select/skip cases based on the installed adapter and make external-package fixtures opt-in.

[P2] Match the Gemini reference hook to the checked-in SDK adapter

ai_agents/agents/integration_tests/tts_guarder/tests/provider_ttfb_hooks/gemini.py:6

The repository's gemini_tts_python/gemini_tts.py uses google.genai.Client and has no httpx binding. With binding = "httpx", hook installation raises AttributeError at module.httpx.AsyncClient before synthesis, breaking the registered Gemini sequential test even when its package is installed. Observe the supported SDK boundary, or explicitly distinguish this implementation from the external HTTP adapter used for validation.

[P2] Add extension-local regressions for the four HTTP timing changes

ai_agents/agents/ten_packages/extension/smallest_tts_python/smallest_tts.py:250

Smallest, Mistral, EZAI, and Sarvam add transport observation/audio confirmation without local tests exercising those changes. Their existing metrics tests mock the client, and the shared HTTP integration suite does not satisfy the required extension-local matrix. Drive each real adapter over a deterministic transport and assert exclusion of empty/control chunks, the send-to-first-audio boundary, exactly one metric, and fresh timing for a second request ID.

[P2] Complete the mandatory local audio-end accounting coverage

ai_agents/agents/ten_packages/extension/cosy_tts_python/tests/test_ttfb_boundary.py:92

This remains a merge-gate gap under the repository's required local-test matrix. None of the 13 touched extension suites asserts request_event_interval_ms; the new boundary testers stop after first audio/TTFB. Add controlled-clock start-to-end interval assertions, exact emitted-PCM duration, and at most one end event on success, partial-audio error, flush/cancellation, and no-audio termination in each extension's local suite.

The previously reported Typecast constructor expectation and Oracle streaming-fixture failures, compressed HTTP reference decoding defect, and mocked timing-accessor failures also remain present at this HEAD.

Reviewed dda41b8a. Validation was static inspection and AST parsing; no PR code was executed or dependencies installed.

@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown

TTS extension review

[P1] Require the ten_ai_base version that provides the new timing API
cosy_tts_python/cosy_tts.py:13, google_tts_python (n/a), openai_tts2_python/openai_tts.py:113, mistral_tts_python/mistral_tts.py:133, ezai_tw_tts_python/ezai_tts.py:62, humeai_tts_python/humeTTS.py:37, and each extension manifest.json
Cosy now imports ten_ai_base.tts_timing.TTSRequestTiming. OpenAI, Mistral, EZAI and Hume call observe_http_ttfb(), record_text_sent() and record_audio_received(). None of these exist in ten_ai_base on main; the PR says they arrive in 0.7.63. The manifests still declare "ten_ai_base": "0.7", so any resolution or existing install at 0.7.0 to 0.7.62 satisfies the dependency. On those versions Cosy fails at import with ImportError, and the four HTTP clients raise AttributeError in __init__. Every request on those extensions fails, not only the TTFB metric. The PR description chooses not to pin, but nothing else blocks an older base. Raise the lower bound to the first base version that ships these helpers (for example >=0.7.63 <0.8, in whatever range syntax tman accepts), or keep a fallback when the helpers are missing.

[P2] Add extension-local tests for the new Mistral and EZAI TTFB boundaries
mistral_tts_python/mistral_tts.py:133,275, ezai_tw_tts_python/ezai_tts.py:62,133
These two adapters change where TTFB starts (HTTPX request-body hook) and where it ends (first nonempty decoded payload). Unlike Google, OpenAI, Hume and Cosy, they get no test_ttfb_boundary.py. The existing tests/test_metrics.py only checks that some TTFB metric arrives. Nothing in either extension checks that:

  • an empty or missing audio field does not stop the clock;
  • EZAI multi-sentence requests (one POST per sentence) report one TTFB, measured from the first POST;
  • a second request ID gets its own TTFB.

The shared guarder and tts_http_timing fixtures sit outside the extension and do not satisfy the local-test requirement. Add a deterministic fake-transport test per extension with a controlled clock, following the OpenAI one.

[P3] Stop dropping the Cosy TTFB metric silently when timing is unavailable
cosy_tts_python/extension.py:549-560
Before this change, the first audio chunk always produced a TTFB metric. Now, when request_ttfb_ms() returns None, the metric is skipped. That happens when _callback is None, or when the callback instance that recorded text_sent differs from the current one, since start() replaces _callback after complete()/cancel(). Yet the following KEYPOINT ... TTFB metrics: None ms log still says the metric was sent. Metrics consumers see a missing TTFB with no diagnostic. Log a warning on the None branch, and change the KEYPOINT log so it does not claim a metric was sent. Optionally, add a local test where complete() is followed by a new request ID and assert each request still emits exactly one TTFB.

@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown

[P2] Keep hook fixtures runnable with only the selected extension installed

ai_agents/agents/integration_tests/tts_guarder/tests/test_provider_ttfb_hooks.py:52

The standard task tts-guarder-test EXTENSION=... installs one extension through manifest-tmpl.json and runs all of tests/. These new parametrized fixtures import each provider's production module regardless of --extension_name, so a clean single-provider run fails on missing adapters or SDKs. The additional HTTP fixtures also require the external Generic HTTP package. Use dependency-binding fakes, or select/skip cases based on installed adapters and make external-package cases opt-in.

[P2] Match the registered Gemini hook to the repository's SDK adapter

ai_agents/agents/integration_tests/tts_guarder/tests/provider_ttfb_hooks/gemini.py:9

The checked-in gemini_tts_python/gemini_tts.py uses google.genai.Client and has no httpx binding. Registering this hook makes install_ttfb_reference("gemini_tts_python", ...) raise AttributeError at module.httpx.AsyncClient, before the existing sequential TTFB test can synthesize anything. Observe the supported SDK boundary, or distinguish the external HTTP implementation used for validation before registering this hook for the repository's adapter.

[P2] Decode HTTP content encodings before recognizing reference audio

ai_agents/agents/integration_tests/tts_guarder/tests/provider_ttfb_hooks/http.py:102

Wrapping response.stream captures bytes before HTTPX applies content decoding, but the deferred Decoder only handles JSON/SSE framing. For a valid JSON response with Content-Encoding: gzip, the adapter receives decoded JSON while the reference tries to parse compressed bytes, never recognizes audio, and returns None; the guarder then rejects the successful request. HTTPX advertises compression support by default. Preserve arrival timestamps while applying content decoding during deferred recognition, and add compressed, fragmented JSON/NDJSON or SSE fixtures.

[P2] Update Cosy's existing callers and test doubles for the required request ID

ai_agents/agents/ten_packages/extension/cosy_tts_python/cosy_tts.py:244

request_id is now mandatory, but tests/test_client.py:30–31 still calls synthesize_audio() with two arguments. Conversely, five test doubles in test_basic.py, test_state_machine.py, and test_robustness.py accept only two arguments while the extension now supplies three. These paths raise TypeError instead of exercising synthesis, flush, and recovery. Update the callers and doubles together with the client contract, then validate the complete local suite alongside the new focused test.

[P2] Add local regressions for EZAI's and Mistral's new transport timing

ai_agents/agents/ten_packages/extension/ezai_tw_tts_python/ezai_tts.py:134 and mistral_tts_python/mistral_tts.py:275

Neither extension's local suite exercises the new request-body trace and audio-confirmation boundary. EZAI's metrics test mocks the whole client; Mistral's uses a mocked raw response and checks only a latency lower bound, bypassing the new SSE branch and real transport hooks. The repository's required local-test matrix does not allow shared integration tests to replace these checks. Drive each real adapter over a deterministic transport and assert exclusion of connection delay and empty/control payloads, exactly one TTFB metric, and fresh timing for a second request ID.

[P2] Complete mandatory local audio-end accounting coverage

ai_agents/agents/ten_packages/extension/cosy_tts_python/tests/test_ttfb_boundary.py:92

This remains a merge-gate gap under the required extension-local test matrix: none of the six touched extension suites checks request_event_interval_ms, and the new boundary testers stop after first audio/TTFB without checking completion. Add controlled-clock assertions from the matching tts_audio_start to tts_audio_end, exact duration calculated from emitted PCM bytes, and at most one end on success, partial-audio error, flush/cancellation, and no-audio termination. Shared integration tests and approximate duration checks do not satisfy this requirement.

Reviewed 64cdf246 against 56f790d4. Static inspection and AST parsing of all 31 changed Python files; no PR code executed or dependencies installed.

@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown

[P2] Update Cosy's existing callers and test doubles for the new signature

ai_agents/agents/ten_packages/extension/cosy_tts_python/cosy_tts.py:244

request_id is now mandatory, but tests/test_client.py:30–31 still calls synthesize_audio() with two arguments. Conversely, five test doubles in test_basic.py, test_state_machine.py, and test_robustness.py accept only two arguments while the extension now supplies three. These paths raise TypeError instead of exercising synthesis, flush, and recovery. Update the callers and doubles with the client contract, then validate the complete local suite alongside the new focused case.

[P2] Keep hook fixtures runnable with only the selected extension installed

ai_agents/agents/integration_tests/tts_guarder/tests/test_provider_ttfb_hooks.py:52

The normal task tts-guarder-test EXTENSION=... installs one extension through manifest-tmpl.json and runs all of tests/. These new cases import each provider's production module regardless of --extension_name, so a clean single-provider run fails on missing adapters or SDKs. test_additional_http_hooks also requires the externally supplied Generic HTTP package. Use dependency-binding fakes, or select/skip cases according to the installed adapter and make external-package fixtures opt-in.

[P2] Match the Gemini hook to the repository's SDK adapter

ai_agents/agents/integration_tests/tts_guarder/tests/provider_ttfb_hooks/gemini.py:9

The checked-in gemini_tts_python/gemini_tts.py uses google.genai.Client and has no httpx binding. Registering this hook makes install_ttfb_reference("gemini_tts_python", ...) raise AttributeError at module.httpx.AsyncClient before the existing sequential TTFB test can synthesize anything. Observe the supported SDK boundary, or distinguish the external HTTP implementation used for validation before registering its hook for this adapter.

[P2] Decode HTTP content encodings before recognizing reference audio

ai_agents/agents/integration_tests/tts_guarder/tests/provider_ttfb_hooks/http.py:102

response.stream supplies bytes before HTTPX applies content decoding, while the deferred Decoder only handles JSON/SSE framing. For a valid JSON response with Content-Encoding: gzip, the adapter receives decoded JSON but the reference parses compressed bytes, never recognizes audio, and returns None; the guarder rejects the successful request. HTTPX advertises compression support by default. Preserve arrival timestamps while applying content decoding during deferred recognition, and cover compressed responses fragmented across chunks.

[P2] Add local regressions for EZAI's and Mistral's transport timing changes

ai_agents/agents/ten_packages/extension/ezai_tw_tts_python/ezai_tts.py:134 and mistral_tts_python/mistral_tts.py:275

Neither local suite exercises the new request-body trace and audio-confirmation boundary. EZAI's metrics test mocks the whole client; Mistral's supplies a mocked raw response and checks a latency lower bound, bypassing the new SSE branch and transport hooks. This is a merge-gate gap under the required extension-local test matrix; shared integration tests cannot replace it. Drive each real adapter over a deterministic transport and assert exclusion of connection delay and empty/control payloads, exactly one metric, and fresh timing for a second request ID. Include EZAI's multiple-POST sentence path.

[P2] Complete mandatory local audio-end accounting coverage

ai_agents/agents/ten_packages/extension/cosy_tts_python/tests/test_ttfb_boundary.py:92

None of the six touched extension suites asserts request_event_interval_ms, and the new boundary testers stop after first audio/TTFB without checking completion. The repository's required local-test matrix makes this a merge gate for every touched TTS extension. Add controlled-clock assertions from the matching tts_audio_start to tts_audio_end, exact duration from emitted PCM bytes, and at most one end on success, partial-audio error, flush/cancellation, and no-audio termination. Existing approximate duration checks and shared integration tests do not satisfy this requirement.

Reviewed a7f0b3d7 against 56f790d4. Validation: static inspection and syntax parsing of all 31 changed Python files; no PR code executed or dependencies installed.

@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown

TTS extension review

[P1] Raise the ten_ai_base floor for the extensions that now call 0.7.63-only APIs
ai_agents/agents/ten_packages/extension/cosy_tts_python/manifest.json:14 (same "version": "0.7" in google, openai_tts2, mistral, ezai_tw, humeai manifests)
The six adapters now call observe_http_ttfb(), record_text_sent() and record_audio_received(), and Cosy imports ten_ai_base.tts_timing.TTSRequestTiming at module level (cosy_tts.py:13). The PR body says these APIs only exist in ten_ai_base 0.7.63, but every manifest keeps the 0.7 range, so any 0.7.x older than 0.7.63 still satisfies it. Apps that already have an older 0.7.x installed or locked will resolve cleanly and then fail. Cosy fails to load with ModuleNotFoundError. The HTTP adapters raise AttributeError in the client constructor, so every request fails. Keeping the minor range is fine, but the lower bound needs to be 0.7.63, for example ^0.7.63 or whatever form tman supports. Without that, the published extension patch versions are installable but broken.

[P2] Add extension-local tests for the new Mistral and EZAI timing boundaries
ai_agents/agents/ten_packages/extension/mistral_tts_python/mistral_tts.py:133, ai_agents/agents/ten_packages/extension/ezai_tw_tts_python/ezai_tts.py:62
Both adapters now take TTFB from the httpx send hook plus record_audio_received(). Neither extension adds a test that goes through these code paths. The existing tests/test_metrics.py replaces the whole client in both extensions: EZAI patches EZAITWTTSClient and Mistral patches AsyncClient. So the httpx event hook never fires, and the tests cannot show that TTFB is measured from the request-body send to the first non-empty decoded audio. The tests also cannot show that empty or audio-less JSON events are skipped, or that TTFB is emitted once per request. Google, OpenAI, Hume and Cosy each got a test_ttfb_boundary.py using a fake transport or SDK. Mistral and EZAI need the same: a fake transport that sends a control event first, then audio, and asserts one TTFB per request against the transport timestamps.

[P3] Mark the deterministic boundary tests as local tests, not api tests
ai_agents/agents/ten_packages/extension/cosy_tts_python/tests/test_ttfb_boundary.py:146 (same in the google, openai_tts2 and humeai test_ttfb_boundary.py / test_ttfb_transport.py)
These tests use mock providers only, yet each file sets pytestmark = pytest.mark.api, and the new pyproject marker defines api as "External TTS API contract". Any run that deselects external-API tests (-m "not api") will skip the only coverage for the new timing code. Remove the marker, or use a separate marker for mocked tests.

Hook coverage: all six touched extensions are registered in provider_ttfb_hooks.HOOKS, and the PR reports live guarder passes for them. No additional hook gap found. Subtitle alignment does not apply to these six, because none of them emits timestamped tts_text_result.

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

[P2] Update Cosy's existing callers and doubles for the new client signature

ai_agents/agents/ten_packages/extension/cosy_tts_python/cosy_tts.py:244

request_id is now required, but tests/test_client.py:30–31 still supplies two arguments. Conversely, five synthesize_audio doubles in test_basic.py, test_state_machine.py, and test_robustness.py accept only two arguments while the extension now passes three. These tests raise TypeError instead of exercising synthesis, flush, and recovery. Update the callers and doubles, give their timing accessors concrete return values, and validate the existing suite alongside the new boundary case.

[P2] Keep hook fixtures runnable with only the selected extension installed

ai_agents/agents/integration_tests/tts_guarder/tests/test_provider_ttfb_hooks.py:52

The normal task tts-guarder-test EXTENSION=... installs one extension through manifest-tmpl.json and runs all of tests/. These new cases import multiple production adapters regardless of --extension_name; the additional HTTP cases also require the externally supplied Generic HTTP package. A clean single-provider run therefore fails on missing adapters or SDKs. Use dependency-binding fakes, or select/skip fixtures according to the installed adapter and make external-package cases opt-in.

[P2] Match the Gemini hook to the checked-in SDK adapter

ai_agents/agents/integration_tests/tts_guarder/tests/provider_ttfb_hooks/gemini.py:9

The repository's gemini_tts_python/gemini_tts.py uses google.genai.Client and exposes no httpx binding. Because Gemini is now registered in HOOKS, installing its reference reaches module.httpx.AsyncClient and raises AttributeError before the existing sequential TTFB test sends any text. Observe the supported SDK boundary, or distinguish the external HTTP implementation before registering this hook for the repository adapter.

[P2] Use one HTTP timing boundary across the new tests

ai_agents/agents/ten_packages/extension/openai_tts2_python/tests/test_ttfb_boundary.py:85

The latest HTTP reference starts in the HTTPX request hook, before connection setup, and the shared adapter fixture expects that setup delay to be included. This local test instead timestamps sent after its 150 ms setup delay and expects approximately 30 ms; Hume's new transport test similarly excludes 120 ms. These expectations cannot all hold for the same timing implementation. Align the local and shared fixtures with the chosen contract, then validate both suites against the same base version.

[P2] Decode HTTP content encodings before recognizing reference audio

ai_agents/agents/integration_tests/tts_guarder/tests/provider_ttfb_hooks/http.py:102

response.stream supplies bytes before HTTPX applies content decoding, while Decoder only handles JSON/SSE framing. With a valid JSON response carrying Content-Encoding: gzip, the adapter receives decoded JSON but the reference parses compressed bytes, never recognizes audio, and returns None; the guarder rejects a successful request. HTTPX advertises compression by default. Preserve arrival timestamps while applying content decoding during deferred recognition, and cover compressed responses fragmented across chunks.

[P2] Add local regressions for EZAI's and Mistral's new timing paths

ai_agents/agents/ten_packages/extension/ezai_tw_tts_python/ezai_tts.py:134 and mistral_tts_python/mistral_tts.py:275

Neither local suite exercises the new transport observation and audio-confirmation boundary. EZAI's metrics test mocks the whole client; Mistral's mocks HTTPX and yields raw bytes, bypassing the changed SSE branch. The required local-test matrix makes this a merge gate; shared integration tests cannot substitute. Drive each real adapter over a deterministic transport and assert the chosen start boundary, exclusion of empty/control payloads, one metric per request, and fresh timing for a second request. Include EZAI's multiple-POST sentence path.

[P2] Complete the mandatory local audio-end accounting coverage

ai_agents/agents/ten_packages/extension/cosy_tts_python/tests/test_ttfb_boundary.py:92

None of the six touched extension suites asserts request_event_interval_ms, and the new boundary testers stop after first audio/TTFB without checking completion. The repository's required local-test matrix makes this a merge gate for every touched TTS extension. Add controlled-clock assertions from the matching tts_audio_start to tts_audio_end, exact duration from emitted PCM bytes, and at most one end on success, partial-audio error, flush/cancellation, and no-audio termination. Approximate duration checks and shared integration tests do not satisfy this requirement.

Reviewed 520e0605 against 56f790d4. Validation: static inspection and syntax parsing of all 31 changed Python files; no PR code executed or dependencies installed.

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

TTS extension review

[P1] Update the Cosy local tests for the new synthesize_audio / request_ttfb_ms contract
ai_agents/agents/ten_packages/extension/cosy_tts_python/cosy_tts.py:243-245, cosy_tts_python/extension.py:549-560
synthesize_audio now takes a required third argument, request_id, and the extension passes t.request_id. None of the existing Cosy tests were updated, so the suite now fails:

  • tests/test_client.py:30-31 calls client.synthesize_audio("hello", False), which raises TypeError for the missing request_id.
  • The side_effect stubs def synthesize_audio(self, text, text_input_end) in test_basic.py:141, test_basic.py:361, test_robustness.py:130 and test_state_machine.py:221/341 receive three arguments and raise TypeError inside request_tts. The request errors out instead of producing audio.
  • test_metrics.py mocks the client with a MagicMock. As a result, self.client.request_ttfb_ms(...) returns a MagicMock rather than None and is passed to send_tts_ttfb_metrics as ttfb_ms. The test's ttfb >= 200 assertion, which depends on the removed request_start_ts path, can no longer hold.

Update these stubs and assertions to match the new signature and TTFB source. Then run the full Cosy local suite, not only test_ttfb_boundary.py.

[P2] Tighten the ten_ai_base range to the release that provides the new timing APIs
ai_agents/agents/ten_packages/extension/openai_tts2_python/openai_tts.py:113 (same pattern in mistral_tts.py:133, ezai_tts.py:62, humeTTS.py:95,104, cosy_tts.py:13)
The adapters now call observe_http_ttfb, record_text_sent and record_audio_received, and import ten_ai_base.tts_timing.TTSRequestTiming. None of these exist in any published ten_ai_base yet; the PR says they arrive in 0.7.63. Every manifest still declares "ten_ai_base": "0.7", so the resolver accepts 0.7.0–0.7.62. An app that already has an older 0.7.x installed or locked will take these patch releases and fail at client construction with AttributeError/ImportError, which leaves the extension unable to synthesize. Either raise the minimum to >=0.7.63 in each changed manifest, or keep this PR from merging and publishing until the base release is the only 0.7 version that resolvers can pick. The description says the range is kept on purpose, but nothing enforces that ordering.

[P2] Add extension-local TTFB boundary tests for Mistral and EZAI
ai_agents/agents/ten_packages/extension/mistral_tts_python/mistral_tts.py:275, ai_agents/agents/ten_packages/extension/ezai_tw_tts_python/ezai_tts.py:62,133-134
Both adapters change how TTFB is measured: HTTPX transport observation plus record_audio_received() on the first nonempty decoded payload. Neither extension got a local test for the new boundary. Their existing test_metrics.py only asserts ttfb >= 200 against a mocked client, and that mock bypasses the new hook entirely. The guarder hook does not replace the local matrix. Following the OpenAI/Google/Hume test_ttfb_boundary.py pattern, add a test with a delayed fake transport that asserts all of the following:

  • connection/request setup time is excluded;
  • an empty audio_data/audio payload does not set first audio;
  • exactly one TTFB metric is emitted per request across two sequential requests.

[P3] Don't mark the deterministic mock TTFB tests as api
cosy_tts_python/tests/test_ttfb_boundary.py:146, likewise in the google/openai/humeai test_ttfb_boundary.py and humeai_tts_python/tests/test_ttfb_transport.py
These tests use fake providers and fake transports and never call a live service. They are still tagged pytest.mark.api, which the new pyproject marker defines as "External TTS API contract". Any run that deselects api tests to skip credentialed calls will also skip the only local regression coverage for the new TTFB boundary. Drop the marker, or use a marker that describes what these tests actually do.

Guarder hooks: TTFB hooks are registered in provider_ttfb_hooks/__init__.py for all six touched extensions (openai, mistral, ezai, hume, google, cosy). Subtitle alignment hooks are N/A, because none of these adapters emit timestamped tts_text_result. None of the touched extensions are new, so the new-extension screenshot/PCM evidence gate does not apply.

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

[P1] Require a base version that provides the new HTTP timing APIs

ai_agents/agents/ten_packages/extension/openai_tts2_python/openai_tts.py:113

OpenAI, Mistral, EZAI, and Hume now unconditionally call timing methods absent from older ten_ai_base 0.7 releases, but their manifests still accept those releases through "version": "0.7". An app locked to an older compatible base can retain it: core/src/ten_manager/src/solver/solve.rs:684 explicitly prioritizes locked packages. Updating these extensions then raises AttributeError during client construction and prevents synthesis. Publishing the new base does not fix existing lockfiles. Declare a minimum base version containing the APIs within the intended 0.7 range, or provide a compatible fallback. This finding applies to the four HTTP adapters; Cosy now uses its own timing module.

[P2] Update Cosy's existing tests for the changed client contract

ai_agents/agents/ten_packages/extension/cosy_tts_python/cosy_tts.py:244

The required third argument breaks both calls in tests/test_client.py:30–31. In the other direction, five synthesize_audio doubles in test_basic.py, test_state_machine.py, and test_robustness.py still accept two arguments while the extension supplies three. These paths raise TypeError instead of exercising synthesis, flush, and recovery. The existing metrics mock also leaves the new request_ttfb_ms() accessor returning a MagicMock, which is passed as the metric value. Update the callers, doubles, and numeric timing return values, then validate the complete Cosy suite alongside the new focused case.

[P2] Align the local HTTP tests with the shared timing boundary

ai_agents/agents/ten_packages/extension/openai_tts2_python/tests/test_ttfb_boundary.py:85

This fixture records sent after 150 ms of connection delay and expects roughly 30 ms TTFB. Hume's new test_ttfb_transport.py:39–43 similarly excludes 120 ms. However, the shared helper consumed from ten_ai_base PR #130 starts at the HTTPX request hook, before connection setup; the new guarder reference, HTTP integration fixture, and final paragraph of PROVIDER_TTFB.md use that same boundary. That implementation produces roughly 180 ms and 140 ms respectively, failing the local tests' 5 ms assertions. Choose one contract and align both local and integration expectations against the same base revision.

[P2] Keep hook fixtures runnable with only the selected extension installed

ai_agents/agents/integration_tests/tts_guarder/tests/test_provider_ttfb_hooks.py:52

The normal guarder task installs one extension through manifest-tmpl.json and runs all of tests/. These new fixtures import OpenAI, Mistral, EZAI, Hume, Google, Gemini, and Generic HTTP production modules regardless of --extension_name. A clean single-provider installation therefore fails on missing adapters or SDK dependencies; Generic HTTP additionally requires an external package. Use dependency-binding fakes for hook unit tests, or select/skip provider fixtures based on installed dependencies and make external-package cases opt-in.

[P2] Match the Gemini reference hook to the repository's SDK adapter

ai_agents/agents/integration_tests/tts_guarder/tests/provider_ttfb_hooks/gemini.py:9

The checked-in gemini_tts_python/gemini_tts.py uses google.genai.Client and has no httpx module binding. Registering this hook for gemini_tts_python makes install_ttfb_reference() access module.httpx.AsyncClient and raise AttributeError before the existing sequential TTFB case sends any text. Observe the supported SDK boundary, or distinguish the external HTTP adapter before selecting this hook for the repository extension.

[P2] Decode HTTP content encodings before recognizing reference audio

ai_agents/agents/integration_tests/tts_guarder/tests/provider_ttfb_hooks/http.py:102

response.stream supplies bytes before HTTPX applies content decoding, while Decoder only handles JSON/SSE framing. For a valid JSON response with Content-Encoding: gzip, the adapter receives decoded JSON but the reference parses compressed bytes, never recognizes audio, and returns None or raises a decoding error. The guarder consequently rejects a successful synthesis; HTTPX advertises compression support by default. Preserve arrival timestamps while applying content decoding during deferred recognition, and cover compressed responses split across chunks.

[P2] Add local regressions for EZAI's and Mistral's timing paths

ai_agents/agents/ten_packages/extension/ezai_tw_tts_python/ezai_tts.py:134 and mistral_tts_python/mistral_tts.py:275

Neither local suite exercises the new transport observation and audio-confirmation boundary. EZAI's metrics test replaces the whole client; Mistral's replaces HTTPX and yields raw bytes, bypassing the changed SSE branch. The required extension-local test matrix makes this a merge gate; shared integration tests cannot substitute. Drive each real adapter over a deterministic transport and assert the chosen start boundary, rejection of empty/control payloads as first audio, one metric per request, and fresh timing for a second request. Include EZAI's multiple-POST sentence path.

[P2] Complete the mandatory local audio-end accounting coverage

ai_agents/agents/ten_packages/extension/cosy_tts_python/tests/test_ttfb_boundary.py:92

None of the six touched extension suites checks request_event_interval_ms, and the new boundary testers stop after first audio/TTFB without checking completion. The repository's required local-test matrix makes this a merge gate for every touched TTS extension. Add controlled-clock assertions from the matching tts_audio_start to tts_audio_end, exact duration from emitted PCM bytes, and at most one end on success, partial-audio error, flush/cancellation, and no-audio termination. Approximate duration checks and shared integration tests do not satisfy this requirement.

Reviewed f131b0aa against 56f790d4. Validation: static inspection and syntax parsing of all 32 changed Python files; no PR code executed or dependencies installed.

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

TTS extension review (f131b0aa)

Scope: the six changed TTS extensions under ai_agents/agents/ten_packages/extension/. Per ten-tts-review, tts_guarder, tts_http_timing and ten_ai_base are out of scope, except for checking hook registration. The latest commit moves Cosy's timing into the local timing.py, so Cosy no longer depends on the unpublished base module. The findings below are still open at this HEAD.

[P1] Update the Cosy local suite for the new synthesize_audio / request_ttfb_ms contract
ai_agents/agents/ten_packages/extension/cosy_tts_python/cosy_tts.py:243-245, cosy_tts_python/extension.py:549-560
synthesize_audio now requires request_id, but no existing Cosy test was changed to match:

  • tests/test_client.py:30-31 still passes two arguments and raises TypeError.
  • The fake synthesize_audio(self, text, text_input_end) in test_basic.py:141,361, test_robustness.py:130 and test_state_machine.py:221,341 now receives three arguments. Each request then fails inside request_tts instead of producing audio.
  • test_metrics.py:76 patches CosyTTSClient with a MagicMock. request_ttfb_ms() therefore returns a MagicMock, not None, and that object is passed as ttfb_ms. The test's ~250 ms assertion relied on the removed request_start_ts path.

Fix these fakes and assertions, then run the full Cosy suite. test_ttfb_boundary.py on its own is not enough.

[P2] Require the ten_ai_base release that provides the HTTP timing API
openai_tts2_python/openai_tts.py:113, mistral_tts_python/mistral_tts.py:133, ezai_tw_tts_python/ezai_tts.py:62, humeai_tts_python/humeTTS.py:37,92
These adapters call observe_http_ttfb(), record_text_sent() and record_audio_received() on the base client. The PR says those methods first ship in 0.7.63, yet each manifest still accepts any "0.7" version. An app that has 0.7.x < 0.7.63 installed or locked can resolve these patch releases. The client constructor then raises AttributeError, and every request fails. Raise the lower bound to 0.7.63 in these manifests. If you want to keep the bare 0.7 range, the PR needs to state and enforce a publish order that guarantees no older 0.7 release can be selected. Cosy is no longer affected after f131b0aa. Google does not call the base API.

[P2] Add local tests for the new Mistral and EZAI timing boundaries
mistral_tts_python/mistral_tts.py:275, ezai_tw_tts_python/ezai_tts.py:133-134
Neither extension got a local test for the new boundary. EZAI's test_metrics.py mocks the whole EZAITWTTSClient. Mistral's patches AsyncClient and returns raw bytes. In both cases the HTTPX hook and the new record_audio_received() calls never run. Following the OpenAI and Hume test_ttfb_boundary.py tests, drive the real adapter over a fake transport. Assert that connection delay is excluded, that an event with empty or missing audio/audio_data does not count as first audio, and that each of two sequential requests emits exactly one TTFB. For EZAI, cover the multi-POST sentence path.

[P2] Cover exact audio-end accounting in the touched suites
cosy_tts_python/tests/test_ttfb_boundary.py:92 (same gap for google, openai_tts2 and humeai)
The new boundary tests stop at first audio. None of the six suites asserts request_event_interval_ms from the matching tts_audio_start to tts_audio_end. None asserts request_total_audio_duration_ms against the emitted PCM bytes on success, partial-audio error, flush and no-audio paths. The required local-test matrix (.agents/skills/ten-tts-review/references/tests.md) makes this a merge gate for every touched extension.

[P3] Stop marking the mocked boundary tests as api
cosy_tts_python/tests/test_ttfb_boundary.py:146, and the same pytestmark in google/openai_tts2/humeai test_ttfb_boundary.py and humeai_tts_python/tests/test_ttfb_transport.py
These tests use only fake providers, but the new pyproject marker defines api as "External TTS API contract". Any CI run with -m "not api" will skip the only regression coverage for the new timing code. Remove the marker or use a separate one.

Guarder hooks: all six extensions are registered in provider_ttfb_hooks/__init__.py. Subtitle alignment is N/A, because none of these adapters emits timestamped tts_text_result. None of the extensions is new, so the screenshot and PCM evidence gate does not apply.

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

[P1] Keep the new HTTP clients compatible with their declared base dependency

ai_agents/agents/ten_packages/extension/openai_tts2_python/openai_tts.py:113

An app locked to an older ten_ai_base such as 0.7.62 still satisfies the unchanged "0.7" dependency, but constructing this client now raises AttributeError because that base lacks observe_http_ttfb(). Mistral, EZAI, and Hume introduce the same unconditional call, so these otherwise valid upgrades fail before synthesis. Publishing the newer base alone does not invalidate an already satisfying lockfile. Declare a minimum compatible patch within the 0.7 range, or provide a compatible fallback for older accepted bases.

[P2] Update the existing Cosy tests for the changed client contract

ai_agents/agents/ten_packages/extension/cosy_tts_python/cosy_tts.py:243

The new required request_id argument leaves tests/test_client.py:30–31 calling this method with too few arguments. The fake methods in test_basic.py:141,361, test_robustness.py:130, and test_state_machine.py:221,341 also retain the old signature, so the extension's new three-argument call raises instead of starting their audio streams. In addition, the metrics test's mocked client never configures the new request_ttfb_ms() result. Update the callers, fakes, and timing return values, then validate the complete Cosy suite; the added boundary case does not exercise these failures.

[P2] Align the local HTTP timing fixtures with the request-start contract

ai_agents/agents/ten_packages/extension/openai_tts2_python/tests/test_ttfb_boundary.py:85

This fixture records sent after its 150 ms connection delay and therefore expects approximately 30 ms. However, the new guarder reference, PROVIDER_TTFB.md:133, and the paired base implementation start at HTTPX's request hook, before that delay, yielding approximately 180 ms. The 5 ms assertion consequently fails with the intended request-start implementation. humeai_tts_python/tests/test_ttfb_transport.py:39–40 has the same mismatch with a 120 ms delay. Timestamp request initiation in these fixtures and align their assertions with the documented boundary.

[P2] Decode HTTP content encoding before recognizing reference audio

ai_agents/agents/integration_tests/tts_guarder/tests/provider_ttfb_hooks/http.py:117

The wrapper captures response.stream, which contains the raw, potentially compressed response body; HTTPX decompresses it later in aiter_bytes(). For a valid JSON or SSE response with Content-Encoding: gzip, the adapter receives decoded audio normally, but the deferred Decoder tries to parse compressed bytes as JSON/SSE and never records reference audio. The newly mandatory provider comparison then fails a successful request. Pass the content encoding into the deferred decoder and decompress before protocol recognition, with a compressed structured-response fixture.

[P2] Add extension-local coverage for the Mistral and EZAI timing paths

ai_agents/agents/ten_packages/extension/mistral_tts_python/mistral_tts.py:275

Neither extension receives a local regression test for its new timing behavior. Mistral's existing metrics fixture returns raw bytes, bypassing this SSE audio-confirmation path; EZAI's fixture mocks the entire client, bypassing both new calls. Add local tests driving the real adapters over deterministic transports, covering empty/control payloads before audio, exactly one TTFB for each of two sequential request IDs, and EZAI's multi-sentence/multi-POST path. The required local-test matrix explicitly says shared integration cases cannot substitute for extension-local coverage.

[P2] Complete the mandatory local audio-end accounting coverage

ai_agents/agents/ten_packages/extension/cosy_tts_python/tests/test_ttfb_boundary.py:92

None of the six touched extension suites asserts request_event_interval_ms, and the new boundary testers stop after first audio/TTFB without validating completion. Existing approximate duration checks do not satisfy the mandatory local-test matrix. Add controlled-clock start-to-end interval assertions, exact duration assertions from emitted PCM bytes, and at-most-once termination checks for success, partial-audio error, flush/cancellation, and no-audio termination. This is an explicit merge requirement for every touched TTS extension.

Validation: reviewed HEAD 10c89b12 against base 56f790d4; syntax-parsed all 32 changed Python files. No PR code was executed and no dependencies were installed; runtime and live-provider tests were not run.

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

TTS extension review (10c89b12)

Scope: the six changed TTS extensions under ai_agents/agents/ten_packages/extension/. Per ten-tts-review, tts_guarder, tts_http_timing and ten_ai_base are out of scope, except for checking hook registration. None of these extensions is new, so the screenshot and PCM evidence gate does not apply.

[P2] Start Cosy's TTFB after DashScope session setup, not before streaming_call()
ai_agents/agents/ten_packages/extension/cosy_tts_python/cosy_tts.py:264-265
text_sent() is stamped right before self.synthesizer.streaming_call(text). A synthesizer is created fresh for each request, because complete() and cancel() set it to None. In DashScope's tts_v2.SpeechSynthesizer, the first streaming_call() on a new synthesizer starts the stream: it opens the WebSocket, sends run-task, and waits for task-started. Only then does it send the text. Every request's TTFB therefore still includes connection and session setup, which is the cost this PR says it removes.

Two things hide this:

  • The new test (tests/test_ttfb_boundary.py) puts the 150 ms delay in the fake's __init__, while its fake streaming_call() returns at once.
  • The guarder hook (provider_ttfb_hooks/cosy.py) records text at the same streaming_call entry, so the 1 ms agreement check passes even though both timestamps include setup.

Fix: stamp the send time after the stream has started. For example, warm the session explicitly before the timed call if the SDK exposes a way to do that, or record the time on the first continue-task send. Then move the test's delay into the fake streaming_call() before it sends the text. If DashScope offers no public boundary after setup, document that setup is included, as the PR body does for other opaque SDKs. (I could not fetch the DashScope source from this environment. Please confirm the streaming_call start-up behavior against the pinned dashscope>=1.26.4.)

Still open at this HEAD (reported earlier, unchanged)

  • [P1] Cosy's existing local suite is still broken by the new contract. tests/test_client.py:30-31 calls synthesize_audio() with two arguments. The two-argument doubles at test_basic.py:141,361, test_robustness.py:130 and test_state_machine.py:221,341 now receive three. In test_metrics.py:76, the MagicMock client's request_ttfb_ms() returns a mock that gets sent as ttfb_ms.
  • [P2] The manifests still declare ten_ai_base: "0.7", but OpenAI, Mistral, EZAI and Hume call observe_http_ttfb() / record_audio_received(), which first ship in 0.7.63. A lock on an older 0.7.x release makes client construction raise AttributeError.
  • [P2] Mistral (mistral_tts.py:275) and EZAI (ezai_tts.py:133-134) have no extension-local test that drives the real adapter through the new HTTPX hook and record_audio_received() path.
  • [P2] None of the touched suites asserts request_event_interval_ms or exact request_total_audio_duration_ms on success, partial-audio error, flush and no-audio paths, which references/tests.md requires.
  • [P3] The fully mocked boundary tests are marked api ("External TTS API contract"), so -m "not api" would skip the only regression coverage for this change.

Guarder hooks: all six extensions are registered in provider_ttfb_hooks/__init__.py. Subtitle alignment does not apply, because none of these adapters emits timestamped tts_text_result.

Static review of 10c89b12. No PR code was executed and no dependencies were installed.

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

[P2] Update Cosy's existing tests for the changed client contract

ai_agents/agents/ten_packages/extension/cosy_tts_python/cosy_tts.py:243

The required request_id argument breaks the unchanged calls in tests/test_client.py:30–31, which still supply only two arguments. The two-argument fakes in test_basic.py:141,361, test_state_machine.py:221,341, and test_robustness.py:130 also raise TypeError when the extension invokes them with three arguments, preventing their synthesis, flush, and recovery checks from running successfully. Additionally, test_metrics.py never configures the mocked client's new request_ttfb_ms() return value, so a MagicMock reaches the metrics helper. Update these callers and fakes, supply explicit timing results, and validate the complete Cosy suite alongside the added boundary case.

[P2] Complete the mandatory local audio-end accounting coverage

ai_agents/agents/ten_packages/extension/openai_tts2_python/tests/test_ttfb_boundary.py:66

This tester stops after first audio and TTFB, before checking request completion; the new Google, Hume, and Cosy testers have the same limitation. None of the six touched extension-local suites asserts request_event_interval_ms, and their existing approximate duration checks do not cover exact accounting across terminal paths. The required TTS test matrix explicitly makes this a merge requirement for every touched TTS extension. Add deterministic local adapter tests that assert the matching audio-start-to-end interval with controlled time, duration calculated from PCM bytes actually emitted, and at most one end event on success, partial-audio error, flush/cancellation, and no-audio termination. Shared integration tests do not replace these local assertions.

Validation: reviewed HEAD b1d2bbfc against base 56f790d4 using the repository's ten-tts-review guidance; syntax-parsed all 32 changed Python files. No PR code was executed or dependencies installed; runtime and live-provider tests were not run.

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

TTS extension review (b1d2bbfc)

Scope: the six touched TTS extensions under ai_agents/agents/ten_packages/extension/, reviewed per ten-tts-review. tts_guarder, tts_http_timing and ten_ai_base are out of scope, except for checking hook registration. None of these extensions is new, so the screenshot and PCM gate does not apply. At this HEAD, the OpenAI, Mistral and EZAI source changes only remove blank lines, so their TTFB behavior now comes entirely from the base.

[P1] Update Cosy's existing local tests for the new synthesize_audio / request_ttfb_ms contract
ai_agents/agents/ten_packages/extension/cosy_tts_python/cosy_tts.py:243-245
synthesize_audio() now requires a third request_id argument, but the existing suite is untouched.
tests/test_client.py:30-31 calls client.synthesize_audio("hello", False) on the real client, which
raises TypeError. The two-argument side_effect doubles in test_basic.py:141,361, test_robustness.py:130
and test_state_machine.py:221,341 now receive three positional args from extension.py:247 and raise
TypeError inside request_tts. In test_metrics.py the MagicMock client's request_ttfb_ms() returns a
MagicMock, which extension.py:549-560 forwards as ttfb_ms. Update the doubles to accept request_id and
stub request_ttfb_ms() with an int, so the existing lifecycle and metrics coverage runs again.
[P2] Cosy TTFB still includes DashScope session setup, and the new test hides it
ai_agents/agents/ten_packages/extension/cosy_tts_python/cosy_tts.py:264-265
text_sent() is stamped right before synthesizer.streaming_call(). complete() and cancel() set
synthesizer to None, so every request builds a new synthesizer. On a fresh synthesizer, DashScope's
first streaming_call() opens the WebSocket, sends run-task and waits for task-started before it sends
the text. Every Cosy TTFB therefore includes connection and session setup. The test
test_ttfb_excludes_sdk_setup_and_callback_queue_wait puts its 150 ms delay in the fake __init__, and its
fake streaming_call() returns at once, so it cannot catch this. The guarder hook (provider_ttfb_hooks/cosy.py)
stamps at the same streaming_call entry, so the 1 ms agreement check passes too. Either record the send
after the stream has started, or state in the PR and test name that setup is included, and move the
fake's delay into streaming_call().
[P2] Cover audio-end accounting on the changed Cosy and Google paths
ai_agents/agents/ten_packages/extension/cosy_tts_python/extension.py:549
ai_agents/agents/ten_packages/extension/google_tts_python/google_tts.py:272-279
The new test_ttfb_boundary suites assert only the TTFB value. No touched suite asserts
request_event_interval_ms (tts_audio_start -> tts_audio_end, controlled clock) or exact
request_total_audio_duration_ms on success, partial audio then error, flush after audio and no audio.
These are mandatory in references/tests.md. Add them to the mocks these tests already use. In Google,
also add a test where an appended segment for the same request_id emits no second TTFB. In Cosy, add a
test where request_ttfb_ms() returns None, which now skips the metric without logging anything.
[P3] Don't mark deterministic boundary tests as external API tests
ai_agents/agents/ten_packages/extension/{cosy,google,humeai,openai_tts2}_python/tests/test_ttfb_boundary.py (pytestmark)
These tests are fully mocked, but they set pytestmark = pytest.mark.api ("External TTS API contract").
Running with -m "not api" to skip credentialed tests would also skip the only regression coverage for
this change. Drop the marker from the mocked tests, or use a separate one.

Guarder hooks: all six extensions are registered in provider_ttfb_hooks/__init__.py. Subtitle alignment does not apply, because none of these adapters emits timestamped tts_text_result.

This was a static review of b1d2bbfc. No PR code was executed and no dependencies were installed.

@YiminW
YiminW force-pushed the fix/tts-ttfb-phase-two branch from b1d2bbf to 84599cf Compare November 4, 2026 02:39
@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

[P2] Update Cosy's timing fixture and observer for pooled synthesizers

ai_agents/agents/ten_packages/extension/cosy_tts_python/tests/test_ttfb_boundary.py:125

The current client obtains synthesizers through SpeechSynthesizerObjectPool; the pinned SDK constructs them using its own module's SpeechSynthesizer, not this extension-module alias. Replacing the alias therefore never installs Provider: startup still opens real provider connections using the fixture's mock key, and the fixture cannot produce the timestamps/audio it asserts. The new provider_ttfb_hooks/cosy.py makes the same replacement, so its reference also captures no events for pooled requests and elapsed_ms() stays None. Use the existing local DashScope server fixture or a compatible fake pool for the test, and observe the actual pooled SDK send/callback boundary for the guarder. Verify both against the current pooled client.

[P2] Provide an independent HTTP provider timing reference

ai_agents/agents/ten_packages/extension/openai_tts2_python/manifest.json:4

The required hook coverage for this release, Mistral, EZAI and Hume delegates to HttpAdapterHook, which timestamps entry to production client.get() and its decoded RESPONSE output. If an adapter buffers audio or delays parsing, both the reported timer and this reference include that delay, so the 1 ms comparison can pass despite a provider-boundary regression. This also uses production decoder outputs as the reference, which the mandatory guarder coverage guide explicitly excludes. Observe request send and incoming audio at the HTTP/SDK boundary with independent deferred protocol recognition; keep the adapter-level measurement as a separate check. Cover controls/empty payloads and two sequential requests.

[P2] Complete the required terminal audio-accounting tests

ai_agents/agents/ten_packages/extension/google_tts_python/tests/test_ttfb_boundary.py:65

This tester stops after first audio and TTFB, without checking completion. Google, OpenAI, Hume, Mistral and EZAI have no local assertion of request_event_interval_ms; existing approximate flush-duration checks do not cover exact accounting across terminal paths. The required local-test matrix makes this a merge requirement for touched extensions. Add controlled-clock start-to-end interval assertions, duration calculated from PCM bytes actually emitted, and at-most-once termination checks for success, partial-audio error, flush/cancellation and no-audio termination. Cosy's existing success/no-audio accounting coverage is present; its partial-error and flush tests still need interval assertions. Shared integration tests do not replace these local checks.

Validation: reviewed HEAD 84599cf8 against base 518d5bf3 and syntax-parsed all 30 changed Python files. No PR code was executed or dependencies installed; runtime and live-provider tests were not run.

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

TTS extension review

[P2] Cosy TTFB hook and local test patch a class the adapter never constructs
ai_agents/agents/integration_tests/tts_guarder/tests/provider_ttfb_hooks/cosy.py:184-193, ai_agents/agents/ten_packages/extension/cosy_tts_python/tests/test_ttfb_boundary.py:128-131

cosy_tts.py does not call SpeechSynthesizer(...). It gets synthesizers from SpeechSynthesizerObjectPool(...) (cosy_tts.py:287) and pool.borrow_synthesizer(callback=...) (cosy_tts.py:343). Inside dashscope, the pool builds and reuses SpeechSynthesizer instances through its own module reference. Patching cosy_tts.SpeechSynthesizer therefore never wraps streaming_call or the callback. The guarder Reference receives no captures, and elapsed_ms() returns None, so Cosy has no usable independent TTFB reference.

The same problem affects the new extension test. The fake Provider is never used, the real pool tries to reach DashScope with api_key="mock", and times["sent"]/times["received"] are never set. With the patch in place, the isinstance(..., SpeechSynthesizer) check in _pool_synthesizers also compares against the fake class.

To fix this, hook the pooled object instead, either by patching SpeechSynthesizerObjectPool / borrow_synthesizer or by wrapping lease.synthesizer.streaming_call and the callback passed to borrow. Then make the local test drive the pool path.

[P2] Hume: injecting a custom httpx_client probably removes the SDK 60 s request timeout
ai_agents/agents/ten_packages/extension/humeai_tts_python/humeTTS.py:35-37

The Hume SDK is Fern-generated. In Fern clients, the wrapper timeout defaults to 60 only when httpx_client is None. When a client is passed in, it defaults to None, and that None is then sent explicitly as timeout= on every stream()/request() call. Explicit None disables httpx timeouts and overrides the timeout=60 set on the injected AsyncClient. If a Hume connection stalls before or during the NDJSON stream, the request hangs indefinitely. The comment "Match AsyncHumeClient default timeout" does not hold for per-request calls.

Please check this against hume==0.9.1; the fix is to also pass timeout=60 to AsyncHumeClient(...).

[P3] Cosy timing.py is dead code
ai_agents/agents/ten_packages/extension/cosy_tts_python/timing.py:1-38

Nothing imports TTSRequestTiming. Production TTFB still comes from first_request_sent_ns/first_audio_ns in cosy_tts.py:752-763. Either wire it in or drop the file so the extension does not carry two timing models.

[P3] Cosy changes ship without the described version bump, and the api marker is not registered
ai_agents/agents/ten_packages/extension/cosy_tts_python/manifest.json, ai_agents/agents/ten_packages/extension/cosy_tts_python/pyproject.toml

The PR body says "Cosy is 0.4.3 -> 0.4.4". On main, Cosy is at 0.5.0, and this PR leaves manifest.json/pyproject.toml unchanged while adding files to the package. Bump both to 0.5.1, or remove the production-side file.

Cosy pyproject.toml also lacks the [tool.pytest.ini_options] markers = ["api: ..."] entry the other extensions got. The new test sets pytestmark = pytest.mark.api, so --strict-markers (as in the PR validation command) fails collection.

[P3] Local TTFB tests allow 5 ms against the stated 1 ms contract, and mock-only tests are marked api
google_tts_python/tests/test_ttfb_boundary.py:121, openai_tts2_python/tests/test_ttfb_boundary.py:107, humeai_tts_python/tests/test_ttfb_boundary.py:100

PROVIDER_TTFB.md and the PR body promise agreement within 1 ms. The local tests use <= 5, so a regression that adds a few ms of queue or conversion time still passes. These tests use only fakes, but pytestmark = pytest.mark.api ("External TTS API contract") labels them as live-API tests. Any -m "not api" run will skip the only local coverage for the new timing boundaries.

Notes

  • Google (google_tts.py, get_streaming): TTFB now runs from the first nonempty text the SDK pulls to the first nonempty audio_content, and cur_request_id is committed only on audio. No defect found.
  • OpenAI, Mistral and EZAI production changes are whitespace and version bumps only. Their guarder hooks go through the shared HttpAdapterHook.

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

[P2] Keep hook fixtures independent of unselected vendor packages

ai_agents/agents/integration_tests/tts_guarder/tests/test_provider_ttfb_hooks.py:22

The default guarder command installs only the selected extension through manifest-tmpl.json, then runs all of tests/. This fixture unconditionally imports the Google adapter before installing its fake. In a fresh app prepared for another provider, such as openai_tts2_python, that adapter is absent, so the full suite fails with ModuleNotFoundError. The focused -k test_sequential_requests_ttfb validation excludes this fixture and misses the regression. Supply a synthetic module, as the existing transport fixtures do, or explicitly limit the fixture to an installation containing Google.

[P2] Update Cosy's timing test and observer for pooled synthesizers

ai_agents/agents/ten_packages/extension/cosy_tts_python/tests/test_ttfb_boundary.py:125

The current client creates a SpeechSynthesizerObjectPool and obtains synthesizers through borrow_synthesizer(). Replacing the extension module's SpeechSynthesizer alias does not replace the class constructed inside the SDK pool. Consequently, the new test still opens real provider connections with the mock key and never populates its expected timestamps. The new Cosy TTFB hook makes the same ineffective replacement, so pooled requests produce no captures and elapsed_ms() remains None. Drive the existing local DashScope server fixture or a compatible fake pool, and observe the actual pooled synthesizer's send/callback boundary.

[P2] Provide HTTP timing references independent of adapter decoding

ai_agents/agents/ten_packages/extension/humeai_tts_python/humeTTS.py:37 (also affects the touched OpenAI and Mistral adapters)

These adapters' required coverage delegates to HttpAdapterHook, which timestamps production client.get() entry and its decoded RESPONSE output. If an adapter buffers audio or delays conversion, both the reported metric and this reference include the delay, so their 1 ms agreement does not independently validate provider timing. The mandatory hook coverage guide explicitly excludes production decoder outputs as the independent reference. Observe the public HTTP/SDK request and incoming audio boundaries with independent protocol recognition, covering controls, empty payloads and sequential requests; retain the documented adapter interval as a separate measurement.

[P2] Complete the required terminal audio-accounting tests

ai_agents/agents/ten_packages/extension/google_tts_python/tests/test_ttfb_boundary.py:66

This tester stops after first audio and TTFB, before checking request completion. Google, OpenAI, Hume and Mistral have no extension-local assertion of request_event_interval_ms; Cosy's existing success/no-audio checks are present, but its partial-error and flush tests omit the interval assertion. The required local-test matrix makes terminal accounting a merge requirement for every touched extension. Add controlled-clock start-to-end interval checks, exact duration from PCM bytes actually emitted, and at-most-once termination assertions for success, partial-audio error, flush/cancellation and no-audio termination. Shared integration tests do not replace these local checks.

Validation: static review of HEAD 423c11d3 against base 518d5bf3, following the repository's ten-tts-review skill for extension files. Syntax-parsed all 28 changed Python files. No PR code was executed or dependencies installed; runtime and live-provider tests were not run.

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

TTS review (ten-tts-review)

Scope: the in-scope TTS extensions are google_tts_python, humeai_tts_python, mistral_tts_python, openai_tts2_python and cosy_tts_python, all existing extensions. The guarder registration and hooks were read only to check TTFB hook coverage for them. The Google, Hume, OpenAI and Mistral changes have no actionable findings. The findings below are all about Cosy, and they come from the rebase.

[P2] Fix the Cosy TTFB test so it does not reach DashScope
ai_agents/agents/ten_packages/extension/cosy_tts_python/tests/test_ttfb_boundary.py:124-127
The test replaces cosy_tts.SpeechSynthesizer with a fake Provider. On current main (after #2383), cosy_tts.py no longer constructs SpeechSynthesizer itself. It builds a SpeechSynthesizerObjectPool(...) (cosy_tts.py:287) and gets synthesizers from pool.borrow_synthesizer(...) (cosy_tts.py:344). Both are created inside the dashscope package, so the module-level patch has no effect. The test then opens a real DashScope connection with api_key: "mock", gets no audio within 0.8 s, and fails with "Missing audio or TTFB" or "Provider error". The fake's times["sent"] and times["received"] are never set either. Replace the pool, or SharedPool.borrow, with the fake instead, and run the test against the rebased head.

[P2] Fix the Cosy guarder TTFB hook for the pooled SDK path
ai_agents/agents/integration_tests/tts_guarder/tests/provider_ttfb_hooks/cosy.py:34-43
The hook subclasses module.SpeechSynthesizer and wraps kwargs["callback"] in __init__. The pooled production path never calls the cosy module's SpeechSynthesizer, so neither streaming_call nor on_data is observed. As a result, Reference.elapsed_ms() always returns None and the 1 ms agreement check cannot pass. This contradicts the PR description's claim that Cosy passed live. The patch also replaces the class that SharedPool._pool_synthesizers uses in its isinstance(synthesizer, SpeechSynthesizer) check (cosy_tts.py:200). Real pool synthesizers fail that check, so cleanup during a guarder run skips closing their sockets. Hook the instance returned by borrow_synthesizer: wrap streaming_call and the callback the borrow sets. Do not replace the module class.

[P3] Wire up or remove cosy_tts_python/timing.py
ai_agents/agents/ten_packages/extension/cosy_tts_python/timing.py:1-38
Nothing imports TTSRequestTiming. Cosy TTFB is still computed from first_request_sent_ns and first_audio_ns in cosy_tts.py:931 and cosy_tts.py:757. The file looks left over from before the rebase. Because no production code in Cosy changed, the "Cosy is 0.4.3 -> 0.4.4" line in the PR description is also stale: main is already at 0.5.0, and this diff does not touch the Cosy manifest. Either use the class in production code (with a version bump), or delete the file and fix the description.

Note: openai_tts2_python and cosy_tts_python add pytestmark = pytest.mark.api but do not register the api marker in their pyproject.toml, unlike Google, Hume and Mistral. If those suites ever run with --strict-markers, collection fails. Register the marker in both.

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

[P2] Exercise Cosy pooled synthesizers in the test and timing hook

cosy_tts_python/tests/test_ttfb_boundary.py:124

Cosy now borrows synthesizers from SpeechSynthesizerObjectPool, which constructs them through the SDK module, so replacing the extension module alias does not install this fake. The test reaches the real service with the mock key and cannot produce the asserted timestamps. The new Cosy guarder hook patches the same unused alias, so it captures no timing events for pooled requests and the required reference comparison fails. Fake the pool or its borrow path in the local test, and instrument the borrowed instance and callback for the hook.

[P2] Keep guarder fixtures independent of unselected extensions

test_provider_ttfb_hooks.py:22

The default guarder installation selects one extension and runs the whole test directory. This fixture unconditionally imports the Google adapter, so a clean run for another provider fails with ModuleNotFoundError before it can complete the suite. The focused -k test_sequential_requests_ttfb run does not exercise this fixture. Use a synthetic module or restrict the fixture to installations that contain Google.

[P2] Capture independent HTTP provider timing references

humeTTS.py:37 (also affects the touched OpenAI and Mistral adapters)

The registered HttpAdapterHook measures entry to production client.get() through its decoded RESPONSE. Buffering or slow decoding inside the adapter affects both the metric and this reference, so their agreement cannot detect a provider-boundary regression. The mandatory guarder coverage guide explicitly excludes production decoder outputs as an independent reference. Observe the HTTP or SDK send and incoming audio boundaries independently, with empty and control payloads and sequential requests covered; the adapter interval can remain a separate measurement.

[P2] Complete terminal audio-accounting coverage in the touched suites

google_tts_python/tests/test_ttfb_boundary.py:66

This tester stops after first audio and TTFB, before completion. The required extension-local matrix calls for exact emitted-PCM duration, controlled-clock tts_audio_start-to-end interval, and at-most-once end assertions for success, partial-audio error, flush or cancellation, and no-audio termination. Google, OpenAI, Hume and Mistral lack local interval assertions; Cosy has success and no-audio accounting coverage but lacks interval assertions for partial-error and flush paths. Add the missing local checks; shared integration tests do not replace them.

Correction to the earlier Hume timeout concern: hume==0.9.1 derives the SDK request timeout from httpx_client.timeout.read when a custom client is supplied, so this 60-second client preserves the SDK timeout. That concern should not be treated as a finding.

Static review of HEAD d21df1b2; no PR code was executed or dependencies installed.

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

TTS extension review (scope: files under ai_agents/agents/ten_packages/extension/<tts>/, plus a read-only check of the guarder hooks registered for them).

[P2] Cosy TTFB hook never observes production traffic
ai_agents/agents/integration_tests/tts_guarder/tests/provider_ttfb_hooks/cosy.py:183-192
On main, cosy_tts.py never builds SpeechSynthesizer through the module attribute. It borrows synthesizers from SpeechSynthesizerObjectPool(...) (cosy_tts.py:287) via pool.borrow_synthesizer(callback=...) (cosy_tts.py:343), and dashscope builds those objects internally. Patching cosy_tts.SpeechSynthesizer therefore wraps neither streaming_call nor the callback, so Reference.elapsed_ms() returns None for every request. That leaves Cosy without a usable independent TTFB reference, even though the description lists Cosy as a live pass. Hook the pool's borrow_synthesizer (wrap the returned synthesizer's streaming_call and the passed callback's on_data), or the adapter's _send_text / _Callback.on_data boundary.

[P2] Cosy local TTFB test mocks a constructor the extension doesn't call
ai_agents/agents/ten_packages/extension/cosy_tts_python/tests/test_ttfb_boundary.py:130-133
The test patches cosy_tts.SpeechSynthesizer with a fake Provider. Production gets its synthesizers from SpeechSynthesizerObjectPool, so the fake is never used. The real SDK then tries to connect with api_key: "mock", and the test either fails or hits the 0.8 s timer. Either way it never checks the TTFB path it claims to cover. Patch SpeechSynthesizerObjectPool (or _SynthesizerPool.borrow) so the fake's streaming_call and on_data drive _send_text and _Callback.on_data.

[P2] Injecting httpx_client into AsyncHumeClient can remove the request timeout
ai_agents/agents/ten_packages/extension/humeai_tts_python/humeTTS.py:35-37
In Fern-generated SDKs such as hume, the client's default timeout is only applied when httpx_client is None. When a client is passed in, base_timeout falls back to None, and that value is passed explicitly per request. In httpx, an explicit timeout=None disables timeouts and overrides the client's timeout=60. So the comment "Match AsyncHumeClient's default timeout" is likely wrong, and a stalled Hume response could hang the request until it is cancelled. Pass timeout=60 to AsyncHumeClient as well, or check the behavior against hume==0.9.1 and add a test. Note also that this injection exists only to support test_ttfb_transport.py. The production TTFB boundary does not need it.

[P3] cosy_tts_python/timing.py is dead code and the Cosy version is not bumped
ai_agents/agents/ten_packages/extension/cosy_tts_python/timing.py:1-38
Nothing imports TTSRequestTiming. Cosy TTFB is still computed in _Callback.on_data from first_request_sent_ns. The PR also adds no Cosy manifest.json or pyproject.toml bump. The description says "Cosy is 0.4.3 -> 0.4.4", but main is already at 0.5.0. Remove the module or wire it in, and correct the version claim.

[P3] Description and version changes don't match the diff

  • EZAI is listed as updated, but no ezai_tw_tts_python file changed and it has no hook in provider_ttfb_hooks.HOOKS.
  • The changes to openai_tts2_python/openai_tts.py and mistral_tts_python/mistral_tts.py only delete a blank line. Mistral is still bumped to 0.1.3 for that. OpenAI gets a new test but no bump, and neither adapter gets a timing change despite the "Update ... OpenAI, Mistral" goal.
  • "All thirteen changed TTS versions increment" doesn't hold: only Google, Hume and Mistral are bumped.

Please align the versions and the description with what actually changes.

[P3] New mock-only tests are marked api, and Cosy/OpenAI don't register the marker
cosy_tts_python/tests/test_ttfb_boundary.py:146, openai_tts2_python/tests/test_ttfb_boundary.py:116 (also the Google and Hume test_ttfb_boundary.py)
These tests use fake providers and make no external call, yet pytestmark = pytest.mark.api makes them look like live-API tests, so -m "not api" runs will skip them. The pyproject.toml files for Cosy and OpenAI don't declare the api marker, so --strict-markers (as used in the documented command) fails at collection. Drop the marker from the mock-only tests, or register it in every touched extension.

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

[P2] Exercise the pooled Cosy client in the local test and TTFB reference

cosy_tts_python/tests/test_ttfb_boundary.py:124

Cosy obtains its synthesizer from SpeechSynthesizerObjectPool.borrow_synthesizer, so replacing the extension module’s SpeechSynthesizer alias does not install this fake. The test instead attempts a real connection with the mock key and cannot record the expected timestamps. The new guarder hook patches the same unused alias, so it also captures no usable reference for pooled requests. Patch the pool/borrow path in the test and instrument the borrowed synthesizer and callback in the hook.

[P2] Measure an independent HTTP provider boundary

humeai_tts_python/humeTTS.py:37 (also OpenAI and Mistral)

The registered HTTP hook timestamps entry to the production get() method and its decoded RESPONSE output. Buffering or slow decoding inside the adapter is therefore included in both the metric and its reference, so agreement cannot detect a provider-boundary timing regression. The required guarder coverage explicitly excludes production decoder output as an independent reference. Observe request send and first valid incoming audio at an independent transport or SDK boundary, with control/empty payloads and sequential requests covered.

[P2] Complete terminal audio-accounting coverage in the touched suites

google_tts_python/tests/test_ttfb_boundary.py:66

This tester stops after first audio and TTFB, before completion. Google, OpenAI, Hume and Mistral lack local request_event_interval_ms assertions; Cosy has success and no-audio checks, but not interval assertions on partial-error and flush paths. The mandatory extension-local matrix requires controlled-clock start-to-end intervals, exact duration from emitted PCM, and at-most-once end assertions for success, partial-audio error, flush/cancellation and no-audio termination. Add the missing local checks; shared integration tests do not replace them.

Static review of HEAD c44e71d3. No PR code was executed or dependencies installed.

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

TTS extension review

[P2] Cosy TTFB hook and local test patch a constructor the adapter never calls
ai_agents/agents/ten_packages/extension/cosy_tts_python/tests/test_ttfb_boundary.py:130-133 (also guarder hook tts_guarder/tests/provider_ttfb_hooks/cosy.py:33-43)

cosy_tts.py gets synthesizers only from SpeechSynthesizerObjectPool(...) / pool.borrow_synthesizer(callback=...) (SharedPool._ensure_pool_locked, SharedPool.borrow). It never calls the module-level SpeechSynthesizer(...). The DashScope pool builds its objects inside the SDK module, and the callback is attached at borrow time. Because of that:

  • the guarder Synthesizer subclass and its Callback wrapper are never instantiated, so streaming_call and on_data are never observed and reference.elapsed_ms() returns None. Cosy has no usable independent TTFB reference, even though the PR lists it as a live pass.
  • the local test's fake Provider is never used. The test reaches the real pool with api_key="mock", so it fails on an auth error or the 0.8 s timer. It does not exercise the provider boundary.

Fix direction: hook the boundary the adapter actually uses. Either patch SpeechSynthesizerObjectPool.borrow_synthesizer to wrap the returned synthesizer's streaming_call and callback, or wrap CosyTTSClient._send_text and the callback's on_data at the public adapter level. Then point the test's patch at the same seam.

[P2] Hume: injecting an external httpx_client likely removes the SDK's 60 s request timeout
ai_agents/agents/ten_packages/extension/humeai_tts_python/humeTTS.py:35-37

Fern-generated clients such as AsyncHumeClient normally set their default timeout (60) only when httpx_client is not provided. Otherwise the base timeout is None, and that None is passed explicitly to each httpx request, which disables the client-level timeout. If hume==0.9.1 follows this pattern, the timeout=60 on the new httpx.AsyncClient has no effect, and a stalled Hume stream can block the request indefinitely. Before this PR it would have failed after 60 s. I could not check the installed SDK source in this environment, so please confirm. Two simple fixes: also pass timeout=60 to AsyncHumeClient(...), or drop the injection. test_ttfb_transport.py patches httpx.AsyncClient globally, which would intercept the SDK's own client anyway, and PROVIDER_TTFB.md says HTTP adapters need no timing hooks.

[P3] Cosy ships an unused timing.py and no version bump
ai_agents/agents/ten_packages/extension/cosy_tts_python/timing.py:1-38

Nothing imports TTSRequestTiming. cosy_tts.py still uses first_request_sent_ns / first_audio_ns. The Cosy manifest.json/pyproject.toml are not in the diff, and main is at 0.5.0, not the 0.4.3 -> 0.4.4 the description states. The description's "thirteen changed TTS versions" also doesn't match the diff: openai_tts2_python has no bump, and mistral_tts_python is bumped for a blank-line removal only. Either wire the module in with a patch bump, or remove it and correct the description.

Notes (non-blocking)

  • Google get_streaming: cur_request_id is now set only after the first nonempty audio. If one request calls get_streaming several times and the first call yields no audio, TTFB is measured from the later call's first text, not from the request's first text. This is probably acceptable, but a two-chunk test would pin down the intended behavior.
  • The new extension-local tests use 5 ms tolerance, while the PR text claims 1 ms agreement. Only the guarder enforces 1 ms. That is fine, but the description should say so.

Out of scope per the TTS review skill: ten_ai_base, tts_guarder internals beyond hook coverage, and tts_http_timing.

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

[P2] Install the Cosy fake and reference on the pooled synthesizer

cosy_tts_python/tests/test_ttfb_boundary.py:124 (also provider_ttfb_hooks/cosy.py:33)

The adapter obtains synthesizers through SpeechSynthesizerObjectPool.borrow_synthesizer, so replacing the module-level SpeechSynthesizer does not install either the test fake or the guarder wrapper. The local test instead reaches the real SDK with a mock key, and the guarder receives no text/audio observations for Cosy. Patch the pool borrow path and wrap its returned synthesizer and callback; verify both the local test and two-request guarder reference against that path.

[P2] Measure HTTP TTFB at an independent provider boundary

humeai_tts_python/humeTTS.py:37 (also the touched OpenAI and Mistral extensions)

The registered HTTP hook observes entry to the production adapter get() and the decoded RESPONSE it yields. Consequently, adapter buffering and decoding delay affect both the reported metric and the reference, so agreement cannot detect a regression at the provider boundary. The required TTS guarder guide explicitly excludes production decoder outputs as an independent reference. Observe send and first valid audio independently at the transport or SDK boundary, with empty/control payloads and sequential requests covered.

[P2] Avoid importing Google in every provider guarder run

test_provider_ttfb_hooks.py:22

The default guarder command runs the entire tests/ directory, but its generated manifest installs only the requested extension. For a clean run targeting a non-Google provider, this unconditional test imports google_tts_python, which is not installed, and fails with ModuleNotFoundError before the suite can pass. Exercise this hook with a synthetic module or condition the test on Google being installed; verify the full default suite for a non-Google extension.

[P2] Cover terminal audio accounting in the touched extension suites

google_tts_python/tests/test_ttfb_boundary.py:66

The new tester stops after first audio and TTFB, before completion. Google, OpenAI, Hume and Mistral lack local request_event_interval_ms assertions; Cosy lacks those assertions on partial-error and flush paths. The mandatory extension-local test matrix requires controlled-clock start-to-end intervals, exact duration from emitted PCM, and at-most-once end assertions on success, partial-audio error, flush/cancellation and no-audio termination. Add the missing local checks; shared integration tests do not replace them.

Static review of head 8a9b7568; no PR code was executed or dependencies installed.

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

TTS review: findings

Reviewed against .agents/skills/ten-tts-review for the touched TTS extensions (google, humeai, mistral, openai_tts2, cosy). The only guarder files I read were the hooks for those extensions.

[P2] Cosy TTFB hook and local test patch a constructor the extension never calls
ai_agents/agents/integration_tests/tts_guarder/tests/provider_ttfb_hooks/cosy.py:34-43, ai_agents/agents/ten_packages/extension/cosy_tts_python/tests/test_ttfb_boundary.py:124-127
cosy_tts.py never calls SpeechSynthesizer(...). It gets its synthesizers from SpeechSynthesizerObjectPool(...) (cosy_tts.py:287) and pool.borrow_synthesizer(callback=...) (cosy_tts.py:344). The pool builds its instances inside the DashScope package, and the callback is supplied at borrow time. Replacing cosy_tts.SpeechSynthesizer therefore changes nothing. The guarder Synthesizer/Callback wrappers never run, Reference.elapsed_ms() returns None, and Cosy has no usable independent TTFB reference. The local test has the same problem: its fake Provider is never constructed, so the test either reaches the real pool with api_key: "mock" or times out. It cannot show the 5 ms agreement it asserts. This conflicts with the "Cosy passed" live claim in the description. Fix: observe the borrowed instance, for example by wrapping SharedPool.borrow/_send_text and the callback passed to borrow_synthesizer, or patch the pool. Then confirm the local test actually uses the fake.

[P3] Unused cosy_tts_python/timing.py, and the Cosy version is not bumped
ai_agents/agents/ten_packages/extension/cosy_tts_python/timing.py:1
Nothing imports TTSRequestTiming. Production TTFB still comes from first_request_sent_ns/on_data in cosy_tts.py. The description says "Cosy is 0.4.3 -> 0.4.4", but the manifest and pyproject are still 0.5.0 and unchanged. Either wire the module in, with a version bump and tests, or drop it. Likewise, the description lists EZAI among the changed adapters, but the diff has no EZAI changes.

[P3] api marker is not registered for openai_tts2 and cosy
ai_agents/agents/ten_packages/extension/openai_tts2_python/tests/test_ttfb_boundary.py:116, ai_agents/agents/ten_packages/extension/cosy_tts_python/tests/test_ttfb_boundary.py:146
Google, Hume and Mistral register markers = ["api: ..."] in pyproject, but openai_tts2 and cosy do not. Under the --strict-markers invocation documented in this PR, those files fail at collection. These tests also run fully offline with fakes. Tagging them as "External TTS API contract" means any -m "not api" selection skips the only coverage for this change. Consider leaving deterministic tests unmarked.

[P3] Google TTFB change is covered only on the single-request success path
ai_agents/agents/ten_packages/extension/google_tts_python/google_tts.py (get_streaming, report_ttfb / cur_request_id hunks)
The new logic computes report_ttfb once per call and moves self.cur_request_id = request_id to the first-audio branch. That changes behavior on paths the new test never runs:

  • A second request with a new ID must emit a new TTFB.
  • A second get_streaming call (appended text) with the same ID must not emit TTFB again.
  • A retry after a retryable failure before any audio must time from that attempt's text send.
  • A no-audio or error response must leave cur_request_id unset, so a later call with the same ID still reports TTFB.

Please add these cases next to test_ttfb_ends_at_first_nonempty_sdk_audio, using the existing fake streaming_synthesize.

Notes (not blocking)

  • Hume: creating httpx.AsyncClient in __init__ and closing it in clean() follows the existing openai/mistral pattern. test_ttfb_transport.py asserts only a lower bound (reported + 1 >= expected). Adding an upper bound would catch decode or queue time leaking into TTFB.
  • Mistral and openai_tts2 production changes are whitespace only. Mistral gets a version bump and openai_tts2 does not, which does not match the stated "one patch bump per changed extension" policy.
  • The release gate is still open. The trace-based boundary depends on unpublished ten_ai_base 0.7.63 while the manifests keep 0.7, so installs that resolve an older 0.7.x will not have the hooks these tests expect.

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

[P2] Measure HTTP TTFB against an independent provider boundary

ai_agents/agents/ten_packages/extension/humeai_tts_python/humeTTS.py:37 (also OpenAI and Mistral)

The registered HTTP hook captures entry to the production adapter get() and the decoded RESPONSE it yields. Adapter-side buffering or decoding delays therefore affect both the metric and its reference, so their agreement cannot detect a regression at the provider boundary. The required TTS guarder guide explicitly excludes production decoder output as an independent reference. Observe text send and first valid nonempty audio at an independent transport or SDK boundary, with empty/control payloads and sequential requests covered.

[P2] Avoid requiring Google for every default guarder run

ai_agents/agents/integration_tests/tts_guarder/tests/test_provider_ttfb_hooks.py:22

The default guarder command runs all of tests/, but its generated manifest installs the selected extension. On a clean run for a non-Google provider, this unconditional import requires the Google extension and its dependencies, so the hook fixture fails instead of completing the guarder suite. Use a synthetic module for this fixture or skip it when Google is not installed, and verify a clean default run for a different provider.

[P2] Cover terminal audio accounting in the touched extension suites

ai_agents/agents/ten_packages/extension/google_tts_python/tests/test_ttfb_boundary.py:66

The new tester stops after first audio and TTFB, before completion. The Google, Hume, Mistral and OpenAI extension-local suites do not assert request_event_interval_ms. The mandatory local test matrix requires controlled-clock start-to-end interval assertions, exact duration from emitted PCM, and at-most-once end assertions for success, partial-audio error, flush/cancellation and no-audio termination. Add the missing checks in each extension suite; shared integration tests do not replace them.

Static review of f4ae6e9d; no PR code was executed or dependencies installed.

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

TTS extension review (ten-tts-review)

In scope: google_tts_python, humeai_tts_python, mistral_tts_python, openai_tts2_python. All four are existing extensions, so the new-extension guarder evidence gate does not apply. Google owns its own streaming/TTFB path. Hume, Mistral and OpenAI use AsyncTTS2HttpExtension and inherit TTFB and tts_audio_end accounting from the base.

[P2] Use a send-boundary reference for the HTTP TTFB hooks instead of re-wrapping get()
ai_agents/agents/ten_packages/extension/openai_tts2_python/openai_tts.py:66 (same for mistral_tts_python/mistral_tts.py:90 and humeai_tts_python/humeTTS.py:19)
The guarder hooks registered for these three extensions (provider_ttfb_hooks/http.py) timestamp the call to client.get() and the first non-empty RESPONSE it yields. According to PROVIDER_TTFB.md in this PR, the base measures the same interval, so the reference re-measures the production boundary instead of checking it independently. The 1 ms agreement then holds even if the base starts its timer too early or too late. The interval also includes DNS/TCP/TLS and connection-pool waits, which the guarder hook contract says to exclude. The PR description says "HTTPX transport hooks record the request-body send boundary". The only code doing that is the local test fakes (trace("http11.send_request_body.started")). The registered hooks do not. Either register an httpx transport/trace observer for these adapters, or correct the description and state the limitation explicitly.

[P2] Cover the new Google TTFB state transitions with local tests
ai_agents/agents/ten_packages/extension/google_tts_python/google_tts.py (changed block in get_streaming)
This change moves self.cur_request_id = request_id from "request started" to "first audio received". It also carries report_ttfb across retries, and first_text_sent_at resets on each attempt. The new test_ttfb_boundary.py covers only one request with one attempt. No test checks that:

  • a second sequential request reports exactly one TTFB;
  • an appended segment with the same request_id does not report TTFB again;
  • a retryable failure (e.g. 503 UNAVAILABLE) followed by success reports TTFB once, measured from the successful attempt;
  • a request with no audio, followed by a segment that has audio, reports TTFB.

Separately, the Google suite has no assertion on request_total_audio_duration_ms or request_event_interval_ms. The Hume, Mistral and OpenAI suites do have one, in their test_basic.py. Google owns its send_tts_audio_end path, so it needs the same coverage.

[P3] Make version bumps match the actual changes and update the stale PR description
ai_agents/agents/ten_packages/extension/mistral_tts_python/manifest.json:4
The only Mistral production change is removing one blank line, yet it gets a patch release (0.1.2 → 0.1.3). OpenAI has the same whitespace change in openai_tts.py with no bump. The PR body also no longer matches the diff:

  • It lists EZAI and Cosy as changed, but neither extension directory is modified. Cosy is at 0.5.0 on main, while the body says "0.4.3 -> 0.4.4".
  • It says "all thirteen changed TTS versions".
  • It says "Draft until shared base 0.7.63 … is published", but the PR is not a draft.

Hume, Mistral and OpenAI rely on base-side TTFB and keep the 0.7 range, so an install that resolves to an older 0.7.x base keeps the old timing. Please confirm 0.7.63 is published before merging, or state that the extensions do not depend on it.

Minor

  • openai_tts2_python/tests/test_ttfb_boundary.py uses pytestmark = pytest.mark.api, but openai_tts2_python/pyproject.toml does not register the api marker the way the other three extensions now do. Running pytest with --strict-markers fails on it.
  • humeai_tts_python/humeTTS.py:36: the adapter now owns the injected httpx.AsyncClient and closes it in clean(). That is safe only if get() is never called after clean(). A short comment, or creating the client lazily, would make this explicit.

ASR checklist: N/A. No ASR extensions are touched.

@YiminW

YiminW commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author
image image image

@YiminW

YiminW commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author
image image

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant