> For clean Markdown of any page, append `.md` to the page URL.
> For a complete documentation index, see https://docs.sarvam.ai/llms.txt.
> For full documentation content in one file, see https://docs.sarvam.ai/llms-full.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.sarvam.ai/_mcp/server.

# Build a Voice Agent using Vobiz

> A beginner-friendly guide to building a real-time phone voice agent using Vobiz and Sarvam AI. Support for 11 languages (10 Indian + English) with natural voices and multilingual conversations.

## Overview

This guide shows you how to build a **real-time voice agent that answers phone calls**, using **Vobiz** for telephony and **Sarvam AI** for everything else: speech-to-text, the LLM, and text-to-speech. Unlike the other integration guides in this section, this one doesn't use Pipecat. Vobiz's bidirectional media stream is a plain JSON-over-WebSocket protocol, so a small FastAPI server talking to Sarvam's REST API directly is all you need.

## What You'll Build

A voice agent that can:

* Answer inbound phone calls on a Vobiz number
* Listen to callers speaking, in multiple Indian languages
* Understand and process what they say, with barge-in support
* Respond back in a natural-sounding voice, over the phone, sentence by sentence as the reply streams in

## Quick Overview

1. Get an API key (Sarvam). Vobiz needs no API credentials for this flow.
2. Install packages: `pip install fastapi httpx python-dotenv python-multipart "uvicorn[standard]"`
3. Create a `.env` file with your API key and public URL
4. Write a FastAPI server that speaks Vobiz's WebSocket media protocol and calls Sarvam directly
5. Create a Vobiz application pointing at your server, and attach your number to it
6. Call your Vobiz number

---

## Quick Start

### 1. Prerequisites

