Voice Connectors
A connector is how deepeval reaches your voice agent. In voice mode it opens a live session, plays the simulated user's audio, captures the spoken reply, and times how long that reply took to start.
Why Agent Connectors?
Every voice platform is reached differently β Vapi mints a call over REST before any audio moves, ElevenLabs takes base64 audio in JSON, Pipecat takes protobuf frames, LiveKit is a WebRTC track in a room. A connector holds all of that, so the simulator asks the same thing of every agent:
Switching platforms is therefore a different connector and nothing else β your goldens, metrics, and simulation config don't change.
Setup Agent Connector
Each connector needs only enough to find your agent β an id, a URL, or credentials β and everything else is shared:
Your agent lives in the ElevenLabs dashboard, so the connector only needs its agent_id. It speaks ElevenLabs' WebSocket API directly, so the elevenlabs package isn't required. Private agents also need an API key.
from deepeval.voice import ElevenLabsConnector
connector = ElevenLabsConnector(agent_id="your-agent-id")Client tools, per-conversation overrides, and every parameter are on the ElevenLabs integration page.
Your assistant lives in Vapi, so the connector needs its assistant_id and an API key to create the call with. Phone parameters are rejected on Vapi's WebSocket transport, so the simulator is the only caller and no Vapi SDK is required.
from deepeval.voice import VapiConnector
connector = VapiConnector(assistant_id="your-assistant-id")Per-call overrides and every parameter are on the Vapi integration page.
A LiveKit agent has no id to dial β it's a worker dispatched into a room. So you give the connector credentials, and it mints a token, creates a room, and joins as another participant. This one needs LiveKit's SDKs:
pip install livekit livekit-apifrom deepeval.voice import LiveKitConnector
connector = LiveKitConnector(
url="wss://your-project.livekit.cloud",
api_key="your-api-key",
api_secret="your-api-secret",
agent_name="restaurant-agent",
)Bringing your own room, transcripts, and every parameter are on the LiveKit integration page.
Your pipeline is self-hosted, so the connector needs the URL its WebSocket transport is serving on and nothing else. It speaks the protobuf frames Pipecat serializes by default, so pipecat-ai isn't required to run the simulation.
from deepeval.voice import PipecatConnector
connector = PipecatConnector(url="ws://localhost:8765/ws")Serving your pipeline, RTVI turn signals, and every parameter are on the Pipecat integration page.
For a custom agent that accepts and emits raw audio over a WebSocket, describe its message shape β which JSON keys carry audio in and out, whether frames are binary, and what marks the end of a turn.
from deepeval.voice import WebSocketConnector
connector = WebSocketConnector(
url="wss://your-agent.example.com/audio",
send_key="audio",
receive_audio_key="audio",
turn_complete_type="turn_end",
)Every parameter is under Generic WebSocket below.
Wraps an in-process Python callable, so you can exercise the full TTS to agent to STT pipeline with no network transport at all.
from deepeval.voice import CallbackVoiceConnector
connector = CallbackVoiceConnector(my_agent)What your callable may return is under Callback below.
A connector is not a tracing callback: it joins the call from outside and owns the live session β auth, transport, teardown β while deepeval layers the turn-taking on top.
Here are the connectors available on deepeval so far:
| Connector | Transport | Use it for |
|---|---|---|
VapiConnector | WebSocket | Vapi assistants, addressed by assistant_id. |
ElevenLabsConnector | WebSocket | ElevenLabs agents, addressed by agent_id. |
LiveKitConnector | WebRTC | Agents dispatched into LiveKit rooms. |
PipecatConnector | WebSocket | Self-hosted Pipecat pipelines, addressed by URL. |
WebSocketConnector | WebSocket | Custom agents that speak raw audio over a WebSocket. |
CallbackVoiceConnector | In-process | A Python callable, no network β handy for testing your own pipeline. |
Generic WebSocket
For custom agents that accept and emit raw audio over a WebSocket, WebSocketConnector lets you describe your agent's message shape β which JSON keys carry audio in and out, whether frames are binary, and what marks the end of a turn.
from deepeval.voice import VoiceConfig, WebSocketConnector
connector = WebSocketConnector(
url="wss://your-agent.example.com/audio",
send_key="audio",
receive_audio_key="audio",
turn_complete_type="turn_end",
)
voice_config = VoiceConfig(
connector=connector,
...,
)There are ONE mandatory and TWELVE optional parameters when creating a WebSocketConnector:
url: a string WebSocket URL for your agent (e.g."wss://your-agent.example.com/audio").- [Optional]
headers: a dictionary of HTTP headers for the WebSocket handshake. Defaulted toNone. - [Optional]
sample_rate: an integer sample rate in Hz for outbound audio. Defaulted to24000. - [Optional]
send_key: a string JSON key for outbound audio payloads. Defaulted to"audio". - [Optional]
binary_outbound: a boolean which when set toTrue, sends outbound frames as binary instead of JSON. Defaulted toFalse. - [Optional]
receive_audio_key: a string JSON key for inbound audio payloads. Defaulted to"audio". - [Optional]
binary_inbound: a boolean which when set toTrue, treats inbound frames as binary audio. Defaulted toFalse. - [Optional]
receive_transcript_key: a string JSON key for inbound transcripts when the platform provides them. Defaulted toNone. - [Optional]
turn_complete_type: a string message type that marks end-of-turn. Defaulted toNone. - [Optional]
type_key: a string JSON key used to read the message type. Defaulted to"type". - [Optional]
init_messages: a list of strings or dicts sent after connect. Defaulted to[]. - [Optional]
ready_on: when the session is considered ready β"connect"by default. Defaulted to"connect". - [Optional]
turn_detection: how long to wait before deciding your agent has finished speaking β"eager","balanced", or"patient". See Turn Detection. Defaulted to"balanced".
Callback
Wraps an in-process Python callable, so you can exercise the full TTS β agent β STT pipeline without any network transport β useful for testing your simulation setup before pointing it at a deployed agent.
from deepeval.voice import CallbackVoiceConnector, VoiceConfig
connector = CallbackVoiceConnector(my_agent)
voice_config = VoiceConfig(
connector=connector,
...,
)There are ONE mandatory and THREE optional parameters when creating a CallbackVoiceConnector:
agent: a sync or async callable that accepts userAudioand returns aConnectorTurnor plainAudio.- [Optional]
sample_rate: an integer sample rate in Hz. Defaulted to24000. - [Optional]
encoding: a string container/codec label for audio metadata. Defaulted to"wav". - [Optional]
turn_detection: how long to wait before deciding your agent has finished speaking β"eager","balanced", or"patient". See Turn Detection. Defaulted to"balanced".
Returning a ConnectorTurn
Your callback is the agent. Return either:
- an
Audioreply β the connector wraps it and fillslatency_msfrom wall time, or - a
ConnectorTurnwhen you also want to supply a transcript and/or your own latency.
class ConnectorTurn:
audio: Audio
transcript: Optional[str] = None
latency_ms: Optional[float] = None
interrupted: bool = FalseThere are ONE mandatory and THREE optional fields on a ConnectorTurn:
audio: anAudioobject with the agent's spoken reply.- [Optional]
transcript: a string with the agent's own transcript. When set,deepevaluses it as assistantTurn.contentand skips STT for that turn. Defaulted toNone. - [Optional]
latency_ms: a number measuring time from receiving the user audio to the start of the reply. When omitted, the connector measures wall time around your callback. Defaulted toNone. - [Optional]
interrupted: a boolean set toTruewhen this reply was cut short by a barge-in. Defaulted toFalseβ leave it alone unless you're simulating interruption behavior yourself.
from deepeval.voice import CallbackVoiceConnector
from deepeval.voice.connectors import ConnectorTurn
# Minimal β return Audio or ConnectorTurn(audio=...)
async def my_agent(user_audio) -> ConnectorTurn:
reply_audio = await your_voice_agent(user_audio)
return ConnectorTurn(audio=reply_audio)
# With a known transcript (skips STT) and measured latency
async def my_agent(user_audio) -> ConnectorTurn:
reply_audio, text, latency_ms = await your_voice_agent(user_audio)
return ConnectorTurn(
audio=reply_audio,
transcript=text,
latency_ms=latency_ms,
)
Full-Duplex Custom Connectors
WebSocketConnector and CallbackVoiceConnector take turns by default. If your transport can carry both voices at once, override supports_duplex and the simulator will keep listening while the caller speaks, even without an interruption_behavior:
from deepeval.voice import WebSocketConnector
class MyDuplexConnector(WebSocketConnector):
@property
def supports_duplex(self) -> bool:
return TrueReturning True requires stream_uplink, iter_agent_events, and stop_uplink to work on your connector, since the duplex path uses those instead of exchange_turn.
Nothing in an audio stream announces that your agent has finished speaking, so the end of its turn is inferred from silence. Every connector takes a turn_detection preset controlling how long that silence has to last, along with the hard ceiling that stops an agent which never goes quiet from hanging the simulation:
turn_detection | Waits through pauses of | Gives up after | Use it when |
|---|---|---|---|
"eager" | 500ms | 20s | Your agent answers in one breath and you want the floor back quickly. |
"balanced" | 800ms | 30s | Default. Suits most agents. |
"patient" | 2.5s | 120s | Your agent pauses mid-reply β to think, call a tool, or look something up. |
connector = CallbackVoiceConnector(my_agent, turn_detection="patient")Pick by listening to your agent's longest natural pause. Too eager and its turn ends at that pause, cutting the reply short and discarding the rest of it; too patient and the simulator sits through dead air before responding, which inflates the gaps in the recording and slows the run.
On the duplex path the preset is a ceiling, not the wait: when the running transcript reads as a finished sentence and covers all speech heard so far, the turn ends after 500ms of silence, while a reply that trails off mid-sentence still waits the full window.
When your transport closes each turn explicitly, silence is only the fallback: ElevenLabsConnector, VapiConnector, CallbackVoiceConnector, and a WebSocketConnector given a turn_complete_type are all believed over a pause, so the reply is heard out in full however long your agent stops for. Only the ceiling still applies.
The preset matters more everywhere else, because silence is the only evidence there is. LiveKitConnector gets a bare audio track with nothing to say when a turn is over, and so does a WebSocketConnector whose agent sends no end-of-turn message to point at. PipecatConnector falls on either side depending on your pipeline: it's believed over a pause once your pipeline has been seen to announce an end of turn, and reads silence until then.