sparse-napkin-34305
12/16/2025, 5:45 PMtall-airline-16756
12/16/2025, 5:45 PMtall-airline-16756
12/16/2025, 5:45 PMaudioEnabled: false in `outputOptions`:
await session.start({
agent,
room: ctx.room,
outputOptions: {
audioEnabled: false,
},
});
With audioEnabled: false the agent will not invoke the TTS system, no audio tracks are published, and text responses still arrive via the lk.transcription text stream. Would you like a snippet showing how to toggle this dynamically at runtime?
Sources: Text and transcriptions | LiveKit docssparse-napkin-34305
12/16/2025, 5:49 PMtall-airline-16756
12/16/2025, 5:49 PMOutputOptions type/object.
• For Node.js/TypeScript: import the voice agent module and pass the options inline:
import { voice } from '@livekit/agents';
await session.start({
room: ctx.room,
agent: myAgent,
outputOptions: {
audioEnabled: false,
},
});
• For Python: use room_io.RoomOptions from the voice agents package:
from livekit.agents.voice import room_io
await session.start(
room=ctx.room,
agent=my_agent,
room_options=room_io.RoomOptions(
audio_output=False
)
)
If you want TypeScript type hints for outputOptions, tell me whether you're using plain JS or TS and I’ll provide the exact type import (if available).
Sources: Agent session | LiveKit docs | Text and transcriptions | LiveKit docssparse-napkin-34305
12/16/2025, 5:52 PMtall-airline-16756
12/16/2025, 5:52 PM// frontend: call RPC when button toggled
await room.localParticipant.sendRpc('toggle_sound', { enabled: false });
// backend: registered RPC handler
ctx.room.localParticipant.registerRpcMethod('toggle_sound', async (data) => {
session.output.setAudioEnabled(data.enabled);
});
• Python (backend RPC handler):
@ctx.room.local_participant.register_rpc_method('toggle_sound')
async def toggle_sound(data):
session.output.set_audio_enabled(data['enabled'])
Notes:
• Ensure the session started with audio enabled so you can toggle it later.
• While audio is disabled, text replies still arrive via `lk.transcription`; TTS will not be invoked or published.
Do you want a full frontend example for your stack (React/Vue/Flutter) to wire the button to the RPC?
Sources: Text and transcriptions | LiveKit docs | Agent session | LiveKit docssparse-napkin-34305
12/16/2025, 5:54 PMtall-airline-16756
12/16/2025, 5:54 PMsession.output.set_audio_enabled(False) (or starting the session with audioEnabled: false) stops the TTS pipeline from being invoked. It does not merely mute playback.
• TTS synthesis is not performed when audio is disabled.
• No audio tracks are published and you won't incur TTS processing costs.
• Text replies continue to be sent via the lk.transcription stream.
If you'd like, I can provide the exact code snippet for your stack (TS or Python) to toggle this from a UI button and ensure the session was started so toggling is valid. Which stack are you using?
Sources: Text and transcriptions | LiveKit docssparse-napkin-34305
12/16/2025, 5:59 PMtall-airline-16756
12/16/2025, 6:00 PMsession.output.set_audio_enabled(False), let's troubleshoot quickly so we can stop TTS synthesis and costs.
Quick checklist — please confirm each item and share short answers or a code snippet:
1. Where are you calling the toggle? Is session.output.set_audio_enabled(False) (Python) or session.output.setAudioEnabled(false) (TS) being called after the session is started and before/while the LLM is about to speak?
2. Is any speech already queued or in-progress when you toggle? If so, call session.interrupt() immediately after disabling audio to stop in-flight synthesis/playback.
3. Which SDK and version are you using (Node/Python + version)?
4. How are you verifying TTS audio is being produced? (Hearing audio in client, seeing audio tracks published, observing TTS metrics/logs)
5. Are you calling any other APIs that could synthesize or publish audio (e.g., say() with pre-synthesized audio, BackgroundAudioPlayer, or external TTS calls)?
6. Do you have any frontend code that might be playing cached or fallback audio from lk.transcription or other sources?
Short-term fixes to try now:
• After disabling audio, call interrupt to stop any ongoing TTS:
◦ Python: session.interrupt()
◦ TypeScript: session.interrupt()
• Add logging around session.output.audio_enabled and TTS-related metrics/events to confirm tts_node is not being invoked.
• Verify the session was originally started with audio enabled so toggling is allowed.
If you share your SDK/version and the small code path where you toggle (and how you detect the audio), I’ll point out exactly where to change it or provide a minimal reproducible snippet to ensure TTS is never invoked.
Sources: Text and transcriptions | LiveKit docs | Agent speech and audio | LiveKit docs | Agents (Node.js) | livekit/livekit_composite | DeepWiki | Observability data hooks | LiveKit docssparse-napkin-34305
12/16/2025, 6:02 PMtall-airline-16756
12/16/2025, 6:02 PMsession.output.set_audio_enabled(False) should stop TTS synthesis entirely — it does not merely mute playback. If you are seeing TTS still produce audio (even if not sent to the client), that indicates either in-flight speech, another TTS path still active, or an implementation detail we need to identify.
Please provide the following so I can pinpoint the cause quickly:
1. SDK and exact version (Node or Python).
2. Minimal code snippet showing:
◦ How you start the session (outputOptions / room_options).
◦ Where and when you call set_audio_enabled(False) (or setAudioEnabled(false)).
◦ Any calls to say(), BackgroundAudioPlayer, or other TTS APIs.
1. How you observe “TTS is still generating audio” — hearing audio, server logs, TTS metrics, or saved files?
2. Whether speech was already in-progress when you toggled sound off.
Quick troubleshooting steps to try now:
• Ensure you call session.output.set_audio_enabled(False) after the session is started and before the LLM begins generating speech.
• If speech may already be in progress, call session.interrupt() immediately after disabling audio to stop queued/in-flight synthesis.
• Check for other TTS calls (manual say() with pre-synthesized audio or external TTS usage).
• Verify TTS metrics/events/logs: if TTSMetrics events continue after disabling audio, share those log lines.
Once you share the SDK/version and snippet, I’ll give an exact code fix (or alternate method) to guarantee TTS synthesis is not invoked when the user turns sound off.sparse-napkin-34305
12/16/2025, 6:03 PMtall-airline-16756
12/16/2025, 6:04 PMsession.output.set_audio_enabled(False) stops the TTS pipeline from being invoked and prevents audio synthesis and publishing. It is not supposed to merely mute playback.
How to verify whether TTS is running in your setup (quick checks you can run now):
1. Inspect TTS metrics/events/logs
◦ Check for any TTS-related metrics or events (e.g., TTSMetrics). If they stop after disabling audio, TTS is not being invoked.
1. Add server-side logging around the TTS node
◦ Log when tts_node() or any internal TTS call is reached.
1. Watch for published audio tracks
◦ If no audio tracks are published after disabling audio, synthesis/publishing is disabled.
1. Check for queued/in-flight speech
◦ If speech was already queued, call session.interrupt() after disabling audio to cancel in-progress synthesis.
1. Search your code for alternate TTS paths
◦ Confirm you’re not calling say() with pre-synthesized audio, BackgroundAudioPlayer, or external TTS services.
If you want, paste:
• SDK (Node/Python) and version,
• The short code path where you start the session and toggle audio,
• Any logs showing TTS activity,
and I’ll point to the exact log lines or minimal code change to prove TTS is not running. Would you like to proceed with that?sparse-napkin-34305
12/16/2025, 6:26 PMtall-airline-16756
12/16/2025, 6:26 PMsession.output.set_audio_enabled(False) will prevent the agent from invoking the TTS pipeline (it does not simply mute playback). A few practical points specific to your code and how to confirm:
1. In-flight speech: If speech was already synthesizing when you flip sound off, synthesis already started may finish unless you interrupt it. Call session.interrupt() right after disabling audio to cancel any in-progress TTS.
2. Your Custom/Logging TTS wrappers: Your LoggingTTS wraps the real TTS and logs when synthesis happens. If you still see those log lines after calling session.output.set_audio_enabled(False), that means TTS was invoked (either in-flight or another path). If those logs stop, TTS is not being called.
3. Your ConditionalTTS: Returning a long-sleeping stream prevents playback but still can let upstream code attempt to call synthesize unless you short-circuit earlier. Prefer returning an immediately-empty stream or raising to avoid starting the underlying TTS:
◦ Instead of asyncio.sleep(10000), return an empty SynthesizeStream or skip calling the underlying self._tts.synthesize(...) entirely when sound_enabled is False.
◦ Or, before any call into TTS, check session.output.audio_enabled and skip synthesize entirely.
1. Quick verification steps:
◦ Toggle sound off and watch your 🎤 TTS synthesizing text: logs from LoggingTTS. No logs → TTS not invoked.
◦ Observe TTS metrics/events (TTSMetrics) — they should stop.
◦ Confirm no new audio tracks are published after disabling audio.
◦ If you suspect in-flight tasks, call session.interrupt() and watch that playback_finished with ev.interrupted fires.
If you want, I can:
• Suggest a minimal change to ConditionalTTS that never calls the wrapped TTS when sound is off, or
• Provide the exact log checks/placement to prove TTS is not running in your current runs.
Which would you prefer?sparse-napkin-34305
12/16/2025, 6:30 PMtall-airline-16756
12/16/2025, 6:30 PMsession.output.audio_enabled changes and call session.interrupt() immediately after disabling to cancel in-flight synthesis.
3) Inspect published tracks and player events
• Verify no new audio tracks are published after audio is disabled.
• Watch playback_finished events and TTS-related event traces to confirm synths stopped.
4) Short-term billing check
• Correlate timestamps of suspected TTS calls with the dashboard usage records or TTSMetrics to see incurred processing.
If you want, I can:
• Give the exact log-check snippet to add to your code (Python) to detect TTSMetrics and synthesis calls, or
• Point out exactly where to look in the LiveKit dashboard if you tell me which console view you see (Metrics or Usage). Which would you prefer?sparse-napkin-34305
12/18/2025, 4:44 AMtall-airline-16756
12/18/2025, 4:44 AMsession.output.set_audio_enabled(False) should stop TTS synthesis entirely (not just mute). To prove that in your app, rely on your LoggingTTS logs and TTS metrics: if the "🎤 TTS synthesizing text" log stops after you toggle off, TTS isn't being invoked.
• Your current LoggingTTS will show whether TTS is being called. If you still see those logs after disabling audio, synthesis is starting (likely in-flight or from another path).
• The AttributeError in your traceback (AudioStream has no attribute '_processor') indicates an SDK/runtime mismatch or unexpected internal state — that can allow stray audio-processing behavior. Fixing that will help ensure audio toggles behave predictably.
What to check / do next (minimal steps)
1. Immediately cancel in-flight synthesis when toggling off
◦ After session.output.set_audio_enabled(False) call session.interrupt() to stop any currently-synthesizing speech.
1. Make LoggingTTS definitive (stop calling underlying TTS when sound off)
◦ Update LoggingTTS.synthesize to short-circuit when session.output.audio_enabled (or your agent flag) is False and return an immediately-empty stream instead of calling the wrapped TTS. Example idea:
▪︎ If sound is off: log "skipping synth" and return an empty SynthesizeStream (or raise/return a no-op stream).
▪︎ This prevents any wrapped TTS from being invoked by mistake.
1. Verify via logs & metrics
◦ Toggle sound OFF and watch for:
▪︎ No "🎤 TTS synthesizing text" logs.
▪︎ No TTSMetrics events after the toggle.
▪︎ No new audio tracks published.
◦ If logs still show synthesis, capture the exact log lines/timestamps.
1. Fix the AudioStream AttributeError
◦ That traceback suggests the installed livekit package version and your runtime are inconsistent (internal attribute missing). Steps:
▪︎ Confirm your livekit and livekit-agents package versions are compatible.
▪︎ Upgrade/reinstall packages (pip install --upgrade livekit livekit-agents) or pin to recommended versions.
▪︎ Restart the worker after reinstalling.
◦ Once that error is gone, re-test toggling audio — the SDK will behave more reliably.
1. Check for other TTS paths
◦ Search for other say(), direct elevenlabs.TTS calls, BackgroundAudioPlayer, or any code that calls your TTS plugin directly outside the AgentSession pipeline. Ensure those paths also respect the audio flag or are gated by your sound_enabled.
If you want, I can:
• Provide the exact small code edits for LoggingTTS/Conditional short-circuiting (paste target function and I’ll return the snippet), and
• Suggest the exact pip commands and version pairs to try for fixing the AudioStream error.
Which would you like me to send next — the LoggingTTS short-circuit snippet or the package/version commands?sparse-napkin-34305
12/18/2025, 4:45 AMtall-airline-16756
12/18/2025, 4:45 AM_processor is missing), or by an object being used after a partial/failed initialization. Here’s how to troubleshoot and fix it quickly.
1) Immediate cause
• The code in livekit/rtc/audio_stream.py expects self._processor to exist, but the AudioStream instance doesn't have that attribute when _run() executes. That means either the AudioStream constructor didn't set _processor (version mismatch / bug) or the object was mutated/partially cleaned up before _run() finished.
2) Quick checks
• Confirm you’re running compatible package versions for livekit and livekit-agents:
◦ Run: pip show livekit livekit-agents
◦ Note the versions and share them if you want help mapping compatibility.
• Check whether the AudioStream object is being stopped/closed while _run() is still running (race condition). Look for early cleanup or exceptions during initialization in preceding logs.
3) Repro & fix steps
• Upgrade/reinstall to the latest compatible packages:
◦ pip install --upgrade livekit livekit-agents
◦ Or pin to a known-compatible pair if you have version constraints.
• Restart your worker after upgrading.
• If the error persists, capture the full traceback and the versions from pip show.
4) Hardening workaround in your app (temporary)
• Surround session start/stop with safe try/except or guard checks so a partially-initialized stream doesn't run:
◦ Ensure you don't call session.input.set_audio_enabled / session.output toggles before session.start completes.
◦ Add logging around session lifecycle events to see if _run() starts after a close.
5) If this is a regression
• If you upgraded recently and this started appearing, pin to the previous working versions you had and test.
If you share the output of:
• pip show livekit
• pip show livekit-agents
• the full, untruncated traceback (a few lines before and after),
I’ll point to the exact package pair or a code change to resolve this. Which would you like to send?sparse-napkin-34305
12/18/2025, 4:46 AMtall-airline-16756
12/18/2025, 4:47 AMlivekit (1.0.21) and livekit-agents (1.3.6). The Agents runtime expects an internal attribute on AudioStream that isn't present in the installed livekit package, causing the AttributeError.
What to do next (minimal, safe steps)
1. Upgrade both packages so they match expected compatibility:
• In your virtualenv run:
pip install --upgrade livekit livekit-agents
1. Restart your worker after upgrading.
2. Verify installed versions:
py -3.10 -m pip show livekit livekit-agents
Expected result: no AttributeError from AudioStream after restart.
If the error persists, capture and share:
• the full, untruncated traceback,
• the output of the pip show command after upgrade.
I can then suggest a specific version pair to pin or a workaround. Which would you like — I can provide the exact pip commands to pin versions or a small defensive code check to avoid the crash.sparse-napkin-34305
12/18/2025, 6:58 AMtall-airline-16756
12/18/2025, 6:58 AMawait session.start(
agent=agent,
room=ctx.room,
room_options=room_io.RoomOptions(
audio_input=room_io.AudioInputOptions(...),
),
)
# ensure STT won't run until explicitly enabled
session.input.set_audio_enabled(False)
• When user clicks mic/sound ON:
session.input.set_audio_enabled(True)
Behavior: while session.input.audio_enabled is False the agent will not publish/process microphone audio and the STT plugin (Deepgram) will not be invoked — avoiding STT processing costs until you enable it.
2) Lazy-initialize the STT plugin (explicit control)
• Don’t attach Deepgram at AgentSession construction. Create the session without STT or with a no-op STT, and swap in Deepgram only when needed:
# create session without real STT
session = AgentSession(stt=NoopSTT(), llm=..., tts=..., ...)
# later, when enabling mic:
session.stt = deepgram.Deepgram(...) # attach real STT
Or implement a wrapper STT that short-circuits when session.input.audio_enabled is False so no network/costly calls occur.
Notes & recommendations
• If you toggle input off while speech is already in flight, call session.interrupt() to cancel in-progress processing.
• Use logging/metrics to verify Deepgram requests stop after disabling input (no STTMetrics or Deepgram calls).
• Pattern 1 is simplest; Pattern 2 gives the most control if you need to fully avoid any STT client initialization or long-lived connections.
Tell me which approach you prefer (simple toggle vs lazy-init) and I’ll provide the exact code snippet for your current Python setup.sparse-napkin-34305
12/18/2025, 7:04 AMtall-airline-16756
12/18/2025, 7:04 AMstt=deepgram.STT(...) then immediately calling session.input.set_audio_enabled(False) still initializes Deepgram (connection created). To avoid any STT work/cost until user enables mic, use lazy init or a no-op wrapper:
◦ Simple: construct session with a Noop STT that does nothing, then assign real Deepgram when mic is enabled:
session = AgentSession(
stt=NoopSTT(), # no network calls
llm=...,
tts=LoggingTTS(...),
vad=...
)
# when user enables mic
session.stt = deepgram.STT(model="nova-2-general")
session.input.set_audio_enabled(True)
• Alternative: in your handle_mic_control before calling set_audio_enabled(True) create and attach session.stt = deepgram.STT(...) so Deepgram is only instantiated when needed.
2) Ensure TTS is never invoked when sound off
• You already toggle session.output.set_audio_enabled(False). To be defensive, short-circuit TTS at wrapper level so the underlying TTS client is never called:
◦ In your LoggingTTS.synthesize check the session/agent flag and return an immediate no-op stream instead of calling self._tts.synthesize(...). That prevents any network/costly TTS calls.
◦ Example idea:
async def synthesize(self, text: str) -> tts.SynthesizeStream:
if not agent.sound_enabled or not session.output.audio_enabled:
<http://logging.info|logging.info>("Skipping TTS synth because sound is disabled")
return tts.SynthesizeStream.empty() # or other immediate no-op
<http://logging.info|logging.info>(f"🎤 TTS synthesizing text: '{text}'")
return await self._tts.synthesize(text)
• Also call session.interrupt() immediately after disabling audio to cancel in-progress synthesis.
3) Address the AudioStream AttributeError
• Cause: livekit (1.0.21) and livekit-agents (1.3.6) are out of sync; Agents expects an internal _processor attribute not present in your livekit version.
• Fix:
◦ Upgrade so packages match:
pip install --upgrade livekit livekit-agents
• Restart your agent process.
• If upgrade introduces other issues, pin a compatible pair; if you want I can recommend exact versions after you upgrade and re-check.
• Also avoid toggling audio before session.start completes — that can cause lifecycle races that surface similar internal errors.
4) Minor correctness checks in your code
• You call session.input.set_audio_enabled(False) right after await session.start(...) — ok, but prefer to:
◦ Start session with room_options and then set input disabled before any participant audio reaches the pipeline.
• Your LoggingTTS.stream uses original_push_text wrapper correctly to log chunks — keep it.
• Registering RPC handlers on ctx.room.local_participant is correct; ensure clients send the correct JSON payload shape.
5) How to verify no STT/TTS costs
• Deepgram: no network calls or STTMetrics after mic disabled or before STT attached.
• TTS: no "🎤 TTS synthesizing text" logs and no TTSMetrics after audio disabled; wrapped TTS short-circuit ensures no external calls.
• Correlate timestamps with dashboard Usage/Metrics for final confirmation.
If you want, I can:
• Provide the exact NoopSTT implementation and SynthesizeStream.empty() pattern for your environment, or
• Give the pip commands and a recommended compatible version pair to fix the AudioStream error.
Which would you like next?sparse-napkin-34305
12/18/2025, 7:07 AMtall-airline-16756
12/18/2025, 7:07 AMNoopSTT but never defined it. Below is a minimal, safe NoopSTT implementation you can drop into your code and use when you want no STT activity until the mic is explicitly enabled.
Paste this near your other class definitions:
from contextlib import asynccontextmanager
from typing import AsyncIterator
from livekit.agents import tts, voice
class NoopSTT(voice.STT):
"""A no-op STT that accepts audio but never sends it to an external STT."""
def __init__(self):
super().__init__(capabilities=None)
async def transcribe(self, audio_stream) -> AsyncIterator[voice.TranscriptionChunk]:
# Yield nothing — acts as if there is no transcription
if False:
yield # type: ignore
@asynccontextmanager
async def stream(self, **kwargs) -> AsyncIterator[voice.TranscriptionStream]:
# Provide a stream object that has a no-op push_frame / close behavior.
class _NoopStream:
def push_frame(self, frame):
pass
async def close(self):
pass
yield _NoopStream()
How to use it
• Construct the session without Deepgram:
session = AgentSession(stt=NoopSTT(), llm=..., tts=..., vad=...)
• When user enables mic, attach Deepgram and enable input:
session.stt = deepgram.STT(model="nova-2-general") session.input.set_audio_enabled(True)
• When user disables mic, disable input and optionally detach STT:
session.input.set_audio_enabled(False) session.stt = NoopSTT() # frees Deepgram usage if you want to stop background connections
Notes
• Assigning session.stt = deepgram.STT(...) instantiates the plugin lazily, avoiding upfront STT costs.
• If you need a more feature-complete Noop (e.g., exposing metadata or methods), tell me what STT methods you rely on and I’ll extend it.
• After switching STT implementations, watch logs/metrics to confirm no Deepgram requests occur.
Would you like a ready-to-paste variant that mirrors any specific stream API your code currently expects (e.g., attributes/method names returned by your existing STT)?sparse-napkin-34305
12/18/2025, 7:52 AMRoomIO -> AgentSession -> TranscriptSynchronizer ->
RoomIO
132141.734 DEBUG livekit.agents using transcript io: AgentSession -> TranscriptSynchronizer -> RoomIO
132142.634 ERROR livekit.agents AgentSession is closing due to unrecoverable error
Traceback (most recent call last):
File "C:\Users\AnmyJosy\AppData\Local\Programs\Python\Python310\lib\site-packages\livekit\agents\stt\stt.py", line 289,
in _main_task
return await self._run()
File "C:\Users\AnmyJosy\AppData\Local\Programs\Python\Python310\lib\site-packages\livekit\plugins\deepgram\stt.py", line
518, in _run
ws = await self._connect_ws()
File "C:\Users\AnmyJosy\AppData\Local\Programs\Python\Python310\lib\site-packages\livekit\plugins\deepgram\stt.py", line
582, in _connect_ws
ws = await asyncio.wait_for(
File "C:\Users\AnmyJosy\AppData\Local\Programs\Python\Python310\lib\asyncio\tasks.py", line 445, in wait_for
return fut.result()
File "C:\Users\AnmyJosy\AppData\Local\Programs\Python\Python310\lib\site-packages\aiohttp\client.py", line 1494, in send
return self._coro.send(arg)
File "C:\Users\AnmyJosy\AppData\Local\Programs\Python\Python310\lib\site-packages\aiohttp\client.py", line 1093, in
_ws_connect
raise WSServerHandshakeError(
aiohttp.client_exceptions.WSServerHandshakeError: 401, message='Invalid response status',
url='wss://api.deepgram.com/v1/listen?model=nova-2-general&punctuate=true&smart_format=false&no_delay=true&interim_results
=true&encoding=linear16&vad_events=true&sample_rate=16000&channels=1&endpointing=25&filler_words=true&profanity_filter=fal
se&numerals=false&mip_opt_out=false&language=en-US'
132142.640 ERROR livekit.agents Error in _stt_task
Traceback (most recent call last):
File "C:\Users\AnmyJosy\AppData\Local\Programs\Python\Python310\lib\site-packages\livekit\agents\stt\stt.py", line 408,
in anext
val = await self._event_aiter.__anext__()
StopAsyncIteration
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "C:\Users\AnmyJosy\AppData\Local\Programs\Python\Python310\lib\site-packages\livekit\agents\utils\log.py", line 16,
in async_fn_logs
return await fn(*args, **kwargs)
File
"C:\Users\AnmyJosy\AppData\Local\Programs\Python\Python310\lib\site-packages\livekit\agents\voice\audio_recognition.py",
line 660, in _stt_task
async for ev in node:
File "C:\Users\AnmyJosy\AppData\Local\Programs\Python\Python310\lib\site-packages\livekit\agents\voice\agent.py", line
391, in stt_node
async for event in stream:
File "C:\Users\AnmyJosy\AppData\Local\Programs\Python\Python310\lib\site-packages\livekit\agents\stt\stt.py", line 411,
in anext
raise exc # noqa: B904
File "C:\Users\AnmyJosy\AppData\Local\Programs\Python\Python310\lib\site-packages\livekit\agents\stt\stt.py", line 289,
in _main_task
return await self._run()
File "C:\Users\AnmyJosy\AppData\Local\Programs\Python\Python310\lib\site-packages\livekit\plugins\deepgram\stt.py", line
518, in _run
ws = await self._connect_ws()
File "C:\Users\AnmyJosy\AppData\Local\Programs\Python\Python310\lib\site-packages\livekit\plugins\deepgram\stt.py", line
582, in _connect_ws
ws = await asyncio.wait_for(
File "C:\Users\AnmyJosy\AppData\Local\Programs\Python\Python310\lib\asyncio\tasks.py", line 445, in wait_for
return fut.result()
File "C:\Users\AnmyJosy\AppData\Local\Programs\Python\Python310\lib\site-packages\aiohttp\client.py", line 1494, in send
return self._coro.send(arg)
File "C:\Users\AnmyJosy\AppData\Local\Programs\Python\Python310\lib\site-packages\aiohttp\client.py", line 1093, in
_ws_connect
raise WSServerHandshakeError(
aiohttp.client_exceptions.WSServerHandshakeError: 401, message='Invalid response status',
url='wss://api.deepgram.com/v1/listen?model=nova-2-general&punctuate=true&smart_format=false&no_delay=true&interim_results
=true&encoding=linear16&vad_events=true&sample_rate=16000&channels=1&endpointing=25&filler_words=true&profanity_filter=fal
se&numerals=false&mip_opt_out=false&language=en-US'
132142.647 DEBUG livekit.agents input stream detached
{"participant": "identity-ogZq", "source": "SOURCE_UNKNOWN", "accepted_sources":
["SOURCE_MICROPHONE"]}
132142.649 DEBUG livekit.agents session closed
{"reason": "error", "error": "type='stt_error' timestamp=1766044302.6341448
label='livekit.plugins.deepgram.stt.STT'
error=WSServerHandshakeError(RequestInfo(url=URL('wss://api.deepgram.com/v1/listen?model=nova-2-general&punctuate=true&sma
rt_format=false&no_delay=true&interim_results=true&encoding=linear16&vad_events=true&sample_rate=16000&channels=1&endpoint
ing=25&filler_words=true&profanity_filter=false&numerals=false&mip_opt_out=false&language=en-US'), method='GET',
headers=<CIMultiDictProxy('Host': 'api.deepgram.com', 'Authorization': 'Token 742ba0d9e5b304fd83c35e90b2bd185e5dcdbfa41',
'Upgrade': 'websocket', 'Connection': 'Upgrade', 'Sec-WebSocket-Version': '13', 'Sec-WebSocket-Key':
'rpxF1u3c1KBoAo8YsnVKRw==', 'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'User-Agent': 'Python/3.10
aiohttp/3.13.2')>,
real_url=URL('wss://api.deepgram.com/v1/listen?model=nova-2-general&punctuate=true&smart_format=false&no_delay=true&interi
m_results=true&encoding=linear16&vad_events=true&sample_rate=16000&channels=1&endpointing=25&filler_words=true&profanity_f
ilter=false&numerals=false&mip_opt_out=false&language=en-US')), (), status=401, message='Invalid response status',
headers=<CIMultiDictProxy('Content-Type': 'application/json', 'dg-error': 'Invalid credentials.', 'Vary': 'origin,
access-control-request-method, access-control-request-headers', 'Vary': 'accept-encoding',
'Access-Control-Allow-Credentials': 'true', 'Access-Control-Expose-Headers':
'dg-model-name,dg-model-uuid,dg-char-count,dg-request-id,dg-project-id,dg-error', 'Content-Encoding': 'gzip',
'dg-request-id': '139bb117-ad0f-4355-97ae-466ec6475d14', 'Transfer-Encoding': 'chunked', 'Date': 'Thu, 18 Dec 2025
075143 GMT')>) recoverable=False"}..........but when i am starting its still connecting to deepgramtall-airline-16756
12/18/2025, 7:53 AMstt=deepgram.STT(...) — that starts Deepgram's connection task even if you immediately call session.input.set_audio_enabled(False). Use a Noop STT at startup and attach Deepgram only when the user enables the mic.
Drop this minimal NoopSTT into your file (near your other class defs) and use it when creating the session:
from contextlib import asynccontextmanager
from typing import AsyncIterator
from livekit.agents import voice
class NoopSTT(voice.STT):
"""No-op STT: never opens a network connection or yields transcription."""
def __init__(self):
super().__init__(capabilities=None)
async def transcribe(self, audio_stream) -> AsyncIterator[voice.TranscriptionChunk]:
if False:
yield # keep as an async generator but yield nothing
@asynccontextmanager
async def stream(self, **kwargs) -> AsyncIterator[voice.TranscriptionStream]:
class _NoopStream:
def push_frame(self, frame):
pass
async def close(self):
pass
yield _NoopStream()
Change session creation and mic toggle as follows:
# create session without instantiating Deepgram
session = AgentSession(
stt=NoopSTT(),
llm=groq.LLM(...),
tts=LoggingTTS(...),
vad=silero.VAD.load(),
)
# when user enables mic (RPC handler)
session.stt = deepgram.STT(model="nova-2-general") # instantiate Deepgram here
session.input.set_audio_enabled(True)
# when user disables mic
session.input.set_audio_enabled(False)
session.stt = NoopSTT() # optionally detach Deepgram to stop background connection
Notes and next steps
• Detaching by reassigning session.stt = NoopSTT() prevents Deepgram from reconnecting and avoids STT costs.
• If you want to fully close any existing Deepgram connection before replacing it, call any cleanup API exposed by the plugin or restart the session task; reassigning will stop the agents pipeline from using the old STT, but the plugin may need internal cleanup.
• Your logs show a 401 from Deepgram: fix your Deepgram API token (Invalid credentials). Until that token is valid, Deepgram will fail to connect even when you attach it.
• Keep session.input.set_audio_enabled(False) to stop audio processing; using NoopSTT prevents initialization cost entirely.
If you want, I can:
• Provide a small helper that lazily constructs Deepgram only once and reuses it, or
• Show how to add logging to confirm Deepgram connections are created/closed. Which would you prefer?sparse-napkin-34305
12/18/2025, 7:53 AMtall-airline-16756
12/18/2025, 7:53 AMdeepgram.STT(...), which starts Deepgram's connection task immediately. Use the Noop STT at startup, then attach Deepgram only when the user enables the mic.
Suggested minimal changes (drop into your file):
1) Define NoopSTT (you already have a variant; use this one):
from contextlib import asynccontextmanager
from typing import AsyncIterator
from livekit.agents import stt, voice, rtc
class NoopSTT(stt.STT):
"""No-op STT: never opens a network connection or yields transcription."""
def __init__(self):
super().__init__(capabilities=None)
async def transcribe(self, audio_stream) -> AsyncIterator[stt.TranscriptionChunk]:
if False:
yield # keep as async generator but yield nothing
@asynccontextmanager
async def stream(self, **kwargs) -> AsyncIterator[stt.TranscriptionStream]:
class _NoopStream:
def push_frame(self, frame: rtc.AudioFrame):
pass
async def close(self):
pass
yield _NoopStream()
2) Create the session with NoopSTT (avoid Deepgram starting):
session = AgentSession(
stt = NoopSTT(),
llm = groq.LLM(model="llama-3.1-8b-instant"),
tts = elevenlabs.TTS(...),
vad = silero.VAD.load(),
)
3) Lazily attach Deepgram when user enables mic (RPC handler):
@ctx.room.local_participant.register_rpc_method("mic_control")
async def handle_mic_control(data: rtc.RpcInvocationData):
payload = json.loads(data.payload)
mic_enabled = payload.get("micEnabled")
if mic_enabled is True:
<http://logging.info|logging.info>("🎤 User turned MIC ON via RPC")
# instantiate Deepgram only when needed
session.stt = deepgram.STT(model="nova-2-general")
session.input.set_audio_enabled(True)
elif mic_enabled is False:
<http://logging.info|logging.info>("🔇 User turned MIC OFF via RPC")
session.input.set_audio_enabled(False)
# optionally detach Deepgram to stop any background connection
session.stt = NoopSTT()
4) Fix Deepgram 401 during testing
• Your logs show Deepgram returns 401 (Invalid credentials). Even when attaching lazily, an invalid API key will cause immediate failed connections. Make sure your Deepgram API token is valid in env before instantiating deepgram.STT().
5) Optional: reuse a single Deepgram instance
• If you prefer not to recreate the client repeatedly, create it on first enable and keep a reference to reuse instead of re-instantiating.
6) Verify
• After these changes, start your agent and do not enable mic — Deepgram should not attempt any connection and you should not see Deepgram logs or 401 errors.
• Enable mic and confirm Deepgram connects successfully (or shows a successful handshake).
If you’d like, I can give:
• a tiny reusable helper to lazily create/reuse Deepgram, or
• the exact log lines to watch to confirm no Deepgram activity. Which do you prefer?sparse-napkin-34305
12/18/2025, 7:56 AMtall-airline-16756
12/18/2025, 7:57 AMAgentSession.stt is read-only — you cannot assign a new STT instance to it after session creation. Two practical ways to get the lazy/conditional STT behavior you want:
Option A — Conditional wrapper (recommended)
• Create an STT wrapper that forwards to a real STT only when mic is enabled. This lets you construct the session once and avoid starting Deepgram until you flip the flag.
Example (drop into your code; adapt names):
from contextlib import asynccontextmanager
from typing import AsyncIterator
from livekit.agents import stt, voice, rtc
import logging
class ConditionalSTT(stt.STT):
def __init__(self, delegate_factory):
# delegate_factory: callable that returns a stt.STT instance when needed
super().__init__(capabilities=None)
self._delegate_factory = delegate_factory
self._delegate = None
self._enabled = False
def enable(self):
self._enabled = True
if self._delegate is None:
self._delegate = self._delegate_factory()
def disable(self):
self._enabled = False
# optional: keep delegate for reuse, or close if desired
# if self._delegate:
# await self._delegate.close()
async def transcribe(self, audio_stream) -> AsyncIterator[stt.TranscriptionChunk]:
if not self._enabled or self._delegate is None:
if False:
yield
return
async for chunk in self._delegate.transcribe(audio_stream):
yield chunk
@asynccontextmanager
async def stream(self, **kwargs) -> AsyncIterator[stt.TranscriptionStream]:
if not self._enabled:
class _NoopStream:
def push_frame(self, frame: rtc.AudioFrame): pass
async def close(self): pass
yield _NoopStream()
return
# ensure delegate exists
if self._delegate is None:
self._delegate = self._delegate_factory()
async with self._delegate.stream(**kwargs) as s:
yield s
Usage:
• Create session with this wrapper:
session = AgentSession(stt=ConditionalSTT(lambda: deepgram.STT(model="nova-2-general")), ...)
• Toggle in your RPC handler:
◦ To enable mic: call session.input.set_audio_enabled(True) then session.stt.enable()
◦ To disable mic: call session.input.set_audio_enabled(False) then session.stt.disable()
Option B — recreate session (less convenient)
• Stop/close the AgentSession and create a new one with Deepgram when you want STT, or create session without STT and recreate with STT. This avoids read-only assignment but is heavier.
Notes and tips
• Deepgram 401 in your logs means your token was invalid when the plugin attempted to connect; using the ConditionalSTT prevents connection attempts until .enable() is called.
• After disabling input, call session.interrupt() if you need to cancel in-flight processing.
• Use your logging wrapper to confirm no Deepgram connections/logs appear while disabled.
If you want, I’ll return a copy-paste-ready ConditionalSTT tailored to your imports and show the exact RPC handler edits. Which would you prefer?sparse-napkin-34305
12/18/2025, 7:59 AMtall-airline-16756
12/18/2025, 7:59 AMdeepgram.STT(...) into AgentSession at construction, the Deepgram plugin will start its connection task immediately even if you call session.input.set_audio_enabled(False) afterward. Disabling input audio does not prevent the plugin from initializing or attempting a network connection.
What to do
• Use a lazy/no-op STT so Deepgram never connects until you want it to.
Recommended option (safe, minimal changes) — ConditionalSTT
• Create a wrapper STT that only instantiates/connects the real Deepgram client when you explicitly enable the mic.
Copy-paste-ready pattern (adapt imports to your file):
from contextlib import asynccontextmanager
from typing import AsyncIterator
from livekit.agents import stt, rtc
import logging
class ConditionalSTT(stt.STT):
def __init__(self, factory):
super().__init__(capabilities=None)
self._factory = factory
self._delegate = None
self._enabled = False
def enable(self):
self._enabled = True
if self._delegate is None:
self._delegate = self._factory()
def disable(self):
self._enabled = False
# optional: close delegate if it exposes a close method
async def transcribe(self, audio_stream) -> AsyncIterator[stt.TranscriptionChunk]:
if not self._enabled or self._delegate is None:
if False:
yield
return
async for chunk in self._delegate.transcribe(audio_stream):
yield chunk
@asynccontextmanager
async def stream(self, **kwargs) -> AsyncIterator[stt.TranscriptionStream]:
if not self._enabled:
class _NoopStream:
def push_frame(self, frame: rtc.AudioFrame): pass
async def close(self): pass
yield _NoopStream()
return
if self._delegate is None:
self._delegate = self._factory()
async with self._delegate.stream(**kwargs) as s:
yield s
Usage
• Construct session with the wrapper:
session = AgentSession(stt=ConditionalSTT(lambda: deepgram.STT(model="...")), ...)
• When user turns mic ON:
session.stt.enable() session.input.set_audio_enabled(True)
• When user turns mic OFF:
session.input.set_audio_enabled(False) session.stt.disable() # stops new connections; existing connection may need cleanup depending on plugin
Why this works
• The wrapper prevents the Deepgram client from being instantiated/connected until you call .enable(). This avoids the 401/connect attempts and STT costs while the mic is off.
Verification
• No Deepgram connection logs (or 401 errors) until you call .enable().
• Timestamps in your dashboard/metrics should show no STT usage while disabled.
If you want, I can:
• Provide a tiny helper that closes the delegate cleanly when disabled, or
• Show the exact RPC handler edits to call `.enable()`/`.disable()` from your UI. Which would you like?sparse-napkin-34305
12/18/2025, 8:38 AMtall-airline-16756
12/18/2025, 8:38 AMsession.input.set_audio_enabled(True) the agent logs "input attaching" and the STT pipeline starts consuming audio.
• When you call session.input.set_audio_enabled(False) the agent logs "input detached" and stops consuming audio.
• However, because you construct the session with stt=deepgram.STT(...), the Deepgram plugin attempts to connect immediately at session creation (which is why you saw the 401). Disabling input does not prevent the Deepgram client from initializing or connecting.
What to change so Deepgram only connects when mic is ON
• Use a Conditional/Lazy STT wrapper (recommended). This lets you create the AgentSession once but only instantiate/connect Deepgram when you explicitly enable the mic.
Minimal ConditionalSTT to drop into your file (fits your imports):
from contextlib import asynccontextmanager
from typing import AsyncIterator
from livekit.agents import stt, rtc
class ConditionalSTT(stt.STT):
def __init__(self, factory):
super().__init__(capabilities=None)
self._factory = factory
self._delegate = None
self._enabled = False
def enable(self):
self._enabled = True
if self._delegate is None:
self._delegate = self._factory()
def disable(self):
self._enabled = False
# optional: close delegate if it exposes close()
async def transcribe(self, audio_stream) -> AsyncIterator[stt.TranscriptionChunk]:
if not self._enabled or self._delegate is None:
if False:
yield
return
async for chunk in self._delegate.transcribe(audio_stream):
yield chunk
@asynccontextmanager
async def stream(self, **kwargs) -> AsyncIterator[stt.TranscriptionStream]:
if not self._enabled:
class _NoopStream:
def push_frame(self, frame: rtc.AudioFrame): pass
async def close(self): pass
yield _NoopStream()
return
if self._delegate is None:
self._delegate = self._factory()
async with self._delegate.stream(**kwargs) as s:
yield s
How to use it in your code
• Construct session with the wrapper:
session = AgentSession(stt=ConditionalSTT(lambda: deepgram.STT(model="nova-2-general")), ...)
• In your RPC handler:
◦ Turn mic ON:
session.stt.enable() session.input.set_audio_enabled(True)
• Turn mic OFF:
session.input.set_audio_enabled(False) session.stt.disable()
Why this fixes your issues
• Deepgram will not attempt to connect until session.stt.enable() is called.
• Disabling input remains effective for stopping audio processing.
• You avoid the 401/connect attempts and any STT costs until the user explicitly enables the mic.
Other small notes from your code
• You're calling session.say("hello how are you") without awaiting — prefer await session.say(...) inside async context to ensure ordering.
• After disabling input or output, call await session.interrupt() if you need to cancel in-flight work.
• Ensure your Deepgram API key is valid before enabling; the 401 you saw is from invalid credentials.
If you want, I can:
• Provide a version of ConditionalSTT that cleanly closes the Deepgram delegate when disabled, or
• Give the exact RPC-handler edits to wire enable/disable in your existing handlers.
Which would you like next?sparse-napkin-34305
12/18/2025, 8:40 AMtall-airline-16756
12/18/2025, 8:41 AMsession.input.set_audio_enabled(False). Disabling input audio does not prevent the Deepgram plugin from initializing or making a websocket connection.
What to do
• Use a lazy/conditional STT so Deepgram only connects when you want it to. Recommended: the ConditionalSTT wrapper (creates Deepgram only on enable) and call:
◦ enable: session.stt.enable(); session.input.set_audio_enabled(True)
◦ disable: session.input.set_audio_enabled(False); session.stt.disable()
Verification
• No Deepgram logs / no 401 errors until you call .enable().
• No STTMetrics or websocket connections in network logs while disabled.
If you want, I’ll provide a copy-paste ConditionalSTT tailored to your imports and the exact RPC handler edits to wire enable/disable. Which would you prefer?sparse-napkin-34305
12/18/2025, 9:18 AMtall-airline-16756
12/18/2025, 9:18 AM.enable() instantiates Deepgram.
◦ Log inside stream() or transcribe() when frames are forwarded to the delegate.
3) Check runtime object type
• At runtime inspect the session STT wrapper to see whether the delegate exists or is a Deepgram instance:
◦ logging.info(type(session.stt)) or, if using ConditionalSTT, logging.info(type(session.stt._delegate)).
4) LiveKit metrics/events & plugin metrics
• Watch STT-related metrics/events (TTSMetrics/STTMetrics) in your application logs or LiveKit dashboard. If STTMetrics appear while mic is off, STT is running.
• Correlate timestamps in the Usage/Billing view with suspected periods of STT activity.
5) Network / websocket inspection
• Monitor outbound websocket connections from the process; Deepgram opens a ws to api.deepgram.com. No ws = not connected.
6) Definitive check (zero ambiguity)
• Replace Deepgram with NoopSTT at startup. If your previous 401/Deepgram logs disappear, Deepgram was being used before. Re-enable Deepgram and confirm logs reappear.
If you want, I can give a 2–3 line copy-paste log insertion for your ConditionalSTT/NoopSTT or show the exact command to inspect open websockets on your environment. Which would you prefer?sparse-napkin-34305
12/18/2025, 9:24 AMtall-airline-16756
12/18/2025, 9:24 AMdeepgram.STT(...) at session creation. That connection can still be established (and may incur cost or show connection activity) even with input disabled.
What to check now
• If you created the session with stt=deepgram.STT(...), Deepgram will try to connect immediately (your 401 shows that).
• If you used a Noop/Conditional wrapper and never enabled the delegate, Deepgram will not connect.
• Verify by watching logs for Deepgram connect attempts (or 401s), monitoring outbound websockets, or checking STTMetrics.
What to do to stop Deepgram completely until user enables mic
• Use the ConditionalSTT or NoopSTT pattern so Deepgram is only instantiated/connected when you explicitly enable the mic (examples we discussed).
• Or, instantiate Deepgram only inside your mic-ON handler (but note AgentSession.stt is read-only — use a wrapper or recreate the session).
How to verify it's stopped
• No Deepgram connection logs or 401s after disabling.
• No STTMetrics events.
• Inspect runtime type: log type(session.stt) and getattr(session.stt, "_delegate", None) for ConditionalSTT.
If you want, I can give a tiny ConditionalSTT snippet tailored to your current imports and show the exact RPC edits to ensure Deepgram never connects until mic-ON. Would you like that?