Voice is the hardest place to use an LLM. In chat, a 3-second wait is fine. On a phone call, the same pause makes the conversation feel broken. In this tutorial you'll build a production-ready streaming voice agent pipeline with Python that keeps total model response latency under 200 milliseconds — using modern streaming APIs, the right latency budget, and a fallback chain so your agent never goes silent.
The Latency Budget of a Natural Conversation
Human conversation researchers put the comfortable gap between speakers at roughly 200–500 milliseconds. Your voice agent's "brain" has to fit inside that window:
Speech end detected ──► [VAD] ──► [LLM first token] ──► [TTS first audio]
└────────── total should be < 500ms ──────────┘| Stage | Budget | Notes |
|---|---|---|
| Voice activity detection (VAD) | 20–50ms | Local, e.g. Silero VAD |
| Network + provider TTFB | 50–100ms | Choose nearby regions |
| LLM time-to-first-token | 50–150ms | The big lever |
| TTS first audio chunk | 50–100ms | Stream, never wait for full audio |
The single biggest mistake developers make: measuring average generation speed instead of time-to-first-token (TTFT). A model that generates 500 tokens/sec but takes 900ms to start is useless for voice.
Step 1: Pick a Latency-Optimized Model
For voice agents you want models with low TTFT and fast streaming — think speed-optimized tiers like GPT-5.6 Luna, Gemini Flash-Lite variants, or purpose-built fast models such as Mercury. On Qubax you can compare real-time prices and pick a primary/fallback pair. A good pattern:
- Primary: fastest low-cost model (e.g., sub-$0.20/M input)
- Fallback: a second provider's fast tier, in case of outages
- Escalation: a stronger model only for hard queries (detected by a cheap classifier call or keyword router)
Step 2: Streaming Chat with Token-by-Token Forwarding
Never wait for the full completion. Forward tokens to TTS as they arrive:
import asyncio
from openai import AsyncOpenAI
client = AsyncOpenAI(
base_url="https://api.qubax.ai/v1",
api_key="YOUR_QUBAX_API_KEY",
)
async def stream_reply(messages, model="gpt-5.6-luna"):
stream = await client.chat.completions.create(
model=model,
messages=messages,
max_tokens=120, # voice replies must be SHORT
stream=True,
)
async for chunk in stream:
delta = chunk.choices[0].delta.content
if delta:
yield deltaTwo voice-specific tips:
- Cap max_tokens around 100–150. Spoken answers should be 1–3 sentences. This caps worst-case latency and cost.
- Flush on punctuation. Your TTS layer should start synthesizing at the first comma or period — don't buffer the whole reply.
Step 3: Sentence-Level TTS Handoff
Synthesize audio sentence-by-sentence while the model is still writing:
import re
async def sentences(token_stream):
buf = ""
async for tok in token_stream:
buf += tok
# flush at sentence-ending punctuation
if re.search(r"[.!?…](\s|$)", buf):
yield buf.strip()
buf = ""
if buf.strip():
yield buf.strip()
async def speak(text):
# call your TTS provider's streaming endpoint here
# and play audio chunks as they arrive
...This overlapping (LLM still generating while TTS speaks sentence one) is how you hide most of the pipeline latency.
Step 4: The Fallback Chain — Never Go Silent
Outages happen. Wrap the primary call with a timeout and an automatic failover:
import httpx
MODELS = ["gpt-5.6-luna", "gemini-3.5-flash-lite", "mercury-2"]
async def reply_with_fallback(messages):
for model in MODELS:
try:
async with asyncio.timeout(1.5): # 1.5s hard budget
return await stream_reply(messages, model=model)
except (TimeoutError, Exception):
continue # try next provider
return "Sorry, could you repeat that?" # last resortThe 1.5-second timeout is deliberate: if the primary hasn't delivered a first token by then, the user already perceives a lag — switch immediately rather than making them wait.
Step 5: Measure the Right Numbers
Log these per turn and alert on percentiles, not averages:
- TTFT p95 — should be under ~200ms for your primary model
- First-audio latency — VAD end → first TTS audio out
- Barge-in rate — how often users interrupt (a proxy for feeling "slow")
- Cost per conversation — short max_tokens keeps this predictable
A simple tracking middleware around each turn gives you the data to tune models, timeouts, and prompt length over time.
Common Pitfalls
- Long system prompts kill TTFT. With prompt caching enabled, keep the stable prefix large and cacheable, and put volatile context at the end.
- Waiting for full TTS audio. Always stream TTS chunk-by-chunk.
- One provider, no fallback. Even a 0.1% outage rate is thousands of dead calls at voice scale.
- Verbose prompts producing verbose replies. Add: "Reply in 1–2 short sentences, conversational tone."
Wrapping Up
Sub-200ms voice agents are achievable today with commodity APIs — the recipe is: latency-optimized model, token streaming, sentence-level TTS overlap, hard timeouts with fallbacks, and percentile-based monitoring. Start by picking a fast primary/fallback model pair on Qubax models, then wire up the streaming loop above. Full API details are in the Qubax documentation.
FAQ
What TTFT should I target for a voice agent?
Under 200ms p95 for the model's first token, keeping total first-audio latency under ~500ms.
Which model is best for voice agents?
Speed-optimized tiers: GPT-5.6 Luna, Gemini Flash-Lite, or diffusion-based models like Mercury. Compare live pricing on the Qubax models page.
Should I use WebSockets or HTTP for streaming?
Server-sent events or streamed HTTP works for the LLM; use WebSockets for the audio leg (microphone in, TTS out) to avoid connection setup cost per turn.
How do I stop the agent from talking too long?
Cap max_tokens (~120) and instruct the model in the system prompt to answer in 1–2 short conversational sentences.
Can I test this without a phone system?
Yes — loop a microphone, VAD, and speakers locally. Only swap in a telephony provider (e.g., Twilio Media Streams) when the pipeline feels right.