Skip to content

feat(telnyx): add Telnyx LLM, STT and TTS - #629

Merged
Nash0x7E2 merged 10 commits into
mainfrom
feat/telnyx-ai-plugins
Aug 13, 2026
Merged

feat(telnyx): add Telnyx LLM, STT and TTS#629
Nash0x7E2 merged 10 commits into
mainfrom
feat/telnyx-ai-plugins

Conversation

@Nash0x7E2

Copy link
Copy Markdown
Member

Why

The Telnyx plugin is a phone transport today. @a692570 contributed the three AI modalities that make a Telnyx phone agent run end to end, as #620 (LLM), #621 (TTS), and #622 (STT). This branch consolidates all three and applies review fixes on top. The original commits are preserved with their authorship; @a692570 offered to fold the three PRs together, and plugins/sarvam/ — the precedent all three cite — landed its LLM, STT, and TTS in a single PR (#488).

Consolidating also settles decisions the three PRs answered inconsistently on their own. #620 argues against pinning a provider catalogue locally ("a hardcoded allowlist would go stale") while #622 shipped a hardcoded SUPPORTED_ENGINES set; that set is now gone, since Telnyx keeps adding engines and the server already rejects unknown ones with an explicit error.

Two fixes came out of measuring the live API rather than reading docs:

telnyx.STT defaulted to interim_results=True on the default Telnyx engine, which accepts the parameter and returns finals only — the same silent-finals-only trap #622 documents for partial_results, but undetected, because both integration tests asserted on finals. Running the same audio through each engine: Speechmatics streams 21 partials and Soniox 41, while Telnyx and Deepgram return none. The default is now False, the behaviour is documented per engine, and an integration test covers the partial-transcript path against an engine that actually emits them — that path had no live coverage before.

The TTS spends roughly 120 lines decoding MP3 and stripping ID3 tags, and the endpoint does take an audio_format parameter, so raw PCM looked like a way to delete all of it. It is not: AWS.Polly.* and Telnyx.NaturalHD.* honour audio_format=linear16, but the default Telnyx.KokoroTTS.* returns MP3 regardless, and the PCM sample rate is not reported on the wire and differs per voice. A voice-to-rate table would be exactly the kind of local catalogue this PR is removing elsewhere, so the MP3 path stays and the reasoning is now written down. Measuring also retired a suspicion that the decoder needed flushing: decoding with and without a flush returns an identical sample count on both voices, so no audio is being clipped.

Rebasing onto main also surfaced a hard failure. #618 added model: str to the STT base class after these PRs branched, so self.model = model with Optional[str] no longer type checks.

