big-postman-34034
05/01/2026, 5:10 PM/v1/chat/completions, but the server does not expose /v1/realtime or any WebSocket realtime endpoint.
We need to use this model in LiveKit Agents for a voice assistant.
Requirements:
• user speaks in LiveKit Room;
• model receives the user’s audio, not only text;
• assistant text streams back and is spoken through our existing TTS;
• multi-turn conversation must work;
• user transcripts should appear in LiveKit UI/transcript history;
• final user and assistant messages should be stored correctly in `chat_ctx`;
• avoid private LiveKit APIs if possible.
Given that the model itself does not provide a realtime transcript stream, what LiveKit architecture would you recommend?
Should we use:
1. standard STT + LLM + TTS pipeline,
2. custom RealtimeModel / RealtimeSession,
3. parallel ASR plus audio-input LLM,
4. or another pattern?
Please explain the recommended design, who owns user transcription/history, how the current turn audio reaches the model, and how multi-turn context should be maintained.tall-airline-16756
05/01/2026, 5:10 PMtall-airline-16756
05/01/2026, 5:11 PM/v1/chat/completions. Stream the text output back through your AgentSession LLM node.
2. Feed LLM text to your existing TTS to speak back into the LiveKit Room.
3. Persist all final user and assistant messages into chat_ctx (user transcripts from STT + assistant text from vLLM).
Who owns what
• User transcription/UI history: The STT component owns producing transcripts and publishing them to the LiveKit UI/transcript history (use the standard STT node so transcripts appear in LiveKit UI). These transcripts are the canonical user transcript shown to users.
• Raw audio to LLM: Your custom LLM node (override) is responsible for collecting the current turn’s raw audio frames and sending them to vLLM.
• Assistant streaming & TTS: The LLM node returns assistant text; your TTS service speaks it back and the agent session streams it into the room.
• chat_ctx (conversation history): The AgentSession’s chat_ctx stores the canonical conversation: STT-produced user text items and assistant text items returned by vLLM. You should append both the STT transcript and the final assistant text to chat_ctx at turn end.
How the current turn’s audio reaches the model
• Capture audio frames during the live turn (use VAD or STT turn detection to mark turn boundaries).
• Buffer or stream those frames in the LLM node. When the turn completes (or when you want to send partial context), POST the buffered audio frames to your vLLM /v1/chat/completions endpoint as the audio input payload.
• If your vLLM supports chunked/streamed REST responses, stream the assistant text back into the agent node for incremental TTS; if not, receive the full text and then feed to TTS.
Multi-turn context management
• Maintain chat_ctx in the AgentSession across turns.
◦ On user turn end: append the final STT transcript as a user message in chat_ctx.
◦ When calling vLLM, include the chat_ctx (previous user/assistant messages) plus a reference to the current-turn audio (e.g., keep audio->text mapping and include the STT text).
◦ On assistant response: append assistant text to chat_ctx.
• If you need the audio itself as part of context beyond text (e.g., prosody or audio-only cues), store a pointer/metadata for the audio chunk in chat_ctx and ensure your vLLM call includes that audio payload each turn.
• Preserve chat_ctx for follow-up turns; pass it on every subsequent call to vLLM so the model has full multi-turn history.
Turn detection and transcript visibility
• Use VAD + STT for robust turn boundaries. STT transcripts stream to the LiveKit UI as they are produced; the final transcript is the one you append to chat_ctx.
• Optionally show interim STT transcripts in UI, then replace with final transcript on turn end.
Implementation notes (concise)
• Implement a custom llm_node (node override) that:
a. Reads the buffered audio frames for the current turn.
b. Sends audio + current chat_ctx to vLLM via REST.
c. Yields assistant text chunks back into the session for TTS and chat storage.
• Keep STT running in parallel (standard STT node) so transcripts are emitted to LiveKit UI without changing UI code.
• Avoid private LiveKit APIs — do this using public node hooks: STT node for transcription/UI, custom LLM node for audio forwarding and assistant responses, TTS node unchanged for playback.
Why this meets your checklist
• User speaks in room: satisfied (LiveKit Room audio).
• Model receives user audio: satisfied (raw audio forwarded to vLLM).
• Assistant text streams and is spoken via existing TTS: satisfied (LLM node → TTS).
• Multi-turn works: satisfied (chat_ctx preserved and passed each turn).
• User transcripts appear in LiveKit UI: satisfied (STT node produces UI transcripts).
• Final messages stored in `chat_ctx`: satisfied (append STT-final and assistant text at turn end).
• Avoid private APIs: satisfied (use public pipeline/node overrides and REST to vLLM).
Questions to finalize an implementation
• Do you need streaming partial assistant text from vLLM, or can responses be returned as whole completions?
• Do you prefer sending raw PCM frames to vLLM, or a container format (wav/opus)? (This affects minimal packaging in your llm_node.)
• Which STT provider are you planning to use (so I can show exact node wiring)?
If you want, I can sketch a minimal llm_node code example showing how to collect audio frames, call vLLM REST, stream text back, and append to chat_ctx.
Sources: Realtime models overview | LiveKit Documentation | Function tool definition | LiveKit Documentation | Agent session | LiveKit Documentation | Pipeline nodes and hooks | LiveKit Documentation | Chat context | LiveKit Documentation, with interactive elementsbig-postman-34034
05/01/2026, 5:29 PMllm_node to access the current user turn’s raw audio buffer?
We want the standard STT node to keep producing transcripts/UI/chat history, but the custom LLM call must send the original user audio to /v1/chat/completions.
Questions:
1. Does llm_node receive or expose the finalized user-turn audio buffer through any public API?
2. If not, where should we tap/buffer audio frames using public LiveKit APIs?
3. Can we buffer audio in a custom STT wrapper while still using the STT provider normally?
4. Can we access the same audio frames from AgentSession or a pipeline hook without subscribing to room tracks manually?
5. How should we align the buffered audio turn with the final STT transcript / chat_ctx user message?
6. What public event should signal “this buffered audio belongs to this finalized user turn”?
7. Should the audio-buffering component live in:
◦ custom STT node/wrapper,
◦ custom LLM node,
◦ Agent/AgentSession hook,
◦ custom pipeline node,
◦ or a custom RealtimeSession?
8. Which private/internal APIs should we avoid for this?
Please propose the cleanest Python LiveKit implementation pattern for “standard STT for transcript + custom LLM node that needs current-turn audio”.tall-airline-16756
05/01/2026, 5:30 PMllm_node send the current turn’s raw audio to your vLLM REST endpoint.
Short answers to your numbered questions
1. No — llm_node does not automatically receive raw audio buffers via a public API.
2. Buffer audio in a custom STT wrapper/node (public STT node hook) that decorates the standard STT node.
3. Yes — you can buffer audio in a custom STT wrapper while delegating actual transcription to the underlying STT provider.
4. No — don’t rely on hidden AgentSession internals. Use the public STT node + Agent lifecycle hooks to share buffered audio with the rest of the session.
5. Align by using the STT turn-final signal (final transcript / END_OF_SPEECH / final transcript event) to mark which buffered audio belongs to that finalized user message, then attach both audio and final transcript to the same turn record.
6. Use the public STT “final transcript” or “end-of-speech” event (the standard final-transcript event your STT node yields) as the signal that “this buffered audio belongs to this finalized user turn.”
7. Best place for audio-buffering: primary logic in a custom STT node/wrapper, publish a turn-complete artifact via an Agent hook; LLM node reads that artifact. Do NOT use RealtimeSession for this when your model lacks WebSocket realtime support.
8. Avoid private/internal APIs like unpublished AgentSession internals, private track subscription hacks, or internal buffer variables. Rely on public node hooks, rtc audio frame handling APIs, and Agent lifecycle hooks.
Clean implementation pattern (step-by-step)
1) Create a custom STT wrapper node
• Implement a node that wraps the standard STT node. Public API signature remains the same so LiveKit still uses it for UI transcripts.
• As frames arrive, append them to an in-memory buffer for the current turn (e.g., list of frames or a streaming buffer). Also forward frames to the wrapped STT processor so transcription proceeds normally and transcripts are published to the UI.
2) Detect turn boundaries via public STT events
• Watch for the standard STT “final transcript” / END_OF_SPEECH event emitted by the underlying STT node.
• When that final event is received:
◦ Snapshot the buffered audio for the turn.
◦ Capture the final transcript text emitted by STT.
◦ Clear the buffer for the next turn.
3) Emit a public turn-complete artifact
• On final transcript, attach a turn object to the Agent instance (public field you manage), or better: call a documented Agent lifecycle hook (e.g., on_user_turn_completed / similar public hook) with a small object:
◦ { audio_bytes: wav/pcm bytes or frames, transcript: "final text", timestamp, turn_id }
• This is your canonical signal proving "this audio belongs to this final transcript."
4) Convert frames to a transfer format
• In the STT wrapper, convert buffered frames to the audio format your vLLM expects (wav/pcm/opus). Use public helper functions for combining frames and converting to WAV bytes before handing to the LLM node.
5) Make llm_node read the buffered audio on turn completion
• In your custom llm_node, before calling vLLM, check whether the Agent has a pending turn-complete artifact (the one emitted in step 3).
• If present, include the artifact.audio_bytes (and optionally metadata like sample rate or turn_id) when you POST to /v1/chat/completions.
• The llm_node still uses chat_ctx for multi-turn textual history; audio is an additional payload tied to the latest user message.
6) Maintain chat_ctx alignment
• When final transcript arrives, append the final STT text to chat_ctx as the user message (this keeps UI and history consistent).
• When LLM returns assistant text, append assistant message to chat_ctx.
• If you want the audio referenced in chat_ctx, add a metadata object or a short pointer in the user message (e.g., { audio_ref: "<turn_id>" }) so chat_ctx indicates which audio was used.
7) Concurrency & partial transcripts
• Continue streaming interim transcripts to the UI as normal (the STT wrapper should forward interim events).
• Only snapshot and send the buffered audio when the final-transcript/turn-complete event fires (avoids partial-audio mismatches).
• If you need lower-latency partial audio sends, implement a policy in the STT wrapper to flush partial buffers with matching partial-transcript markers, but align finalization on the final transcript event.
Why this pattern is clean and public-API friendly
• Uses public STT node hook to capture raw audio — no manual room-track subscriptions or private internals.
• Leaves STT provider unchanged for UI/transcript history.
• Keeps llm_node simple: it consumes a clearly-signaled turn artifact rather than trying to intercept audio streams itself.
• Keeps multi-turn chat_ctx semantics intact (STT user message appended first, assistant message appended after LLM reply).
• Avoids requiring /v1/realtime or any private LiveKit APIs.
Minimal code sketch (conceptual)
• STT wrapper buffers frames, forwards frames to inner STT, on final event creates TurnArtifact {turn_id, audio_bytes, transcript} and stores it on the Agent (or passes to on_user_turn_completed hook).
• llm_node checks Agent.pending_turn_artifact, converts audio_bytes if needed, POSTs to vLLM, streams text back, appends assistant message to chat_ctx, clears artifact.
Final checklist to confirm
• Do you want audio sent only after final transcript, or do you need partial/streaming audio to vLLM?
• What audio format does vLLM expect (PCM, WAV, opus)? I can show the exact buffer-to-bytes conversion for that format.
• Which STT provider are you using (so I can show the precise event names / helper functions)?
If you want, I’ll provide a compact Python code example for the STT wrapper + turn artifact + llm_node wiring for your exact STT provider and vLLM audio format.
Sources: Pipeline nodes and hooks | LiveKit Documentation | Capturing the First Few Seconds of a Call for Processing, with interactive elementsbig-postman-34034
05/01/2026, 5:33 PMTurnArtifact containing:
◦ turn_id
◦ final transcript
◦ audio bytes or frames for that turn
◦ timestamps / sample rate if needed
• custom llm_node reads the matching `TurnArtifact`;
• llm_node calls /v1/chat/completions with:
◦ selected chat_ctx
◦ current turn audio
◦ final STT transcript
• assistant text streams to the existing TTS node.
Please answer with exact Python SDK mechanisms:
1. What class/function should we wrap or override for STT?
2. Does the STT node/wrapper receive `rtc.AudioFrame`s directly? If yes, what is the method signature?
3. How do we forward the same frames to the underlying STT provider without breaking normal transcript behavior?
4. What exact event/object indicates interim transcript vs final transcript?
5. What exact public hook should create/store the TurnArtifact?
6. How can the custom llm_node access that TurnArtifact?
7. What is the exact Python signature for overriding llm_node?
8. In llm_node, how do we stream assistant text chunks back so LiveKit TTS consumes them?
9. Who commits the final user transcript into `chat_ctx`: the standard STT lifecycle or our wrapper?
10. Who commits the assistant final text into `chat_ctx`: LiveKit LLM/TTS lifecycle or our llm_node?
11. What public APIs should we use to avoid private methods like _conversation_item_added or direct _chat_ctx mutation?
12. Please provide a compact code skeleton showing:
◦ STT wrapper
◦ TurnArtifact storage
◦ custom llm_node
◦ AgentSession wiring
Please mark any part that is approximate or version-dependent.tall-airline-16756
05/01/2026, 5:35 PMbig-postman-34034
05/01/2026, 5:39 PMTurnArtifact containing:
turn_id
final transcript
audio bytes or frames for that turn
timestamps / sample rate if needed
• custom llm_node reads the matching `TurnArtifact`;
• llm_node calls /v1/chat/completions with:
selected chat_ctx
current turn audio
final STT transcript
• assistant text streams to the existing TTS node.
Please answer with exact Python SDK mechanisms:
1. What class/function should we wrap or override for STT?
2. Does the STT node/wrapper receive `rtc.AudioFrame`s directly? If yes, what is the method signature?
3. How do we forward the same frames to the underlying STT provider without breaking normal transcript behavior?
4. What exact event/object indicates interim transcript vs final transcript?
5. What exact public hook should create/store the TurnArtifact?
6. How can the custom llm_node access that TurnArtifact?
7. What is the exact Python signature for overriding llm_node?
8. In llm_node, how do we stream assistant text chunks back so LiveKit TTS consumes them?
9. Who commits the final user transcript into `chat_ctx`: the standard STT lifecycle or our wrapper?
10. Who commits the assistant final text into `chat_ctx`: LiveKit LLM/TTS lifecycle or our llm_node?
11. What public APIs should we use to avoid private methods like _conversation_item_added or direct _chat_ctx mutation?
12. Please provide a compact code skeleton showing:
STT wrapper
TurnArtifact storage
custom llm_node
AgentSession wiring
Please mark any part that is approximate or version-dependent.big-postman-34034
05/07/2026, 10:57 PM