Repository navigation
fix(gradium): identify the TTS plugin to the API with x-api-source (1/4) - #7815
Conversation
28c51f2 to
e28149a
Compare
e28149a to
f9c472a
Compare
f9c472a to
87a77bb
Compare
87a77bb to
e588b1a
Compare
e588b1a to
02e1d2e
Compare
364926d to
f522e3f
Compare
f522e3f to
6379d2f
Compare
6379d2f to
8f6a87d
Compare
The TTS streams opened their websockets with only the API key, so TTS sessions were recorded by the API with no source, while the STT stream has sent `x-api-source: livekit-plugin` since livekit#7873. The `_connect_ws` helper on the TTS class that did set the header was never called. Send the same value from TTS through the shared `ws_headers(api_key)` in `api.py`. Make `_connect_ws` the single connect path for both TTS streams, keeping both the receive timeout the call sites set and the handshake bound the helper already had, and delete the unused `_close_ws` (supersedes livekit#7220). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
8f6a87d to
78c21af
Compare
There was a problem hiding this comment.
Devin Review found 3 new potential issues.
2 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)
There was a problem hiding this comment.
🔴 Premature closure delivers truncated speech
When Gradium closes after partial audio, ChunkedStream returns success without an end-of-stream frame. The TTS base checks for some audio, not complete audio, so callers receive truncated speech.
(Refers to this code)
Learn more
Both TTS paths consume WebSocket frames until synthesis is complete. A close before the end_of_stream response can also mean a worker failure or an interrupted connection. This branch now exits normally and flushes already-received audio; the streaming branch also ends its segment normally. The base synthesis checks and base stream checks accept a partial result once audio has been emitted, so neither detects the truncated response.
Example: Gradium sends 0.5 seconds of a two-second utterance and closes with code 1011. The caller receives 0.5 seconds as a successful synthesis instead of a failure.
Recommended fix: Track protocol completion separately from socket closure. Raise an API error on a premature close in both receive loops, retaining the close code and retryability where applicable; only treat a close as success if the API contract explicitly guarantees it marks completion.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
🔴 Provider failures lose their error details
When Gradium sends a type: error frame, ChunkedStream and SynthesizeStream ignore its message and code. Callers get a generic failure without the provider's reason or correct retry policy.
(Refers to this code)
Learn more
Gradium reports synthesis failures in a JSON text frame with type: error, a message, and a WebSocket close code. Both receive loops now pass this frame to the unknown-message warning instead of surfacing it. If no audio arrives, the base synthesis check or base stream check supplies a generic no-audio failure. A request rejected for an invalid model can then be retried rather than failing immediately; if partial audio already arrived, it can even appear successful.
Example: An error frame with message Unknown model 'nope' and code 1003 followed by a close produces a generic no-audio failure, with no model name and potentially unnecessary retries.
Recommended fix: Handle type: error explicitly in both receive loops. Preserve its message, code, and non-retryable protocol-error policy when raising the provider error.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
🔴 Closed streaming socket leaves synthesis waiting
When Gradium closes before the caller ends input, recv_task completes while send_task waits for more words. The surrounding gather waits for send_task, leaving synthesis stuck on a closed socket.
(Refers to this code)
Learn more
Streaming synthesis runs a sender that reads text from a word stream alongside a WebSocket receiver. The sender waits for further input until the application flushes it, and gather waits for both tasks. On a close, the receiver now ends the segment and returns normally, leaving a sender with no input waiting indefinitely; previously the close raised an error and the pending sender was cancelled.
Example: An agent pushes hello , pauses for more LLM tokens, and Gradium closes the socket. The streaming TTS operation keeps waiting for tokens rather than reporting the lost connection.
Recommended fix: Raise a provider/connection error on unexpected closure so the gather exits and the finally block cancels the sender. If normal close before input termination is supported, explicitly cancel the sender and decide how to account for unsent input.
Was this helpful? React with 👍 or 👎 to provide feedback.
|
@tinalenguyen, this is now rebased on top of #7873 |
|
cc @longcw |
Summary
The TTS streams opened their websockets with only the API key, so the Gradium API recorded TTS sessions with no source, while the STT stream has sent
x-api-source: livekit-pluginsince #7873. The_connect_wshelper on the TTS class that did set the header was never called.TTS now sends the same value through the
ws_headers(api_key)helper that #7873 added inapi.py._connect_wsbecomes the single connect path for both TTS streams, keeping the receive timeout the call sites set and theasyncio.wait_forbound on the handshake the helper already had, and the unused_close_wsis deleted, so this supersedes #7220. The value identifies sessions opened by this plugin, as distinct from direct API traffic on the same key and from other LiveKit integrations. The header is attribution only; the API does not change behaviour on it.Testing
tests/test_plugin_gradium_api_source.py(unit category, from fix(gradium): identify the STT plugin to the API with x-api-source: livekit-plugin #7873) gains a recording session that asserts the headers of both TTS connect paths (synthesize()andstream()), next to the STT test already there. The same module gains two tests for a hung handshake, which now fails withAPITimeoutErrorafterconn_options.timeouton both paths.tests/test_plugin_gradium_stt.pystill passes.ruff,ruff format --checkandmypy --strictclean on the plugin.api.gradium.ai: one-shot TTS and streaming TTS sessions recorded withsource = livekit-plugin.Base of the stack
This PR sits directly on
main. #7672 (error handling), #7817 (heartbeat) and #7818 (multiplexing) are stacked on it in that order, because all of them build on the single_connect_wspath this PR introduces.🤖 Generated with Claude Code