Changes

  • telnyx.LLM, telnyx.STT, and telnyx.TTS, plus tests and README sections (@a692570, feat(telnyx): add Telnyx LLM plugin #620/feat(telnyx): add Telnyx TTS plugin #621/feat(telnyx): add Telnyx STT plugin #622)
  • plugins/telnyx/examples/voice_agent_call.py: an inbound call answered by an all-Telnyx pipeline, reusing the existing example helpers
  • STT: model typed str, interim_results defaults to False, SUPPORTED_ENGINES removed, close() flattened to match sarvam, no duplicate error event on the close that follows an error payload, audio dropped rather than blocking forever when start() has not run, turn_detection declared explicitly
  • TTS: voice reported as model so agent metadata is populated, a dedicated handshake timeout, connect/disconnect emitted once per plugin instead of once per utterance, PcmData.from_av_frame instead of hand-built PcmData
  • LLM: the redundant AsyncOpenAI construction dropped in favour of the base class's, and a single exported name
  • Tests: TTS test classes folded into one, static helpers replaced with fixtures, private-client assertions replaced with behavioural ones

Open questions for the reviewer

Dependencies. This makes vision-agents-plugins-openai and aiohttp hard dependencies of a plugin most people install for phone transport. The four plugins that depend on plugins-openai today (inworld, minimax, openai, sarvam) are all AI-vendor plugins; telnyx would be the first transport plugin to do it. Left as-is to match sarvam, but gating the AI modalities behind an extra is the alternative.

Duplication with sarvam. stt.py's _receive_loop, process_audio, and _build_ws_url, and tts.py's _receive_audio, are near-copies of the sarvam equivalents — roughly 120 lines of aiohttp WebSocket scaffolding now living in two plugins. Extracting a shared base into agents-core would rewrite a shipped plugin from inside a plugin-addition PR, so it is left for a follow-up. Two of the smaller fixes here (the duplicate error event, and process_audio blocking when start() never ran) apply to sarvam as well.

Supersedes #620, #621, #622.

Telnyx Inference serves an OpenAI-compatible /v2/ai/chat/completions
endpoint, so the LLM is a thin ChatCompletionsLLM subclass pointed at
the Telnyx base URL with bearer auth. Follows the plugins/sarvam
precedent of one vendor plugin covering LLM, STT, and TTS.
Streams text to speech over the Telnyx WebSocket endpoint and decodes the
returned MP3 into PcmData as it arrives. Telnyx closes the socket after
each stop frame, so the plugin connects per synthesis rather than holding
one socket open. Follows the plugins/sarvam precedent of one vendor plugin
covering LLM, STT, and TTS.
Comment thread plugins/telnyx/examples/voice_agent_call.py Fixed
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 09f01aae-6997-4e1e-a1fd-e509c94a8b7e

📥 Commits

Reviewing files that changed from the base of the PR and between 811f2cd and 45d4a6b.

📒 Files selected for processing (2)
  • plugins/telnyx/tests/test_telnyx_tts.py
  • plugins/telnyx/vision_agents/plugins/telnyx/tts.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • plugins/telnyx/tests/test_telnyx_tts.py
  • plugins/telnyx/vision_agents/plugins/telnyx/tts.py

📝 Walkthrough

Walkthrough

Added Telnyx LLM, streaming STT, and streaming TTS integrations with API-key configuration and lifecycle handling. Exported the integrations through the package interface. Added unit and opt-in integration tests. Added an inbound Telnyx voice-agent example with webhook verification, media streaming, call setup, cleanup, CLI options, and startup validation. Updated documentation and dependencies.

Mergeability Score: ⚪ Minimal · up to 45d4a

This PR adds Telnyx LLM, STT, and TTS support with related tests and documentation; no actionable merge-blocking risk remains beyond normal checks and review.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
plugins/telnyx/tests/test_telnyx_stt.py (1)

120-126: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the emitted error, not just the absence of an exception.

test_send_failure_does_not_propagate passes even if process_audio silently drops the failure without reporting it. Collect the output and assert that the send failure surfaces as an error event, so the test covers the behavior of _emit_error_event.

Source: Path instructions

plugins/telnyx/vision_agents/plugins/telnyx/tts.py (1)

143-152: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Close the WebSocket in close().

close() releases the aiohttp session but leaves self._ws set and open. If close() runs while a synthesis is in flight, the socket is torn down only as a side effect of the session close, and self._ws keeps a stale reference. Call _close_ws() first.

♻️ Proposed change
     async def close(self) -> None:
         """Close the current WebSocket and release the aiohttp session."""
         await super().close()
+        await self._close_ws()
         session = self._session
         self._session = None

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 871d2669-480e-4bea-abd9-5c6202ebf214

📥 Commits

Reviewing files that changed from the base of the PR and between d98a1f4 and 592f833.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • CHANGELOG.md
  • plugins/telnyx/README.md
  • plugins/telnyx/examples/README.md
  • plugins/telnyx/examples/voice_agent_call.py
  • plugins/telnyx/pyproject.toml
  • plugins/telnyx/tests/test_telnyx_llm.py
  • plugins/telnyx/tests/test_telnyx_stt.py
  • plugins/telnyx/tests/test_telnyx_tts.py
  • plugins/telnyx/vision_agents/plugins/telnyx/__init__.py
  • plugins/telnyx/vision_agents/plugins/telnyx/llm.py
  • plugins/telnyx/vision_agents/plugins/telnyx/stt.py
  • plugins/telnyx/vision_agents/plugins/telnyx/tts.py

Comment thread plugins/telnyx/examples/voice_agent_call.py Outdated
Comment thread plugins/telnyx/README.md Outdated
Comment on lines +269 to +273
<<<<<<< HEAD
- vision-agents-plugins-openai
=======
>>>>>>> 4c8d97ad (feat(telnyx): add Telnyx STT plugin)
- aiohttp

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.

📐 Maintainability & Code Quality | 🔴 Critical | ⚡ Quick win

Unresolved merge conflict markers in the README.

Lines 269-273 still contain <<<<<<< HEAD, =======, and >>>>>>> 4c8d97ad. This README is the PyPI description page for the plugin, so the markers ship to users. Resolve the conflict and keep vision-agents-plugins-openai, which plugins/telnyx/pyproject.toml now declares as a dependency.

🐛 Proposed fix
 - vision-agents
-<<<<<<< HEAD
 - vision-agents-plugins-openai
-=======
->>>>>>> 4c8d97ad (feat(telnyx): add Telnyx STT plugin)
 - aiohttp

a692570 and others added 5 commits August 10, 2026 19:22
Streams audio to the Telnyx transcription WebSocket as raw linear16 frames
and emits transcripts as they arrive. The configurable sample rate lets
telephony audio from TelnyxMediaStream pass through at 8kHz without an
upsample. Follows the plugins/sarvam precedent of one vendor plugin
covering LLM, STT, and TTS.
LLM
- Drop the hand-built AsyncOpenAI client; ChatCompletionsLLM already builds
  one from api_key/base_url. Follows the minimax plugin.
- Export a single LLM name, matching the sarvam plugin.

STT
- self.model is str, not Optional[str]. Since #618 the STT base class
  declares `model: str`, so the Optional assignment failed mypy.
- Default interim_results to False. Measured against the live API with the
  same audio: Speechmatics and Soniox stream partials, while the default
  Telnyx engine and Deepgram accept the parameter and return finals only.
  The old default advertised partials that never arrived.
- Drop the SUPPORTED_ENGINES allowlist. The catalogue is served by Telnyx
  and grows over time, and the server already rejects unknown engines with
  an explicit error.
- Flatten close() to the same shape as the sarvam plugin.
- Do not emit a second error event for the socket close that an error
  payload causes.
- Drop audio instead of blocking forever when start() has not run.
- Declare turn_detection explicitly.

TTS
- Report the voice as `model` so agent metadata is populated.
- Give the handshake its own timeout instead of reusing idle_timeout.
- Emit connected/disconnected once per plugin rather than once per
  utterance; this provider opens a socket per synthesis.
- Build PcmData with PcmData.from_av_frame.

Tests
- Fold the TTS test classes into one and replace static helpers with
  fixtures.
- Cover the interim-transcript path end to end against an engine that
  emits partials; it had no live coverage.
- Assert behaviour rather than private client attributes.
- Add `examples/voice_agent_call.py`: an inbound call answered by a pipeline
  that runs entirely on Telnyx, reusing the existing example helpers. Turn
  detection is local because the transcription endpoint sends no VAD signals.
- Add a CHANGELOG entry, as the speechify plugin did when it landed.
- Document what the live API actually does: which engines honour
  `interim_results`, and why the TTS decodes MP3 for every voice even though
  `audio_format` exists.
…f sensitive information'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
@Nash0x7E2
Nash0x7E2 force-pushed the feat/telnyx-ai-plugins branch from 7608dea to 811f2cd Compare August 11, 2026 01:24
@Nash0x7E2
Nash0x7E2 marked this pull request as ready for review August 11, 2026 01:47
@Nash0x7E2 Nash0x7E2 self-assigned this Aug 11, 2026
stop_audio() set the stop event and closed the WebSocket, but the receive
loop only checked the event once per WebSocket frame. Telnyx serves a whole
synthesis in very few large frames, so a single payload decodes to hundreds
of PcmData chunks and the check never ran again: measured against the live
API, stopping after 3 chunks still delivered all 445, the same as an
uninterrupted synthesis.

The Agent's own barge-in was unaffected, because interrupt() also bumps the
epoch and send_iter() checks that per chunk. Direct users of stream_audio()
plus stop_audio() got the whole utterance regardless.

Check the stop event per decoded chunk as well. Stopping after 3 chunks now
yields 3. The integration test pins this against an uninterrupted baseline.
@Nash0x7E2
Nash0x7E2 merged commit a935f32 into main Aug 13, 2026
14 checks passed
@Nash0x7E2
Nash0x7E2 deleted the feat/telnyx-ai-plugins branch August 13, 2026 05:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants