Skip to main content
Uses tuner-core plus one observer per role in your pipeline. Not on Python? See Direct API.
This guide shows the Tuner lines you add to a voice agent you already have. The examples assume Deepgram for STT, LangGraph for the LLM layer, and Cartesia for TTS — swap or skip any role you don’t use. Your audio loops, agent logic, and synthesis code stay exactly as they are. Attach the observers and you get, without writing any of it: user and agent turns with timing, per-turn STT/LLM/TTS and end-to-end latency, tool calls and results in the same timeline, word-accurate interruption capture, and usage plus cost.

Prerequisites

  • Tuner Active Account
  • Configured Agent with provider “Custom API” in Tuner
  • Python ≥ 3.10 and a custom voice stack you control

The mental model

1

Create one TunerSession per call

tuner-core is the backbone. The session holds your call’s identity — call_id, workspace, and agent mapping — and coordinates everything else.
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:

Step 3: Create a session per call

One TunerSession at the start of each call. The model names are used for cost calculation. Keep call_id stable across retries so ingestion stays idempotent.
TunerConfig.from_env() also takes extra_metadata={...} to attach arbitrary key-value data to the call record, and sip_call_id=... to link a simulated call back to its simulation run — see Simulation SIP setup.

Step 4: Attach observers

STT — Deepgram

Send audio to Deepgram exactly as you normally would — the adapter only listens on the connection’s events. Your 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

Node transitions, per-node LLM latency, and tool calls land in the timeline. Plain chains and privacy controls: LangChain / LangGraph.

TTS — Cartesia

Word-accurate interruption capture, the SSE pattern for HTTP-only stacks, and what your app owns for barge-in: Text-to-Speech.

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:
Open Tuner → Calls to see the processed result.

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.

Troubleshooting

  • Verify TUNER_AGENT_ID is the Agent Remote ID from Agent Settings → Agent Connection.
  • Confirm TUNER_WORKSPACE_ID is correct.
  • Confirm session.flush() actually ran — put it in a finally block so exceptions don’t skip it.
  • Ensure TUNER_API_KEY starts with tr_api_ and is valid.
  • Confirm the API key belongs to the correct workspace.
Role-specific problems — missing user turns, interrupted turns recording in full, latency fields absent — are on the Speech-to-Text and Text-to-Speech pages.

What’s next?

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.