Blaze Livekit plugin integration - #5050
Conversation
4653f88 to
77090e4
Compare
|
Hi @tinalenguyen, This PR adds the Blaze provider integration for LiveKit Agents and keeps the implementation scoped under It follows the existing STT/TTS/LLM plugin structure used by other provider integrations. Iβve also addressed the outstanding review comments on the PR. Would appreciate a review when you have time. Thanks! |
|
hi @HoangPN711! thank you for the contribution, could you bump the version and add the plugin to this pyproject file |
|
Hi @tinalenguyen , thanks for reviewing this PR! Updated as requested: Bumped livekit-plugins-blaze to 1.5.9 (aligned with the current livekit-agents release) Please let me know if any further changes are needed. |
2b94037 to
e9052a6
Compare
1cea05c to
2c82c75
Compare
- Track audio_emitted separately from stream_initialized so a WS drop after started-byte-stream but before first PCM stays private-reconnect and framework-retryable. - Key STT empty-segment PCM buffer by (task, STT instance id) so FallbackAdapter with multiple Blaze STTs cannot mix pending audio.
|
Addressed latest Devin findings in
Unit tests: |
- Race audio reader against input drain / reconnect resend so failed-request, close, or idle timeout abort the text pump immediately. - Restrict plaintext ws:// to true loopback only (drop .local exception) so bearer tokens never cross the LAN in cleartext; handle [IPv6] hosts.
|
Addressed latest Devin findings in
Unit tests: |
Homogeneous Task[Any] cast for asyncio.wait and wrap drain in create_task so type-check (3.10/3.13) passes after mid-turn race.
- Key task-local STT pending PCM with WeakKeyDictionary so GC'd instances cannot leak audio via recycled id(self). - Parse timeout env vars with empty/invalid fallback (no crash). - Move TTS query text and STT transcript content to DEBUG; keep length/metrics at INFO.
|
Addressed latest Devin findings in
Unit tests: 60 passed ( |
Devin found that JSON lines that decode to str/int/list (e.g. keepalive) raise AttributeError in _extract_tool_calls and abort a partially delivered reply as a non-retryable APIConnectionError. Skip non-dict payloads with a warning; add a regression test.
Devin: effective_connect_timeout only matched DEFAULT_API_CONNECT_OPTIONS by identity, so stream-adapter/voice copies kept the short framework timeout and ignored BLAZE_*_TIMEOUT. Compare timeout values instead. Also strip URL userinfo in ws_base_url so a misconfigured BLAZE_API_URL cannot redirect the first-frame bearer token via embedded credentials.
Devin: empty-segment pending PCM was prepended without comparing the stored sample_rate/num_channels to the new segment, so a format change garbled the WAV header and transcription. Discard pending (and reset empty_count) on mismatch; compute pending_duration with capture-time format. Regression test included.
Devin: SpeechStream._run set closing_ws when the input channel drained but never closed the WebSocket, so async-for on recv hung until task cancel. After end_input, wait a short grace for trailing finals then close the WS so _run completes cleanly on intentional shutdown.
Devin: empty pcm path returned alternatives=[], while all other paths return one SpeechData β callers that index alternatives[0] crash. Return a single empty SpeechData (text="", confidence=0.0) for shape parity. Update unit test.
The gateway protocol (and agents-js) does not define a speech-start acknowledgement. Unconditional recv after speech-start either stalled until idle timeout (60s) when the server waits for a query, or dropped the first status/audio frame when pipelined. Let the normal reader loop handle whatever follows speech-start.
- Emit END_OF_SPEECH on SpeechStream teardown if speaking and no final arrived (grace close / peer drop left the user turn open). - Normalize api_url (config + STT/TTS/LLM constructors) so trailing slashes never produce //v1/... HTTP paths.
|
Hi @tinalenguyen β friendly re-review nudge when you have a moment. Status on
Happy to address any further feedback. Thanks! |
|
Hi @tinalenguyen β friendly re-review nudge when you have a moment. Status on
Happy to rebase or take any further feedback. Thanks! |
Keep main's plugin optional-deps pins at 1.6.10 and re-add blaze entry. Bump livekit-plugins-blaze to 1.6.10 to match workspace pins.
|
Automated fix: resolved merge conflicts with
Head: |
| finally: | ||
| # Always cancel and await reader_task to prevent | ||
| # "Task destroyed but pending" / exception-not-retrieved. | ||
| await utils.aio.gracefully_cancel(reader_task) |
There was a problem hiding this comment.
π‘ Audio-reader failures can be swallowed and surface later as stray asyncio error logs
The background audio reader is only cancelled and waited on (utils.aio.gracefully_cancel(reader_task) at livekit-plugins/livekit-plugins-blaze/livekit/plugins/blaze/tts.py:938) without ever collecting the error it already failed with, so a failed speech turn can later print an unrelated "exception was never retrieved" error in the logs.
Impact: Operators see confusing stray error tracebacks that are not tied to the request that actually failed.
Why cancel-only does not consume a completed task's exception
utils.aio.gracefully_cancel is an alias of cancel_and_wait (livekit-agents/livekit/agents/utils/aio/utils.py:1-24): it attaches a done-callback, calls fut.cancel() and awaits an internal waiter. It never calls task.result()/task.exception(), so for a task that had already completed with an exception the exception stays unretrieved and asyncio logs it when the task is garbage collected.
Reachable path: input drains successfully, then await ws.send(json.dumps({"event": "speech-end"})) (livekit-plugins/livekit-plugins-blaze/livekit/plugins/blaze/tts.py:928) raises ConnectionClosed before await reader_task on line 930 is reached. Control jumps to the finally on lines 935-938, which only cancels the reader β but _read_audio has typically already terminated with its own ConnectionClosed/APITimeoutError. That exception is never retrieved.
A safe fix is to await the reader inside a try/except Exception: pass (or check reader_task.done() and read reader_task.exception()) before/after cancelling.
| finally: | |
| # Always cancel and await reader_task to prevent | |
| # "Task destroyed but pending" / exception-not-retrieved. | |
| await utils.aio.gracefully_cancel(reader_task) | |
| finally: | |
| # Always cancel and await reader_task to prevent | |
| # "Task destroyed but pending" / exception-not-retrieved. | |
| await utils.aio.gracefully_cancel(reader_task) | |
| if reader_task.done() and not reader_task.cancelled(): | |
| # Consume any pending reader exception so asyncio | |
| # does not log "exception was never retrieved". | |
| reader_task.exception() |
Was this helpful? React with π or π to provide feedback.
Summary
Add Blaze plugin support for LiveKit Agents.
Changes
Motivation
Enable Blaze voice AI services to be used through the existing LiveKit Agents plugin architecture.
Notes
The implementation is isolated under
livekit-plugins/livekit-plugins-blazeand follows the existing provider plugin pattern.