* Python 3.9 or higher
* [ngrok](https://ngrok.com/download), to expose your local server during development
* A [Vobiz](https://vobiz.ai) account with a provisioned voice number
* A [Sarvam AI](https://dashboard.sarvam.ai) API key from your dashboard

Vobiz doesn't need an account SID, auth token, or any credentials in your agent code for this flow. It fetches your **Answer URL** as plain HTTP when a call comes in, and everything it needs to route the call, including the stream and call identifiers, arrives over the WebSocket itself, just like Exotel's Voicebot Applet.

### 2. Install Dependencies

#### macOS/Linux

```bash
pip install fastapi httpx python-dotenv python-multipart "uvicorn[standard]"
```

#### Windows

```bash
pip install fastapi httpx python-dotenv python-multipart uvicorn[standard]
```

This guide talks to Sarvam over plain `httpx` calls rather than the `sarvamai` SDK or Pipecat, so the whole pipeline has one HTTP dependency and one authentication header (`api-subscription-key`). That also means audio format handling, including resampling, WAV wrapping, and container stripping, is your responsibility here, unlike the Pipecat-based guides where the transport and Sarvam services handle it for you.

### 3. Create Environment File

Create a file named `.env` in your project folder:

```env
SARVAM_API_KEY=sk_xxxxxxxxxxxxxxxxxxxxxxxx
SARVAM_STT_MODEL=saaras:v3
SARVAM_LLM_MODEL=sarvam-105b
SARVAM_TTS_MODEL=bulbul:v3

PUBLIC_URL=https://your-tunnel.example.com
HTTP_PORT=8000

AGENT_LANGUAGE=en-IN
TTS_SPEAKER=anand

# Vobiz sends caller audio as mu-law at 8 kHz or Linear16 at 16 kHz.
STREAM_CONTENT_TYPE=audio/x-mulaw;rate=8000
OUTBOUND_SAMPLE_RATE=8000
```

Replace `SARVAM_API_KEY` with your real key from the [Sarvam dashboard](https://dashboard.sarvam.ai), and `PUBLIC_URL` with your ngrok URL once you have one (Step 5).

`sarvam-30b` has been deprecated. `sarvam-105b` is now the only supported chat model, and the `/v1/chat/completions` API rejects the `model="sarvam-30b"` value. If you have existing agents pinned to `sarvam-30b`, update them to `sarvam-105b` before it stops responding entirely.

`sarvam-105b` reasons before replying, and that reasoning is billed against `max_tokens`. Budget at least 1500 tokens per turn. Set it too low, and the API returns a valid response with empty content, which would otherwise strand the caller in silence.

### 4. Write the Sarvam Client

Create `sarvam.py`, a thin async wrapper over the three Sarvam endpoints the agent needs: speech-to-text, chat, and text-to-speech.

```python
import base64
import io
import json
import os
import wave
from typing import AsyncIterator

import httpx

BASE_URL = "https://api.sarvam.ai"
STT_MODEL = os.getenv("SARVAM_STT_MODEL", "saaras:v3")
LLM_MODEL = os.getenv("SARVAM_LLM_MODEL", "sarvam-105b")
TTS_MODEL = os.getenv("SARVAM_TTS_MODEL", "bulbul:v3")
SAMPLE_WIDTH_BYTES = 2


class SarvamError(RuntimeError):
    pass


def pcm_to_wav(pcm_audio: bytes, sample_rate: int) -> bytes:
    """Wrap raw mono Linear16 samples in a WAV container for the STT upload."""
    buffer = io.BytesIO()
    with wave.open(buffer, "wb") as wav_file:
        wav_file.setnchannels(1)
        wav_file.setsampwidth(SAMPLE_WIDTH_BYTES)
        wav_file.setframerate(sample_rate)
        wav_file.writeframes(pcm_audio)
    return buffer.getvalue()


def raw_linear16(audio: bytes, expected_rate: int) -> bytes:
    """Vobiz playAudio needs container-free samples; Sarvam TTS may return a WAV."""
    if audio.startswith(b"RIFF"):
        with wave.open(io.BytesIO(audio), "rb") as wav_file:
            audio = wav_file.readframes(wav_file.getnframes())
    return audio


class SarvamClient:
    def __init__(self, api_key: str) -> None:
        self._headers = {"api-subscription-key": api_key}
        self._http = httpx.AsyncClient(base_url=BASE_URL, timeout=30.0)

    async def aclose(self) -> None:
        await self._http.aclose()

    async def transcribe(self, pcm_audio: bytes, sample_rate: int, language_code: str) -> str:
        wav_audio = pcm_to_wav(pcm_audio, sample_rate)
        response = await self._http.post(
            "/speech-to-text",
            headers=self._headers,
            files={"file": ("caller.wav", wav_audio, "audio/wav")},
            data={"model": STT_MODEL, "mode": "codemix", "language_code": language_code},
        )
        response.raise_for_status()
        return (response.json().get("transcript") or "").strip()

    async def stream_reply(
        self, messages: list[dict[str, str]], max_tokens: int = 2000
    ) -> AsyncIterator[str]:
        """Yield only reply text. These models also stream a private
        ``reasoning_content`` field that must never reach the caller's ear."""
        payload = {
            "model": LLM_MODEL,
            "messages": messages,
            "max_tokens": max_tokens,
            "stream": True,
        }
        async with self._http.stream(
            "POST", "/v1/chat/completions", headers=self._headers, json=payload
        ) as response:
            response.raise_for_status()
            async for line in response.aiter_lines():
                if not line.startswith("data:"):
                    continue
                data = line[len("data:"):].strip()
                if data in ("", "[DONE]"):
                    continue
                for choice in json.loads(data).get("choices", []):
                    delta = (choice.get("delta") or {}).get("content")
                    if delta:
                        yield delta

    async def synthesize(self, text: str, language_code: str, speaker: str, sample_rate: int) -> bytes:
        response = await self._http.post(
            "/text-to-speech",
            headers=self._headers,
            json={
                "text": text,
                "target_language_code": language_code,
                "speaker": speaker,
                "model": TTS_MODEL,
                "speech_sample_rate": sample_rate,
                "output_audio_codec": "linear16",
            },
        )
        response.raise_for_status()
        audios = response.json().get("audios") or []
        if not audios:
            raise SarvamError("Sarvam TTS returned no audio")
        return raw_linear16(base64.b64decode("".join(audios)), sample_rate)
```

`raw_linear16()` strips a WAV header if Sarvam returns one. Vobiz's `playAudio` message requires raw, container-free Linear16 samples: sending it a WAV file (with its 44-byte header) produces a burst of noise at the start of every utterance.

### 5. Write the Call Session

Create `agent.py`. This holds the per-call pipeline: a simple energy-based VAD (voice activity detector) over inbound audio, then speech-to-text, the chat model, and text-to-speech, streamed back to the caller sentence by sentence:

```python
import asyncio
import audioop
import base64
import json
import os
import re
from typing import Any

from sarvam import SarvamClient

OUTBOUND_SAMPLE_RATE = int(os.getenv("OUTBOUND_SAMPLE_RATE", "8000"))
SAMPLE_WIDTH_BYTES = 2
AGENT_LANGUAGE = os.getenv("AGENT_LANGUAGE", "en-IN")
TTS_SPEAKER = os.getenv("TTS_SPEAKER", "anand")

SYSTEM_PROMPT = (
    "You are a Sarvam AI voice assistant speaking with a caller on a phone "
    "line. You understand English and Indian languages, including code-mixed "
    "speech. Keep every reply to one or two short spoken sentences. Never use "
    "markdown, bullet points, emoji, or symbols, because your reply is read aloud."
)
GREETING = "Namaste! I am your Sarvam AI assistant. How can I help you today?"

# Split streamed LLM text on sentence enders, including the Devanagari danda,
# so the caller hears sentence one while the model is still writing sentence two.
SENTENCE_END = re.compile(r"[.!?|।॥]+[\s\"')\]]*|\n+")
MIN_TTS_CHARS = 24

SILENCE_THRESHOLD = 250       # RMS energy that counts as speech
END_OF_SPEECH_MS = 700        # trailing silence that ends an utterance
MIN_SPEECH_MS = 200           # shorter utterances are treated as noise


class CallSession:
    def __init__(self, websocket: Any) -> None:
        self.websocket = websocket
        self.stream_id: str | None = None
        self.input_sample_rate = 8000
        self.input_encoding = "audio/x-mulaw"

        self.sarvam = SarvamClient(os.getenv("SARVAM_API_KEY", ""))
        self.conversation = [{"role": "system", "content": SYSTEM_PROMPT}]

        self._audio = bytearray()
        self._speech_ms = 0.0
        self._silence_ms = 0.0
        self._speech_active = False
        self._playback_active = False
        self._tasks: set[asyncio.Task] = set()

    async def handle_message(self, raw_message: str) -> None:
        data = json.loads(raw_message)
        event = data.get("event")

        if event == "start":
            await self._handle_start(data)
        elif event == "media":
            await self._handle_media(data)
        elif event in {"playedStream", "clearedAudio"}:
            self._playback_active = False

    async def _handle_start(self, data: dict) -> None:
        start = data.get("start") or {}
        self.stream_id = data.get("streamId") or start.get("streamId")

        # Trust the negotiated format rather than assuming a rate: Vobiz sends
        # mu-law at 8 kHz or Linear16 at 16 kHz depending on <Stream contentType>.
        media_format = start.get("mediaFormat") or {}
        self.input_sample_rate = int(media_format.get("sampleRate", 8000))
        self.input_encoding = str(media_format.get("encoding", "audio/x-mulaw"))

        self._spawn(self._speak(GREETING))

    async def _handle_media(self, data: dict) -> None:
        media = data.get("media") or {}
        payload = media.get("payload")
        if not payload:
            return

        raw = base64.b64decode(payload)
        pcm = audioop.ulaw2lin(raw, 2) if self.input_encoding == "audio/x-mulaw" else raw
        chunk_ms = len(pcm) * 1000 / (self.input_sample_rate * SAMPLE_WIDTH_BYTES)
        rms = audioop.rms(pcm, SAMPLE_WIDTH_BYTES)

        if rms >= SILENCE_THRESHOLD:
            if self._playback_active:
                await self._clear_audio()  # barge-in
            self._speech_active = True
            self._speech_ms += chunk_ms
            self._silence_ms = 0
            self._audio.extend(pcm)
        elif self._speech_active:
            self._silence_ms += chunk_ms
            self._audio.extend(pcm)

        if self._speech_active and self._silence_ms >= END_OF_SPEECH_MS:
            utterance, speech_ms = bytes(self._audio), self._speech_ms
            self._audio.clear()
            self._speech_ms = self._silence_ms = 0
            self._speech_active = False
            if speech_ms >= MIN_SPEECH_MS:
                self._spawn(self._process_utterance(utterance))

    async def _process_utterance(self, pcm_audio: bytes) -> None:
        transcript = await self.sarvam.transcribe(pcm_audio, self.input_sample_rate, AGENT_LANGUAGE)
        if not transcript:
            return
        self.conversation.append({"role": "user", "content": transcript})

        pending = ""
        async for delta in self.sarvam.stream_reply(self.conversation):
            pending += delta
            match = SENTENCE_END.search(pending)
            if match and match.end() >= MIN_TTS_CHARS:
                sentence, pending = pending[:match.end()].strip(), pending[match.end():]
                if sentence:
                    await self._speak(sentence)
        if pending.strip():
            await self._speak(pending.strip())

    async def _speak(self, text: str) -> None:
        pcm = await self.sarvam.synthesize(text, AGENT_LANGUAGE, TTS_SPEAKER, OUTBOUND_SAMPLE_RATE)
        await self._play(pcm)

    async def _play(self, pcm_audio: bytes) -> None:
        chunk_size = OUTBOUND_SAMPLE_RATE * SAMPLE_WIDTH_BYTES * 20 // 1000  # 20 ms chunks
        self._playback_active = True
        for offset in range(0, len(pcm_audio), chunk_size):
            await self.websocket.send_text(json.dumps({
                "event": "playAudio",
                "streamId": self.stream_id,
                "media": {
                    "contentType": "audio/x-l16",
                    "sampleRate": OUTBOUND_SAMPLE_RATE,
                    "payload": base64.b64encode(pcm_audio[offset:offset + chunk_size]).decode(),
                },
            }))
        await self.websocket.send_text(json.dumps({"event": "checkpoint", "streamId": self.stream_id, "name": "tts"}))

    async def _clear_audio(self) -> None:
        await self.websocket.send_text(json.dumps({"event": "clearAudio", "streamId": self.stream_id}))
        self._playback_active = False

    def _spawn(self, coroutine) -> None:
        task = asyncio.create_task(coroutine)
        self._tasks.add(task)
        task.add_done_callback(self._tasks.discard)

    async def close(self) -> None:
        for task in list(self._tasks):
            task.cancel()
        await self.sarvam.aclose()
```

The chat model's streamed reply is split into sentences with `SENTENCE_END` and spoken as each one completes, rather than waiting for the full reply. This is what keeps turn latency down: the caller hears sentence one while the model is still generating sentence two.

Barge-in works by watching inbound RMS energy during playback: if the caller starts talking while the agent is still speaking, the session sends `clearAudio` and starts listening for a new utterance. This energy-based VAD can't distinguish speech from background noise, so on noisy phone lines you may need to raise `SILENCE_THRESHOLD` or disable barge-in entirely.

### 6. Write the FastAPI Server

Create `server.py`, exposing the Answer URL Vobiz fetches when a call comes in, and the WebSocket it streams audio over:

```python
import html
import os
from pathlib import Path

import uvicorn
from dotenv import load_dotenv
from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
from fastapi.responses import Response

from agent import CallSession

load_dotenv(Path(__file__).resolve().parent / ".env")

PUBLIC_URL = os.getenv("PUBLIC_URL", "").rstrip("/")
STREAM_CONTENT_TYPE = os.getenv("STREAM_CONTENT_TYPE", "audio/x-mulaw;rate=8000")

app = FastAPI()


def websocket_url(public_url: str) -> str:
    if public_url.startswith("https://"):
        return "wss://" + public_url.removeprefix("https://") + "/ws"
    return "ws://" + public_url.removeprefix("http://") + "/ws"


@app.api_route("/answer", methods=["GET", "POST"])
async def answer(request: Request) -> Response:
    """The XML Vobiz executes once the call is answered."""
    base_url = html.escape(PUBLIC_URL, quote=True)
    ws_url = html.escape(websocket_url(PUBLIC_URL), quote=False)
    xml = f"""<?xml version="1.0" encoding="UTF-8"?>
<Response>
    <Stream bidirectional="true"
            audioTrack="inbound"
            keepCallAlive="true"
            contentType="{html.escape(STREAM_CONTENT_TYPE, quote=True)}"
            statusCallbackUrl="{base_url}/stream-status"
            statusCallbackMethod="POST">
        {ws_url}
    </Stream>
    <Hangup/>
</Response>"""
    return Response(content=xml, media_type="application/xml")


@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket) -> None:
    await websocket.accept()
    session = CallSession(websocket)
    try:
        async for message in websocket.iter_text():
            await session.handle_message(message)
    except WebSocketDisconnect:
        pass
    finally:
        await session.close()


@app.post("/stream-status")
async def stream_status(request: Request) -> Response:
    await request.form()
    return Response(status_code=204)


@app.post("/hangup")
async def hangup(request: Request) -> Response:
    await request.form()
    return Response(status_code=204)


if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=int(os.getenv("HTTP_PORT", "8000")))
```

`<Record>` and `<Stream>` are siblings under `<Response>`, not nested inside one another. If you add call recording later with `<Record recordSession="true" .../>`, place it as its own top-level element alongside `<Stream>`. Nesting `<Stream>` inside `<Record>`, or letting a Record callback return `<Hangup/>`, ends the call before the agent WebSocket ever starts.

### 7. Configure Vobiz to Reach Your Agent

**Start ngrok:**

```bash
ngrok http 8000
```

ngrok prints a forwarding URL that looks like `https://xxxx.ngrok-free.app`. Put that exact URL, unchanged, into `PUBLIC_URL` in your `.env` file. The server derives the `wss://` WebSocket URL from it automatically.

Free ngrok URLs change every time you restart ngrok. If your agent stops receiving calls, update `PUBLIC_URL` in `.env` to match the URL ngrok is currently printing, and restart `server.py`.

**Create an application and point it at the server:**

The Answer and Hangup URLs live on a Vobiz **application**, not directly on the number. You create the application first, then attach your number to it.

1. In the Vobiz dashboard, go to **Applications**
2. Create a new application
3. Set the **Answer URL** to `https://xxxx.ngrok-free.app/answer` (method: `POST`)
4. Set the **Hangup URL** to `https://xxxx.ngrok-free.app/hangup` (method: `POST`)
5. Save the application
6. Go to your number's settings and attach it to the application you just created

Step 6 is easy to miss but required. Without a number attached to the application, calls have nowhere to route to, even if the application itself is configured correctly.

### 8. Run and Test Your Agent

```bash
python server.py
```

Call your Vobiz number from any phone. Vobiz fetches `/answer`, opens the WebSocket at `/ws`, and your agent greets the caller.

---

## Audio Formats

The two directions of a Vobiz call are independent and configured separately.

**Vobiz → your agent**, set by `<Stream contentType>`:

| Format                    | Use                                   |
| ------------------------- | ------------------------------------- |
| `audio/x-mulaw;rate=8000` | 8 kHz inbound (default in this guide) |
| `audio/x-l16;rate=16000`  | 16 kHz inbound                        |

**Your agent → Vobiz**, set per `playAudio` message: Linear16 at 8000, 16000, or 24000 Hz. 24 kHz applies to playback only, not inbound audio.

`playAudio` payloads must be raw mono little-endian Linear16, Base64-encoded, with **no WAV header**. Declaring a `sampleRate` in the `playAudio` message does not resample the bytes you send. If you declare 24000 while the payload is actually 8 kHz audio, the caller hears distorted, sped-up "chipmunk" audio. `raw_linear16()` in Step 4 strips a WAV container if Sarvam happens to return one, but it doesn't resample.

Read `start.mediaFormat` on the `start` event rather than assuming a rate, as this guide's `_handle_start` does. Vobiz tells you the exact encoding and sample rate it negotiated for each call.

---

## Customization Examples

### Hindi Voice Agent

```python
AGENT_LANGUAGE = "hi-IN"
TTS_SPEAKER = "simran"  # or: priya, ishita, kavya, aditya, anand, rohan
```

### Multilingual Agent (Auto-detect)

```python
AGENT_LANGUAGE = "unknown"  # Sarvam auto-detects the caller's spoken language
TTS_SPEAKER = "anand"
```

Use `AGENT_LANGUAGE = "unknown"` for support lines where callers might speak any of several languages. Sarvam detects the spoken language per utterance, so the same agent can handle a Hindi caller followed by a Tamil caller without any code changes.

### Available Language Codes

| Language        | Code      |
| --------------- | --------- |
| English (India) | `en-IN`   |
| Hindi           | `hi-IN`   |
| Bengali         | `bn-IN`   |
| Tamil           | `ta-IN`   |
| Telugu          | `te-IN`   |
| Gujarati        | `gu-IN`   |
| Kannada         | `kn-IN`   |
| Malayalam       | `ml-IN`   |
| Marathi         | `mr-IN`   |
| Punjabi         | `pa-IN`   |
| Odia            | `od-IN`   |
| Auto-detect     | `unknown` |

### Speaker Voices (Bulbul v3)

**Male:** Anand (default here), Aditya, Rahul, Rohan, Amit, Dev, Ratan, Varun, Manan, Sumit, Kabir, Aayan, Ashutosh, Advait, Tarun, Sunny, Mani, Gokul, Vijay, Mohit, Rehan, Soham, Shubh

**Female:** Ritu, Priya, Neha, Pooja, Simran, Kavya, Ishita, Shreya, Roopa, Tanya, Shruti, Suhani, Kavitha, Rupali

Speakers are model-specific. A `bulbul:v2` voice such as `anushka` is rejected by `bulbul:v3`. Check that the speaker you pick is valid for the `SARVAM_TTS_MODEL` you're using.

---

## Understanding the Call Flow

```mermaid
flowchart LR
    caller(["📞 Caller"]) <--> vobiz["Vobiz"]
    vobiz <-->|"wss://<br />media"| agent

    subgraph agent["Your Agent Server (FastAPI)"]
        direction LR
        vad["Energy VAD"] --> stt["STT"] --> llm["LLM"] --> tts["TTS"]
    end

    agent -.->|HTTPS| sarvam["Sarvam AI<br />Saaras · sarvam-105b · Bulbul"]
```

1. **Caller dials your Vobiz number.** Vobiz fetches your **Answer URL**, and executes the returned `<Stream>` XML, opening a bidirectional WebSocket to your agent.
2. **`start` event:** Vobiz sends the stream ID and negotiated audio format. Your agent speaks a greeting.
3. **`media` events:** Caller audio arrives as base64-encoded chunks. An energy-based VAD buffers speech and detects when the caller has stopped talking.
4. **STT:** The buffered utterance is sent to Sarvam's Saaras model for transcription.
5. **LLM:** The transcript is added to the conversation and streamed through a Sarvam chat model.
6. **TTS:** Each completed sentence of the reply is synthesized with Sarvam's Bulbul and sent back as `playAudio` events, so playback starts before the whole reply is ready.
7. **Barge-in:** If the caller speaks while the agent is still playing audio, a `clearAudio` event stops playback and a new utterance begins.

---

## Pro Tips

* Use `AGENT_LANGUAGE = "unknown"` to auto-detect the caller's language on multilingual support lines.
* Sarvam's models understand code-mixing, so your agent can naturally handle Hinglish, Tanglish, and other mixed languages common on real support calls.
* Keep the system prompt explicit about reply length and formatting. Since the reply is read aloud, ask the model to avoid markdown, bullet points, and emoji.
* If a chat completion ever returns empty content (the model spent its whole token budget reasoning), have the agent speak a fallback line instead of going silent.

Log every transcript and reply to a file or database for post-call analytics. It's cheap and doesn't slow down the live pipeline.

---

## Troubleshooting

**Call connects but there's no audio.** Confirm `PUBLIC_URL` in `.env` matches your current ngrok URL exactly, and that the Answer URL configured on your Vobiz application points at `/answer` on that same URL.

**Call doesn't reach your agent at all.** Confirm your number is actually attached to the application, not just that the application has the right URLs. A number with no application attached has nowhere to route an incoming call.

**Choppy or chipmunk-pitched audio.** Check that `OUTBOUND_SAMPLE_RATE` and the `sampleRate` you declare in each `playAudio` message match the actual bytes you're sending. Declaring a rate that doesn't match the payload distorts pitch and speed without erroring.

**Playback has a burst of noise at the start.** Sarvam TTS occasionally returns a WAV-wrapped file instead of raw PCM. Make sure `raw_linear16()` runs on every synthesized chunk before it's sent as `playAudio`.

**API key errors.** Make sure `SARVAM_API_KEY` is in your `.env` file and that the file sits next to `server.py`.

**Empty or truncated replies.** `sarvam-105b` spends tokens on internal reasoning before writing the reply. Raise `max_tokens` on the chat request; 1500 to 2000 is a safe starting point.

**Barge-in doesn't trigger, or triggers on background noise.** The VAD here is a simple RMS energy threshold, not real speech detection. Raise `SILENCE_THRESHOLD` on noisy lines, or set it high enough to ignore ambient noise from the caller's environment.

---

## Additional Resources

* [Sarvam AI Documentation](https://docs.sarvam.ai)
* [Vobiz Documentation](https://vobiz.ai)

---

## Need Help?

* Sarvam Support: [developer@sarvam.ai](mailto:developer@sarvam.ai)
* Community: [Join the Discord Community](https://discord.com/invite/5rAsykttcs)

---

**Happy Building!**