Repository navigation
fix(gradium): STT model delay and clean session end (1/2) - #7870
Draft
hhugo-gradium wants to merge 4 commits into
Draft
hhugo-gradium wants to merge 4 commits into
hhugo-gradium wants to merge 4 commits into
Conversation
The STT stream looked up `delay_in_tokens` in the ready message, a key the server never sends (it is `delay_in_frames`), so the hard-coded fallback of 6 steps was always used. Production models run with a delay of 10, so a turn was finalized 4 steps (320ms) before the model had emitted its last words, which then showed up as a spurious new turn. Adds a scripted fake websocket harness for the STT tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
When the input channel closed, send_task returned without telling the
server; `closing_ws` was never set, so recv_task polled until the server
dropped the idle socket, which was then reported as an unexpected close
and retried against a closed input. The unused `_CLOSE_MSG`
(`terminate_session`) was another provider's protocol.
Send `{"type": "end_of_stream"}` once the input ends, mark the socket as
closing, and on the server's end_of_stream emit the buffered words as
the final transcript before returning. Raise APIError with the server's
message on `error` frames instead of logging them as unknown. Skip the
VAD silence flush once the input channel is closed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
They only talk to fake websockets, so they belong in the hermetic category that CI runs rather than the provider category. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Only the server's end_of_stream path emitted the words buffered since the last VAD finalization. The two other clean exits after end_input(), a close frame instead of the reply and the receive timeout, dropped them without a FINAL_TRANSCRIPT. Finalize on all three. A server-initiated end_of_stream while audio is still being sent (a session duration limit, for instance) was treated as a clean end: the receive task returned and the next audio write failed with a raw connection error the base class does not retry. Finalize, then raise a retryable APIStatusError so the stream reconnects. The final end_of_stream send is guarded: if the server closed the socket at the same moment, that is not an error. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Three commits on the Gradium STT stream's session end, plus the test harness they share.
delay_in_frames; the stream looked updelay_in_tokensand always fell back to 6. Production models run with a delay of 10, so a turn was finalized 4 steps (320 ms) before the model had emitted its last words, which then surfaced as a spurious new turn: aSTART_OF_SPEECH, a one-wordFINAL_TRANSCRIPT, anEND_OF_SPEECH.send_taskreturned without telling the server and theclosing_wsflag was never set, sorecv_taskpolled until the server dropped the idle socket two minutes later, which was then reported as an unexpected close and retried against a closed input. The unused_CLOSE_MSG(terminate_session) was another provider's protocol. Now{"type": "end_of_stream"}is sent once the input ends, and on the server'send_of_streamthe buffered words are emitted as the final transcript before the stream returns. Servererrorframes raiseAPIErrorwith the server's message instead of being logged as an unknown type, and the VAD silence flush is skipped once the input channel is closed.end_of_streampath emitted the words buffered since the last VAD finalization; a close frame in its place, or the receive timeout while closing, dropped them. All three exits finalize now. A server-initiatedend_of_streamwhile audio is still being sent (a duration limit, for instance) used to be treated as a clean end, after which the next audio write failed with a raw, non-retryable connection error; it now finalizes and raises a retryableAPIStatusErrorso the stream reconnects. The finalend_of_streamsend is guarded against the server closing first.Testing
tests/test_plugin_gradium_stt.pygains a scripted fake websocket that replaysready,textandstepframes, blocks until the client'send_of_streamlike a live socket, then replies with the server's EOS and a normal close. Six tests: a positive VAD finalizes after the delay the ready message announced;end_input()sendsend_of_streamand the stream ends with the pending words as a final transcript; anerrorframe raises with the server message; a close in place of the server's reply still yields the final transcript; a serverend_of_streammid-input finalizes then raises a retryable error; the final send on an already-closed socket is not an error. All fail onmain. The module is hermetic and now carries theunitmarker so CI runs it.ruff,ruff format --checkandmypy --strictclean on the plugin.api.gradium.aiwith the repository's 46 s speech sample streamed in real time: one turn, and the stream ended 0.5 s afterend_input(). Onmainthe same run split the speech into two turns and, afterend_input(), hung until the server closed the socket with 1008 "Session produced no output for 120 seconds". WER against the reference was 0.034 on 2026-10-14 and 0.074 on 2026-10-16, when the server returned lowercase, unpunctuated text that the scorer counts word by word; the plugin relays the text unchanged.Related
#7814 reports the configured model name for STT as well. The remaining STT fixes (cumulative interim transcripts,
vad_bucket=0, connect timeout) are stacked on this PR as the next one.🤖 Generated with Claude Code