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 for Call Simulation. Skip it otherwise.Inbound
Tuner calls your agent over SIP.
If your calls come through a carrier, the carrier sends each call to a webhook, then opens a WebSocket for the audio. Tuner’s SIP data reaches the webhook but not the WebSocket. Forward it from the webhook, then attach it to the session.
1
Add your SIP URI in Tuner
Add your agent’s SIP URI in Agent Settings > Simulation Setup. Get it from your SIP provider, then see Set up inbound simulation.
2
Forward the SIP data from your webhook
Read the SIP data from the carrier’s request and put it on the WebSocket.
- Twilio
- Telnyx
3
Attach the SIP data to the session
In your WebSocket handler, read the SIP data from the first message and attach it to the session. Do this before you call
flush().SipCallId and X-Correlation-Id. Telnyx’s call.initiated event has no SIP Call-ID, so X-Correlation-Id links a Telnyx call. Tuner links the call on either one. You can call attach_sip more than once. Headers merge, and the first non-empty value wins. It never changes the shared config.
Your own SIP trunk or another carrier
Your own SIP trunk or another carrier
If you already have the values, pass them directly:To read a carrier’s payload with
extract_sip, register a class with a name and extract(payload) -> SipInfo. Add forward_to_websocket(info, ws_url) -> Forward to support forwarding.Set the values in the config instead
Set the values in the config instead
TunerConfig also takes sip_call_id, sip_headers and recipient. They are per call, so use a separate config for each call. They have no environment variables.Older SDK versions
Older SDK versions
tuner-core 0.1.1 to 0.1.2 accepts sip_call_id on TunerConfig, which links calls on the SIP Call-ID. sip_headers, recipient, attach_sip and tuner_core.telephony need 0.1.3 or later.sip_call_id doesn’t replace call_id. Tuner uses call_id to detect duplicates.
Outbound
Your agent calls a Simulated Caller at a SIP URI that Tuner sends to your trigger endpoint.- Configure the trigger endpoint in Agent Settings > Simulation Setup. See Set up outbound simulation.
- Report the SIP URI your agent dialed as
recipient, exactly as Tuner sent it. It needstuner-core0.1.3 or later.
start message.
- Twilio
- Telnyx
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.