Uses
tuner-core plus one observer per role in your pipeline. Not on Python? See Direct API.Prerequisites
- An active Tuner account
- A Tuner agent with provider Custom API
- A Tuner API key, your workspace ID and the agent’s Agent Remote ID. See API keys.
- Python 3.10 to 3.13 and a custom voice stack you control
The mental model
1
Create one TunerSession per call
tuner-core coordinates the call. The session holds your call’s identity (call_id, workspace, and agent mapping) and coordinates the observers.2
Attach one observer per role
STT, LLM, and TTS each have their own package. Attach only the roles your stack has; each is independent.
3
flush() at call end
The session assembles what the observers captured into a single timeline and sends it to Tuner via the Create Call API. One network call, at the end.
Step 1: Install
tuner-core provides TunerSession and TunerConfig. Provider SDKs sit behind extras so you only pull in what you use. Other combinations are in Mix and match below.
Step 2: Set your credentials
TunerConfig.from_env() reads these from the environment. Keyword arguments you pass to it take priority.
Step 3: Create a session per call
Create oneTunerSession at the start of each call. The model names are recorded as call metadata. Keep call_id stable across retries so ingestion stays idempotent.
Extra metadata
Attach arbitrary key-value data to every call, such as environment, region, deployment version, or tenant:ai_models and usage_token are reserved and ignored in extra_metadata.
Cost calculation
Pass acost_calculator callable. At flush, it receives a CallUsage and returns the call cost in USD cents. The rates below are illustrative.
CallUsage fields: llm_prompt_tokens, llm_completion_tokens, llm_total_tokens, tts_characters, stt_audio_seconds. If the callable raises, the call is sent without a cost.
Disconnection reason
Setsession.disconnection_reason before flush() to record why the call ended. Use a DisconnectReason value or your own string:
Step 4: Attach observers
STT: Deepgram
LiveOptions must set utterance_end_ms and vad_events=True, or you’ll see calls with no user turns. Speechmatics, custom providers, and what each adapter captures: Speech-to-Text.
LLM: LangGraph
TTS: Cartesia
Step 5: Flush at call end
flush() is the only network call the libraries make. Put it in a finally block so the call reaches Tuner even when the connection drops or your loop raises:
Enable simulation
Required if you’ll use Call Simulation. Skip it otherwise. Tuner links each simulated call to its run through a value you send back:sip_call_id for inbound agents and recipient for outbound agents (how simulated calls are linked). Do the steps for your agent’s call direction.
Inbound
Tuner calls your agent over SIP.- Add your agent’s SIP URI in Agent Settings > Simulation Setup. Get it from your SIP provider, then see Set up inbound simulation.
- Pass the SIP
Call-IDof the call Tuner placed assip_call_id. Read it from the inbound call in your SIP stack; it may arrive under a different name.
sip_call_id, Tuner uses it in place of call_id to detect duplicates.
Outbound
Your agent calls a Simulated Caller at a SIP URI that Tuner sends to your trigger endpoint. Tuner links the call throughrecipient, and tuner-core can’t send recipient yet.
- Configure the trigger endpoint in Agent Settings > Simulation Setup. See Set up outbound simulation.
- Send outbound calls with the Create Call API instead of
session.flush(), with the dialed SIP URI inrecipient. See Direct API: Enable simulation.
Check simulation works
Start a simulation run. See Run a simulation. When the call ends, check its result in the run. If it shows Unreported, Tuner never received a matching call from your agent. See Fix an Unreported run.Mix and match
Missing an adapter for one of your providers? Extend the base adapter for that role. See Custom providers on the Speech-to-Text and Text-to-Speech pages.
Before you finish
- A test call appears in Call Logs.
- Your API key, workspace ID and Agent Remote ID come from API keys.
- If you use Call Simulation: Enable simulation is done for your agent’s call direction, and one simulated call shows a result other than Unreported.
Troubleshooting
Calls don't appear in Tuner
Calls don't appear in Tuner
- Verify
TUNER_AGENT_IDis the Agent Remote ID from Agent Settings > Agent Connection. - Set
TUNER_DEBUG=trueto log the full payload on flush. - Confirm
TUNER_WORKSPACE_IDis correct. - Confirm
session.flush()ran. Put it in afinallyblock so exceptions don’t skip it.
Next steps
Speech-to-Text
Deepgram and Speechmatics adapters, options, custom providers.
Text-to-Speech
Cartesia over WebSocket or SSE, word-level interruption capture.
LangChain / LangGraph
Node transitions, tool calls, and LLM latency in your timeline.
Review your first call
What Tuner does with the data once it lands.