Skip to content

Blaze Livekit plugin integration - #5050

Open
HoangPN711 wants to merge 78 commits into
livekit:mainfrom
Actable-AI:feat/blaze-voicebot-plugin
Open

Blaze Livekit plugin integration#5050
HoangPN711 wants to merge 78 commits into
livekit:mainfrom
Actable-AI:feat/blaze-voicebot-plugin

Conversation

@HoangPN711

@HoangPN711 HoangPN711 commented Mar 9, 2026

Copy link
Copy Markdown

Summary

Add Blaze plugin support for LiveKit Agents.

Changes

  • add Blaze STT plugin
  • add Blaze TTS plugin
  • add Blaze LLM plugin
  • add Blaze plugin packaging/config helpers
  • add Blaze plugin README

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-blaze and follows the existing provider plugin pattern.

@CLAassistant

CLAassistant commented Mar 9, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@HoangPN711 HoangPN711 changed the title feat: add blaze voicebot plugin Blaze Livekit plugin integration Apr 3, 2026
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@HoangPN711
HoangPN711 force-pushed the feat/blaze-voicebot-plugin branch from 4653f88 to 77090e4 Compare April 18, 2026 15:06
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@HoangPN711

Copy link
Copy Markdown
Author

Hi @tinalenguyen,

This PR adds the Blaze provider integration for LiveKit Agents and keeps the implementation scoped under livekit-plugins/livekit-plugins-blaze.

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!

@tinalenguyen

Copy link
Copy Markdown
Member

hi @HoangPN711! thank you for the contribution, could you bump the version and add the plugin to this pyproject file

@HoangPN711

HoangPN711 commented May 13, 2026

Copy link
Copy Markdown
Author

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)
Added blaze = ["livekit-plugins-blaze>=1.5.9"] to livekit-agents/pyproject.toml

Please let me know if any further changes are needed.

@HoangPN711
HoangPN711 force-pushed the feat/blaze-voicebot-plugin branch 3 times, most recently from 2b94037 to e9052a6 Compare May 19, 2026 22:52
devin-ai-integration[bot]

This comment was marked as resolved.

@HoangPN711
HoangPN711 force-pushed the feat/blaze-voicebot-plugin branch 3 times, most recently from 1cea05c to 2c82c75 Compare June 11, 2026 22:13
- 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.
@trunghlt

Copy link
Copy Markdown

Addressed latest Devin findings in fd5540c61:

  1. TTS pre-audio drop abandoned the turn β€” reconnect/retry now uses audio_emitted (real push) instead of stream_initialized (can flip true on started-byte-stream alone).
  2. STT pending PCM shared across instances β€” pending buffer is keyed by (task, STT instance id) so FallbackAdapter with multiple Blaze STTs cannot mix audio.

Unit tests: tests/test_plugin_blaze.py (58 passed locally).

devin-ai-integration[bot]

This comment was marked as resolved.

- 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.
@trunghlt

Copy link
Copy Markdown

Addressed latest Devin findings in d59ab2bc2:

  1. TTS mid-turn reader failures delayed until end of text β€” race reader_task against input drain / reconnect resend (FIRST_COMPLETED) so errors abort the pump immediately.
  2. Bearer over plaintext ws:// for .local hosts β€” only true loopback keeps ws://; mDNS/LAN .local now forces wss://.

Unit tests: tests/test_plugin_blaze.py (58 passed).

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.
devin-ai-integration[bot]

This comment was marked as resolved.

- 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.
@trunghlt

Copy link
Copy Markdown

Addressed latest Devin findings in 2d4cfc250:

  1. STT pending keyed by id(self) β†’ task-local WeakKeyDictionary on the instance (no GC/id-reuse leak)
  2. Blank/invalid timeout env crashes startup β†’ _env_float with safe fallback
  3. User speech/transcript at INFO β†’ content at DEBUG; metrics remain INFO

Unit tests: 60 passed (tests/test_plugin_blaze.py).

devin-ai-integration[bot]

This comment was marked as resolved.

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-ai-integration[bot]

This comment was marked as resolved.

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-ai-integration[bot]

This comment was marked as resolved.

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-ai-integration[bot]

This comment was marked as resolved.

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-ai-integration[bot]

This comment was marked as resolved.

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.
devin-ai-integration[bot]

This comment was marked as resolved.

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.
devin-ai-integration[bot]

This comment was marked as resolved.

- 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.
devin-ai-integration[bot]

This comment was marked as resolved.

@trunghlt

Copy link
Copy Markdown

Hi @tinalenguyen β€” friendly re-review nudge when you have a moment.

Status on 6ae1f7da7:

  • CI green: ruff, unit-tests, type-check (3.10/3.13), blockguard, release gate, CLA
  • Devin Review SUCCESS (latest findings addressed; remaining E501 note is a false positive β€” E501 ignored project-wide and line is exactly 100 chars)
  • Prior review points from your 2026-07-24 pass remain addressed (blaze.vn URLs, authors, py.typed, PCMβ†’WAV via rtc.AudioFrame.to_wav_bytes(), framework SentenceTokenizer, tests)
  • MERGEABLE; only blocked on review approval

Happy to address any further feedback. Thanks!

@trunghlt

Copy link
Copy Markdown

Hi @tinalenguyen β€” friendly re-review nudge when you have a moment.

Status on 6ae1f7da7:

  • CI green: ruff, unit-tests, type-check (3.10/3.13), blockguard, release gate, CLA
  • Devin Review SUCCESS (latest findings addressed; remaining E501 note is a false positive β€” E501 ignored project-wide and ruff is green)
  • Branch is MERGEABLE with main
  • Prior review feedback (README/blaze.vn, authors, py.typed, PCMβ†’WAV, tokenizer) already addressed in earlier commits

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.
@trunghlt

Copy link
Copy Markdown

Automated fix: resolved merge conflicts with origin/main (1.6.10).

  • Kept main’s optional-deps pins at >=1.6.10 / browser >=0.2.10
  • Re-added blaze = ["livekit-plugins-blaze>=1.6.10"]
  • Bumped livekit-plugins-blaze to 1.6.10 to match workspace pins

Head: eb3f6b1af

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 9 additional findings in Devin Review.

Open in Devin Review

Comment on lines +935 to +938
finally:
# Always cancel and await reader_task to prevent
# "Task destroyed but pending" / exception-not-retrieved.
await utils.aio.gracefully_cancel(reader_task)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟑 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.

Suggested change
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()
Open in Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

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.

5 participants