Why is LLM.with_openrouter not working right now
# ask-ai
a
Why is LLM.with_openrouter not working right now
t
Thanks for reaching out, David. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
You're right to ask — I can help diagnose this. To troubleshoot why
LLM.with_openrouter
isn't working right now, could you share these details? • The exact error message or logs you see (copy/paste). • Your code snippet showing how you call
LLM.with_openrouter
. • Your environment: Python/Node version, LiveKit Agents version, and whether you installed the OpenAI plugin (
livekit-agents[openai]
). • How you set the OpenRouter key (confirm
OPENROUTER_API_KEY
in your
.env
or env vars). • Whether this fails locally, in CI, or in production, and any recent changes (network, firewall, dependency updates). Quick checklist you can verify now: •
OPENROUTER_API_KEY
is set and valid. • Correct model name (example:
anthropic/claude-sonnet-4.5
) or use
openrouter/auto
. • Network allows outbound calls to OpenRouter. • OpenAI plugin installed (
uv add "livekit-agents[openai]~=1.2"
). Share the error and environment details and I’ll point to the specific fix. Sources: OpenRouter LLM plugin guide | LiveKit docs
a
Code: async def entrypoint(ctx: agents.JobContext): initial_ctx = ChatContext() # Get the unique room name from the job context room_name = ctx.room.name session_id = None # Store session_id in local scope # Connect to the room FIRST await ctx.connect(auto_subscribe=AutoSubscribe.AUDIO_ONLY) # NOW wait for participant to join participant = await ctx.wait_for_participant() # Extract the Supabase JWT and conversation_id from the participant's metadata supabase_jwt = None timezone = None conversation_id = None is_reconnection = False user_id = None if participant and participant.metadata: try: metadata = json.loads(participant.metadata) supabase_jwt = metadata.get("supabase_jwt") timezone = metadata.get("timezone") conversation_id = metadata.get("conversation_id") if supabase_jwt: logger.info(f"Supabase JWT successfully extracted for room {room_name}") user_id = participant.identity else: logger.warning(f"No supabase_jwt found in participant metadata for room {room_name}") if timezone: logger.info(f"Timezone successfully extracted for room {room_name}: {timezone}") else: logger.warning(f"No timezone found in participant metadata for room {room_name}, defaulting to UTC") timezone = 'UTC' if conversation_id: logger.info(f"Conversation ID found: {conversation_id}. This is a reconnection.") is_reconnection = True except json.JSONDecodeError as e: logger.warning(f"Could not parse participant metadata in room {room_name}: {e}") else: logger.warning(f"No participant metadata available for room {room_name}") # Track call session start and store session_id if supabase_jwt and user_id: session_id = await track_call_start(room_name, user_id, supabase_jwt, conversation_id) # Define cleanup function async def cleanup(): """Cleanup function to track call end""" if session_id and supabase_jwt: logger.info(f"Running cleanup for room {room_name}, session_id: {session_id}") await track_call_end(session_id, supabase_jwt, room_name) else: logger.warning(f"Cleanup skipped for room {room_name} - missing session_id or JWT") # Register cleanup callback CORRECTLY ctx.add_shutdown_callback(cleanup) # If reconnection, load conversation history if is_reconnection and conversation_id and supabase_jwt: try: backend_url = os.getenv('BACKEND_URL') if os.getenv('ENVIRONMENT') == 'production' else 'http://localhost:8000' headers = {'Authorization': f'Bearer {supabase_jwt}'} response = requests.get( f'{backend_url}/get-conversation-context?conversation_id={conversation_id}', headers=headers, timeout=10 ) if response.status_code == 200: context_data = response.json() # Add PDF content if available if context_data.get('pdf_content'): initial_ctx.add_message( role="assistant", content=f"Study material ({context_data.get('pdf_name', 'document')}):\n{context_data['pdf_content']}" ) logger.info(f"Loaded PDF content for conversation {conversation_id}") # Add conversation history for msg in context_data.get('messages', []): initial_ctx.add_message( role=msg['role'], content=msg['content'] ) logger.info(f"Loaded {len(context_data.get('messages', []))} messages for conversation {conversation_id}") else: logger.warning(f"Failed to load conversation context: {response.status_code}") is_reconnection = False except Exception as e: logger.error(f"Error loading conversation context: {e}") is_reconnection = False # If not reconnection, wait for study material if not is_reconnection: study_material = await wait_for_study_material(room_name) if study_material: initial_ctx.add_message( role="assistant", content=f"Study material:\n{study_material}" ) print(f"Agent for room {room_name} initialized with study material") else: print(f"Agent for room {room_name} initialized without study material") session = AgentSession( stt = assemblyai.STT( format_turns=True, ), llm = openai.LLM.with_openrouter( model="z-ai/glm-4.6", provider={ "order": ["deepinfra/fp4", "novita/bf16", "z-ai"], "allow_fallbacks": False, "sort": "price", }, ), tts=inworld.TTS(voice="Deborah", model="inworld-tts-1-max"), vad=silero.VAD.load(), turn_detection=MultilingualModel(), ) # Create the assistant instance with room_name and JWT for calendar access assistant = Assistant(chat_ctx=initial_ctx, room_name=room_name, supabase_jwt=supabase_jwt, timezone=timezone, room=ctx.room) # Handle data messages for interrupt functionality @ctx.room.on("data_received") def on_data_received(data): try: payload = json.loads(data.data.decode("utf-8")) message = payload["message"] if message == "__interrupt__": logger.info("Interrupt command received. Stopping current TTS playback.") session.interrupt() session.interrupt() session.interrupt() except Exception as e: logger.error(f"Error processing data message: {e}") # Handle participant disconnection to delete the room immediately @ctx.room.on("participant_disconnected") def on_participant_disconnected(participant): async def delete_room(): api_client = None try: logger.info(f"Participant {participant.identity} disconnected from room {room_name}") # Delete the room immediately api_client = api.LiveKitAPI( os.getenv("LIVEKIT_URL"), os.getenv("LIVEKIT_API_KEY"), os.getenv("LIVEKIT_API_SECRET"), ) await api_client.room.delete_room(api.DeleteRoomRequest( room=ctx.job.room.name, )) logger.info(f"Room {room_name} deleted successfully after participant disconnect") except Exception as e: logger.error(f"Error deleting room {room_name} after participant disconnect: {e}") finally: # Properly close the API client session if api_client is not None: await api_client.aclose() # Run the async deletion as a task asyncio.create_task(delete_room()) await session.start( room=ctx.room, agent=assistant, room_input_options=RoomInputOptions( noise_cancellation=noise_cancellation.BVC(), text_enabled=True, audio_enabled=True, close_on_disconnect=True, ), ) # Choose welcome message based on reconnection status if is_reconnection: welcome_msg = "Don't answer the question you previously asked if any. Just carry on the session as you were already in a call with the student." elif study_material: if len(study_material) > 10: welcome_msg = WELCOME_MESSAGE_1 else: welcome_msg = WELCOME_MESSAGE_2 else: welcome_msg = WELCOME_MESSAGE_3 await session.generate_reply(instructions=welcome_msg) Error: 2025-12-16 122959,108 - DEBUG pydot - pydot initializing {"pid": 5520, "inference": true} 2025-12-16 122959,108 - DEBUG pydot - pydot 4.0.1 {"pid": 5520, "inference": true} 2025-12-16 122959,117 - DEBUG pydot.core - pydot core module initializing {"pid": 5520, "inference": true} 2025-12-16 123010,333 - DEBUG livekit.agents - inference runner initialized {"runner": "lk_end_of_utterance_multilingual", "elapsed_time": 14.583666599995922, "pid": 5520, "inference": true} 2025-12-16 123010,334 - INFO livekit.agents - process initialized {"pid": 5520, "inference": true, "elapsed_time": 20.23} 2025-12-16 123010,334 - DEBUG asyncio - Using proactor: IocpProactor {"pid": 5520, "inference": true} 2025-12-16 123010,509 - INFO livekit.agents - registered worker {"id": "AW_kDZM799Ah3si", "url": "wss://studybuddy-k4puxyiy.livekit.cloud", "region": "US East B", "protocol": 16} 2025-12-16 123118,204 - INFO livekit.agents - received job request {"job_id": "AJ_tfdMSMZvBRBC", "dispatch_id": "AD_Pz4xnt6DGRiN", "room_name": "room-edf551a8-2f7a-4628-b5a3-eea159b0e2a3", "agent_name": "", "resuming": false} 2025-12-16 123118,237 - INFO livekit.agents - initializing job runner {"tid": 22272} 2025-12-16 123118,238 - DEBUG asyncio - Using proactor: IocpProactor 2025-12-16 123118,238 - INFO livekit.agents - job runner initialized {"tid": 22272, "elapsed_time": 0.0} 2025-12-16 123118,793 - INFO __mp_main__ - Supabase JWT successfully extracted for room room-edf551a8-2f7a-4628-b5a3-eea159b0e2a3 2025-12-16 123118,794 - INFO __mp_main__ - Timezone successfully extracted for room room-edf551a8-2f7a-4628-b5a3-eea159b0e2a3: America/New_York 2025-12-16 123118,794 - INFO __mp_main__ - Conversation ID found: 595d0b69-1476-443f-af99-4f93a5404108. This is a reconnection. 2025-12-16 123123,163 - INFO __mp_main__ - Call session started for room room-edf551a8-2f7a-4628-b5a3-eea159b0e2a3, session_id: d739259c-0b89-45ca-8c98-1d803381cdbe 2025-12-16 123126,299 - INFO __mp_main__ - Loaded PDF content for conversation 595d0b69-1476-443f-af99-4f93a5404108 2025-12-16 123126,301 - INFO __mp_main__ - Loaded 20 messages for conversation 595d0b69-1476-443f-af99-4f93a5404108 2025-12-16 123126,301 - WARNING livekit.agents - Running <Task finished name='job_user_entrypoint' coro=<_JobProc._run_job_task.<locals>._traceable_entrypoint() done, defined at C\Users\User\AppData\Roaming\Python\Python313\site packages\opentelemetry\util\ decorator.py68> exception=AttributeError("type object 'LLM' has no attribute 'with_openrouter'")> took too long: 7.51 seconds 2025-12-16 123126,302 - ERROR livekit.agents - unhandled exception while running the job task Traceback (most recent call last): File "C:\Users\User\AppData\Roaming\Python\Python313\site-packages\opentelemetry\util\_decorator.py", line 71, in async_wrapper return await func(*args, **kwargs) # type: ignore ^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "C:\Users\User\AppData\Roaming\Python\Python313\site-packages\livekit\agents\ipc\job_proc_lazy_main.py", line 240, in _traceable_entrypoint await self._job_entrypoint_fnc(job_ctx) File "C:\Users\User\Documents\GitHub\Study_Buddy\livekit-agent\agent.py", line 231, in entrypoint llm = openai.LLM.with_openrouter( ^^^^^^^^^^^^^^^^^^^^^^^^^^ AttributeError: type object 'LLM' has no attribute 'with_openrouter'
t
Thanks — this error shows you're hitting an AttributeError: Python can't find
with_openrouter
on
openai.LLM
. That means your runtime's LiveKit Agents package doesn't expose that helper. To fix this, please check these items: 1. Verify the OpenAI plugin is installed: ◦ Run: pip show livekit-agents ◦ If you installed with extras, ensure you used the openai extra: uv add "livekit-agents[openai]~=1.2" (or install the matching pip wheel for your environment). 1. Confirm the Agents version supports `with_openrouter`: ◦ Check the installed version from pip show.
with_openrouter
is available in the OpenAI plugin for the Agents release that includes the OpenRouter integration (install the same or newer than the example you followed). 1. Confirm imports are using the plugin namespace: ◦ Your code uses
from livekit.plugins import openai
— keep that import and then call
openai.LLM.with_openrouter(...)
. 1. If you upgraded/installed while a worker was running, restart the worker process so the new package is loaded. Please share the output of: • pip show livekit-agents • the top of your file imports (so I can confirm you're importing
openai
correctly) With those I’ll tell you the exact next step.
a
This is the current set up and i am still getting that error pip show livekit-agents Name: livekit-agents Version: 1.2.8 Summary: A powerful framework for building realtime voice AI agents Home-page: Author: Author-email: LiveKit <hello@livekit.io> License-Expression: Apache-2.0 Location: C:\Users\User\AppData\Roaming\Python\Python313\site-packages Requires: aiohttp, av, click, colorama, docstring-parser, eval-type-backport, livekit, livekit-api, livekit-blingfire, livekit-protocol, nest-asyncio, numpy, opentelemetry-api, opentelemetry-exporter-otlp, opentelemetry-sdk, prometheus-client, protobuf, psutil, pydantic, pyjwt, sounddevice, types-protobuf, typing-extensions, watchfiles Required-by: livekit-plugins-assemblyai, livekit-plugins-baseten, livekit-plugins-cartesia, livekit-plugins-deepgram, livekit-plugins-elevenlabs, livekit-plugins-google, livekit-plugins-inworld, livekit-plugins-openai, livekit-plugins-silero, livekit-plugins-turn-detector from livekit.plugins import ( openai, noise_cancellation, silero, inworld, assemblyai, baseten, ) the attached image are the only with_ functions being seen
t
Thanks — you're on the right track. I can see
livekit-agents
1.2.8 is installed and your editor's autocomplete shows other
with_…
helpers but not
with_openrouter
, which matches the AttributeError. Quick actionable steps to resolve this: 1. Install or upgrade the OpenAI plugin package (it provides the OpenRouter helper): ◦ Run: pip install --upgrade livekit-plugins-openai 1. Restart the worker/process that runs your job so Python loads the updated package. 2. Confirm the plugin version and presence of the helper: ◦ Run: pip show livekit-plugins-openai ◦ In a Python REPL run: from livekit.plugins import openai dir(openai.LLM) Confirm
with_openrouter
appears in that listing. If that doesn't expose
with_openrouter
, share the output of: • pip show livekit-plugins-openai • the dir(openai.LLM) result I’ll guide the next step from there.
a
this issue has been resolved with the upgrade but now i have this one Traceback (most recent call last): File "C:\Users\User\AppData\Roaming\Python\Python313\site-packages\livekit\agents\voice\agent_activity.py", line 405, in _context_aware_coro return await coro ^^^^^^^^^^ File "C:\Users\User\AppData\Roaming\Python\Python313\site-packages\livekit\agents\utils\log.py", line 16, in async_fn_logs return await fn(*args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^ File "C:\Users\User\AppData\Roaming\Python\Python313\site-packages\livekit\agents\voice\agent_activity.py", line 1398, in _user_turn_completed_task await old_task File "C:\Users\User\AppData\Roaming\Python\Python313\site-packages\livekit\agents\voice\agent_activity.py", line 405, in _context_aware_coro return await coro ^^^^^^^^^^ File "C:\Users\User\AppData\Roaming\Python\Python313\site-packages\livekit\agents\utils\log.py", line 16, in async_fn_logs return await fn(*args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^ File "C:\Users\User\AppData\Roaming\Python\Python313\site-packages\livekit\agents\voice\agent_activity.py", line 1547, in _user_turn_completed_task model_name=self._turn_detection.model, model_provider=self._turn_detection.provider ^^^^^^^^^^^^^^^^^^^^^^^^^^ AttributeError: 'MultilingualModel' object has no attribute 'model'
t
Thanks — this traceback shows the agent runtime is trying to read
.model
and
.provider
from your
turn_detection
instance, but
MultilingualModel
doesn't define those attributes. That causes the AttributeError. Two ways to resolve it: 1) Preferred — upgrade so the runtime and turn-detector plugin match • Upgrade LiveKit Agents and the turn-detector plugin so the framework handles the detector type correctly. • Install/update the extras: ◦ pip install --upgrade livekit-agents livekit-plugins-turn-detector • Restart your worker/process after upgrading. 2) Quick workaround — provide compatible attributes via a thin wrapper • Wrap the
MultilingualModel
so it exposes
model
and
provider
while delegating behavior:
Copy code
class WrappedTurnDetector:
    def __init__(self, detector):
        self._detector = detector
        self.model = "multilingual-eou-model"   # compatibility value
        self.provider = "livekit"               # compatibility value

    def __getattr__(self, name):
        return getattr(self._detector, name)
Then use:
Copy code
turn_detection = WrappedTurnDetector(MultilingualModel())
session = AgentSession(..., turn_detection=turn_detection)
Which path would you prefer — upgrade now or use the wrapper so you can continue without upgrading? Sources: LiveKit turn detector plugin | LiveKit docs | Turn detection and interruptions | LiveKit Docs | Improved End-of-Turn Model Cuts Voice AI Interruptions 39%
a
what is this 131550.497 DEBUG livekit.agents flush audio emitter due to slow audio generation 131557.892 DEBUG livekit.agents flush audio emitter due to slow audio generation
t
That debug line means the agent cleared buffered audio because TTS production was too slow to keep up with real-time playback. LiveKit drops the backlog instead of playing stale audio to avoid long delays or choppy speech. Common causes: • TTS backend latency or network delays • LLM producing text faster than TTS can synthesize • Host CPU / I/O resource constraints • Long responses that overwhelm streaming TTS Actions you can take: • Use a streaming-capable TTS provider or a faster model. • Break long replies into smaller chunks so TTS can stream incrementally. • Enable preemptive generation if available so TTS starts from partial output. • Check host CPU/memory and network; scale or move to a machine with better throughput. • Ensure your TTS integration is non-blocking (runs in its own async task) and restart the worker after config changes. • If using a remote TTS service, add retries/timeouts and measure round-trip latency. If you want, share: • which TTS provider & model you're using, • whether you see long LLM outputs, • CPU/network metrics from the agent host — and I’ll suggest the most targeted changes. Sources: Agent speech and audio | LiveKit docs | Pipeline nodes and hooks | LiveKit docs
a
from future import annotations from livekit.agents import ( AutoSubscribe, JobContext, WorkerOptions, cli ) from livekit import agents, api from api import Assistant from prompt import WELCOME_MESSAGE_1, WELCOME_MESSAGE_2, WELCOME_MESSAGE_3 from livekit.agents import AgentSession, RoomOptions, ChatContext from livekit.plugins import ( openai, noise_cancellation, silero, inworld, assemblyai, ) from livekit.plugins.turn_detector.multilingual import MultilingualModel import os import requests import asyncio import json from dotenv import load_dotenv from datetime import datetime, timezone as dt_timezone why am i getting this error Traceback (most recent call last): File "C:\Users\User\Documents\GitHub\Study_Buddy\livekit-agent\agent.py", line 12, in <module> from livekit.agents import AgentSession, RoomOptions, ChatContext ImportError: cannot import name 'RoomOptions' from 'livekit.agents' (C:\Users\User\AppData\Roaming\Python\Python313\site-packages\livekit\agents\__init__.py)
t
You're seeing that ImportError because
RoomOptions
was moved out of the top-level
livekit.agents
namespace. Update your imports and any usages to the new location. Do this: 1. Replace the import line:
Copy code
from livekit.agents import AgentSession, RoomOptions, ChatContext
with:
Copy code
from livekit.agents import AgentSession, ChatContext
from livekit.agents.voice import room_io
1. Replace
RoomOptions(...)
usages with
room_io.RoomOptions(...)
(and similarly for
AudioInputOptions
, etc.), for example:
Copy code
await session.start(
  ...,
  room_options=room_io.RoomOptions(
    audio_input=room_io.AudioInputOptions(
      noise_cancellation=noise_cancellation.BVC(),
    ),
  ),
)
Make that change and restart your worker. Tell me if you want me to scan the file and suggest exact replacements. Sources: Text and transcriptions | LiveKit docs | Agent session | LiveKit docs
a
my tts model is way slower than the llm so the llm finishes texting and the llm stops before finishing what is written. it is not in sync and for some reason it is reffering to "." as period instead of treating it as a punctuation and this only started when i usedmade all the changes to the version and deployed it. what is the issue here. async def entrypoint(ctx: agents.JobContext): initial_ctx = ChatContext() # Get the unique room name from the job context room_name = ctx.room.name session_id = None # Store session_id in local scope # Connect to the room FIRST await ctx.connect(auto_subscribe=AutoSubscribe.AUDIO_ONLY) # NOW wait for participant to join participant = await ctx.wait_for_participant() # Extract the Supabase JWT and conversation_id from the participant's metadata supabase_jwt = None timezone = None conversation_id = None is_reconnection = False user_id = None if participant and participant.metadata: try: metadata = json.loads(participant.metadata) supabase_jwt = metadata.get("supabase_jwt") timezone = metadata.get("timezone") conversation_id = metadata.get("conversation_id") if supabase_jwt: logger.info(f"Supabase JWT successfully extracted for room {room_name}") user_id = participant.identity else: logger.warning(f"No supabase_jwt found in participant metadata for room {room_name}") if timezone: logger.info(f"Timezone successfully extracted for room {room_name}: {timezone}") else: logger.warning(f"No timezone found in participant metadata for room {room_name}, defaulting to UTC") timezone = 'UTC' if conversation_id: logger.info(f"Conversation ID found: {conversation_id}. This is a reconnection.") is_reconnection = True except json.JSONDecodeError as e: logger.warning(f"Could not parse participant metadata in room {room_name}: {e}") else: logger.warning(f"No participant metadata available for room {room_name}") # Track call session start and store session_id if supabase_jwt and user_id: session_id = await track_call_start(room_name, user_id, supabase_jwt, conversation_id) # Define cleanup function async def cleanup(): """Cleanup function to track call end""" if session_id and supabase_jwt: logger.info(f"Running cleanup for room {room_name}, session_id: {session_id}") await track_call_end(session_id, supabase_jwt, room_name) else: logger.warning(f"Cleanup skipped for room {room_name} - missing session_id or JWT") # Register cleanup callback CORRECTLY ctx.add_shutdown_callback(cleanup) # If reconnection, load conversation history if is_reconnection and conversation_id and supabase_jwt: try: backend_url = os.getenv('BACKEND_URL') if os.getenv('ENVIRONMENT') == 'production' else 'http://localhost:8000' headers = {'Authorization': f'Bearer {supabase_jwt}'} response = requests.get( f'{backend_url}/get-conversation-context?conversation_id={conversation_id}', headers=headers, timeout=10 ) if response.status_code == 200: context_data = response.json() # Add PDF content if available if context_data.get('pdf_content'): initial_ctx.add_message( role="assistant", content=f"Study material ({context_data.get('pdf_name', 'document')}):\n{context_data['pdf_content']}" ) logger.info(f"Loaded PDF content for conversation {conversation_id}") # Add conversation history for msg in context_data.get('messages', []): initial_ctx.add_message( role=msg['role'], content=msg['content'] ) logger.info(f"Loaded {len(context_data.get('messages', []))} messages for conversation {conversation_id}") else: logger.warning(f"Failed to load conversation context: {response.status_code}") is_reconnection = False except Exception as e: logger.error(f"Error loading conversation context: {e}") is_reconnection = False # If not reconnection, wait for study material if not is_reconnection: study_material = await wait_for_study_material(room_name) if study_material: initial_ctx.add_message( role="assistant", content=f"Study material:\n{study_material}" ) print(f"Agent for room {room_name} initialized with study material") else: print(f"Agent for room {room_name} initialized without study material") session = AgentSession( stt = assemblyai.STT( format_turns=True, ), llm=openai.LLM(model="gpt-4o-mini"), tts=inworld.TTS(voice="Deborah", model="inworld-tts-1-max"), vad=silero.VAD.load(), turn_detection=MultilingualModel(), ) # Create the assistant instance with room_name and JWT for calendar access assistant = Assistant(chat_ctx=initial_ctx, room_name=room_name, supabase_jwt=supabase_jwt, timezone=timezone, room=ctx.room) # Handle data messages for interrupt functionality @ctx.room.on("data_received") def on_data_received(data): try: payload = json.loads(data.data.decode("utf-8")) message = payload["message"] if message == "__interrupt__": logger.info("Interrupt command received. Stopping current TTS playback.") session.interrupt() session.interrupt() session.interrupt() except Exception as e: logger.error(f"Error processing data message: {e}") # Handle participant disconnection to delete the room immediately @ctx.room.on("participant_disconnected") def on_participant_disconnected(participant): async def delete_room(): api_client = None try: logger.info(f"Participant {participant.identity} disconnected from room {room_name}") # Delete the room immediately api_client = api.LiveKitAPI( os.getenv("LIVEKIT_URL"), os.getenv("LIVEKIT_API_KEY"), os.getenv("LIVEKIT_API_SECRET"), ) await api_client.room.delete_room(api.DeleteRoomRequest( room=ctx.job.room.name, )) logger.info(f"Room {room_name} deleted successfully after participant disconnect") except Exception as e: logger.error(f"Error deleting room {room_name} after participant disconnect: {e}") finally: # Properly close the API client session if api_client is not None: await api_client.aclose() # Run the async deletion as a task asyncio.create_task(delete_room()) await session.start( room=ctx.room, agent=assistant, room_options=room_io.RoomOptions( audio_input=room_io.AudioInputOptions( noise_cancellation=noise_cancellation.BVC(), ), delete_room_on_close=True, ), ) # Choose welcome message based on reconnection status if is_reconnection: welcome_msg = "Don't answer the question you previously asked if any. Just carry on the session as you were already in a call with the student." elif study_material: if len(study_material) > 10: welcome_msg = WELCOME_MESSAGE_1 else: welcome_msg = WELCOME_MESSAGE_2 else: welcome_msg = WELCOME_MESSAGE_3 await session.generate_reply(instructions=welcome_msg) if name == "__main__": options = agents.WorkerOptions( entrypoint_fnc=entrypoint, initialize_process_timeout=30.0, # Increase from default 10 seconds ) agents.cli.run_app(options)
t
Thanks — that behavior happens when the LLM finishes producing text faster than your TTS can synthesize audio, and your TTS is pronouncing punctuation literally. Here’s a focused checklist and fixes you can apply now. 1) Enable TTS-aligned transcript (best fix) • When creating the session, set:
Copy code
session = AgentSession(..., use_tts_aligned_transcript=True)
This syncs chat context commits with actual audio playback so the agent won’t treat text as finished until TTS finishes. 2) Prevent punctuation from being spoken • Normalize/transforms before TTS by overriding
transcription_node
(strip or replace punctuation like "." so TTS won’t say “period”):
Copy code
async def transcription_node(self, text, model_settings):
    async for chunk in text:
        if isinstance(chunk, str):
            yield chunk.replace(".", "")
        else:
            yield chunk.model_copy(update={"text": chunk.text.replace(".", "")})
3) Reduce TTS latency • Use a streaming/low-latency TTS provider (Cartesia Sonic, ElevenLabs) or a faster model. • Tune provider parameters (speed/rate) if supported. • If your TTS supports incremental streaming, enable it so audio begins on partial LLM output. 4) Avoid premature context commits • Ensure you aren’t calling
chat_ctx.add_message()
manually before playback completes. Let the session add messages after playback or use aligned transcripts. 5) Observability and diagnostics • Add observability to see timings:
Copy code
ctx.add_observability_handler()
• Capture: LLM finish time, TTS start/stop, audio frame delays, CPU/network metrics. Tell me: • Which TTS provider/model you’re using and whether you call
generate_reply()
or
say()
— I’ll give exact parameter suggestions and a sample config to test. Sources: Text and transcriptions | LiveKit docs | Pipeline nodes and hooks | LiveKit docs | Rime TTS plugin guide | LiveKit Docs | ElevenLabs TTS plugin guide | LiveKit Docs | Agent speech and audio | LiveKit docs