> ## Documentation Index
> Fetch the complete documentation index at: https://docs.usetuner.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# SDK Quickstart

> Add Tuner observability to a custom Python voice stack — a session, an observer per pipeline role, one flush at call end.

<Note>
  Uses [`tuner-core`](https://pypi.org/project/tuner-core/) plus one observer per role in your pipeline. Not on Python? See [Direct API](/docs/integrations/custom-stack/direct-api).
</Note>

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

<Steps>
  <Step title="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.
  </Step>

  <Step title="Attach one observer per role">
    STT, LLM, and TTS each have their own package. Attach only the roles your stack has; each is independent.
  </Step>

  <Step title="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](/docs/api-reference/create-call). One network call, at the end.
  </Step>
</Steps>

## Step 1: Install

```shell theme={null} theme={null}
pip install tuner-core "tuner-stt-observer[deepgram]" "tuner-tts-observer[cartesia]" tuner-langchain
```

`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](#mix-and-match) below.

## Step 2: Set your credentials

`TunerConfig.from_env()` reads these from the environment:

```shell theme={null} theme={null}
export TUNER_API_KEY="tr_api_..."
export TUNER_WORKSPACE_ID="123"
export TUNER_AGENT_ID="fa4da74c-..."
```

| Variable             | Required | Description                                                    |
| -------------------- | -------- | -------------------------------------------------------------- |
| `TUNER_API_KEY`      | ✅        | Bearer token (starts with `tr_api_`)                           |
| `TUNER_WORKSPACE_ID` | ✅        | Your Tuner workspace ID                                        |
| `TUNER_AGENT_ID`     | ✅        | **Agent Remote ID** from **Agent Settings → Agent Connection** |

## 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.

```python theme={null} theme={null}
from tuner_core import TunerConfig, TunerSession

session = TunerSession(
    config=TunerConfig.from_env(
        asr_model="nova-2",
        llm_model="gpt-4o-mini",
        tts_model="sonic-2",
    ),
    call_id=call_id,  # your unique, retry-stable identifier for this call
)
```

`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](/docs/simulation/inbound/setup).

## Step 4: Attach observers

### STT — Deepgram

```python theme={null} theme={null}
from tuner_stt_observer import DeepgramAdapter

stt = DeepgramAdapter(connection=dg_connection)   # your existing Deepgram connection
stt.on_utterance = handle_user_utterance          # your callback: complete user turn text
session.attach(stt)
```

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](/docs/integrations/custom-stack/speech-to-text).

### LLM — LangGraph

```python theme={null} theme={null}
from tuner_langchain import wrap_graph

graph = session.attach(wrap_graph(your_graph))

# then invoke as usual:
result = await graph.ainvoke({"messages": history})
```

Node transitions, per-node LLM latency, and tool calls land in the timeline. Plain chains and privacy controls: [LangChain / LangGraph](/docs/integrations/custom-stack/langchain).

### TTS — Cartesia

```python theme={null} theme={null}
from tuner_tts_observer import CartesiaAdapter

tts = session.attach(CartesiaAdapter())

# around your existing Cartesia WebSocket receive loop:
async with tts.track_ws(agent_text) as tracked:
    async for chunk in tracked(ctx.receive()):    # ctx: your Cartesia context
        ...                                        # your playback / send code
```

Word-accurate interruption capture, the SSE pattern for HTTP-only stacks, and what your app owns for barge-in: [Text-to-Speech](/docs/integrations/custom-stack/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:

```python theme={null} theme={null}
try:
    ...  # your call loop
finally:
    await session.flush()
```

Open **Tuner → Calls** to see the processed result.

## Mix and match

| Your stack                      | Install                                                                                            |
| ------------------------------- | -------------------------------------------------------------------------------------------------- |
| Deepgram + LangGraph + Cartesia | `tuner-core "tuner-stt-observer[deepgram]" "tuner-tts-observer[cartesia]" tuner-langchain`         |
| Speechmatics + Cartesia         | `tuner-core "tuner-stt-observer[speechmatics]" "tuner-tts-observer[cartesia]"`                     |
| STT observability only          | `tuner-core "tuner-stt-observer[deepgram]"`                                                        |
| LLM tracing only                | `tuner-core tuner-langchain`                                                                       |
| LiveKit or Pipecat              | Use the [LiveKit](/docs/integrations/livekit) / [Pipecat](/docs/integrations/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](/docs/integrations/custom-stack/speech-to-text) and [Text-to-Speech](/docs/integrations/custom-stack/text-to-speech) pages.

## Troubleshooting

<AccordionGroup>
  <Accordion title="Calls don't appear in Tuner">
    * 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.
  </Accordion>

  <Accordion title="Unauthorized (401)">
    * Ensure `TUNER_API_KEY` starts with `tr_api_` and is valid.
    * Confirm the API key belongs to the correct workspace.
  </Accordion>
</AccordionGroup>

Role-specific problems — missing user turns, interrupted turns recording in full, latency fields absent — are on the [Speech-to-Text](/docs/integrations/custom-stack/speech-to-text) and [Text-to-Speech](/docs/integrations/custom-stack/text-to-speech) pages.

## What's next?

<CardGroup cols={2}>
  <Card title="Speech-to-Text" icon="microphone" href="/docs/integrations/custom-stack/speech-to-text">
    Deepgram and Speechmatics adapters, options, custom providers.
  </Card>

  <Card title="Text-to-Speech" icon="volume-high" href="/docs/integrations/custom-stack/text-to-speech">
    Cartesia over WebSocket or SSE, word-level interruption capture.
  </Card>

  <Card title="LangChain / LangGraph" icon="link" href="/docs/integrations/custom-stack/langchain">
    Node transitions, tool calls, and LLM latency in your timeline.
  </Card>

  <Card title="Review your first call" icon="magnifying-glass" href="/docs/observability/overview">
    What Tuner does with the data once it lands.
  </Card>
</CardGroup>
