Skip to main content
This guide uses Tuner’s observability libraries for custom voice stacks: tuner-core, tuner-stt-observer, tuner-tts-observer, and tuner-langchain.
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 any component you don’t use. Your audio loops, agent logic, and synthesis code stay exactly as they are; Tuner observes them through each provider’s native event system.

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

Every Tuner library follows the same three-step lifecycle:
1

Create one TunerSession per call

The session coordinates everything and holds your call’s identity — call_id, workspace, and agent mapping.
2

Attach observers

Each observer hooks into one component of your stack — STT, TTS, or your LLM framework — using that component’s native event system.
3

flush() at call end

The session assembles everything the observers captured into a single structured timeline and sends it to Tuner via the Create Call API. One network call, at the end.

Step 1: Install the libraries

Full stack from this guide:
tuner-core provides TunerSession and TunerConfig. Provider SDKs are behind extras so you only pull in what you use.

Step 2: Set Your Credentials

TunerConfig.from_env() reads your credentials from environment variables:
VariableRequiredDescription
TUNER_API_KEYBearer token (starts with tr_api_)
TUNER_WORKSPACE_IDYour Tuner workspace ID
TUNER_AGENT_IDAgent Remote ID from Agent Settings → Agent Connection (UUID — not your agent’s display name)

Step 3: Create a session per call

At the start of each call, create one TunerSession. The model names you pass are used for cost calculation. call_id is your unique identifier for the call — keep it stable across retries so ingestion stays idempotent.

Step 4: Attach observers

Attach only the ones that match your stack — each is independent.

STT — Deepgram

Attach the adapter to the Deepgram connection you already have. It’s a pure observer — it captures complete user utterances, turn timing, STT latency, and STT usage; it never touches your audio.
Your LiveOptions must set utterance_end_ms (e.g. "1000") and vad_events=True. Without them, UtteranceEnd never fires and the utterance buffer never flushes — you’ll see calls with no user turns.
Using Speechmatics instead? See Speech-to-Text.

LLM — LangGraph

Wrap your graph and attach it. Node transitions, per-node LLM latency, and tool calls and results all land in the call timeline. Invoke the wrapped graph exactly as you would the original.
Privacy controls and plain-chain support are covered in LangChain / LangGraph.

TTS — Cartesia

Wrap your synthesis stream with track_ws(). Tuner observes synthesis — it never owns or drives it. When the user barges in and you stop streaming, Tuner records only the words actually spoken, not the full intended response.
Word-accurate interruption capture requires add_timestamps=True on your Cartesia context. An SSE pattern is available for HTTP-only stacks — see Text-to-Speech.
track_ws() only records an interruption once your app reports one — building working barge-in also needs a detection signal, mark_interrupted()/wait_for_interruption(), and client-side playback stopping. See 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 agent loop raises:
Open Tuner → Calls to see the processed result.

Mix and match

Your stackInstall
Deepgram + LangGraph + Cartesiatuner-core "tuner-stt-observer[deepgram]" "tuner-tts-observer[cartesia]" tuner-langchain
Speechmatics + Cartesiatuner-core "tuner-stt-observer[speechmatics]" "tuner-tts-observer[cartesia]"
STT observability onlytuner-core "tuner-stt-observer[deepgram]"
LLM tracing onlytuner-core tuner-langchain
LiveKit or PipecatUse the LiveKit / Pipecat SDKs instead
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. The Create Call API is the alternative to the libraries, not a supplement: each call is submitted exactly once, either by session.flush() or by your own request.

Troubleshooting

  • Verify that TUNER_AGENT_ID is the Agent Remote ID from Agent Settings → Agent Connection (not your agent’s display name).
  • 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.
  • Your LiveOptions must set utterance_end_ms (e.g. "1000") and vad_events=True — without them Deepgram never emits UtteranceEnd and the utterance buffer never flushes.
  • Confirm caller audio is routed through stt.send_audio(), not sent to the Deepgram connection directly.

What’s Next?

Speech-to-Text

Deepgram and Speechmatics adapters and 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 call timeline.

Review your first call

See what Tuner does with the data once it lands.