# tts-cache-router docs > tts-cache-router is a text-to-speech router for voice agents. It splits each reply into sentences, plays every sentence your org has synthesized before from a per-org cache, and sends the rest to your provider (Sarvam, Raya, ElevenLabs, OpenAI, Deepgram, xAI, Soniox, Smallest AI, Cartesia or Gemini) with your own provider key, so repeated sentences cost nothing at the provider. The audio is the provider's own, never re-encoded. Connect with the LiveKit Agents plugin (`livekit-plugins-tts-cache-router`), the Pipecat service (`pipecat-tts-cache-router`), an OpenAI-shaped `POST /v1/audio/speech`, the `/v1/stream` WebSocket, or drop-in mirrors of each provider's own API that keep the provider's SDK. Authenticate with a `tcr_` API key from the dashboard, and store your provider keys there. --- # Overview > How the router serves each sentence from your cache or your provider, what it caches, and the ways to connect. ## How it works tts-cache-router sits between your voice agent and your text-to-speech provider. Every sentence your agent has said before plays from a cache; the rest go to the provider with your own key. Your agent sends text, all at once or streamed from an LLM. The router splits it into sentences as it arrives. A sentence your org has synthesized before, with the same voice and settings, is replayed from your org’s cache. Any other sentence goes to your provider (Sarvam, Raya, ElevenLabs, OpenAI, Deepgram, xAI, Soniox, Smallest AI, Cartesia or Gemini) with the provider key you stored, streams back as the provider produces it, and is cached once the provider finishes it. You hear the sentences in order, as one reply. | Sentence | Served from | Why | | --- | --- | --- | | Thanks for calling City Hospital. | cache | Said before: replayed from your cache. The provider isn't called. | | Your appointment with Dr. Sharma is on Thursday at ten. | provider | New: synthesized by your provider with your key, streamed to you as it arrives, then cached. | | Is there anything else I can help you with? | cache | Said before: replayed from your cache. | Each repeat served from the cache is a sentence your provider doesn’t synthesize or bill. The dashboard’s [Usage](https://docs.ttscacherouter.dev/dashboard.md#usage) page shows what it saved. ## Same output as the provider Calling a provider through the router sounds the same as calling it directly: - **Each sentence reaches the provider verbatim**, with your exact model, voice, language, speed, sample rate, format and options. The only change to the text is collapsing runs of whitespace. - **A cache hit replays a real provider take**: audio the provider made for the same sentence and settings. (Two identical direct calls aren’t byte-identical either; each is a new take.) - **Audio is never spliced, re-encoded or altered.** Every format comes from the provider; a format the router can’t serve without re-encoding is refused. - **A partial clip is never cached.** A sentence is cached only once the provider has completed it. Sentence boundaries come from the splitter each provider’s LiveKit plugin uses: LiveKit’s BlingFire tokenizer for ElevenLabs, OpenAI, Deepgram, xAI, Soniox, Smallest AI, Cartesia and Gemini, plus a sentence end at every danda (`।`, `॥`), so Hindi, Bengali, Punjabi and other replies written with it stream sentence by sentence. For Sarvam and Raya, the router’s own splitter, built on Raya’s, also ends sentences at the danda and doesn’t split inside common abbreviations (`Dr.`, `a.m.`), decimals or URLs. Both also send a long sentence clause by clause: a piece ends after a comma followed by a space (`1,250` is never cut), once it is at least 20 characters long. The first audio of a reply waits only for its first clause, not its first sentence. ## What is cached One entry per sentence: the provider's audio, for your org, keyed by the exact request that produced it. ### When a sentence matches A sentence plays from the cache when all of these match an earlier, completed one: - your org (orgs never share audio; every API key in an org shares its cache); - the provider, model, voice, language, speed, sample rate, format and every option; - the text, after whitespace is collapsed. Case and punctuation count. Leaving an option out and sending its default value match. The LiveKit plugin, `/v1/stream` and `/v1/audio/speech` share entries for the same settings; the provider-API drop-ins cache the provider’s own request as your client sent it. Settings stored at the provider can change under the same name: a Sarvam pronunciation dictionary (`dict_id`), an ElevenLabs voice’s stored settings, an ElevenLabs dictionary named without a version, a Cartesia pronunciation dictionary (`pronunciation_dict_id`). The cache keys them by name, not content: after you change one, sentences cached before keep their audio until they go unused for your retention period (use a new name to hear the change at once, or turn retention off, which deletes cached audio within about two days). ### How long it is kept Your org’s **retention**, set under [Settings](https://app.ttscacherouter.dev/app/settings): 365 days by default, 1 to 3,650 days, or `0` (off). - It counts from when the audio was last used, so audio your agents keep playing stays cached. Audio is deleted within your retention plus one day of its last use. - A change applies to everything already cached: lowering it deletes older audio, raising it keeps audio longer. - At `0`, nothing new is cached and cached audio is deleted within about two days. Requests still work, every sentence synthesized by the provider. ### What isn’t cached - **Text.** Audio is stored under a one-way hash of the sentence and its settings. The sentence itself is never stored or logged. - **Anything incomplete:** a sentence that failed, timed out or was cut off. When your agent is interrupted, the provider stops the sentence in progress, so it isn’t cached (ElevenLabs finishes it, and it is); sentences not yet sent are dropped. When a client disconnects instead, a sentence already being synthesized finishes and is cached. - **Sentences the provider can’t speak**, which are skipped rather than failing the reply: for Sarvam, a sentence with no letter or digit (an emoji alone); for Raya, punctuation alone. - **A sentence using a Sarvam pronunciation dictionary for the first time**, until the router has read the dictionary (a moment later). It is synthesized and played, just not cached. - **Voice lists**, always read live from the provider. > The drop-ins’ REST and HTTP-stream endpoints cache each request whole, not per sentence: the same request body again replays the same audio. Every WebSocket, and every other way in, caches per sentence. ## Ways to connect Every way in uses your org's cache and stored provider keys. - [Quickstart](https://docs.ttscacherouter.dev/quickstart.md): Sign up, store a provider key and hear a sentence through the router. - [Our LiveKit plugin](https://docs.ttscacherouter.dev/livekit.md): tcr.TTS for any provider. Cache hits count zero characters in LiveKit's metrics. - [Our Pipecat service](https://docs.ttscacherouter.dev/pipecat.md): TTSCacheRouterTTSService for any provider. Cache hits count zero characters in Pipecat's usage metrics. - [Your provider's LiveKit plugin](https://docs.ttscacherouter.dev/livekit/sarvam.md#provider-plugin): Keep the plugin you use; change its base URL and key. - [OpenAI-compatible HTTP](https://docs.ttscacherouter.dev/clients/speech.md): POST /v1/audio/speech for any provider, with the OpenAI SDK or curl. - [WebSocket protocol](https://docs.ttscacherouter.dev/clients/stream.md): /v1/stream: text in, audio out, from any language. - [Provider API drop-ins](https://docs.ttscacherouter.dev/clients/sarvam.md): Each provider's own API, mirrored: keep its SDK, change the base URL and key. ## Open source tts-cache-router is open source under the Apache-2.0 licence. The router, this website, the LiveKit plugin and the Pipecat service are on [GitHub](https://github.com/talqing/tts-cache-router), whose README shows how to run your own router with Docker Compose; point the plugin or the service at it with `base_url`. The hosted router at `api.ttscacherouter.dev` runs the same code. --- # Quickstart > Sign up, store your provider key, and hear a sentence through the router: once from the provider, then from the cache. You need an API key from one of the providers, Python 3.10 or later, and, for step 4, a LiveKit agent on `livekit-agents` 1.8.5 or later. Step 5 works without an agent. ## 1. Sign up Create your account at [ttscacherouter.dev](https://ttscacherouter.dev/signup). Signing up creates your **org**, which owns your API keys, provider keys, cache and usage, and an API key for it. The next two steps happen on the [dashboard](https://app.ttscacherouter.dev/app)’s home. ## 2. Add your provider key Pick the provider your agent uses and add your key for it. The router calls the provider with this key, so the provider bills you directly for the sentences it synthesizes. The key is stored encrypted and never shown again; your agent never sends it. ## 3. Copy your API key Your agent authenticates to the router with this key alone. Copy it from the top of the dashboard. It starts with `tcr_`. Set it in your agent’s environment: ```shell export TTS_CACHE_ROUTER_API_KEY=tcr_… ``` ## 4. Connect your agent Install our LiveKit plugin and make it your agent's TTS. ```shell pip install livekit-plugins-tts-cache-router ``` **Sarvam** ```python from livekit.plugins import tts_cache_router as tcr # The API key comes from TTS_CACHE_ROUTER_API_KEY. tts = tcr.TTS( model="sarvam/bulbul:v3", voice="shubh", language="hi-IN", ) # then, in your entrypoint: # session = AgentSession(vad=..., stt=..., llm=..., tts=tts) ``` **Raya** ```python from livekit.plugins import tts_cache_router as tcr # The API key comes from TTS_CACHE_ROUTER_API_KEY. tts = tcr.TTS( model="raya/standard", voice="d6a002d0-230c-49b1-a137-b8a7d564b1ae", language="hi", ) # then, in your entrypoint: # session = AgentSession(vad=..., stt=..., llm=..., tts=tts) ``` **ElevenLabs** ```python from livekit.plugins import tts_cache_router as tcr # The API key comes from TTS_CACHE_ROUTER_API_KEY. tts = tcr.TTS( model="elevenlabs/eleven_flash_v2_5", voice="JBFqnCBsd6RMkjVDRZzb", ) # then, in your entrypoint: # session = AgentSession(vad=..., stt=..., llm=..., tts=tts) ``` **OpenAI** ```python from livekit.plugins import tts_cache_router as tcr # The API key comes from TTS_CACHE_ROUTER_API_KEY. tts = tcr.TTS( model="openai/gpt-4o-mini-tts", voice="ash", ) # then, in your entrypoint: # session = AgentSession(vad=..., stt=..., llm=..., tts=tts) ``` **Deepgram** ```python from livekit.plugins import tts_cache_router as tcr # The API key comes from TTS_CACHE_ROUTER_API_KEY. tts = tcr.TTS( model="deepgram/aura-2", voice="thalia-en", ) # then, in your entrypoint: # session = AgentSession(vad=..., stt=..., llm=..., tts=tts) ``` **xAI** ```python from livekit.plugins import tts_cache_router as tcr # The API key comes from TTS_CACHE_ROUTER_API_KEY. tts = tcr.TTS( model="xai/xai-tts", voice="ara", language="auto", ) # then, in your entrypoint: # session = AgentSession(vad=..., stt=..., llm=..., tts=tts) ``` **Soniox** ```python from livekit.plugins import tts_cache_router as tcr # The API key comes from TTS_CACHE_ROUTER_API_KEY. tts = tcr.TTS( model="soniox/tts-rt-v2", voice="Adrian", language="en", ) # then, in your entrypoint: # session = AgentSession(vad=..., stt=..., llm=..., tts=tts) ``` **Smallest AI** ```python from livekit.plugins import tts_cache_router as tcr # The API key comes from TTS_CACHE_ROUTER_API_KEY. tts = tcr.TTS( model="smallestai/lightning_v3.1_pro", voice="meher", language="en", ) # then, in your entrypoint: # session = AgentSession(vad=..., stt=..., llm=..., tts=tts) ``` **Cartesia** ```python from livekit.plugins import tts_cache_router as tcr # The API key comes from TTS_CACHE_ROUTER_API_KEY. tts = tcr.TTS( model="cartesia/sonic-3", voice="f786b574-daa5-4673-aa0c-cbe3e8534c02", language="en", ) # then, in your entrypoint: # session = AgentSession(vad=..., stt=..., llm=..., tts=tts) ``` **Gemini** ```python from livekit.plugins import tts_cache_router as tcr # The API key comes from TTS_CACHE_ROUTER_API_KEY. tts = tcr.TTS( model="gemini/gemini-3.8-flash-tts", voice="Kore", ) # then, in your entrypoint: # session = AgentSession(vad=..., stt=..., llm=..., tts=tts) ``` The settings use the same names for every provider: `model` as `/`, `voice`, `language`, `speed` and the provider’s `options`. Every option is on the provider’s page: [Sarvam](https://docs.ttscacherouter.dev/livekit/sarvam.md), [Raya](https://docs.ttscacherouter.dev/livekit/raya.md), [ElevenLabs](https://docs.ttscacherouter.dev/livekit/elevenlabs.md), [OpenAI](https://docs.ttscacherouter.dev/livekit/openai.md), [Deepgram](https://docs.ttscacherouter.dev/livekit/deepgram.md), [xAI](https://docs.ttscacherouter.dev/livekit/xai.md), [Soniox](https://docs.ttscacherouter.dev/livekit/soniox.md), [Smallest AI](https://docs.ttscacherouter.dev/livekit/smallestai.md), [Cartesia](https://docs.ttscacherouter.dev/livekit/cartesia.md), [Gemini](https://docs.ttscacherouter.dev/livekit/gemini.md). Each page also shows how to keep the provider’s own LiveKit plugin and change only its URL and key. > Building with Pipecat? `pip install pipecat-tts-cache-router` and put [TTSCacheRouterTTSService](https://docs.ttscacherouter.dev/pipecat.md) in your pipeline instead. ## 5. Hear it, twice Run your agent and talk to it, or speak one sentence with this script. **Sarvam** ```python import asyncio import aiohttp from livekit import rtc from livekit.plugins import tts_cache_router as tcr async def main() -> None: async with aiohttp.ClientSession() as http: tts = tcr.TTS( model="sarvam/bulbul:v3", voice="shubh", language="hi-IN", http_session=http, ) tts.on("metrics_collected", lambda m: print(f"characters billed by the provider: {m.characters_count}")) frames = [event.frame async for event in tts.synthesize("धन्यवाद, आपकी चार लोगों की टेबल आज रात आठ बजे के लिए बुक है।")] with open("hello.wav", "wb") as f: f.write(rtc.combine_audio_frames(frames).to_wav_bytes()) await tts.aclose() asyncio.run(main()) ``` **Raya** ```python import asyncio import aiohttp from livekit import rtc from livekit.plugins import tts_cache_router as tcr async def main() -> None: async with aiohttp.ClientSession() as http: tts = tcr.TTS( model="raya/standard", voice="d6a002d0-230c-49b1-a137-b8a7d564b1ae", language="hi", http_session=http, ) tts.on("metrics_collected", lambda m: print(f"characters billed by the provider: {m.characters_count}")) frames = [event.frame async for event in tts.synthesize("धन्यवाद, आपकी चार लोगों की टेबल आज रात आठ बजे के लिए बुक है।")] with open("hello.wav", "wb") as f: f.write(rtc.combine_audio_frames(frames).to_wav_bytes()) await tts.aclose() asyncio.run(main()) ``` **ElevenLabs** ```python import asyncio import aiohttp from livekit import rtc from livekit.plugins import tts_cache_router as tcr async def main() -> None: async with aiohttp.ClientSession() as http: tts = tcr.TTS( model="elevenlabs/eleven_flash_v2_5", voice="JBFqnCBsd6RMkjVDRZzb", http_session=http, ) tts.on("metrics_collected", lambda m: print(f"characters billed by the provider: {m.characters_count}")) frames = [event.frame async for event in tts.synthesize("Thanks for calling. Your table for four is booked for eight tonight.")] with open("hello.wav", "wb") as f: f.write(rtc.combine_audio_frames(frames).to_wav_bytes()) await tts.aclose() asyncio.run(main()) ``` **OpenAI** ```python import asyncio import aiohttp from livekit import rtc from livekit.plugins import tts_cache_router as tcr async def main() -> None: async with aiohttp.ClientSession() as http: tts = tcr.TTS( model="openai/gpt-4o-mini-tts", voice="ash", http_session=http, ) tts.on("metrics_collected", lambda m: print(f"characters billed by the provider: {m.characters_count}")) frames = [event.frame async for event in tts.synthesize("Thanks for calling. Your table for four is booked for eight tonight.")] with open("hello.wav", "wb") as f: f.write(rtc.combine_audio_frames(frames).to_wav_bytes()) await tts.aclose() asyncio.run(main()) ``` **Deepgram** ```python import asyncio import aiohttp from livekit import rtc from livekit.plugins import tts_cache_router as tcr async def main() -> None: async with aiohttp.ClientSession() as http: tts = tcr.TTS( model="deepgram/aura-2", voice="thalia-en", http_session=http, ) tts.on("metrics_collected", lambda m: print(f"characters billed by the provider: {m.characters_count}")) frames = [event.frame async for event in tts.synthesize("Thanks for calling. Your table for four is booked for eight tonight.")] with open("hello.wav", "wb") as f: f.write(rtc.combine_audio_frames(frames).to_wav_bytes()) await tts.aclose() asyncio.run(main()) ``` **xAI** ```python import asyncio import aiohttp from livekit import rtc from livekit.plugins import tts_cache_router as tcr async def main() -> None: async with aiohttp.ClientSession() as http: tts = tcr.TTS( model="xai/xai-tts", voice="ara", language="auto", http_session=http, ) tts.on("metrics_collected", lambda m: print(f"characters billed by the provider: {m.characters_count}")) frames = [event.frame async for event in tts.synthesize("Thanks for calling. Your table for four is booked for eight tonight.")] with open("hello.wav", "wb") as f: f.write(rtc.combine_audio_frames(frames).to_wav_bytes()) await tts.aclose() asyncio.run(main()) ``` **Soniox** ```python import asyncio import aiohttp from livekit import rtc from livekit.plugins import tts_cache_router as tcr async def main() -> None: async with aiohttp.ClientSession() as http: tts = tcr.TTS( model="soniox/tts-rt-v2", voice="Adrian", language="en", http_session=http, ) tts.on("metrics_collected", lambda m: print(f"characters billed by the provider: {m.characters_count}")) frames = [event.frame async for event in tts.synthesize("Thanks for calling. Your table for four is booked for eight tonight.")] with open("hello.wav", "wb") as f: f.write(rtc.combine_audio_frames(frames).to_wav_bytes()) await tts.aclose() asyncio.run(main()) ``` **Smallest AI** ```python import asyncio import aiohttp from livekit import rtc from livekit.plugins import tts_cache_router as tcr async def main() -> None: async with aiohttp.ClientSession() as http: tts = tcr.TTS( model="smallestai/lightning_v3.1_pro", voice="meher", language="en", http_session=http, ) tts.on("metrics_collected", lambda m: print(f"characters billed by the provider: {m.characters_count}")) frames = [event.frame async for event in tts.synthesize("Thanks for calling. Your table for four is booked for eight tonight.")] with open("hello.wav", "wb") as f: f.write(rtc.combine_audio_frames(frames).to_wav_bytes()) await tts.aclose() asyncio.run(main()) ``` **Cartesia** ```python import asyncio import aiohttp from livekit import rtc from livekit.plugins import tts_cache_router as tcr async def main() -> None: async with aiohttp.ClientSession() as http: tts = tcr.TTS( model="cartesia/sonic-3", voice="f786b574-daa5-4673-aa0c-cbe3e8534c02", language="en", http_session=http, ) tts.on("metrics_collected", lambda m: print(f"characters billed by the provider: {m.characters_count}")) frames = [event.frame async for event in tts.synthesize("Thanks for calling. Your table for four is booked for eight tonight.")] with open("hello.wav", "wb") as f: f.write(rtc.combine_audio_frames(frames).to_wav_bytes()) await tts.aclose() asyncio.run(main()) ``` **Gemini** ```python import asyncio import aiohttp from livekit import rtc from livekit.plugins import tts_cache_router as tcr async def main() -> None: async with aiohttp.ClientSession() as http: tts = tcr.TTS( model="gemini/gemini-3.8-flash-tts", voice="Kore", http_session=http, ) tts.on("metrics_collected", lambda m: print(f"characters billed by the provider: {m.characters_count}")) frames = [event.frame async for event in tts.synthesize("Thanks for calling. Your table for four is booked for eight tonight.")] with open("hello.wav", "wb") as f: f.write(rtc.combine_audio_frames(frames).to_wav_bytes()) await tts.aclose() asyncio.run(main()) ``` Run it twice. The first run synthesizes the sentence at the provider, which bills its characters (60 for the Hindi sentence, 68 for the English one), and caches it. The second plays it from the cache and bills nothing. Both write the same audio to `hello.wav`. ```shell $ python hello.py characters billed by the provider: 60 $ python hello.py characters billed by the provider: 0 ``` > In an agent, the same happens per sentence: the first time your agent says something it comes from the provider, every time after from the cache. The plugin’s `TTSMetrics` count only the characters the provider billed. ## 6. See what it saved The dashboard's Usage page counts sentences served from the cache against those the provider synthesized. Open [Usage](https://app.ttscacherouter.dev/app/usage) for the hit rate and the characters kept off your provider’s bill, per hour or day and per provider. Counts arrive within about a minute. Next: [how the cache works](https://docs.ttscacherouter.dev/index.md), or every setting of the [LiveKit plugin](https://docs.ttscacherouter.dev/livekit.md). --- # AI coding tools > Let Claude Code, Codex, Cursor or another coding agent search and read these docs, through the docs' MCP server or as Markdown. ## The docs MCP server Add the docs to your coding agent, and it can search and read them while it connects your agent to the router. The server is at `https://docs.ttscacherouter.dev/mcp`, over Streamable HTTP. It is public and needs no key. ### Claude Code ```shell claude mcp add --transport http tts-cache-router-docs https://docs.ttscacherouter.dev/mcp ``` This adds it to the project you run it in. Add `--scope user` to have it in every project. ### Codex ```shell codex mcp add tts-cache-router-docs --url https://docs.ttscacherouter.dev/mcp ``` This adds it to every project. ### Cursor Add it to `.cursor/mcp.json` in your project, or to `~/.cursor/mcp.json` for every project: **.cursor/mcp.json** ```json { "mcpServers": { "tts-cache-router-docs": { "url": "https://docs.ttscacherouter.dev/mcp" } } } ``` ### Other clients Any MCP client that connects to a server by URL can use it: give it `https://docs.ttscacherouter.dev/mcp` as a Streamable HTTP (sometimes called HTTP) server. ## Its tools - `search_docs` (`query, limit`): Searches every page and returns the best-matching sections, with each one's page and link. - `get_doc_page` (`path`): One page as Markdown, with its code samples, by path (`/livekit/elevenlabs`) or URL. - `list_doc_pages`: Every page, with its path and what it covers. ## The docs as Markdown Without MCP, an agent can read the docs as Markdown: every page has a copy at its URL plus `.md`, like [`https://docs.ttscacherouter.dev/quickstart.md`](https://docs.ttscacherouter.dev/quickstart.md). [llms.txt](https://docs.ttscacherouter.dev/llms.txt) lists every page, and [llms-full.txt](https://docs.ttscacherouter.dev/llms-full.txt) holds them all in one file. --- # The LiveKit plugin > livekit-plugins-tts-cache-router: install it, construct TTS, change settings mid-session, and what it reports. ## Install `livekit-plugins-tts-cache-router` is a LiveKit Agents TTS that speaks through the router, for every provider. It needs Python 3.10 or later and `livekit-agents` 1.8.5 or later. This page describes version 0.5.0. ```shell pip install livekit-plugins-tts-cache-router ``` **agent.py** ```python from livekit.plugins import tts_cache_router as tcr # The API key comes from TTS_CACHE_ROUTER_API_KEY. tts = tcr.TTS( model="sarvam/bulbul:v3", voice="shubh", language="hi-IN", ) ``` Pass `tts` to `AgentSession(tts=tts)`. Set `TTS_CACHE_ROUTER_API_KEY` to your router API key, and store your provider key under [Provider keys](https://app.ttscacherouter.dev/app/provider-keys): the plugin never sends one. It streams, keeps transcripts in step with speech, connects to the provider when the session starts (`prewarm()`), and keeps its connections warm across turns. ## TTS(...) The plugin’s one class. Its settings are in the router’s own names, those of [/v1/audio/speech](https://docs.ttscacherouter.dev/clients/speech.md#body) and `GET /v1/models`, the same for every provider. **ElevenLabs** ```python from livekit.plugins import tts_cache_router as tcr tts = tcr.TTS( model="elevenlabs/eleven_flash_v2_5", voice="JBFqnCBsd6RMkjVDRZzb", language="en", options={"voice_settings": {"stability": 0.5, "similarity_boost": 0.75}}, ) ``` **From stored settings** ```python from livekit.plugins import tts_cache_router as tcr # Settings kept as data, in the names of /v1/audio/speech and GET /v1/models: config = { "model": "sarvam/bulbul:v3", "voice": "shubh", "language": "hi-IN", "speed": 1.1, "options": {"temperature": 0.5}, } tts = tcr.TTS(**config) ``` - `model` (`str`, Required): Any model `GET /v1/models` lists, with its provider: `sarvam/bulbul:v3`, `elevenlabs/eleven_flash_v2_5`, `openai/tts-1`, `deepgram/aura-2`, `xai/xai-tts`, … ([Models and options](https://docs.ttscacherouter.dev/reference/models.md)). - `voice` (`str | None`, Required): The provider’s voice id, as [GET /v1/voices](https://docs.ttscacherouter.dev/reference/voices.md) lists it. `None` only for Gemini’s two speakers, which `options["speakers"]` names. - `language` (`str | None`, Default `None`): The provider’s language code, sent as written: `hi-IN` for Sarvam, `hi` for Raya, `en` for the others. Required for Sarvam, Raya and Soniox. Optional for ElevenLabs and xAI: left out, the provider detects it. Optional for Smallest AI and Cartesia: left out, `en`. Not taken by OpenAI, Deepgram or Gemini. - `speed` (`float | None`, Default `None`): The speaking rate, where the provider takes one. None: the router's default. - `options` (`dict | None`, Default `None`): The provider’s other settings, by their names in [Models and options](https://docs.ttscacherouter.dev/reference/models.md): `{"temperature": 0.6}` for Sarvam, `{"voice_settings": {...}}` for ElevenLabs, `{"instructions": "..."}` for OpenAI and Gemini. Each provider’s page under LiveKit Agents lists its own. - `api_key` (`str | None`, Default `$TTS_CACHE_ROUTER_API_KEY`): Your router API key. Without one, the constructor raises `ValueError`. - `base_url` (`str`, Default `wss://api.ttscacherouter.dev`): The router. Change it only for a router you run yourself. - `sample_rate` (`int`, Default `24000`): Of the PCM audio your agent receives, one the provider streams: 24000 works with every provider; each provider’s list is in [Models and options](https://docs.ttscacherouter.dev/reference/models.md). - `http_session` (`aiohttp.ClientSession | None`, Default `None`): The aiohttp session to connect with. Inside an agent job, leave it out: the job's is used. Outside one, pass your own. The router is the one that checks the settings. The constructor raises `ValueError` only for a `model` that isn’t `/` or a missing `voice`. When the session starts, the plugin connects and sends the settings, and a setting the router refuses is logged as an error then; a reply with it fails with `APIStatusError` (`bad_request`, not retryable). `tts.provider` and `tts.model` name the underlying provider and model (`Sarvam`, `bulbul:v3`) for cost dashboards. ## Changing settings `update_options` changes the voice or settings mid-session. Replies that start afterwards use them; the reply being spoken finishes as it began. ```python # Another voice and speed, for the same provider: tts.update_options(voice="priya", speed=1.1) # Or another provider altogether; its language, speed and options start from none: tts.update_options(model="elevenlabs/eleven_flash_v2_5", voice="JBFqnCBsd6RMkjVDRZzb") ``` `options` replaces the options as a whole. A sentence in a new voice or with new settings is a new cache entry: synthesized once, then cached. ## Interruptions When the user barges in, LiveKit cancels the reply; the plugin tells the router to stop and keeps the connection. The router stops sending the reply’s audio at once, and the provider stops the sentence it was synthesizing, so nothing cut off is cached (ElevenLabs and Cartesia finish their sentence, and cache it). The connection returns to the plugin’s pool, ready for the next reply. ## Transcripts The plugin supports LiveKit's aligned transcripts, so captions follow the speech. Each sentence’s text is timed to the moment its audio starts. With ElevenLabs, transcripts are word by word, from ElevenLabs’ character timings, and with Cartesia from its own (turn them off with `options={"sync_alignment": False}` or `options={"word_timestamps": False}`). Word timings are cached with the audio, so a reply from the cache is aligned too. ## Metrics TTSMetrics, once per reply, count only the characters the provider billed. Sentences from the cache count zero, so a fully cached reply reports `characters_count` `0`. `ttfb`, `audio_duration` and the connection fields are as usual, and the metadata names the underlying provider and model. ```python from livekit.agents.metrics import TTSMetrics def on_tts_metrics(metrics: TTSMetrics) -> None: # What the provider billed for this reply: 0 when it all came from the cache. print(metrics.characters_count, metrics.ttfb) tts.on("metrics_collected", on_tts_metrics) ``` ## Cache stats Once a reply is all sent, the plugin emits `cache_stats`: how much of it came from your org’s cache. This script speaks a sentence twice, outside an agent, and prints both: **speak_twice.py** ```python import asyncio import aiohttp from livekit import rtc from livekit.plugins import tts_cache_router as tcr def on_cache_stats(stats: tcr.CacheStats) -> None: print(f"{stats.cached_sentences}/{stats.sentences} sentences from the cache, " f"{stats.billed_characters}/{stats.characters} characters billed by the provider") async def main() -> None: async with aiohttp.ClientSession() as http: tts = tcr.TTS(model="openai/tts-1", voice="alloy", http_session=http) tts.on("cache_stats", on_cache_stats) for _ in range(2): # the second time, from the cache frames = [event.frame async for event in tts.synthesize("Your order has shipped.")] with open("speech.wav", "wb") as f: f.write(rtc.combine_audio_frames(frames).to_wav_bytes()) await tts.aclose() asyncio.run(main()) ``` ```text 0/1 sentences from the cache, 23/23 characters billed by the provider 1/1 sentences from the cache, 0/23 characters billed by the provider ``` - `request_id` (`str`): The reply’s request id, as in its `TTSMetrics`. - `segment_id` (`str | None`): The reply’s segment id when streamed, as in its `TTSMetrics`; `None` for `synthesize()`. - `sentences` (`int`): The sentences the reply was split into. - `cached_sentences` (`int`): Those served from the cache. - `characters` (`int`): The characters of all the reply's sentences. - `billed_characters` (`int`): The characters the provider synthesized and billed, as `TTSMetrics.characters_count`. `cached_characters` is the rest. An interrupted reply emits both too, with `TTSMetrics.cancelled` set: they count the sentences the router sent in full before the interruption, so `characters_count` is what the provider billed for the audio your agent received. ## Errors and retries Router errors become LiveKit's API errors, so LiveKit's retries and FallbackAdapter work as with any TTS. - A router [error](https://docs.ttscacherouter.dev/reference/errors.md) raises `APIStatusError` with the error’s `retryable` flag, so LiveKit retries only what can succeed (per your `conn_options`). Its `status_code` is the provider’s HTTP status when there is one. - A refused router API key raises `APIStatusError` with status 403, not retryable. - A failed connection raises `APIConnectionError`, and a router that stops answering `APITimeoutError`; LiveKit retries both on a fresh connection. > Waiting for the LLM is not a timeout: the plugin times out only after it has sent all of a reply’s text and the router goes quiet. - [Sarvam](https://docs.ttscacherouter.dev/livekit/sarvam.md): Bulbul v3 and v4-flash. - [Raya](https://docs.ttscacherouter.dev/livekit/raya.md): Bakbak standard and m1. - [ElevenLabs](https://docs.ttscacherouter.dev/livekit/elevenlabs.md): Flash, Turbo, Multilingual, v3 and v4. - [OpenAI](https://docs.ttscacherouter.dev/livekit/openai.md): gpt-4o-mini-tts, tts-1 and tts-1-hd. - [Deepgram](https://docs.ttscacherouter.dev/livekit/deepgram.md): Aura-2, Aura-1 and Flux. - [xAI](https://docs.ttscacherouter.dev/livekit/xai.md): xAI's voices in twenty languages. - [Soniox](https://docs.ttscacherouter.dev/livekit/soniox.md): tts-rt-v2's voices in sixty-three languages. - [Smallest AI](https://docs.ttscacherouter.dev/livekit/smallestai.md): Lightning v3.1 and v3.1 Pro. - [Cartesia](https://docs.ttscacherouter.dev/livekit/cartesia.md): Sonic 3, 3.5 and 3.6. - [Gemini](https://docs.ttscacherouter.dev/livekit/gemini.md): Gemini 3.8 Flash and Flash-Lite, 3.1 and 2.5. - [WebSocket protocol](https://docs.ttscacherouter.dev/clients/stream.md): What the plugin sends and receives. --- # Sarvam with LiveKit > Sarvam Bulbul in a LiveKit agent: through our plugin, or through LiveKit's Sarvam plugin with a changed URL. ## With our plugin Sarvam Bulbul in eleven Indian languages. ```shell pip install livekit-plugins-tts-cache-router ``` **agent.py** ```python from livekit.plugins import tts_cache_router as tcr # The API key comes from TTS_CACHE_ROUTER_API_KEY. tts = tcr.TTS( model="sarvam/bulbul:v3", voice="shubh", language="hi-IN", ) ``` Pass `tts` to `AgentSession(tts=tts)`. Set `TTS_CACHE_ROUTER_API_KEY` in the agent’s environment, and store your Sarvam key under [Provider keys](https://app.ttscacherouter.dev/app/provider-keys). `TTS`’s own arguments are on [the plugin’s page](https://docs.ttscacherouter.dev/livekit.md#tts). ### Options `tcr.TTS(...)`’s settings for Sarvam, in the router’s names: `model`, `voice`, `language` and `speed` as arguments, and the rest in the `options` dict (`options={"name": value}`). The router checks them; the languages per model are in [Models and options](https://docs.ttscacherouter.dev/reference/models.md#sarvam). | Field | Type | Required or default | Values | Description | | --- | --- | --- | --- | --- | | `model` | string | Required | sarvam/bulbul:v3, sarvam/bulbul:v4-flash | Which model synthesizes the replies, with its provider. | | `voice` | string | Required | | A voice of the chosen model, passed to Sarvam as given. See [models and voices](https://docs.ttscacherouter.dev/reference/models.md#sarvam-models). | | `language` | string | Required | | A Sarvam language code from [languages](https://docs.ttscacherouter.dev/reference/models.md#sarvam-languages), passed through as given. | | `speed` | number | Default `1.0` | 0.5 – 2.0 | Speech speed. 1.0 is normal speed. | | `sample_rate` | int | Default `24000` | 8000, 16000, 22050, 24000 | Of the audio you receive, in Hz. | | `options.temperature` | number | Default `0.6` | 0.01 – 1.0 | Sampling temperature: lower is steadier, higher is more expressive. | | `options.dict_id` | string \| null | Default `null` | | A Sarvam pronunciation dictionary to apply. | | `options.pitch` | number | `bulbul:v4-flash` only. Default `0.0` | -0.5 – 0.5 | Pitch: lower is deeper, higher is sharper. | | `options.loudness` | number | `bulbul:v4-flash` only. Default `1.0` | 0.1 – 2.5 | Loudness: lower is quieter, higher is louder. | | `options.enable_preprocessing` | boolean | `bulbul:v4-flash` only. Default `false` | | Normalize numbers and English words in the text before synthesis. | | `options.min_buffer_size` | integer \| null | Default `null` | 30 – 200 | Characters Sarvam buffers before it starts synthesizing. Null: the router's choice, the least Sarvam allows. | | `options.max_chunk_length` | integer \| null | Default `null` | 50 – 500 | The longest text Sarvam synthesizes in one piece; it splits longer sentences. Null: the router's choice, the most Sarvam allows, so each sentence is synthesized whole. | ### Changing settings Replies that start afterwards use the new settings: ```python tts.update_options(voice="priya", speed=1.1, options={"temperature": 0.4}) ``` ## With LiveKit's Sarvam plugin Keep `livekit-plugins-sarvam` and point it at the router: your router API key in place of your Sarvam key, and the router’s address. ```shell pip install livekit-plugins-sarvam ``` **agent.py** ```python import os from livekit.plugins import sarvam tts = sarvam.TTS( model="bulbul:v3", speaker="shubh", target_language_code="hi-IN", api_key=os.environ["TTS_CACHE_ROUTER_API_KEY"], base_url="https://api.ttscacherouter.dev/sarvam/text-to-speech", ws_url="wss://api.ttscacherouter.dev/sarvam/text-to-speech/ws", ) ``` `ws_url` carries streaming (Sarvam’s WebSocket; for `bulbul:v4-flash` the plugin adds `/v2` itself, which the router serves too) and `base_url` carries `synthesize()` (Sarvam’s REST API). Set both. Every other argument, `pitch`, `loudness` and `speech_sample_rate` included, goes to Sarvam as the plugin sends it. Requests, responses and errors keep Sarvam’s own shape; the endpoints are on [Sarvam API drop-in](https://docs.ttscacherouter.dev/clients/sarvam.md). ## Which to choose Both use your org's cache and your stored provider key, and sound the same. - **Our plugin** reports only the characters Sarvam billed in `TTSMetrics` (cache hits count zero), and switches provider with `update_options`. - **Sarvam’s plugin** is the smallest change to an agent that already uses it. Its metrics count every character, cached or not; [Usage](https://docs.ttscacherouter.dev/dashboard.md#usage) has the real split. --- # Raya with LiveKit > Raya Bakbak in a LiveKit agent: through our plugin, or through Raya's LiveKit plugin with a changed URL. ## With our plugin Raya Bakbak voices for Indian languages. ```shell pip install livekit-plugins-tts-cache-router ``` **agent.py** ```python from livekit.plugins import tts_cache_router as tcr # The API key comes from TTS_CACHE_ROUTER_API_KEY. tts = tcr.TTS( model="raya/standard", voice="d6a002d0-230c-49b1-a137-b8a7d564b1ae", language="hi", ) ``` Pass `tts` to `AgentSession(tts=tts)`. Set `TTS_CACHE_ROUTER_API_KEY` in the agent’s environment, and store your Raya key under [Provider keys](https://app.ttscacherouter.dev/app/provider-keys). `TTS`’s own arguments are on [the plugin’s page](https://docs.ttscacherouter.dev/livekit.md#tts). ### Options `tcr.TTS(...)`’s settings for Raya, in the router’s names: `model`, `voice`, `language` and `speed` as arguments, and the rest in the `options` dict (`options={"name": value}`). The router checks them; the languages per model are in [Models and options](https://docs.ttscacherouter.dev/reference/models.md#raya). | Field | Type | Required or default | Values | Description | | --- | --- | --- | --- | --- | | `model` | string | Required | raya/m1, raya/standard | Which model synthesizes the replies, with its provider. | | `voice` | string | Required | | A voice of the chosen model, passed to Raya as given. See [models and voices](https://docs.ttscacherouter.dev/reference/models.md#raya-models). | | `language` | string | Required | | A Raya language code from [languages](https://docs.ttscacherouter.dev/reference/models.md#raya-languages), passed through as given. | | `speed` | number | Default `1.0` | 0.5 – 1.5 | Speech speed. 1.0 is normal speed. | | `sample_rate` | int | Default `24000` | 8000, 16000, 22050, 24000 | Of the audio you receive, in Hz. | ### Changing settings Replies that start afterwards use the new settings: ```python tts.update_options(speed=1.1) ``` ## With Raya's LiveKit plugin Keep `livekit-plugins-raya` and point it at the router: your router API key in place of your Raya key, and the router’s address. ```shell pip install "livekit-plugins-raya @ git+https://github.com/raya-voice-ai/livekit-plugins-raya" ``` **agent.py** ```python import os from livekit.plugins import raya tts = raya.TTS( model="standard", voice_id="d6a002d0-230c-49b1-a137-b8a7d564b1ae", language="hi", api_key=os.environ["TTS_CACHE_ROUTER_API_KEY"], base_url="https://api.ttscacherouter.dev/raya", ) ``` Raya publishes its plugin on GitHub rather than PyPI. Instead of `base_url` you can set `RAYA_BASE_URL` in the environment. Streaming uses Raya’s SSE endpoint, and `list_voices()` works through the router too. Requests, responses and errors keep Raya’s own shape; the endpoints are on [Raya API drop-in](https://docs.ttscacherouter.dev/clients/raya.md). ## Which to choose Both use your org's cache and your stored provider key, and sound the same. - **Our plugin** reports only the characters Raya billed in `TTSMetrics` (cache hits count zero), and switches provider with `update_options`. - **Raya’s plugin** is the smallest change to an agent that already uses it. Its metrics count every character, cached or not; [Usage](https://docs.ttscacherouter.dev/dashboard.md#usage) has the real split. --- # ElevenLabs with LiveKit > ElevenLabs in a LiveKit agent: through our plugin, or through LiveKit's ElevenLabs plugin with a changed URL. ## With our plugin ElevenLabs, with every setting of ElevenLabs' own LiveKit plugin. eleven_v3 and eleven_v4 models go through ElevenLabs' text-to-dialogue API, as that plugin sends them. ```shell pip install livekit-plugins-tts-cache-router ``` **agent.py** ```python from livekit.plugins import tts_cache_router as tcr # The API key comes from TTS_CACHE_ROUTER_API_KEY. tts = tcr.TTS( model="elevenlabs/eleven_flash_v2_5", voice="JBFqnCBsd6RMkjVDRZzb", ) ``` Pass `tts` to `AgentSession(tts=tts)`. Set `TTS_CACHE_ROUTER_API_KEY` in the agent’s environment, and store your ElevenLabs key under [Provider keys](https://app.ttscacherouter.dev/app/provider-keys). `TTS`’s own arguments are on [the plugin’s page](https://docs.ttscacherouter.dev/livekit.md#tts). ### Options `tcr.TTS(...)`’s settings for ElevenLabs, in the router’s names: `model`, `voice`, `language` and `speed` as arguments, and the rest in the `options` dict (`options={"name": value}`). The router checks them; the languages per model are in [Models and options](https://docs.ttscacherouter.dev/reference/models.md#elevenlabs). | Field | Type | Required or default | Values | Description | | --- | --- | --- | --- | --- | | `model` | string | Required | elevenlabs/eleven_flash_v2_5, elevenlabs/eleven_turbo_v2_5, elevenlabs/eleven_multilingual_v2, elevenlabs/eleven_flash_v2, elevenlabs/eleven_turbo_v2, elevenlabs/eleven_v3, elevenlabs/eleven_v3_conversational, elevenlabs/eleven_v4, elevenlabs/eleven_v4_turbo | Which model synthesizes the replies, with its provider. | | `voice` | string | Required | | A voice of the chosen model, passed to ElevenLabs as given. See [models and voices](https://docs.ttscacherouter.dev/reference/models.md#elevenlabs-models). | | `language` | string \| null | Optional | | A ElevenLabs language code from [languages](https://docs.ttscacherouter.dev/reference/models.md#elevenlabs-languages), passed through as given. Leave it out and ElevenLabs detects the language. | | `sample_rate` | int | Default `24000` | 8000, 16000, 22050, 24000, 32000, 44100, 48000 | Of the audio you receive, in Hz. | | `options.voice_settings` | object \| null | Default `null` | | Any of stability, similarity_boost and style (0–1), speed (0.7–1.2) and use_speaker_boost; one left out keeps the voice's stored setting. Null: the voice's stored settings. Dialogue models (eleven_v3, eleven_v4) use stability only. | | `options.apply_text_normalization` | string | Default `"auto"` | auto, on, off | Spell out numbers, dates and the like before speaking. | | `options.apply_language_text_normalization` | boolean \| null | Default `null` | | Language-specific normalization (Japanese); adds latency. | | `options.enable_ssml_parsing` | boolean | Default `false` | | Parse SSML tags in the text. | | `options.enable_logging` | boolean | Default `true` | | false: ElevenLabs' zero-retention mode (enterprise accounts). | | `options.pronunciation_dictionary_locators` | array \| null | Default `null` | | Up to 3 {pronunciation_dictionary_id, version_id}. | | `options.sync_alignment` | boolean | Default `true` | | /v1/stream: send each sentence's word timings (`words` messages). | | `options.preferred_alignment` | string \| null | Default `null` | normalized, original | /v1/stream: words of the text as sent (`original`) or as ElevenLabs normalized it. Null: `original` for Japanese, Korean and Chinese, else `normalized`. | ### Changing settings Replies that start afterwards use the new settings: ```python tts.update_options( options={"voice_settings": {"stability": 0.4, "similarity_boost": 0.8, "speed": 1.1}}, ) ``` ## With LiveKit's ElevenLabs plugin Keep `livekit-plugins-elevenlabs` and point it at the router: your router API key in place of your ElevenLabs key, and the router’s address. ```shell pip install livekit-plugins-elevenlabs ``` **agent.py** ```python import os from livekit.plugins import elevenlabs tts = elevenlabs.TTS( voice_id="JBFqnCBsd6RMkjVDRZzb", model="eleven_flash_v2_5", api_key=os.environ["TTS_CACHE_ROUTER_API_KEY"], base_url="https://api.ttscacherouter.dev/elevenlabs/v1", ) ``` `base_url` ends in `/v1`, as ElevenLabs’ own does. Streaming (the multi-context WebSocket), `synthesize()`, word-aligned transcripts, `eleven_v3` and `eleven_v4` through text-to-dialogue, and `list_voices()` all work as before. Requests, responses and errors keep ElevenLabs’s own shape; the endpoints are on [ElevenLabs API drop-in](https://docs.ttscacherouter.dev/clients/elevenlabs.md). ## Which to choose Both use your org's cache and your stored provider key, and sound the same. - **Our plugin** reports only the characters ElevenLabs billed in `TTSMetrics` (cache hits count zero), and switches provider with `update_options`. - **ElevenLabs’s plugin** is the smallest change to an agent that already uses it. Its metrics count every character, cached or not; [Usage](https://docs.ttscacherouter.dev/dashboard.md#usage) has the real split. --- # OpenAI with LiveKit > OpenAI in a LiveKit agent: through our plugin, or through LiveKit's OpenAI plugin with a changed URL. ## With our plugin OpenAI, with every setting of OpenAI's own LiveKit plugin. OpenAI speaks the language of the text, at 24000 Hz. ```shell pip install livekit-plugins-tts-cache-router ``` **agent.py** ```python from livekit.plugins import tts_cache_router as tcr # The API key comes from TTS_CACHE_ROUTER_API_KEY. tts = tcr.TTS( model="openai/gpt-4o-mini-tts", voice="ash", ) ``` Pass `tts` to `AgentSession(tts=tts)`. Set `TTS_CACHE_ROUTER_API_KEY` in the agent’s environment, and store your OpenAI key under [Provider keys](https://app.ttscacherouter.dev/app/provider-keys). `TTS`’s own arguments are on [the plugin’s page](https://docs.ttscacherouter.dev/livekit.md#tts). ### Options `tcr.TTS(...)`’s settings for OpenAI, in the router’s names: `model`, `voice`, `language` and `speed` as arguments, and the rest in the `options` dict (`options={"name": value}`). The router checks them; the languages per model are in [Models and options](https://docs.ttscacherouter.dev/reference/models.md#openai). | Field | Type | Required or default | Values | Description | | --- | --- | --- | --- | --- | | `model` | string | Required | openai/gpt-4o-mini-tts, openai/gpt-4o-mini-tts-2025-12-15, openai/tts-1, openai/tts-1-hd | Which model synthesizes the replies, with its provider. | | `voice` | string | Required | | A voice of the chosen model, passed to OpenAI as given. See [models and voices](https://docs.ttscacherouter.dev/reference/models.md#openai-models). | | `speed` | number | Default `1.0` | 0.25 – 4.0 | Speech speed. 1.0 is normal speed. | | `sample_rate` | int | Default `24000` | 24000 | Of the audio you receive, in Hz. | | `options.instructions` | string \| null | Default `null` | | How to speak: tone, accent, emotion, pace. gpt-4o-mini-tts follows them; tts-1 and tts-1-hd ignore them. | ### Changing settings Replies that start afterwards use the new settings: ```python tts.update_options(options={"instructions": "Speak calmly and slowly."}) ``` ## With LiveKit's OpenAI plugin Keep `livekit-plugins-openai` and point it at the router: your router API key in place of your OpenAI key, and the router’s address. ```shell pip install livekit-plugins-openai ``` **agent.py** ```python import os from livekit.plugins import openai tts = openai.TTS( model="gpt-4o-mini-tts", voice="ash", api_key=os.environ["TTS_CACHE_ROUTER_API_KEY"], base_url="https://api.ttscacherouter.dev/openai/v1", ) ``` `base_url` ends in `/v1`, as OpenAI’s own does. The plugin doesn’t stream text, so an `AgentSession` sends it one sentence at a time, and each sentence is cached on its own. Requests, responses and errors keep OpenAI’s own shape; the endpoints are on [OpenAI API drop-in](https://docs.ttscacherouter.dev/clients/openai.md). ## Which to choose Both use your org's cache and your stored provider key, and sound the same. - **Our plugin** reports only the characters OpenAI billed in `TTSMetrics` (cache hits count zero), and switches provider with `update_options`. - **OpenAI’s plugin** is the smallest change to an agent that already uses it. Its metrics count every character, cached or not; [Usage](https://docs.ttscacherouter.dev/dashboard.md#usage) has the real split. --- # Deepgram with LiveKit > Deepgram's Aura and Flux in a LiveKit agent: through our plugin, or through LiveKit's Deepgram plugin with a changed URL. ## With our plugin Deepgram’s Aura and Flux with every setting of LiveKit’s Deepgram plugin (`TTS` and `TTSv2`). Deepgram’s model id is the model and the voice joined: `model="deepgram/aura-2"` and `voice="thalia-en"` is `aura-2-thalia-en`. With Aura, the first sentence of a reply after a pause of about five seconds or more takes about 0.7–1.2 s longer to start than with LiveKit’s plugin; sentences from the cache, and Flux, are unaffected. ```shell pip install livekit-plugins-tts-cache-router ``` **agent.py** ```python from livekit.plugins import tts_cache_router as tcr # The API key comes from TTS_CACHE_ROUTER_API_KEY. tts = tcr.TTS( model="deepgram/aura-2", voice="thalia-en", ) ``` Pass `tts` to `AgentSession(tts=tts)`. Set `TTS_CACHE_ROUTER_API_KEY` in the agent’s environment, and store your Deepgram key under [Provider keys](https://app.ttscacherouter.dev/app/provider-keys). `TTS`’s own arguments are on [the plugin’s page](https://docs.ttscacherouter.dev/livekit.md#tts). ### Options `tcr.TTS(...)`’s settings for Deepgram, in the router’s names: `model`, `voice`, `language` and `speed` as arguments, and the rest in the `options` dict (`options={"name": value}`). The router checks them; the languages per model are in [Models and options](https://docs.ttscacherouter.dev/reference/models.md#deepgram). | Field | Type | Required or default | Values | Description | | --- | --- | --- | --- | --- | | `model` | string | Required | deepgram/aura-2, deepgram/aura-1, deepgram/flux | Which model synthesizes the replies, with its provider. | | `voice` | string | Required | | A voice of the chosen model, passed to Deepgram as given. See [models and voices](https://docs.ttscacherouter.dev/reference/models.md#deepgram-models). | | `speed` | number | Optional | 0.7 – 1.5 | Speech speed, aura-2 only. 1.0 is normal speed; left out, the provider's default. | | `sample_rate` | int | Default `24000` | 8000, 16000, 22050, 24000, 32000, 44100, 48000 | Of the audio you receive, in Hz. | | `options.mip_opt_out` | boolean | Default `false` | | Opt out of Deepgram's Model Improvement Program. | ### Changing settings Replies that start afterwards use the new settings: ```python tts.update_options(voice="andromeda-en") ``` ## With LiveKit's Deepgram plugin Keep `livekit-plugins-deepgram` and point it at the router: your router API key in place of your Deepgram key, and the router’s address. ```shell pip install livekit-plugins-deepgram ``` **agent.py** ```python import os from livekit.plugins import deepgram tts = deepgram.TTS( model="aura-2-thalia-en", api_key=os.environ["TTS_CACHE_ROUTER_API_KEY"], base_url="https://api.ttscacherouter.dev/deepgram/v1/speak", ) ``` For Flux, use `deepgram.TTSv2(model="flux-alexis-en", …)` with `base_url` ending in `/deepgram/v2/speak`. The plugin streams your agent’s reply word by word; the router splits it into sentences and caches each one, with [a few differences from Deepgram’s own messages](https://docs.ttscacherouter.dev/clients/deepgram.md#websocket). Requests, responses and errors keep Deepgram’s own shape; the endpoints are on [Deepgram API drop-in](https://docs.ttscacherouter.dev/clients/deepgram.md). ## Which to choose Both use your org's cache and your stored provider key, and sound the same. - **Our plugin** reports only the characters Deepgram billed in `TTSMetrics` (cache hits count zero), and switches provider with `update_options`. - **Deepgram’s plugin** is the smallest change to an agent that already uses it. Its metrics count every character, cached or not; [Usage](https://docs.ttscacherouter.dev/dashboard.md#usage) has the real split. --- # xAI with LiveKit > xAI in a LiveKit agent: through our plugin, or through LiveKit's xAI plugin pointed at the router. ## With our plugin xAI's text to speech, with every setting of LiveKit's xAI plugin. ```shell pip install livekit-plugins-tts-cache-router ``` **agent.py** ```python from livekit.plugins import tts_cache_router as tcr # The API key comes from TTS_CACHE_ROUTER_API_KEY. tts = tcr.TTS( model="xai/xai-tts", voice="ara", language="auto", ) ``` Pass `tts` to `AgentSession(tts=tts)`. Set `TTS_CACHE_ROUTER_API_KEY` in the agent’s environment, and store your xAI key under [Provider keys](https://app.ttscacherouter.dev/app/provider-keys). `TTS`’s own arguments are on [the plugin’s page](https://docs.ttscacherouter.dev/livekit.md#tts). ### Options `tcr.TTS(...)`’s settings for xAI, in the router’s names: `model`, `voice`, `language` and `speed` as arguments, and the rest in the `options` dict (`options={"name": value}`). The router checks them; the languages per model are in [Models and options](https://docs.ttscacherouter.dev/reference/models.md#xai). | Field | Type | Required or default | Values | Description | | --- | --- | --- | --- | --- | | `model` | string | Required | xai/xai-tts | Which model synthesizes the replies, with its provider. | | `voice` | string | Required | | A voice of the chosen model, passed to xAI as given. See [models and voices](https://docs.ttscacherouter.dev/reference/models.md#xai-models). | | `language` | string \| null | Optional | | A xAI language code from [languages](https://docs.ttscacherouter.dev/reference/models.md#xai-languages), passed through as given. Leave it out and xAI detects the language. | | `speed` | number | Optional | 0.7 – 1.5 | Speech speed. 1.0 is normal speed; left out, the provider's default. | | `sample_rate` | int | Default `24000` | 8000, 16000, 22050, 24000, 44100, 48000 | Of the audio you receive, in Hz. | | `options.optimize_streaming_latency` | integer \| null | Default `null` | 0 – 2 | 0: best quality (xAI's default); 1 and 2: a smaller first chunk for lower time to first audio, with a quality tradeoff at chunk boundaries. Null: xAI's default. | | `options.text_normalization` | boolean \| null | Default `null` | | Normalize written forms (numbers, abbreviations, symbols) into spoken forms before synthesis. Null: xAI's default (false). | ### Changing settings Replies that start afterwards use the new settings: ```python tts.update_options(voice="eve", speed=1.1) ``` ## With LiveKit's xAI plugin Keep `livekit-plugins-xai` and point it at the router: your router API key in place of your xAI key, and the router’s address. ```shell pip install livekit-plugins-xai ``` **agent.py** ```python import os from livekit.agents import tokenize from livekit.plugins import xai from livekit.plugins.xai import tts as xai_tts xai_tts.XAI_WEBSOCKET_URL = "wss://api.ttscacherouter.dev/xai/v1/tts" tts = xai.TTS( voice="ara", language="auto", tokenizer=tokenize.basic.WordTokenizer(ignore_punctuation=False, retain_format=True), api_key=os.environ["TTS_CACHE_ROUTER_API_KEY"], ) ``` LiveKit’s xAI plugin has no `base_url`: set its module’s `XAI_WEBSOCKET_URL` to the router before creating the TTS, as above. Pass the `tokenizer` shown, with or without the router: the plugin’s default drops the spaces between words, and xAI speaks `Hi` then `there.` as `Hithere.`. With the default, the router also can’t tell where sentences end, so each reply is synthesized and cached whole. The router splits the reply into sentences and caches each one, with [a few differences from xAI’s own messages](https://docs.ttscacherouter.dev/clients/xai.md#websocket). Requests, responses and errors keep xAI’s own shape; the endpoints are on [xAI API drop-in](https://docs.ttscacherouter.dev/clients/xai.md). ## Which to choose Both use your org's cache and your stored provider key, and sound the same. - **Our plugin** reports only the characters xAI billed in `TTSMetrics` (cache hits count zero), and switches provider with `update_options`. - **xAI’s plugin** is the smallest change to an agent that already uses it. Its metrics count every character, cached or not; [Usage](https://docs.ttscacherouter.dev/dashboard.md#usage) has the real split. --- # Soniox with LiveKit > Soniox in a LiveKit agent: through our plugin, or through LiveKit's Soniox plugin with a changed URL. ## With our plugin Soniox’s text to speech with every setting of LiveKit’s Soniox plugin. The router calls Soniox’s US endpoint: store a key from a Soniox project in the US region. ```shell pip install livekit-plugins-tts-cache-router ``` **agent.py** ```python from livekit.plugins import tts_cache_router as tcr # The API key comes from TTS_CACHE_ROUTER_API_KEY. tts = tcr.TTS( model="soniox/tts-rt-v2", voice="Adrian", language="en", ) ``` Pass `tts` to `AgentSession(tts=tts)`. Set `TTS_CACHE_ROUTER_API_KEY` in the agent’s environment, and store your Soniox key under [Provider keys](https://app.ttscacherouter.dev/app/provider-keys). `TTS`’s own arguments are on [the plugin’s page](https://docs.ttscacherouter.dev/livekit.md#tts). ### Options `tcr.TTS(...)`’s settings for Soniox, in the router’s names: `model`, `voice`, `language` and `speed` as arguments, and the rest in the `options` dict (`options={"name": value}`). The router checks them; the languages per model are in [Models and options](https://docs.ttscacherouter.dev/reference/models.md#soniox). | Field | Type | Required or default | Values | Description | | --- | --- | --- | --- | --- | | `model` | string | Required | soniox/tts-rt-v2 | Which model synthesizes the replies, with its provider. | | `voice` | string | Required | | A voice of the chosen model, passed to Soniox as given. See [models and voices](https://docs.ttscacherouter.dev/reference/models.md#soniox-models). | | `language` | string | Required | | A Soniox language code from [languages](https://docs.ttscacherouter.dev/reference/models.md#soniox-languages), passed through as given. | | `speed` | number | Default `1.0` | 0.7 – 1.3 | Speech speed. 1.0 is normal speed. | | `sample_rate` | int | Default `24000` | 8000, 16000, 24000, 32000, 44100, 48000 | Of the audio you receive, in Hz. | | `options.reduce_silence` | boolean \| null | Default `null` | | Shorten the pauses between words. Null: Soniox's default (false). | ### Changing settings Replies that start afterwards use the new settings: ```python tts.update_options(voice="Nina", speed=1.1) ``` ## With LiveKit's Soniox plugin Keep `livekit-plugins-soniox` and point it at the router: your router API key in place of your Soniox key, and the router’s address. ```shell pip install livekit-plugins-soniox ``` **agent.py** ```python import os from livekit.plugins import soniox tts = soniox.TTS( model="tts-rt-v2", voice="Adrian", language="en", api_key=os.environ["TTS_CACHE_ROUTER_API_KEY"], websocket_url="wss://api.ttscacherouter.dev/soniox/tts-websocket", ) ``` Pass `model="tts-rt-v2"` and a voice explicitly: the plugin defaults to `tts-rt-v1-preview`, a deprecated alias, and `Maya`, a voice `tts-rt-v2` doesn’t have. Each sentence is a take of its own, so the pause between two sentences is shorter (about 0.2 s) than when the plugin streams a whole reply to Soniox (about 0.5 s), and the first sentence starts as soon as it is complete. The router splits the reply into sentences and caches each one, with [a few differences from Soniox’s own messages](https://docs.ttscacherouter.dev/clients/soniox.md#websocket). Requests, responses and errors keep Soniox’s own shape; the endpoints are on [Soniox API drop-in](https://docs.ttscacherouter.dev/clients/soniox.md). ## Which to choose Both use your org's cache and your stored provider key, and sound the same. - **Our plugin** reports only the characters Soniox billed in `TTSMetrics` (cache hits count zero), and switches provider with `update_options`. - **Soniox’s plugin** is the smallest change to an agent that already uses it. Its metrics count every character, cached or not; [Usage](https://docs.ttscacherouter.dev/dashboard.md#usage) has the real split. --- # Smallest AI with LiveKit > Smallest AI in a LiveKit agent: through our plugin, or through LiveKit's Smallest AI plugin with changed URLs. ## With our plugin Smallest AI’s Lightning voices with every setting of LiveKit’s Smallest AI plugin. `TTS`’s `sample_rate` is 8000, 16000, 24000 or 44100. ```shell pip install livekit-plugins-tts-cache-router ``` **agent.py** ```python from livekit.plugins import tts_cache_router as tcr # The API key comes from TTS_CACHE_ROUTER_API_KEY. tts = tcr.TTS( model="smallestai/lightning_v3.1_pro", voice="meher", language="en", ) ``` Pass `tts` to `AgentSession(tts=tts)`. Set `TTS_CACHE_ROUTER_API_KEY` in the agent’s environment, and store your Smallest AI key under [Provider keys](https://app.ttscacherouter.dev/app/provider-keys). `TTS`’s own arguments are on [the plugin’s page](https://docs.ttscacherouter.dev/livekit.md#tts). ### Options `tcr.TTS(...)`’s settings for Smallest AI, in the router’s names: `model`, `voice`, `language` and `speed` as arguments, and the rest in the `options` dict (`options={"name": value}`). The router checks them; the languages per model are in [Models and options](https://docs.ttscacherouter.dev/reference/models.md#smallestai). | Field | Type | Required or default | Values | Description | | --- | --- | --- | --- | --- | | `model` | string | Required | smallestai/lightning_v3.1, smallestai/lightning_v3.1_pro | Which model synthesizes the replies, with its provider. | | `voice` | string | Required | | A voice of the chosen model, passed to Smallest AI as given. See [models and voices](https://docs.ttscacherouter.dev/reference/models.md#smallestai-models). | | `language` | string | Default `"en"` | | A Smallest AI language code from [languages](https://docs.ttscacherouter.dev/reference/models.md#smallestai-languages), passed through as given. | | `speed` | number | Default `1.0` | 0.5 – 2.0 | Speech speed. 1.0 is normal speed. | | `sample_rate` | int | Default `24000` | 8000, 16000, 24000, 44100 | Of the audio you receive, in Hz. | ### Changing settings Replies that start afterwards use the new settings: ```python tts.update_options(language="hi", speed=1.1) ``` ## With LiveKit's Smallest AI plugin Keep `livekit-plugins-smallestai` and point it at the router: your router API key in place of your Smallest AI key, and the router’s address. ```shell pip install livekit-plugins-smallestai ``` **agent.py** ```python import os from livekit.plugins import smallestai tts = smallestai.TTS( model="lightning_v3.1_pro", voice_id="meher", language="en", api_key=os.environ["TTS_CACHE_ROUTER_API_KEY"], base_url="https://api.ttscacherouter.dev/smallestai/waves/v1", ws_url="wss://api.ttscacherouter.dev/smallestai/waves/v1/tts/live", ) ``` Set both URLs: `ws_url` for streaming, as an agent speaks, and `base_url` for `synthesize()`. The router splits the reply into sentences and synthesizes each on its own: a sentence’s audio starts once the sentence is complete, and each sentence is a take of its own rather than a continuation of the one before. `use_continuations`, `max_buffer_delay_ms` and `max_buffer_flush_ms` change nothing, and `word_timestamps=True` gets no word timings. The other differences from Smallest AI’s own messages are on [Smallest AI API drop-in](https://docs.ttscacherouter.dev/clients/smallestai.md#websocket). Requests, responses and errors keep Smallest AI’s own shape; the endpoints are on [Smallest AI API drop-in](https://docs.ttscacherouter.dev/clients/smallestai.md). ## Which to choose Both use your org's cache and your stored provider key, and sound the same. - **Our plugin** reports only the characters Smallest AI billed in `TTSMetrics` (cache hits count zero), and switches provider with `update_options`. - **Smallest AI’s plugin** is the smallest change to an agent that already uses it. Its metrics count every character, cached or not; [Usage](https://docs.ttscacherouter.dev/dashboard.md#usage) has the real split. --- # Cartesia with LiveKit > Cartesia in a LiveKit agent: through our plugin, or through LiveKit's Cartesia plugin with a changed URL. ## With our plugin Cartesia’s Sonic voices with every setting of LiveKit’s Cartesia plugin. `TTS`’s `sample_rate` is 8000, 16000, 22050, 24000, 44100 or 48000. ```shell pip install livekit-plugins-tts-cache-router ``` **agent.py** ```python from livekit.plugins import tts_cache_router as tcr # The API key comes from TTS_CACHE_ROUTER_API_KEY. tts = tcr.TTS( model="cartesia/sonic-3", voice="f786b574-daa5-4673-aa0c-cbe3e8534c02", language="en", ) ``` Pass `tts` to `AgentSession(tts=tts)`. Set `TTS_CACHE_ROUTER_API_KEY` in the agent’s environment, and store your Cartesia key under [Provider keys](https://app.ttscacherouter.dev/app/provider-keys). `TTS`’s own arguments are on [the plugin’s page](https://docs.ttscacherouter.dev/livekit.md#tts). ### Options `tcr.TTS(...)`’s settings for Cartesia, in the router’s names: `model`, `voice`, `language` and `speed` as arguments, and the rest in the `options` dict (`options={"name": value}`). The router checks them; the languages per model are in [Models and options](https://docs.ttscacherouter.dev/reference/models.md#cartesia). | Field | Type | Required or default | Values | Description | | --- | --- | --- | --- | --- | | `model` | string | Required | cartesia/sonic-3.6, cartesia/sonic-3.5, cartesia/sonic-3 | Which model synthesizes the replies, with its provider. | | `voice` | string | Required | | A voice of the chosen model, passed to Cartesia as given. See [models and voices](https://docs.ttscacherouter.dev/reference/models.md#cartesia-models). | | `language` | string | Default `"en"` | | A Cartesia language code from [languages](https://docs.ttscacherouter.dev/reference/models.md#cartesia-languages), passed through as given. | | `speed` | number | Optional | 0.6 – 1.5 | Speech speed. 1.0 is normal speed; left out, the provider's default. | | `sample_rate` | int | Default `24000` | 8000, 16000, 22050, 24000, 44100, 48000 | Of the audio you receive, in Hz. | | `options.volume` | number \| null | Default `null` | 0.5 – 2.0 | Loudness relative to the voice's own. Null: Cartesia's default (1.0). | | `options.emotion` | string \| null | Default `null` | | Guide the delivery (English): neutral, calm, angry, content, sad, scared, and the others Cartesia lists. Null: Cartesia reads the emotion from the text. | | `options.pronunciation_dict_id` | string \| null | Default `null` | | A Cartesia pronunciation dictionary's id. Cached takes keep the dictionary as it was when they were synthesized, until they expire. | | `options.word_timestamps` | boolean | Default `true` | | /v1/stream: send each sentence's word timings (`words` messages). Not with mp3. | ### Changing settings Replies that start afterwards use the new settings: ```python tts.update_options(speed=1.1, options={"emotion": "calm"}) ``` ## With LiveKit's Cartesia plugin Keep `livekit-plugins-cartesia` and point it at the router: your router API key in place of your Cartesia key, and the router’s address. ```shell pip install livekit-plugins-cartesia ``` **agent.py** ```python import os from livekit.plugins import cartesia tts = cartesia.TTS( model="sonic-3", voice="f786b574-daa5-4673-aa0c-cbe3e8534c02", language="en", api_key=os.environ["TTS_CACHE_ROUTER_API_KEY"], base_url="https://api.ttscacherouter.dev/cartesia", ) ``` `base_url` is the only URL to change: the plugin derives its WebSocket URL from it. Streaming, `synthesize()` and word timestamps work as before. The router splits the reply into sentences and synthesizes each on its own, through either plugin: each sentence is a take of its own rather than a continuation of the one before, and the pauses between sentences are shorter than in one Cartesia context. When the user interrupts, the sentence being synthesized finishes at Cartesia and is cached; the rest of the reply isn’t sent. The other differences from Cartesia’s own messages are on [Cartesia API drop-in](https://docs.ttscacherouter.dev/clients/cartesia.md#websocket). Requests, responses and errors keep Cartesia’s own shape; the endpoints are on [Cartesia API drop-in](https://docs.ttscacherouter.dev/clients/cartesia.md). ## Which to choose Both use your org's cache and your stored provider key, and sound the same. - **Our plugin** reports only the characters Cartesia billed in `TTSMetrics` (cache hits count zero), and switches provider with `update_options`. - **Cartesia’s plugin** is the smallest change to an agent that already uses it. Its metrics count every character, cached or not; [Usage](https://docs.ttscacherouter.dev/dashboard.md#usage) has the real split. --- # Gemini with LiveKit > Gemini in a LiveKit agent: through our plugin, or through LiveKit's Gemini plugin pointed at the router. ## With our plugin Gemini’s speech models with every setting of LiveKit’s Gemini plugin, from version 0.5.0 of our plugin. Gemini speaks the language of the text, at 24000 Hz, and takes no `language` or `speed`: ask for a pace or a tone in `instructions`. The 3.8 and 3.1 models stream a sentence’s audio as they make it; the 2.5 models send it once it is complete, so their first audio takes a few seconds longer. ```shell pip install livekit-plugins-tts-cache-router ``` **agent.py** ```python from livekit.plugins import tts_cache_router as tcr # The API key comes from TTS_CACHE_ROUTER_API_KEY. tts = tcr.TTS( model="gemini/gemini-3.8-flash-tts", voice="Kore", ) ``` Pass `tts` to `AgentSession(tts=tts)`. Set `TTS_CACHE_ROUTER_API_KEY` in the agent’s environment, and store your Gemini key under [Provider keys](https://app.ttscacherouter.dev/app/provider-keys). `TTS`’s own arguments are on [the plugin’s page](https://docs.ttscacherouter.dev/livekit.md#tts). ### Options `tcr.TTS(...)`’s settings for Gemini, in the router’s names: `model`, `voice`, `language` and `speed` as arguments, and the rest in the `options` dict (`options={"name": value}`). The router checks them; the languages per model are in [Models and options](https://docs.ttscacherouter.dev/reference/models.md#gemini). | Field | Type | Required or default | Values | Description | | --- | --- | --- | --- | --- | | `model` | string | Required | gemini/gemini-3.8-flash-tts, gemini/gemini-3.8-flash-lite-tts, gemini/gemini-3.1-flash-tts-preview, gemini/gemini-2.5-flash-preview-tts, gemini/gemini-2.5-pro-preview-tts | Which model synthesizes the replies, with its provider. | | `voice` | str \| None | Required, unless `options.speakers` | | A voice of the chosen model, passed to Gemini as given. See [models and voices](https://docs.ttscacherouter.dev/reference/models.md#gemini-models). With `options.speakers`, `None`. | | `sample_rate` | int | Default `24000` | 24000 | Of the audio you receive, in Hz. | | `options.instructions` | string \| null | Default `null` | | How to speak: style, tone, accent, pace. Null: for gemini-2.5 and gemini-3.1, LiveKit's plugin's default ("Say the text with a proper tone, don't omit or add any words"), which keeps them reading the text; for gemini-3.8, none. | | `options.speakers` | object \| null | `gemini-3.8-flash-tts` , `gemini-3.8-flash-lite-tts` only. Default `null` | | Two speakers' names, each with a voice (a multi-speaker voice config); replaces voice. Requires speaker. | | `options.speaker` | string \| null | `gemini-3.8-flash-tts` , `gemini-3.8-flash-lite-tts` only. Default `null` | | Which of speakers voices this request. | ### Changing settings Replies that start afterwards use the new settings: ```python tts.update_options(options={"instructions": "Speak calmly and slowly."}) # Two speakers (the 3.8 models): no voice, and the speaker of each reply in options. tts = tcr.TTS( model="gemini/gemini-3.8-flash-tts", voice=None, options={"speakers": {"Joe": "Kore", "Jane": "Puck"}, "speaker": "Jane"}, ) tts.update_options(options={"speakers": {"Joe": "Kore", "Jane": "Puck"}, "speaker": "Joe"}) ``` ## With LiveKit's Gemini plugin Keep `livekit-plugins-google` and point it at the router: your router API key in place of your Gemini key, and the router’s address. ```shell pip install livekit-plugins-google ``` **agent.py** ```python import os from livekit.plugins import google os.environ["GOOGLE_GEMINI_BASE_URL"] = "https://api.ttscacherouter.dev/gemini" tts = google.beta.GeminiTTS( model="gemini-3.8-flash-tts", voice_name="Kore", api_key=os.environ["TTS_CACHE_ROUTER_API_KEY"], ) ``` The plugin has no `base_url`: Google’s GenAI SDK reads `GOOGLE_GEMINI_BASE_URL` when the plugin creates its client, so set it before creating the TTS, in code as above or in the agent’s environment. It applies to every Gemini API client the process creates, so in an agent that also uses Gemini as its LLM, use our plugin instead. Vertex AI (`vertexai=True`) doesn’t go through the router. The plugin doesn’t stream text, so an `AgentSession` sends it one sentence at a time, and each sentence is cached on its own. With the 3.8 models, its requests are the ones our plugin makes, so the two share cached sentences. The other differences from calling Google are on [Gemini API drop-in](https://docs.ttscacherouter.dev/clients/gemini.md#caching). Requests, responses and errors keep Gemini’s own shape; the endpoints are on [Gemini API drop-in](https://docs.ttscacherouter.dev/clients/gemini.md). ## Which to choose Both use your org's cache and your stored provider key, and sound the same. - **Our plugin** reports only the characters Gemini billed in `TTSMetrics` (cache hits count zero), and switches provider with `update_options`. - **Gemini’s plugin** is the smallest change to an agent that already uses it. Its metrics count every character, cached or not; [Usage](https://docs.ttscacherouter.dev/dashboard.md#usage) has the real split. --- # The Pipecat service > pipecat-tts-cache-router: install it, construct TTSCacheRouterTTSService, change settings mid-session, and what it reports. ## Install `pipecat-tts-cache-router` is a Pipecat TTS service that speaks through the router, for every provider. It needs Python 3.11 or later and `pipecat-ai` 1.12 or later. This page describes version 0.2.0. ```shell pip install pipecat-tts-cache-router ``` **bot.py** ```python import os from pipecat_tts_cache_router import TTSCacheRouterTTSService tts = TTSCacheRouterTTSService( api_key=os.environ["TTS_CACHE_ROUTER_API_KEY"], settings=TTSCacheRouterTTSService.Settings( model="sarvam/bulbul:v3", voice="shubh", language="hi-IN" ), ) # then, in your bot: # pipeline = Pipeline([transport.input(), stt, user_aggregator, llm, tts, # transport.output(), assistant_aggregator]) ``` Put `tts` in your pipeline where a TTS service goes, after the LLM. Set `TTS_CACHE_ROUTER_API_KEY` to your router API key, and store your provider key under [Provider keys](https://app.ttscacherouter.dev/app/provider-keys): the service never sends one. It connects when the pipeline starts, and the router connects to your provider at once, so the first reply doesn’t wait for either. ## TTSCacheRouterTTSService(...) The service's one class. Give it your API key and the provider's settings. - `api_key` (`str`, Required): Your router API key. - `settings` (`TTSCacheRouterTTSService.Settings`, Required): The provider, model, voice and settings: see [Settings](https://docs.ttscacherouter.dev/pipecat.md#settings). - `base_url` (`str`, Default `wss://api.ttscacherouter.dev`): The router. Change it only for a router you run yourself. - `sample_rate` (`int | None`, Default `None`): Of the PCM audio the service produces, one the provider streams. `None`: the pipeline’s output sample rate (`PipelineParams.audio_out_sample_rate`, 24000 by default, which every provider streams). Each provider’s rates are in [Models and options](https://docs.ttscacherouter.dev/reference/models.md). Other keyword arguments go to Pipecat’s `WebsocketTTSService`, except `text_aggregation_mode`: the router splits the text into sentences itself, so the service streams the LLM’s text to it as it arrives and each sentence starts as soon as it is complete. ## Settings The settings are in the router’s own names, those of [/v1/audio/speech](https://docs.ttscacherouter.dev/clients/speech.md#body) and `GET /v1/models`, the same for every provider. **Sarvam** ```python settings=TTSCacheRouterTTSService.Settings( model="sarvam/bulbul:v3", voice="shubh", language="hi-IN", options={"temperature": 0.6}, ) ``` **Raya** ```python settings=TTSCacheRouterTTSService.Settings( model="raya/standard", voice="d6a002d0-230c-49b1-a137-b8a7d564b1ae", language="hi", ) ``` **ElevenLabs** ```python settings=TTSCacheRouterTTSService.Settings( model="elevenlabs/eleven_flash_v2_5", voice="JBFqnCBsd6RMkjVDRZzb", options={"voice_settings": {"stability": 0.5, "similarity_boost": 0.75}}, ) ``` **OpenAI** ```python settings=TTSCacheRouterTTSService.Settings( model="openai/gpt-4o-mini-tts", voice="ash", options={"instructions": "Speak warmly."}, ) ``` **Deepgram** ```python settings=TTSCacheRouterTTSService.Settings( model="deepgram/aura-2", voice="thalia-en", ) ``` **xAI** ```python settings=TTSCacheRouterTTSService.Settings( model="xai/xai-tts", voice="ara", language="auto", ) ``` **Soniox** ```python settings=TTSCacheRouterTTSService.Settings( model="soniox/tts-rt-v2", voice="Adrian", language="en", ) ``` **Smallest AI** ```python settings=TTSCacheRouterTTSService.Settings( model="smallestai/lightning_v3.1_pro", voice="meher", language="en", ) ``` **Cartesia** ```python settings=TTSCacheRouterTTSService.Settings( model="cartesia/sonic-3", voice="f786b574-daa5-4673-aa0c-cbe3e8534c02", language="en", ) ``` **Gemini** ```python settings=TTSCacheRouterTTSService.Settings( model="gemini/gemini-3.8-flash-tts", voice="Kore", options={"instructions": "Warm and upbeat."}, ) ``` - `model` (`str`, Required): Any model `GET /v1/models` lists, with its provider: `sarvam/bulbul:v3`, `elevenlabs/eleven_flash_v2_5`, `openai/tts-1`, `deepgram/aura-2`, `xai/xai-tts`, … ([Models and options](https://docs.ttscacherouter.dev/reference/models.md)). - `voice` (`str | None`, Required): The provider’s voice id, as [GET /v1/voices](https://docs.ttscacherouter.dev/reference/voices.md) lists it. `None` only for Gemini’s two speakers, which `options["speakers"]` names. - `language` (`str | Language | None`, Default `None`): The provider’s language code, sent as written: `hi-IN` for Sarvam, `hi` for Raya, `en` for ElevenLabs, Soniox, Smallest AI and Cartesia. Required for Sarvam, Raya and Soniox. Optional for ElevenLabs and xAI: left out, the provider detects it. Optional for Smallest AI and Cartesia: left out, `en`. Not taken by OpenAI, Deepgram or Gemini. - `speed` (`float | None`, Default `None`): The speaking rate, where the provider takes one. None: the router's default. - `options` (`dict`, Default `{}`): The provider’s other settings, by their names in [Models and options](https://docs.ttscacherouter.dev/reference/models.md): `{"temperature": 0.6}` for Sarvam, `{"voice_settings": {...}}` for ElevenLabs, `{"instructions": "..."}` for OpenAI and Gemini. The service checks `model` and `voice` when you construct it, and raises `ValueError` for a malformed one. The router checks the rest as the pipeline starts, when the service connects: a wrong setting is reported then as an error (`bad_request`), not at the first reply. ## Changing settings A `TTSUpdateSettingsFrame` changes the voice or settings mid-session. Replies that start afterwards use them; the reply being spoken finishes as it began. ```python from pipecat.frames.frames import TTSUpdateSettingsFrame # Another voice: await worker.queue_frame( TTSUpdateSettingsFrame(delta=TTSCacheRouterTTSService.Settings(voice="echo")) ) # Or another provider; its language, speed and options start from none: await worker.queue_frame( TTSUpdateSettingsFrame( delta=TTSCacheRouterTTSService.Settings( model="sarvam/bulbul:v3", voice="shubh", language="hi-IN" ) ) ) ``` A `model` of another provider starts from no `language`, `speed` or `options` unless the update gives them, and `options` are replaced as a whole. Between replies, the router connects to the provider for the new settings at once. A sentence in a new voice or with new settings is a new cache entry: synthesized once, then cached. ## Interruptions When the user interrupts, the service tells the router to stop the reply and keeps the connection. The router stops sending the reply’s audio at once, and the provider stops the sentence it was synthesizing, so nothing cut off is cached (ElevenLabs and Cartesia finish their sentence, and cache it). The next reply starts on the same connection. ## Transcripts The service's TTSTextFrames follow the audio, so the conversation context holds what was spoken. Each sentence’s words are timed to the moment its audio starts. With ElevenLabs and Cartesia, transcripts are word by word, from the provider’s own timings (turn them off with `options={"sync_alignment": False}` or `options={"word_timestamps": False}`). Timings are cached with the audio, so a reply from the cache is aligned too. ## Metrics The TTS usage metric counts only the characters the provider billed. With usage metrics on, each reply reports one `TTSUsageMetricsData`: the characters of its sentences the provider synthesized. Sentences from the cache count zero, so a fully cached reply reports `0`. Its `model` is the settings’ `/`, so usage dashboards can attribute cost to the provider. TTFB is Pipecat’s, as for any TTS service. ```python from pipecat.pipeline.worker import PipelineParams, PipelineWorker worker = PipelineWorker( pipeline, params=PipelineParams(enable_metrics=True, enable_usage_metrics=True), ) ``` ## Cache stats Once a reply is all sent, the service calls its `on_cache_stats` handlers: how much of the reply came from your org’s cache. This script speaks a sentence twice, without a transport, and prints both: **speak_twice.py** ```python import asyncio import os from pipecat.frames.frames import EndFrame, TTSSpeakFrame from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.worker import PipelineWorker from pipecat.workers.runner import WorkerRunner from pipecat_tts_cache_router import TTSCacheRouterTTSService async def main() -> None: tts = TTSCacheRouterTTSService( api_key=os.environ["TTS_CACHE_ROUTER_API_KEY"], settings=TTSCacheRouterTTSService.Settings(model="openai/tts-1", voice="alloy"), ) @tts.event_handler("on_cache_stats") async def on_cache_stats(tts, stats): print(f"{stats.cached_sentences}/{stats.sentences} sentences from the cache, " f"{stats.billed_characters}/{stats.characters} characters billed by the provider") worker = PipelineWorker(Pipeline([tts])) runner = WorkerRunner(handle_sigint=False) await runner.add_workers(worker) # The second time, from the cache. await worker.queue_frames( [TTSSpeakFrame("Your order has shipped."), TTSSpeakFrame("Your order has shipped."), EndFrame()] ) await runner.run() asyncio.run(main()) ``` ```text 0/1 sentences from the cache, 23/23 characters billed by the provider 1/1 sentences from the cache, 0/23 characters billed by the provider ``` - `context_id` (`str`): The reply's TTS context id. - `sentences` (`int`): The sentences the reply was split into. - `cached_sentences` (`int`): Those served from the cache. - `characters` (`int`): The characters of all the reply's sentences. - `billed_characters` (`int`): The characters the provider synthesized and billed, as the usage metric. `cached_characters` is the rest. An interrupted reply reports both too: they count the sentences the router sent in full before the interruption, so the usage is what the provider billed for the audio your bot received. ## Errors Router errors become Pipecat ErrorFrames, with a category, so your pipeline's error handling and ServiceSwitcher work as with any TTS. | What happened | ErrorFrame category | | --- | --- | | The router refused the API key | `AUTHENTICATION` | | Your org is out of credit | `QUOTA` | | A setting the router refused (`bad_request`) | `INVALID_REQUEST` | | No key stored for the provider (`provider_key_missing`) | `AUTHENTICATION` | | The provider refused the request (`provider_rejected`) | From the provider’s HTTP status, as Pipecat classifies it (`AUTHENTICATION` for 401, `AUTHORIZATION` for 403, `QUOTA` for 402); `INVALID_REQUEST` without one | | Your provider quota (`provider_rate_limited`) | `RATE_LIMIT` | | The provider failed or was late, or the router failed | `SERVER` | | The connection failed or dropped | `CONNECTIVITY` | A reply that fails ends without the rest of its audio; the next reply starts on a new connection. An `AUTHENTICATION`, `AUTHORIZATION` or `INVALID_REQUEST` error marks the service unusable, as Pipecat does for any service, until its settings change. When the router restarts, the service reconnects at once. > Waiting for the LLM is not a timeout: the service gives up only after it has sent all of a reply’s text and the router goes quiet for 135 seconds. - [Models and options](https://docs.ttscacherouter.dev/reference/models.md): Every provider's models, formats and options. - [Voices](https://docs.ttscacherouter.dev/reference/voices.md): GET /v1/voices: every voice your org can speak with. - [WebSocket protocol](https://docs.ttscacherouter.dev/clients/stream.md): What the service sends and receives. --- # /v1/audio/speech > Speech over HTTP, shaped like OpenAI's audio/speech: the OpenAI SDK works with the base URL and key changed. ## Request `POST https://api.ttscacherouter.dev/v1/audio/speech` with your router API key as `Authorization: Bearer tcr_…`. Audio streams back as each sentence is ready: from your org’s cache when it has been synthesized before, otherwise from the provider, with the key stored for your org. **OpenAI SDK** ```python import os from openai import OpenAI client = OpenAI( base_url="https://api.ttscacherouter.dev/v1", api_key=os.environ["TTS_CACHE_ROUTER_API_KEY"], ) with client.audio.speech.with_streaming_response.create( model="sarvam/bulbul:v3", voice="shubh", input="नमस्ते, मैं सिटी हॉस्पिटल से प्रिया बोल रही हूँ।", response_format="mp3", extra_body={"language": "hi-IN"}, ) as response: response.stream_to_file("speech.mp3") ``` **curl** ```shell curl https://api.ttscacherouter.dev/v1/audio/speech \ -H "Authorization: Bearer $TTS_CACHE_ROUTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "raya/standard", "voice": "d6a002d0-230c-49b1-a137-b8a7d564b1ae", "language": "hi", "input": "नमस्ते, मैं प्रिया बोल रही हूँ।", "response_format": "wav"}' \ --output speech.wav ``` Fields the OpenAI SDK doesn’t have go in `extra_body`. For each provider, swap in a model and voice: | model | voice | extra_body | | --- | --- | --- | | sarvam/bulbul:v3 | `shubh` | `{"language": "hi-IN"}` | | raya/standard | `d6a002d0-230c-49b1-a137-b8a7d564b1ae` | `{"language": "hi"}` | | elevenlabs/eleven_flash_v2_5 | `JBFqnCBsd6RMkjVDRZzb` | `{"voice_settings": {"stability": 0.5, "similarity_boost": 0.75}}` | | openai/gpt-4o-mini-tts | `ash` | None; `instructions` is an SDK argument | | deepgram/aura-2 | `thalia-en` | None | | xai/xai-tts | `ara` | `{"language": "en"}` | | soniox/tts-rt-v2 | `Adrian` | `{"language": "en"}` | | smallestai/lightning_v3.1_pro | `meher` | `{"language": "en"}` | | cartesia/sonic-3 | `f786b574-daa5-4673-aa0c-cbe3e8534c02` | `{"language": "en"}` | | gemini/gemini-3.8-flash-tts | `Kore` | None; `instructions` is an SDK argument. Pass `response_format="wav"` or `"pcm"`: Gemini has no `mp3` | ### Body - `model` (`string`, Required): `/`, e.g. `sarvam/bulbul:v3`, `raya/standard`, `elevenlabs/eleven_flash_v2_5`, `openai/tts-1`, `deepgram/aura-2`, `xai/xai-tts`, `soniox/tts-rt-v2`, `smallestai/lightning_v3.1`, `cartesia/sonic-3` or `gemini/gemini-3.8-flash-tts`. Every model is in [Models and options](https://docs.ttscacherouter.dev/reference/models.md). - `input` (`string`, Required): The text to speak, up to 20,000 characters. - `voice` (`string`, Required): A voice of the model, as [GET /v1/voices](https://docs.ttscacherouter.dev/reference/voices.md) lists it: a Sarvam speaker, a Raya, ElevenLabs or Cartesia voice id, or a voice name (`ash`, `thalia-en`, `ara`, `Adrian`, `meher`, `Kore`). Gemini: left out or null with `speakers`. - `language` (`string`, Sarvam, Raya and Soniox: required. ElevenLabs and xAI: optional. Smallest AI and Cartesia: default `en`. OpenAI, Deepgram and Gemini: not taken): The provider’s language code: `hi-IN` for Sarvam, `hi` for Raya, `en` for Soniox (which reads the text in that language). Left out, ElevenLabs detects the language, and xAI too (or send `auto`); Smallest AI gets `en` (send `auto` for Smallest AI to detect it), and Cartesia too. OpenAI and Gemini speak the language of the text; a Deepgram voice names its language. - `response_format` (`string`, Default `"mp3"`): See [Formats](https://docs.ttscacherouter.dev/clients/speech.md#formats). Gemini has no `mp3`: send one of its formats. - `speed` (`number`, Default `1.0`): Sarvam 0.5–2.0, Raya 0.5–1.5, OpenAI 0.25–4.0, Soniox 0.7–1.3, Smallest AI 0.5–2.0. Deepgram 0.7–1.5 (`aura-2` only), xAI 0.7–1.5 and Cartesia 0.6–1.5: left out, the provider’s default. ElevenLabs refuses it: use `voice_settings.speed`. Gemini refuses it: ask for a pace in `instructions`. - `sample_rate` (`integer`, Default `24000`): Of the audio. Each provider’s rates per format are in [Models and options](https://docs.ttscacherouter.dev/reference/models.md). OpenAI takes only `24000`; Gemini `24000` for `pcm` and `wav`, `8000` for `mulaw` and `alaw`. Left out, `mulaw` and `alaw` default to `8000` with ElevenLabs, Deepgram, Soniox and Gemini, and Deepgram’s `mp3` to `22050`, its only rate. Sarvam also takes `32000`, `44100` and `48000` here, where each sentence arrives only once it is fully synthesized and `bitrate` is fixed at `128k`. - `temperature` (`number`, Sarvam only): 0.01–1.0, default 0.6. - `dict_id` (`string`, Sarvam only): A Sarvam pronunciation dictionary to apply. - `pitch` (`number`, Sarvam bulbul:v4-flash only): -0.5 (deeper) to 0.5 (higher), default 0. - `loudness` (`number`, Sarvam bulbul:v4-flash only): 0.1–2.5, default 1.0. - `enable_preprocessing` (`boolean`, Sarvam bulbul:v4-flash only): Normalize numbers and English words in the text before synthesis. Default `false`. - `bitrate` (`string | integer`, Sarvam, ElevenLabs, Soniox): Sarvam, for `mp3` and `aac`: `32k`, `64k`, `96k`, `128k` or `192k`, default `128k`. ElevenLabs, for `mp3`: `32k` at 22050 Hz, `48k` at 24000 Hz, `32k`–`192k` at 44100 Hz (default `128k`). Soniox, for `mp3`: an integer in bits per second, `32000`, `64000`, `96000`, `128000`, `192000`, `256000` or `320000` (left out, Soniox’s default, `128000`). - `voice_settings` (`object | null`, ElevenLabs only): Any of `stability`, `similarity_boost` and `style` (0–1), `speed` (0.7–1.2) and `use_speaker_boost`; one left out keeps the voice’s stored setting. Null: the voice’s stored settings. - `apply_text_normalization` (`string`, ElevenLabs only): `auto` (default), `on` or `off`. - `apply_language_text_normalization` (`boolean`, ElevenLabs only): Language-specific normalization (Japanese). - `enable_ssml_parsing` (`boolean`, ElevenLabs only): Parse SSML tags. Default `false`. - `enable_logging` (`boolean`, ElevenLabs only): `false` for ElevenLabs’ zero-retention mode (enterprise accounts). Default `true`. - `pronunciation_dictionary_locators` (`array`, ElevenLabs only): Up to 3 `{pronunciation_dictionary_id, version_id}`. - `instructions` (`string`, OpenAI and Gemini): How to speak: tone, accent, emotion, pace. OpenAI: `gpt-4o-mini-tts` follows them; `tts-1` and `tts-1-hd` ignore them. Gemini: style, tone, accent and pace; left out, the 2.5 and 3.1 models get LiveKit’s Gemini plugin’s default (`Say the text with a proper tone, don't omit or add any words`), the 3.8 models none. - `mip_opt_out` (`boolean`, Deepgram only): Opt out of Deepgram’s Model Improvement Program. Default `false`. - `bit_rate` (`integer`, Deepgram, xAI, Cartesia; mp3 only): Deepgram: `32000` or `48000`. xAI and Cartesia: `32000`, `64000`, `96000`, `128000` or `192000`. Left out, the provider’s default. - `optimize_streaming_latency` (`integer`, xAI only): `0` (best quality), `1` or `2` (a smaller first chunk for lower latency, with a quality tradeoff at chunk boundaries). Left out, xAI’s default (`0`). - `text_normalization` (`boolean`, xAI only): Speak numbers, abbreviations and symbols in their spoken forms. Left out, xAI’s default (`false`). - `reduce_silence` (`boolean`, Soniox only): Shorten the pauses between words. Left out, Soniox’s default (`false`). - `volume` (`number`, Cartesia only): Loudness relative to the voice’s own, 0.5–2.0. Left out, Cartesia’s default (`1.0`). - `emotion` (`string`, Cartesia only): Guide the delivery (English), e.g. `calm`, `sad` or `angry`; Cartesia lists the rest. Left out, Cartesia reads the emotion from the text. - `pronunciation_dict_id` (`string`, Cartesia only): A Cartesia pronunciation dictionary to apply. - `speakers` (`object`, Gemini 3.8 models only): Two speakers’ names, each with a voice: `{"Joe": "Kore", "Jane": "Puck"}`. Replaces `voice`, which is then left out or null. Requires `speaker`. - `speaker` (`string`, Gemini 3.8 models only): Which of `speakers` voices this request. - `stream_format` (`"audio"`, Default `"audio"`): The body is the audio itself. `sse` is not supported here; OpenAI’s SSE is on the [OpenAI API drop-in](https://docs.ttscacherouter.dev/clients/openai.md). Any other field, or another provider’s, is a `bad_request`. ## Formats Every format comes from the provider itself, never re-encoded. A provider and format marked No are refused with HTTP 400. | response_format | Sarvam | Raya | ElevenLabs | OpenAI | Deepgram | xAI | Soniox | Smallest AI | Cartesia | Gemini | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | pcm | Yes | Yes | Yes | 24000 Hz | Yes | Yes | Yes | Yes | Yes | 24000 Hz | | wav | Yes | Yes | Yes | 24000 Hz | Yes | Yes | Yes | Yes | Yes | 24000 Hz | | mp3 | Yes | No | 22050, 24000, 44100 Hz | Yes | Aura, 22050 Hz | Yes | Yes | Yes | Yes | No | | aac | Yes | No | No | Yes | No | No | No | No | No | No | | mulaw | Yes | Yes | 8000 Hz | No | 8000, 16000 Hz | Yes | 8000 Hz | Yes | Yes | 3.8 models, 8000 Hz | | alaw | Yes | No | 8000 Hz | No | 8000, 16000 Hz | Yes | 8000 Hz | Yes | Yes | 3.8 models, 8000 Hz | | flac, opus | No | No | No | No | No | No | No | No | No | No | - `pcm` is raw 16-bit little-endian mono at `sample_rate`; `wav` is the same with a WAV header for streaming (its size fields are left open). - `mulaw` and `alaw` are 8-bit G.711 at `sample_rate`, for telephony. Content types: `audio/pcm`, `audio/wav`, `audio/mpeg`, `audio/aac`, `audio/mulaw`, `audio/alaw`. ## Errors An error before any audio is a JSON body with an HTTP status. The codes are the WebSocket protocol's. ```json { "error": { "code": "provider_rejected", "message": "…", "retryable": false, "provider_status": 403 } } ``` - `bad_request` (HTTP 400, not retryable): The request failed validation or a message arrived out of order. The message says which field or rule. - `unauthorized` (HTTP 401, not retryable): The API key is missing, wrong or deleted, or its org is disabled. - `insufficient_credits` (HTTP 402, not retryable): Your org is out of credit, so the router refuses new connections and requests until you [buy more](https://app.ttscacherouter.dev/app/billing). Connections already open carry on. - `provider_key_missing` (HTTP 400, not retryable): Your org has no key stored for the provider. Add one under [Provider keys](https://app.ttscacherouter.dev/app/provider-keys). - `provider_rejected` (HTTP 400, not retryable): The provider refused the request, e.g. a wrong stored provider key or an unknown voice. The provider’s message is passed through. - `provider_rate_limited` (HTTP 429, retryable): Your own quota at the provider is exhausted. - `provider_unavailable` (HTTP 502, retryable): The provider returned a server error, or the connection to it failed or closed unexpectedly. - `provider_timeout` (HTTP 504, retryable): The provider took too long to send audio. - `internal` (HTTP 500, retryable): The router failed. > The status line is sent with the first audio. An error after audio has started can only abort the body: the response ends without its final chunk. Treat an incomplete body as a failed request. --- # WebSocket protocol > /v1/stream, the protocol the LiveKit plugin speaks: stream text in, sentences and audio out, from any language. ## Connecting One WebSocket: JSON messages in, JSON messages and binary audio frames out. The LiveKit plugin and the Pipecat service speak this protocol; use it from any language. Open a WebSocket to `wss://api.ttscacherouter.dev/v1/stream` with the header `Authorization: Bearer `. A missing, wrong or deleted key is refused at the handshake with HTTP 403. A key deleted while a connection is open is refused at its next `start` with `unauthorized`. A complete client in Python (`pip install websockets`). It speaks one sentence into `out.wav`; run it twice and the second run plays from the cache. **speak.py** ```python import asyncio import json import os import uuid import wave from websockets.asyncio.client import connect ROUTER_URL = "wss://api.ttscacherouter.dev/v1/stream" SAMPLE_RATE = 24000 async def main() -> None: headers = {"Authorization": f"Bearer {os.environ['TTS_CACHE_ROUTER_API_KEY']}"} segment_id = uuid.uuid4().hex async with connect(ROUTER_URL, additional_headers=headers) as ws: await ws.send(json.dumps({ "type": "start", "segment_id": segment_id, "provider": "sarvam", "model": "bulbul:v3", "voice": "shubh", "language": "hi-IN", "speed": 1.0, "sample_rate": SAMPLE_RATE, })) await ws.send(json.dumps({ "type": "text", "segment_id": segment_id, "text": "नमस्ते, मैं प्रिया बोल रही हूँ।", })) await ws.send(json.dumps({"type": "flush", "segment_id": segment_id})) pcm = bytearray() async for message in ws: if isinstance(message, bytes): pcm += message # 16-bit little-endian mono PCM continue event = json.loads(message) if event["type"] == "sentence": print(f"sentence {event['index']} from {event['source']}") elif event["type"] == "done": print(f"done: {event['billed_characters']} characters billed") break elif event["type"] == "error": raise RuntimeError(f"{event['code']}: {event['message']}") with wave.open("out.wav", "wb") as wav: wav.setnchannels(1) wav.setsampwidth(2) wav.setframerate(SAMPLE_RATE) wav.writeframes(pcm) asyncio.run(main()) ``` ## Segments A connection carries **segments**, one after another. A segment is one `start`, any number of `text` messages and one `flush`, each carrying the segment’s `segment_id`. The router answers each sentence with a `sentence` message followed by its audio as binary frames, and ends the segment with `done`. Send the next `start` on the same connection after `done` (or `cancelled`). Sentences are spoken as soon as they’re complete, so audio usually starts before you send `flush`: One segment on the wire (→ you send, ← the router sends): ```text → {"type": "start", "segment_id": "c0ffee", "provider": "sarvam", …} → {"type": "text", "segment_id": "c0ffee", "text": "नमस्ते, मैं सिटी हॉस्पिटल से "} → {"type": "text", "segment_id": "c0ffee", "text": "प्रिया बोल रही हूँ। मैं आपकी "} ← {"type": "sentence", "index": 0, "source": "cache", "text": "नमस्ते, मैं सिटी हॉस्पिटल से प्रिया बोल रही हूँ।"} ← binary frames: audio for sentence 0, replayed from the cache → {"type": "text", "segment_id": "c0ffee", "text": "क्या मदद कर सकती हूँ?"} → {"type": "flush", "segment_id": "c0ffee"} ← {"type": "sentence", "index": 1, "source": "provider", "text": "मैं आपकी क्या मदद कर सकती हूँ?"} ← binary frames: audio for sentence 1, streamed as the provider synthesizes it ← {"type": "done", "segment_id": "c0ffee", "sentences": 2, "cached_sentences": 1, "billed_characters": 30} ``` Messages out of order (`text` before `start`, a second `start`, anything after `flush` before `done`, a `segment_id` that isn’t the open segment’s, a `warm` while a segment is open) end the segment with a `bad_request` error. ## start Opens a segment and sets the provider, voice and settings for all of its sentences. The router calls the provider with the key stored for your org. ```json { "type": "start", "segment_id": "c0ffee", "provider": "sarvam", "model": "bulbul:v3", "voice": "shubh", "language": "hi-IN", "speed": 1.0, "sample_rate": 24000, "format": "pcm", "options": {"temperature": 0.6, "dict_id": null} } ``` - `type` (`"start"`, Required): Identifies the message. - `segment_id` (`string`, Required): Your id for the segment, 1–128 characters. Sent with every `text`, `flush` and `cancel`, and echoed in `done`, `cancelled` and `error`. - `provider` (`"sarvam" | "raya" | "elevenlabs" | "openai" | "deepgram" | "xai" | "soniox" | "smallestai" | "cartesia" | "gemini"`, Required): Which provider synthesizes, and which values below apply. - `model` (`string`, Required): The provider’s model, without the provider: `bulbul:v3`, `standard`, `eleven_flash_v2_5`, `tts-1`, `aura-2`, `xai-tts`, `tts-rt-v2`, `lightning_v3.1`, `sonic-3`, `gemini-3.8-flash-tts`, … Every model is in [Models and options](https://docs.ttscacherouter.dev/reference/models.md). - `voice` (`string`, Required): A voice of the model, as [GET /v1/voices](https://docs.ttscacherouter.dev/reference/voices.md) lists it, passed to the provider as given; the provider checks it. Gemini with `options.speakers`: left out or null. - `language` (`string`, Sarvam, Raya and Soniox: required. ElevenLabs and xAI: optional. Smallest AI and Cartesia: default `en`. OpenAI, Deepgram and Gemini: not taken): The provider’s language code, passed through as given: `hi-IN` for Sarvam, `hi` for Raya, `en` for Soniox (which reads the text in that language). Left out, ElevenLabs detects the language, and xAI too (or send `auto`); Smallest AI gets `en` (send `auto` for Smallest AI to detect it), and Cartesia too. OpenAI and Gemini speak the language of the text; a Deepgram voice names its language. - `speed` (`number`, Sarvam, Raya and OpenAI: required. Soniox and Smallest AI: default 1.0. Deepgram, xAI and Cartesia: optional. ElevenLabs and Gemini: not taken): 1.0 is normal speed; ranges are in [Models and options](https://docs.ttscacherouter.dev/reference/models.md). Deepgram takes it with `aura-2` only, and with Deepgram, xAI and Cartesia, leaving it out leaves it to the provider. ElevenLabs takes speed in `options.voice_settings.speed`; with Gemini, ask for a pace in `options.instructions`. - `sample_rate` (`integer`, Required): Of the audio you receive, one the provider streams in `format`: see [Models and options](https://docs.ttscacherouter.dev/reference/models.md). - `format` (`string`, Default `"pcm"`): The audio format, produced by the provider itself: `pcm` (16-bit little-endian mono) for every provider; `mp3`, `aac`, `mulaw` or `alaw` where the provider makes it ([Models and options](https://docs.ttscacherouter.dev/reference/models.md)). - `options` (`object`, Default `{}`): The provider’s other settings, such as `temperature` for Sarvam, `voice_settings` for ElevenLabs or `instructions` for OpenAI and Gemini. Each provider’s options, ranges and defaults are in [Models and options](https://docs.ttscacherouter.dev/reference/models.md); Raya and Smallest AI take none. A left-out option and its default share cache entries. Any other field is a `bad_request`. [Models and options](https://docs.ttscacherouter.dev/reference/models.md) has a `start` message for every provider. ## text and flush Send text as it arrives, chunked any way you like, such as an LLM’s tokens; the router finds the sentence boundaries. `flush` ends the segment’s input and speaks whatever text remains. A `flush` with no text gets a `done` with zeros. ```json {"type": "text", "segment_id": "c0ffee", "text": "नमस्ते, मैं सिटी हॉस्पिटल से "} {"type": "flush", "segment_id": "c0ffee"} ``` ## warm Send `warm` right after connecting, or when you change settings, and the router connects to the provider now instead of at the next `start` (a few hundred milliseconds to a second), so that segment’s audio doesn’t wait. The LiveKit plugin sends it on every new connection. ```json { "type": "warm", "provider": "sarvam", "model": "bulbul:v3", "voice": "shubh", "language": "hi-IN", "speed": 1.0, "sample_rate": 24000, "format": "pcm" } ``` It takes the fields of `start` without `segment_id`, validated the same way; an invalid one, or a `warm` while a segment is open, is a `bad_request` error. Otherwise the router doesn’t answer it. The next `start` with the same settings uses the warm connection; other settings open a new one. Nothing is billed until a sentence isn’t in the cache. If the provider can’t be reached, the next `start` tries again and reports its error. ## Router messages `sentence` comes before each sentence’s audio. `index` counts from 0 within the segment, `text` is the sentence as it was spoken (use it to align transcripts), and `source` is `cache` or `provider`. ```json {"type": "sentence", "index": 0, "text": "नमस्ते, मैं सिटी हॉस्पिटल से प्रिया बोल रही हूँ।", "source": "cache"} ``` The audio follows as one or more **binary frames** in the segment’s `format`, with no header. Concatenate the frames in order. `words` (ElevenLabs and Cartesia) gives the timings of a sentence’s words. It follows the sentence’s `sentence` message, interleaved with its audio, each batch before the audio it covers. `index` is the sentence’s; `start` and `end` are seconds from the start of that sentence’s audio. Each word’s `text` runs up to the next word, trailing whitespace included, so a sentence’s words concatenate to its spoken text. Turn them off with `options.sync_alignment: false` (ElevenLabs) or `options.word_timestamps: false` (Cartesia, which sends none with `mp3`). ```json {"type": "words", "index": 0, "words": [{"text": "Hello, ", "start": 0.02, "end": 0.35}, {"text": "this ", "start": 0.35, "end": 0.52}]} ``` `done` ends the segment. `sentences` counts every sentence, `cached_sentences` the ones from the cache, and `billed_characters` the characters of the provider sentences: what the provider charges you for. ```json {"type": "done", "segment_id": "c0ffee", "sentences": 2, "cached_sentences": 1, "billed_characters": 30} ``` ## Interrupting To stop speaking, for example when the user barges in, send `cancel` with the segment’s id. The router stops sending its audio and answers `cancelled` after the last frame it sends for the segment. Binary frames carry no segment id, so discard frames until `cancelled` arrives. The connection is then ready for the next `start`, already warm. `cancelled` carries `done`’s counts for the sentences whose audio the router sent in full before the cancel: `sentences`, `cached_sentences`, and `billed_characters`, what the provider billed for them. A sentence cut off mid-synthesis is not counted. ```json {"type": "cancel", "segment_id": "c0ffee"} {"type": "cancelled", "segment_id": "c0ffee", "sentences": 1, "cached_sentences": 0, "billed_characters": 30} ``` A `cancel` for a segment that already finished is answered with `cancelled` too, with zeros: its `done` had its counts. The provider stops the sentence it was synthesizing, which isn’t cached (ElevenLabs and Cartesia finish it, and it is cached and billed, but `cancelled` doesn’t wait for it or count it; [Usage](https://app.ttscacherouter.dev/app/usage) does). Closing the connection also stops the segment, but a sentence already being synthesized then finishes and is cached. Segment length, message size and idle timeouts are listed in [Limits](https://docs.ttscacherouter.dev/reference/limits.md). ## Errors An `error` message ends the segment. Close the connection and open a new one before sending anything else. ```json { "type": "error", "segment_id": "c0ffee", "code": "provider_rejected", "message": "…", "retryable": false, "provider_status": 403 } ``` `segment_id` is `null` if the error came before a valid `start`. `provider_status` is the provider’s HTTP status when there is one, and `null` otherwise. `retryable` says whether the same request can succeed if you try again. - `bad_request` (not retryable): The request failed validation or a message arrived out of order. The message says which field or rule. - `unauthorized` (not retryable): The API key is missing, wrong or deleted, or its org is disabled. - `insufficient_credits` (not retryable): Your org is out of credit, so the router refuses new connections and requests until you [buy more](https://app.ttscacherouter.dev/app/billing). Connections already open carry on. - `provider_key_missing` (not retryable): Your org has no key stored for the provider. Add one under [Provider keys](https://app.ttscacherouter.dev/app/provider-keys). - `provider_rejected` (not retryable): The provider refused the request, e.g. a wrong stored provider key or an unknown voice. The provider’s message is passed through. - `provider_rate_limited` (retryable): Your own quota at the provider is exhausted. - `provider_unavailable` (retryable): The provider returned a server error, or the connection to it failed or closed unexpectedly. - `provider_timeout` (retryable): The provider took too long to send audio. - `internal` (retryable): The router failed. --- # Sarvam API drop-in > Sarvam's own REST, HTTP stream and WebSocket APIs through the cache: keep the Sarvam SDK, change the URL and key. ## Sarvam's Python SDK Sarvam’s API, mirrored under `https://api.ttscacherouter.dev/sarvam`. Keep your Sarvam client: give it the router as its environment and your router API key as its `api_subscription_key`. **REST** ```python import base64 import os from sarvamai import SarvamAI from sarvamai.environment import SarvamAIEnvironment client = SarvamAI( api_subscription_key=os.environ["TTS_CACHE_ROUTER_API_KEY"], environment=SarvamAIEnvironment( base="https://api.ttscacherouter.dev/sarvam", production="wss://api.ttscacherouter.dev/sarvam", creative="https://api.ttscacherouter.dev/sarvam/dubbing", # unused: the router serves text-to-speech only ), ) response = client.text_to_speech.convert( text="नमस्ते, मैं प्रिया बोल रही हूँ।", language_code="hi-IN", speaker="shubh", model="bulbul:v3", ) with open("speech.wav", "wb") as f: f.write(base64.b64decode(response.audios[0])) ``` **WebSocket** ```python import asyncio import base64 import os from sarvamai import AsyncSarvamAI from sarvamai.environment import SarvamAIEnvironment HTTP = "https://api.ttscacherouter.dev/sarvam" async def main() -> None: client = AsyncSarvamAI( api_subscription_key=os.environ["TTS_CACHE_ROUTER_API_KEY"], environment=SarvamAIEnvironment(base=HTTP, production="wss://api.ttscacherouter.dev/sarvam", creative=f"{HTTP}/dubbing"), ) pcm = b"" async with client.text_to_speech_streaming.connect(model="bulbul:v3", send_completion_event=True) as ws: await ws.configure(target_language_code="hi-IN", speaker="shubh", output_audio_codec="linear16") await ws.convert("नमस्ते, मैं प्रिया बोल रही हूँ।") await ws.flush() async for message in ws: if message.type == "audio": pcm += base64.b64decode(message.data.audio) elif message.type == "event" and message.data.event_type == "final": break print(f"{len(pcm)} bytes of 16-bit PCM") asyncio.run(main()) ``` `convert`, `convert_stream` and `text_to_speech_streaming.connect` work through the router. Requests, responses, status codes, error bodies and WebSocket messages are Sarvam’s own; your request is forwarded to Sarvam as you sent it, with the Sarvam key stored under [Provider keys](https://app.ttscacherouter.dev/app/provider-keys). For a LiveKit agent, see [Sarvam with LiveKit](https://docs.ttscacherouter.dev/livekit/sarvam.md#provider-plugin). ## WebSocket `wss://api.ttscacherouter.dev/sarvam/text-to-speech/ws`, and `/ws/v2`, which LiveKit’s Sarvam plugin uses for `bulbul:v4-flash`. Sarvam’s `model` and `send_completion_event` query parameters and its `config`, `text`, `flush` and `ping` messages. The router splits your text into sentences and synthesizes each one on its own, so each sentence your org has synthesized before with the same config plays from the cache. - As with Sarvam, text that ends a sentence is spoken without waiting for `flush`, and one `final` event follows each `flush`, after all its audio (when you set `send_completion_event=true`). - A sentence with no letter or digit (an emoji on its own) is skipped, since Sarvam refuses it, and so is such a run at the end of a sentence (`…shipped it. 🚀`), which Sarvam would refuse along with the sentence. - Every codec works, `wav`, `flac` and `opus` included, message for message as Sarvam sends them. - A connection with no messages for 60 seconds is closed with Sarvam’s own idle error. ## REST and HTTP stream `POST https://api.ttscacherouter.dev/sarvam/text-to-speech` and `POST https://api.ttscacherouter.dev/sarvam/text-to-speech/stream`, with your router API key in `api-subscription-key`. **HTTP stream** ```shell curl https://api.ttscacherouter.dev/sarvam/text-to-speech/stream \ -H "api-subscription-key: $TTS_CACHE_ROUTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"text": "नमस्ते, मैं प्रिया बोल रही हूँ।", "language_code": "hi-IN", "speaker": "shubh", "model": "bulbul:v3", "output_audio_codec": "mp3"}' \ --output speech.mp3 ``` **REST** ```shell curl https://api.ttscacherouter.dev/sarvam/text-to-speech \ -H "api-subscription-key: $TTS_CACHE_ROUTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"text": "नमस्ते, मैं प्रिया बोल रही हूँ।", "language_code": "hi-IN", "speaker": "shubh", "model": "bulbul:v3"}' ``` Each request is cached as a whole: the same body again replays the same audio. A REST reply from the cache has `"request_id": null`. Only text-to-speech is served: any other path under `/sarvam` answers 404. ## Errors Errors arrive as Sarvam sends them, so your client's error handling keeps working. - A wrong or missing router API key gets Sarvam’s own refusal, HTTP 403 (at the WebSocket handshake too). So does an org with no Sarvam key stored, with a message that links to the dashboard. - Errors from Sarvam, such as a wrong stored Sarvam key or an unknown speaker, reach you as Sarvam sent them. - When Sarvam reports an error and still speaks (a `dict_id` it can’t find, for example), you get the error, the audio and the `final` as Sarvam sends them, and that audio isn’t cached. ```http HTTP/1.1 403 Forbidden {"error": {"message": "Invalid or missing authentication credentials", "code": "invalid_api_key_error", "request_id": null}} ``` --- # Raya API drop-in > Raya's own REST, SSE and voices APIs through the cache: keep your Raya client, change the URL and key. ## Endpoints Raya’s API, mirrored under `https://api.ttscacherouter.dev/raya`. Requests, responses, status codes and error bodies are Raya’s own. Send your router API key in `X-API-Key`; your request is forwarded to Raya as you sent it, with the Raya key stored under [Provider keys](https://app.ttscacherouter.dev/app/provider-keys). - `POST /raya/v1/text-to-speech/stream`: Raya’s server-sent events. A reply from the cache sends the same `chunk` events, without `step_time`, then `done`. - `POST /raya/v1/text-to-speech`: the whole clip in the requested codec, with Raya’s headers. - `GET /raya/v1/voices`: Raya’s voice list, never cached. [GET /v1/voices](https://docs.ttscacherouter.dev/reference/voices.md) lists it in the router’s own shape. For a LiveKit agent, see [Raya with LiveKit](https://docs.ttscacherouter.dev/livekit/raya.md#provider-plugin). Each synthesis request is cached as a whole: the same body again replays the same audio. Any other path under `/raya` answers 404. **SSE** ```shell curl -N https://api.ttscacherouter.dev/raya/v1/text-to-speech/stream \ -H "X-API-Key: $TTS_CACHE_ROUTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"text": "नमस्ते, मैं प्रिया बोल रही हूँ।", "voice_id": "d6a002d0-230c-49b1-a137-b8a7d564b1ae", "language": "hi", "model": "standard", "sample_rate": 24000}' ``` **REST** ```shell curl https://api.ttscacherouter.dev/raya/v1/text-to-speech \ -H "X-API-Key: $TTS_CACHE_ROUTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"text": "नमस्ते, मैं प्रिया बोल रही हूँ।", "voice_id": "d6a002d0-230c-49b1-a137-b8a7d564b1ae", "language": "hi", "model": "standard", "codec": "mulaw", "sample_rate": 8000}' \ --output speech.mulaw ``` **Voices** ```shell curl https://api.ttscacherouter.dev/raya/v1/voices -H "X-API-Key: $TTS_CACHE_ROUTER_API_KEY" ``` ## Errors A wrong or missing router key gets Raya’s own refusal, HTTP 401. So does an org with no Raya key stored, with a message that links to the dashboard. Errors from Raya, such as a wrong stored Raya key or an unknown voice, reach you as Raya sent them. ```http HTTP/1.1 401 Unauthorized WWW-Authenticate: Key realm="bakbak-tts" {"message": "Unauthorized", "request_id": null} ``` --- # ElevenLabs API drop-in > ElevenLabs' own WebSockets, REST and voices APIs through the cache: keep the ElevenLabs SDK, change the URL and key. ## ElevenLabs' Python SDK ElevenLabs’ API, mirrored under `https://api.ttscacherouter.dev/elevenlabs`. Keep the ElevenLabs SDK: pass the router as its `base_url` and your router API key as its `api_key`. **speak.py** ```python import os from elevenlabs.client import ElevenLabs client = ElevenLabs( base_url="https://api.ttscacherouter.dev/elevenlabs", api_key=os.environ["TTS_CACHE_ROUTER_API_KEY"], ) audio = client.text_to_speech.convert( "JBFqnCBsd6RMkjVDRZzb", text="Your order has shipped. It arrives on Friday.", model_id="eleven_flash_v2_5", output_format="mp3_22050_32", ) with open("speech.mp3", "wb") as f: f.writelines(audio) ``` `convert`, `stream`, `convert_with_timestamps`, `stream_with_timestamps` and `convert_realtime` work through the router. Requests, responses, messages, status codes and error bodies are ElevenLabs’ own; your query parameters, settings and body are forwarded as you sent them, with the ElevenLabs key stored under [Provider keys](https://app.ttscacherouter.dev/app/provider-keys). For a LiveKit agent, see [ElevenLabs with LiveKit](https://docs.ttscacherouter.dev/livekit/elevenlabs.md#provider-plugin). ## WebSockets - `/elevenlabs/v1/text-to-speech/{voice_id}/multi-stream-input`: contexts by id, as LiveKit’s plugin uses it. - `/elevenlabs/v1/text-to-speech/{voice_id}/stream-input`: one stream per connection, as the SDK’s `convert_realtime` uses it. - `/elevenlabs/v1/text-to-dialogue/multi-stream-input`: `eleven_v3` and `eleven_v4` models. ```text wss://api.ttscacherouter.dev/elevenlabs/v1/text-to-speech/JBFqnCBsd6RMkjVDRZzb/multi-stream-input?model_id=eleven_flash_v2_5&output_format=pcm_24000&auto_mode=true&sync_alignment=true ``` Audio messages carry the same timings (`alignment`, `normalizedAlignment`) a direct call gets, from the cache too, and each context ends with its `isFinal` (`is_final` on dialogue) once you close it. Every output format works, `opus` included. In MP3, a context’s later sentences carry no ID3 tag, as one direct stream carries only one. ## REST and voices - `POST /elevenlabs/v1/text-to-speech/{voice_id}` and `…/stream`: the audio in the requested `output_format`. - `POST /elevenlabs/v1/text-to-speech/{voice_id}/with-timestamps` and `…/stream/with-timestamps`: audio with character timings, as JSON. - `POST /elevenlabs/v1/text-to-dialogue/stream`: dialogue models. - `GET /elevenlabs/v1/voices` and `GET /elevenlabs/v2/voices`: your voices. - `GET /elevenlabs/v1/voices/{voice_id}/settings`: a voice’s stored settings. - `GET /elevenlabs/v1/shared-voices`: the voice library. - `POST /elevenlabs/v1/voices/add/{public_user_id}/{voice_id}`: adds a library voice to your ElevenLabs account. The voice endpoints are never cached. [GET /v1/voices](https://docs.ttscacherouter.dev/reference/voices.md) lists your ElevenLabs voices in the router’s own shape, beside every other provider’s. Any other path under `/elevenlabs` answers 404. **Stream** ```shell curl -N "https://api.ttscacherouter.dev/elevenlabs/v1/text-to-speech/JBFqnCBsd6RMkjVDRZzb/stream?output_format=mp3_22050_32" \ -H "xi-api-key: $TTS_CACHE_ROUTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"text": "Your order has shipped. It arrives on Friday.", "model_id": "eleven_flash_v2_5"}' \ --output speech.mp3 ``` **With timestamps** ```shell curl "https://api.ttscacherouter.dev/elevenlabs/v1/text-to-speech/JBFqnCBsd6RMkjVDRZzb/with-timestamps?output_format=pcm_16000" \ -H "xi-api-key: $TTS_CACHE_ROUTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"text": "Your order has shipped.", "model_id": "eleven_flash_v2_5"}' ``` **Voices** ```shell curl "https://api.ttscacherouter.dev/elevenlabs/v2/voices?page_size=100" -H "xi-api-key: $TTS_CACHE_ROUTER_API_KEY" ``` ## What is cached - **WebSockets:** each sentence. With `auto_mode=true` (LiveKit’s default) each text message you send is synthesized on its own, as at ElevenLabs. Without it, the router splits buffered text into sentences (LiveKit’s BlingFire tokenizer) and synthesizes each once complete, where ElevenLabs would choose its own chunks. `flush` ends the sentence in progress. - **REST:** each request as a whole. The same query and body again replays the same response. - A voice’s stored settings (used when `voice_settings` is left out or partial) and a pronunciation dictionary named without a `version_id` aren’t part of the cache key: after you change one, sentences cached before keep their audio until they go unused for your cache retention period. ## Errors A wrong router key gets ElevenLabs’ own refusal: HTTP 400 `invalid_api_key` (401 when no key is sent), and on a WebSocket the same error message followed by a close with code 1008. So does an org with no ElevenLabs key stored, with a message that links to the dashboard. Errors from ElevenLabs, such as an unknown voice, reach you as ElevenLabs sent them. **REST** ```http HTTP/1.1 400 Bad Request {"detail": {"type": "authentication_error", "code": "invalid_api_key", "message": "API key is invalid.", "status": "invalid_api_key", "request_id": null, "param": "api_key"}} ``` **WebSocket** ```text {"message": "API key is invalid.", "error": "invalid_api_key", "code": 1008} (close 1008) ``` --- # OpenAI API drop-in > OpenAI's own speech API through the cache: keep the OpenAI SDK, change the base URL and key. ## OpenAI's Python SDK OpenAI’s speech API, mirrored under `https://api.ttscacherouter.dev/openai/v1`. Keep the OpenAI SDK: pass the router as its `base_url` and your router API key as its `api_key`. **speak.py** ```python import os from openai import OpenAI client = OpenAI( base_url="https://api.ttscacherouter.dev/openai/v1", api_key=os.environ["TTS_CACHE_ROUTER_API_KEY"], ) with client.audio.speech.with_streaming_response.create( model="gpt-4o-mini-tts", voice="ash", input="Your order has shipped. It arrives on Friday.", instructions="Speak in a warm, friendly tone.", ) as response: response.stream_to_file("speech.mp3") ``` `audio.speech.create` works through the router with every model, voice, format and setting, streamed or not. Requests, responses, status codes and error bodies are OpenAI’s own; your body is forwarded as you sent it, with the OpenAI key stored under [Provider keys](https://app.ttscacherouter.dev/app/provider-keys). For a LiveKit agent, see [OpenAI with LiveKit](https://docs.ttscacherouter.dev/livekit/openai.md#provider-plugin). This is OpenAI’s API, with OpenAI’s model names. The router’s own [/v1/audio/speech](https://docs.ttscacherouter.dev/clients/speech.md) takes the same request with `openai/gpt-4o-mini-tts` as the model, and serves every provider. ## Speech - `POST /openai/v1/audio/speech`: the audio in the requested `response_format`, or with `stream_format: "sse"` OpenAI’s `speech.audio.delta` events (`tts-1` and `tts-1-hd` answer that with the audio, as OpenAI does). Any other path under `/openai` answers 404. **Audio** ```shell curl -N https://api.ttscacherouter.dev/openai/v1/audio/speech \ -H "Authorization: Bearer $TTS_CACHE_ROUTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "tts-1", "voice": "alloy", "input": "Your order has shipped.", "response_format": "pcm"}' \ --output speech.pcm ``` **SSE** ```shell curl -N https://api.ttscacherouter.dev/openai/v1/audio/speech \ -H "Authorization: Bearer $TTS_CACHE_ROUTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "gpt-4o-mini-tts", "voice": "ash", "input": "Your order has shipped.", "response_format": "pcm", "stream_format": "sse"}' ``` ## What is cached - Each request as a whole: the same body again replays the same audio, in every format. - With `stream_format: "sse"`, a reply from the cache sends the same `speech.audio.delta` events, then `speech.audio.done` with zero usage (OpenAI spent no tokens on it), then `[DONE]`. - A reply from the cache carries only `Content-Type`: no OpenAI request produced it, so there is no `x-request-id`. - `gpt-4o-mini-tts` is an alias OpenAI can move to a newer snapshot; audio already cached under it keeps the voice it was made with. To pin the voice, name a snapshot such as `gpt-4o-mini-tts-2025-12-15`. ## Errors A wrong or missing router key gets OpenAI’s own refusal, HTTP 401. So does an org with no OpenAI key stored, with a message that links to the dashboard. Errors from OpenAI, such as a voice the model doesn’t have, reach you as OpenAI sent them. ```http HTTP/1.1 401 Unauthorized {"error": {"message": "Incorrect API key provided.", "type": "invalid_request_error", "param": null, "code": "invalid_api_key"}} ``` --- # Deepgram API drop-in > Deepgram's own speak APIs, Aura and Flux, through the cache: keep the Deepgram SDK, change the base URL and key. ## Deepgram's Python SDK Deepgram’s speak APIs, mirrored under `https://api.ttscacherouter.dev/deepgram`: `/v1/speak` (Aura) and `/v2/speak` (Flux), REST and WebSocket. Keep the Deepgram SDK: give it the router as its environment and your router API key as its `api_key`. **speak.py** ```python import asyncio import os from deepgram import AsyncDeepgramClient from deepgram.environment import DeepgramClientEnvironment async def main(): client = AsyncDeepgramClient( api_key=os.environ["TTS_CACHE_ROUTER_API_KEY"], environment=DeepgramClientEnvironment( base="https://api.ttscacherouter.dev/deepgram", production="wss://api.ttscacherouter.dev/deepgram", agent="wss://agent.deepgram.com", agent_rest="https://agent.deepgram.com", ), ) with open("speech.mp3", "wb") as f: async for chunk in client.speak.v1.audio.generate( text="Your order has shipped. It arrives on Friday.", model="aura-2-thalia-en", ): f.write(chunk) asyncio.run(main()) ``` `speak.v1` and `speak.v2`, REST (`audio.generate`) and WebSocket (`connect`), work through the router with every model, voice, encoding and setting. Requests are forwarded as you sent them, with the Deepgram key stored under [Provider keys](https://app.ttscacherouter.dev/app/provider-keys); your key goes in `Authorization: Token`, as Deepgram’s does. For a LiveKit agent, see [Deepgram with LiveKit](https://docs.ttscacherouter.dev/livekit/deepgram.md#provider-plugin). ## REST - `POST /deepgram/v1/speak`: Aura. The audio in the encoding and container the query asks for, with Deepgram’s own `Content-Type`. - `POST /deepgram/v2/speak`: Flux, batch. - `GET /deepgram/v1/models` and `GET /deepgram/v2/models`: Deepgram’s models, Aura’s voices on `v1` and Flux’s on `v2`. Never cached. [GET /v1/voices](https://docs.ttscacherouter.dev/reference/voices.md) lists them in the router’s own shape. Any other path under `/deepgram` answers 404. **Aura** ```shell curl -N "https://api.ttscacherouter.dev/deepgram/v1/speak?model=aura-2-thalia-en&encoding=linear16&container=none&sample_rate=24000" \ -H "Authorization: Token $TTS_CACHE_ROUTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"text": "Your order has shipped."}' \ --output speech.pcm ``` **Flux** ```shell curl -N "https://api.ttscacherouter.dev/deepgram/v2/speak?model=flux-alexis-en" \ -H "Authorization: Token $TTS_CACHE_ROUTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"text": "Your order has shipped."}' \ --output speech.mp3 ``` ## WebSocket - `wss://api.ttscacherouter.dev/deepgram/v1/speak`: Aura’s streaming API. Send `Speak` text and `Flush`; you get the audio as binary messages, `Flushed` after each flush’s audio, `Cleared` after `Clear`. - `wss://api.ttscacherouter.dev/deepgram/v2/speak`: Flux. Each turn (the text up to a `Flush`) gets `SpeechStarted`, its audio, `Flushed` and `SpeechMetadata`; `Interrupt`, `Configure` and `Close` work as at Deepgram. The router splits your text into sentences and serves each sentence from the cache or synthesizes it at Deepgram, so the messages are Deepgram’s with these differences: - Audio for a sentence starts once the sentence is complete. Deepgram’s Aura starts at a comma. - Aura’s opening `Metadata` and Flux’s `Connected` carry the router’s own request and model ids, and `model_version` is `unknown`: no Deepgram request exists yet when you connect. A Flux turn’s `speech_id` is the router’s own too. - A Flux turn’s `SpeechMetadata` covers the whole turn. `billable_character_count` counts only the sentences Deepgram synthesized: sentences served from the cache cost nothing at Deepgram. - `SpeechInterrupted` splits the turn’s text into `text_spoken` and `text_remaining` at sentence boundaries: a sentence whose audio had started playing at `playback_offset` counts as spoken. - `Configure` applies to the sentences after it. - Aura’s limit of 20 flushes a minute doesn’t apply. - A failure at Deepgram (a wrong voice, your Deepgram quota) arrives as an `Error` message once a sentence needs Deepgram, then the router closes the connection (code 1011). A connection idle for 30 minutes is closed. ## What is cached - REST: each request as a whole. The same query and text again replays the same audio. - WebSocket: each sentence, so a sentence you have sent before with the same settings comes from the cache whatever came around it. Aura’s sentences share the cache with the router’s own [/v1/stream](https://docs.ttscacherouter.dev/clients/stream.md) and REST requests with the same settings. - A reply from the cache carries only `Content-Type`: no Deepgram request produced it, so there are no `dg-*` headers. - Aura: the first sentence of a reply after a pause of about five seconds or more takes about 0.7–1.2 s longer to start than on Deepgram’s own WebSocket. Sentences from the cache, and Flux, are unaffected. ## Errors A wrong or missing router key gets Deepgram’s own refusal, HTTP 401, on REST and at the WebSocket handshake. So does an org with no Deepgram key stored, with a message that links to the dashboard. Errors from Deepgram, such as a model your Deepgram project can’t use, reach you as Deepgram sent them. ```http HTTP/1.1 401 Unauthorized {"err_code": "INVALID_AUTH", "err_msg": "Invalid credentials.", "request_id": null} ``` --- # xAI API drop-in > xAI's own text-to-speech API, REST and WebSocket, through the cache: change the base URL and key. ## WebSocket xAI’s text-to-speech API, mirrored under `https://api.ttscacherouter.dev/xai`. Use xAI’s URLs with `https://api.x.ai` replaced by `https://api.ttscacherouter.dev/xai`, and your router API key in `Authorization: Bearer`. **speak.py** ```python import asyncio import base64 import json import os import websockets async def main(): uri = "wss://api.ttscacherouter.dev/xai/v1/tts?voice=ara&language=en&codec=pcm&sample_rate=24000" headers = {"Authorization": f"Bearer {os.environ['TTS_CACHE_ROUTER_API_KEY']}"} async with websockets.connect(uri, additional_headers=headers) as ws: await ws.send(json.dumps({"type": "text.delta", "delta": "Your order has shipped. "})) await ws.send(json.dumps({"type": "text.delta", "delta": "It arrives on Friday."})) await ws.send(json.dumps({"type": "text.done"})) audio = bytearray() async for message in ws: event = json.loads(message) if event["type"] == "audio.delta": audio.extend(base64.b64decode(event["delta"])) elif event["type"] == "audio.done": break elif event["type"] == "error": raise RuntimeError(event["message"]) with open("speech.pcm", "wb") as f: f.write(audio) asyncio.run(main()) ``` `wss://api.ttscacherouter.dev/xai/v1/tts` takes xAI’s query (`voice`, `language`, `codec`, `sample_rate`, `bit_rate`, `speed`, `optimize_streaming_latency`, `text_normalization`) and refuses a bad one at the handshake, as xAI does. Send `text.delta` messages and end each utterance with `text.done`; you get `audio.delta` messages, then `audio.done`. `text.clear` stops the utterance and is answered with `audio.clear`. Requests go to xAI with the xAI key stored under [Provider keys](https://app.ttscacherouter.dev/app/provider-keys). For a LiveKit agent, see [xAI with LiveKit](https://docs.ttscacherouter.dev/livekit/xai.md#provider-plugin). The router splits your text into sentences and serves each sentence from the cache or synthesizes it at xAI, so the messages are xAI’s with these differences: - Audio for a sentence starts once the sentence is complete. - `text.delta` messages are joined exactly as sent, as xAI joins them: put the spaces between words in your text. - `audio.done`’s `trace_id` is the router’s own. - `with_timestamps=true` is refused at the handshake, and `session.update` (pronunciation replacements) is answered with an `error` message: use the REST endpoint for either. `codec=opus` is refused. - With `codec=wav`, each utterance starts with one WAV header, as from xAI. - A failure at xAI (an unknown voice, your xAI quota) arrives as an `error` message once a sentence needs xAI, then the router closes the connection (code 1011). A connection idle for 30 minutes is closed. ## REST - `POST /xai/v1/tts`: the audio in the codec the body asks for (MP3 when it doesn’t), with xAI’s own `Content-Type`; JSON with `with_timestamps`. `replace` works as at xAI. - `GET /xai/v1/tts/voices` and `GET /xai/v1/tts/voices/{voice_id}`: xAI’s voices, not cached. Any other path under `/xai` answers 404. **Speech** ```shell curl "https://api.ttscacherouter.dev/xai/v1/tts" \ -H "Authorization: Bearer $TTS_CACHE_ROUTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"text": "Your order has shipped.", "voice_id": "eve", "language": "en"}' \ --output speech.mp3 ``` **Voices** ```shell curl "https://api.ttscacherouter.dev/xai/v1/tts/voices" \ -H "Authorization: Bearer $TTS_CACHE_ROUTER_API_KEY" ``` ## What is cached - REST: each request as a whole. The same body again replays the same audio. - WebSocket: each sentence, so a sentence you have sent before with the same query comes from the cache whatever came around it. Sentences share the cache with the router’s own [/v1/stream](https://docs.ttscacherouter.dev/clients/stream.md) when the settings are the same. - A reply from the cache carries only `Content-Type`: no xAI request produced it, so there is no `x-trace-id`. ## Errors A wrong router key gets xAI’s own refusal, HTTP 400, and a missing one HTTP 401, on REST and at the WebSocket handshake. An org with no xAI key stored gets the same 400, with a message that links to the dashboard. Errors from xAI, such as an unknown voice, reach you as xAI sent them. ```http HTTP/1.1 400 Bad Request {"code": "Client specified an invalid argument", "error": "Incorrect API key provided."} ``` --- # Soniox API drop-in > Soniox's own text-to-speech API, REST and real-time WebSocket, through the cache: keep Soniox's SDK, change the base URLs and key. ## Soniox's Python SDK Soniox’s text-to-speech API, mirrored under `https://api.ttscacherouter.dev/soniox`. Point Soniox’s SDK at the router and use your router API key as the API key. **Real-time** ```python import os from soniox import SonioxClient from soniox.types import RealtimeTTSConfig client = SonioxClient( api_key=os.environ["TTS_CACHE_ROUTER_API_KEY"], tts_api_base_url="https://api.ttscacherouter.dev/soniox", tts_websocket_base_url="wss://api.ttscacherouter.dev/soniox/tts-websocket", ) config = RealtimeTTSConfig( stream_id="reply-1", model="tts-rt-v2", language="en", voice="Adrian", audio_format="pcm_s16le", sample_rate=24000, ) with client.realtime.tts.connect(config=config) as session: session.send_text_chunk("Your order has shipped. ") session.send_text_chunk("It arrives on Friday.", text_end=True) audio = b"".join(session.receive_audio_chunks()) with open("speech.pcm", "wb") as f: f.write(audio) ``` **REST** ```python # client: as in the real-time tab from soniox.types import CreateTtsConfig audio = client.tts.generate( text="Your order has shipped.", model="tts-rt-v2", language="en", voice="Adrian", config=CreateTtsConfig(audio_format="wav"), ) with open("speech.wav", "wb") as f: f.write(audio) ``` Requests go to Soniox with the Soniox key stored under [Provider keys](https://app.ttscacherouter.dev/app/provider-keys). The router calls Soniox’s US endpoint, so store a key from a Soniox project in the US region. For a LiveKit agent, see [Soniox with LiveKit](https://docs.ttscacherouter.dev/livekit/soniox.md#provider-plugin). ## WebSocket `wss://api.ttscacherouter.dev/soniox/tts-websocket` speaks Soniox’s real-time protocol: up to five streams on a connection, each a config message with its `stream_id`, text messages and `text_end`, answered with `audio` messages (the last with `audio_end`) and `terminated`. Send your router API key in `Authorization: Bearer`, or, as LiveKit’s Soniox plugin 1.8.5 does, as the `api_key` of each config. `cancel` ends a stream with `terminated`, and `{"keep_alive": true}` keeps an idle connection open: one with no stream and no message for 40 seconds is closed (code 1001), and a stream with no text for 5 seconds and no `text_end` ends with `request_timeout`, as at Soniox. The router splits each stream’s text into sentences and serves each sentence from the cache or synthesizes it at Soniox, so the messages are Soniox’s with these differences: - Audio for a sentence starts once the sentence is complete, and the last sentence is spoken without waiting for `text_end`. Soniox holds a stream’s text until more text or `text_end` arrives. - Each sentence is a take of its own, so the pause between two sentences is shorter (about 0.2 s) than inside one Soniox stream (about 0.5 s). - A config’s values (voice, language, rate) are checked by Soniox when its first sentence is synthesized, so an error for a bad one arrives then, not right after the config. - A stream with no speakable text gets `audio_end` on an empty `audio`. - `return_timestamps` and the `aac`, `opus` and `flac` formats are refused with an error for that stream: use the REST endpoint for them. With `wav` and `mp3`, a stream starts with one WAV header or ID3 tag, as from Soniox. - Each sentence is a stream of its own on the router’s connection to Soniox, one at a time per stream of yours: the same Soniox concurrency as your stream would use directly. ## REST - `POST /soniox/tts`: the audio in the format the body asks for, with Soniox’s own `Content-Type`. - `GET /soniox/v1/voices` (your cloned voices) and `GET /soniox/v1/tts-models` (the models and their voices): Soniox’s account API at `api.soniox.com`, never cached. With Soniox’s SDK, set its `api_base_url`to `https://api.ttscacherouter.dev/soniox/v1`. [GET /v1/voices](https://docs.ttscacherouter.dev/reference/voices.md) lists both in the router’s own shape. Any other path under `/soniox` answers 404. ```shell curl "https://api.ttscacherouter.dev/soniox/tts" \ -H "Authorization: Bearer $TTS_CACHE_ROUTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "tts-rt-v2", "language": "en", "voice": "Adrian", "audio_format": "wav", "text": "Your order has shipped."}' \ --output speech.wav ``` ## What is cached - REST: each request as a whole. The same body again replays the same audio. - WebSocket: each sentence, so a sentence you have sent before with the same config comes from the cache whatever came around it. Sentences share the cache with the router’s own [/v1/stream](https://docs.ttscacherouter.dev/clients/stream.md) when the settings are the same. ## Errors A missing or wrong router key gets Soniox’s own refusal: HTTP 401 `unauthenticated` over REST; on the WebSocket, that error message right after the connection opens, then a close (with the key in a config, an error for that stream). An org with no Soniox key stored gets the same, with a message that links to the dashboard. Errors from Soniox, such as an unknown voice or your Soniox limits, reach you as Soniox sent them; on the WebSocket they end only their stream. ```json {"stream_id": "", "error_code": 401, "error_type": "unauthenticated", "error_message": "Incorrect API key provided.", "more_info": null, "request_id": null} ``` --- # Smallest AI API drop-in > Smallest AI's own text-to-speech API, REST, SSE and WebSocket, through the cache: keep Smallest AI's SDK, change the base URLs and key. ## Smallest AI's Python SDK Smallest AI’s text-to-speech API, mirrored under `https://api.ttscacherouter.dev/smallestai`. Point Smallest AI’s SDK at the router and use your router API key as the API key. **WebSocket** ```python import os from smallestai.waves import TTSConfig, WavesStreamingTTS tts = WavesStreamingTTS( TTSConfig( voice_id="meher", api_key=os.environ["TTS_CACHE_ROUTER_API_KEY"], model="lightning_v3.1_pro", language="en", sample_rate=24000, ) ) tts.ws_url = "wss://api.ttscacherouter.dev/smallestai/waves/v1/tts/live" audio = b"".join(tts.synthesize("Your order has shipped. It arrives on Friday.")) with open("speech.pcm", "wb") as f: f.write(audio) ``` **REST** ```python import os from smallestai import SmallestAI, SmallestAIEnvironment client = SmallestAI( api_key=os.environ["TTS_CACHE_ROUTER_API_KEY"], environment=SmallestAIEnvironment( atoms=SmallestAIEnvironment.PRODUCTION.atoms, waves="https://api.ttscacherouter.dev/smallestai", waves_ws="wss://api.ttscacherouter.dev/smallestai", payment=SmallestAIEnvironment.PRODUCTION.payment, ), ) audio = b"".join( client.waves.synthesize_tts( text="Your order has shipped.", voice_id="meher", model="lightning_v3.1_pro", language="en", sample_rate=24000, output_format="wav", ) ) with open("speech.wav", "wb") as f: f.write(audio) ``` The SDK’s `environment` sets the text-to-speech base URLs (`waves` and `waves_ws`); `WavesStreamingTTS` takes its URL in `ws_url`. Requests go to Smallest AI with the Smallest AI key stored under [Provider keys](https://app.ttscacherouter.dev/app/provider-keys). For a LiveKit agent, see [Smallest AI with LiveKit](https://docs.ttscacherouter.dev/livekit/smallestai.md#provider-plugin). ## WebSocket `wss://api.ttscacherouter.dev/smallestai/waves/v1/tts/live` speaks Smallest AI’s WebSocket protocol: messages with text and the synthesis settings, answered with `chunk` messages of base64 PCM and `complete`. Send your router API key in `Authorization: Bearer`. Both of Smallest AI’s ways of streaming text work: `continue` and `flush` (one `complete` after a request’s audio), and continuations with a `context_id`, ended by `continue: false` or `context_close: true` and stopped by `cancel_request: true`. `{"type": "ping"}` gets `{"type": "pong"}`, and a connection with no message for 60 seconds is closed (the `timeout` query parameter sets up to 180), as at Smallest AI. The router splits the text into sentences and serves each sentence from the cache or synthesizes it at Smallest AI, so the messages are Smallest AI’s with these differences: - Audio for a sentence starts once the sentence is complete. - Each sentence is a take of its own: it doesn’t continue from the audio of the sentence before, as text in one Smallest AI continuation does. - With continuations, a `complete` follows each sentence. - `session_id` and `request_id` are the router’s own; yours come back as `external_session_id` and `external_request_id`. - A sentence that fails gets an `error` message for its request, and the rest of that request is dropped. - `word_timestamps` is accepted, and no word timings are sent. `max_buffer_flush_ms` and `max_buffer_delay_ms` change nothing. As at Smallest AI, the audio is always 16-bit PCM at `sample_rate`, and text with no letter or digit (`...` alone) gets no audio. ## REST and SSE - `POST /smallestai/waves/v1/tts`: the audio in the body’s `output_format`, with Smallest AI’s own `Content-Type`. - `POST /smallestai/waves/v1/tts/live`: Smallest AI’s server-sent events, base64 audio then `done`. - `GET /smallestai/waves/v1/lightning-v3.1/get_voices` (and `lightning-v3.1-pro`) and `GET /smallestai/waves/v1/voice-cloning` (your cloned voices): Smallest AI’s voice lists, never cached. With the SDK, `client.waves.get_voices(...)` and `client.waves.list_voice_clones()`. [GET /v1/voices](https://docs.ttscacherouter.dev/reference/voices.md) lists both in the router’s own shape. Any other path under `/smallestai` answers 404. **REST** ```shell curl "https://api.ttscacherouter.dev/smallestai/waves/v1/tts" \ -H "Authorization: Bearer $TTS_CACHE_ROUTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"text": "Your order has shipped.", "voice_id": "meher", "model": "lightning_v3.1_pro", "language": "en", "sample_rate": 24000, "output_format": "wav"}' \ --output speech.wav ``` **SSE** ```shell curl -N "https://api.ttscacherouter.dev/smallestai/waves/v1/tts/live" \ -H "Authorization: Bearer $TTS_CACHE_ROUTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"text": "Your order has shipped.", "voice_id": "meher", "model": "lightning_v3.1_pro", "language": "en", "sample_rate": 24000, "output_format": "pcm"}' ``` ## What is cached - REST and SSE: each request as a whole. The same body again replays the same audio. - WebSocket: each sentence, so a sentence you have sent before with the same settings comes from the cache whatever came around it. Sentences share the cache with the router’s own [/v1/stream](https://docs.ttscacherouter.dev/clients/stream.md) when the settings are the same. - A request with `pronunciation_dicts` is cached by the dictionaries’ ids: if you edit a dictionary in place, entries cached before keep the old pronunciation until they go unused for your cache retention period. ## Errors A missing or wrong router key gets Smallest AI’s own refusal: HTTP 401 `unauthorized`, at the WebSocket handshake too. An org with no Smallest AI key stored gets the same, with a message that links to the dashboard. Errors from Smallest AI, such as a voice of another model or your Smallest AI concurrency limit, reach you as Smallest AI sent them; on the WebSocket, as an `error` message carrying Smallest AI’s message, and the connection stays open. ```http HTTP/1.1 401 Unauthorized {"error": "unauthorized"} ``` --- # Cartesia API drop-in > Cartesia's own text-to-speech API, bytes, SSE and WebSocket, through the cache: keep Cartesia's SDK, change the base URL and key. ## Cartesia's Python SDK Cartesia’s text-to-speech API, mirrored under `https://api.ttscacherouter.dev/cartesia`. Point Cartesia’s SDK at the router and use your router API key as the API key. **Bytes** ```python import os from cartesia import Cartesia client = Cartesia( api_key=os.environ["TTS_CACHE_ROUTER_API_KEY"], base_url="https://api.ttscacherouter.dev/cartesia", ) response = client.tts.generate( model_id="sonic-3", transcript="Your order has shipped. It arrives on Friday.", voice={"mode": "id", "id": "f786b574-daa5-4673-aa0c-cbe3e8534c02"}, language="en", output_format={"container": "wav", "encoding": "pcm_s16le", "sample_rate": 24000}, ) response.write_to_file("speech.wav") ``` **WebSocket** ```python import asyncio import os from cartesia import AsyncCartesia async def main() -> None: client = AsyncCartesia( api_key=os.environ["TTS_CACHE_ROUTER_API_KEY"], websocket_base_url="wss://api.ttscacherouter.dev/cartesia", ) pcm = bytearray() async with client.tts.websocket_connect() as connection: ctx = connection.context( model_id="sonic-3", voice={"mode": "id", "id": "f786b574-daa5-4673-aa0c-cbe3e8534c02"}, language="en", output_format={"container": "raw", "encoding": "pcm_s16le", "sample_rate": 24000}, ) await ctx.push("Your order has shipped. ") await ctx.push("It arrives on Friday.") await ctx.no_more_inputs() async for response in ctx.receive(): if response.type == "chunk" and response.audio: pcm += response.audio elif response.type == "done": break with open("speech.pcm", "wb") as f: f.write(pcm) asyncio.run(main()) ``` The SDK takes the router in `base_url` (bytes, SSE and voices) and `websocket_base_url`. `generate`, `generate_sse` and `websocket_connect` work through the router. Requests, responses, messages and errors are Cartesia’s own, sent to Cartesia with the Cartesia key stored under [Provider keys](https://app.ttscacherouter.dev/app/provider-keys). For a LiveKit agent, see [Cartesia with LiveKit](https://docs.ttscacherouter.dev/livekit/cartesia.md#provider-plugin). ## WebSocket `wss://api.ttscacherouter.dev/cartesia/tts/websocket` speaks Cartesia’s WebSocket protocol: contexts by `context_id`, answered with `chunk`, `timestamps`, `flush_done` and `done` messages. Send your router API key as Cartesia’s clients send theirs, and the API version in `Cartesia-Version` or the `cartesia_version` query parameter, as at Cartesia. Contexts run side by side on one connection, each with its own settings, and a connection with no message for 300 seconds is closed, as at Cartesia. The router splits each context’s text into sentences and serves each sentence from the cache or synthesizes it at Cartesia, so the messages are Cartesia’s with these differences: - Each sentence is a take of its own: it doesn’t continue from the audio of the sentence before, as text in one Cartesia context does, and the pauses between sentences are shorter. - With `max_buffer_delay_ms: 0` (as LiveKit’s plugin sends it), each message’s text is synthesized as it comes. With buffering, a sentence is synthesized once it ends, and a trailing fragment after `max_buffer_delay_ms` without more text. - Word timestamps run on across a context, as Cartesia’s do. - `add_phoneme_timestamps` is refused with an error for that context. - `cancel` drops the context’s sentences not yet sent to Cartesia; one being synthesized finishes and is cached. You get `done` at once. - Cartesia checks a context’s settings when its first sentence is synthesized, so an error for a bad setting comes then. A context served entirely from the cache isn’t checked. ## Bytes, SSE and voices - `POST /cartesia/tts/bytes`: the audio in the body’s `output_format`, with Cartesia’s own `Content-Type`. - `POST /cartesia/tts/sse`: Cartesia’s server-sent events, `chunk` and `timestamps` then `done`. - `GET /cartesia/voices` and `GET /cartesia/voices/{id}`: Cartesia’s voice library and your own voices, never cached. [GET /v1/voices](https://docs.ttscacherouter.dev/reference/voices.md) lists them in the router’s own shape. Any other path under `/cartesia` answers Cartesia’s `404 page not found`. **Bytes** ```shell curl "https://api.ttscacherouter.dev/cartesia/tts/bytes" \ -H "X-API-Key: $TTS_CACHE_ROUTER_API_KEY" \ -H "Cartesia-Version: 2026-08-14" \ -H "Content-Type: application/json" \ -d '{"model_id": "sonic-3", "transcript": "Your order has shipped.", "voice": {"mode": "id", "id": "f786b574-daa5-4673-aa0c-cbe3e8534c02"}, "language": "en", "output_format": {"container": "mp3", "sample_rate": 44100, "bit_rate": 128000}}' \ --output speech.mp3 ``` **SSE** ```shell curl -N "https://api.ttscacherouter.dev/cartesia/tts/sse" \ -H "X-API-Key: $TTS_CACHE_ROUTER_API_KEY" \ -H "Cartesia-Version: 2026-08-14" \ -H "Content-Type: application/json" \ -d '{"model_id": "sonic-3", "transcript": "Your order has shipped.", "voice": {"mode": "id", "id": "f786b574-daa5-4673-aa0c-cbe3e8534c02"}, "language": "en", "output_format": {"container": "raw", "encoding": "pcm_s16le", "sample_rate": 24000}, "add_timestamps": true}' ``` **Voices** ```shell curl "https://api.ttscacherouter.dev/cartesia/voices?limit=20" \ -H "X-API-Key: $TTS_CACHE_ROUTER_API_KEY" \ -H "Cartesia-Version: 2026-08-14" ``` ## What is cached - Bytes and SSE: each request as a whole. The same body again replays the same audio (on SSE, whatever its `context_id`). - WebSocket: each sentence, so a sentence you have sent before with the same settings comes from the cache whatever came around it. Sentences share the cache with the router’s own [/v1/stream](https://docs.ttscacherouter.dev/clients/stream.md) when the settings are the same. - A request with `pronunciation_dict_id` is cached by the dictionary’s id: if you edit the dictionary, entries cached before keep the old pronunciation until they go unused for your cache retention period. ## Errors Errors come in the shape of your API version: `Title: Message` text before `2026-03-01`, JSON with `error_code`, `title`, `message` and `request_id` from it on. A missing or wrong router key gets Cartesia’s own refusal, HTTP 401, at the WebSocket handshake too. An org with no Cartesia key stored gets the same, with a message that links to the dashboard. Errors from Cartesia, such as an unknown voice, reach you as Cartesia sent them; on the WebSocket, as an error message for that context, and the connection stays open. ```http HTTP/1.1 401 Unauthorized {"error_code": null, "message": "Invalid API key.", "request_id": null, "title": "Unauthorized"} ``` --- # Gemini API drop-in > Google's Gemini API for speech, streamed and not, through the cache: keep Google's GenAI SDK, change the base URL and key. ## Google's GenAI SDK The Gemini API’s speech generation, mirrored under `https://api.ttscacherouter.dev/gemini`. Keep Google’s GenAI SDK (`google-genai`): pass the router as its `base_url` and your router API key as its `api_key`. **Streaming** ```python import os import wave from google import genai from google.genai import types client = genai.Client( api_key=os.environ["TTS_CACHE_ROUTER_API_KEY"], http_options=types.HttpOptions(base_url="https://api.ttscacherouter.dev/gemini"), ) config = types.GenerateContentConfig( response_modalities=["AUDIO"], speech_config=types.SpeechConfig( voice_config=types.VoiceConfig(prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name="Kore")), ), ) pcm = bytearray() for chunk in client.models.generate_content_stream( model="gemini-3.8-flash-tts", contents="Your order has shipped. It arrives on Friday.", config=config, ): content = chunk.candidates[0].content # None when Gemini stops a take without audio for part in content.parts if content else []: pcm += part.inline_data.data # 16-bit PCM, 24000 Hz, mono with wave.open("speech.wav", "wb") as f: f.setnchannels(1) f.setsampwidth(2) f.setframerate(24000) f.writeframes(pcm) ``` **generateContent** ```python import os from google import genai from google.genai import types client = genai.Client( api_key=os.environ["TTS_CACHE_ROUTER_API_KEY"], http_options=types.HttpOptions(base_url="https://api.ttscacherouter.dev/gemini"), ) response = client.models.generate_content( model="gemini-3.8-flash-tts", contents="Your order has shipped. It arrives on Friday.", config=types.GenerateContentConfig( response_modalities=["AUDIO"], speech_config=types.SpeechConfig( voice_config=types.VoiceConfig(prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name="Kore")), ), ), ) audio = response.candidates[0].content.parts[0].inline_data with open("speech.wav", "wb") as f: # the 3.8 models answer a WAV file here f.write(audio.data) ``` `generate_content_stream` and `generate_content` work through the router with every speech model, voice and setting, sync or async (`client.aio`). Instead of `http_options`, you can set `GOOGLE_GEMINI_BASE_URL=https://api.ttscacherouter.dev/gemini` in the environment, which every GenAI client in the process then uses. Requests, responses and errors are Google’s own, sent to the Gemini API with the Gemini key stored under [Provider keys](https://app.ttscacherouter.dev/app/provider-keys). The Gemini API only: Vertex AI doesn’t go through the router. LiveKit’s Gemini plugin (`google.beta.GeminiTTS`) uses the same SDK, so `GOOGLE_GEMINI_BASE_URL` points it at the router too: see [Gemini with LiveKit](https://docs.ttscacherouter.dev/livekit/gemini.md#provider-plugin). This is Google’s API, with Google’s request shape. The router’s own [/v1/audio/speech](https://docs.ttscacherouter.dev/clients/speech.md) takes `gemini/gemini-3.8-flash-tts` as the model, with `voice` and `instructions` as fields, and serves every provider. ## REST Send your router API key where Google takes its key: the `x-goog-api-key` header, or the `key` query parameter. - `POST /gemini/v1beta/models/{model}:streamGenerateContent?alt=sse`: Gemini’s server-sent events, each with a part of the audio. - `POST /gemini/v1beta/models/{model}:generateContent`: one JSON response with the whole audio. - `GET /gemini/v1beta/voices`: Gemini’s voices, never cached. [GET /v1/voices](https://docs.ttscacherouter.dev/reference/voices.md) lists them in the router’s own shape. - `GET /gemini/v1beta/models` and `GET /gemini/v1beta/models/{model}`: Google’s model list, never cached. Any other path under `/gemini` answers Google’s empty 404. **Streaming** ```shell curl -N "https://api.ttscacherouter.dev/gemini/v1beta/models/gemini-3.8-flash-tts:streamGenerateContent?alt=sse" \ -H "x-goog-api-key: $TTS_CACHE_ROUTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"contents": [{"parts": [{"text": "Your order has shipped."}]}], "generationConfig": {"responseModalities": ["AUDIO"], "speechConfig": {"voiceConfig": {"prebuiltVoiceConfig": {"voiceName": "Kore"}}}}}' ``` **generateContent** ```shell curl "https://api.ttscacherouter.dev/gemini/v1beta/models/gemini-3.8-flash-tts:generateContent" \ -H "x-goog-api-key: $TTS_CACHE_ROUTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"contents": [{"parts": [{"text": "Your order has shipped."}]}], "generationConfig": {"responseModalities": ["AUDIO"], "speechConfig": {"voiceConfig": {"prebuiltVoiceConfig": {"voiceName": "Kore"}}}}}' ``` **Voices** ```shell curl "https://api.ttscacherouter.dev/gemini/v1beta/voices?pageSize=100" \ -H "x-goog-api-key: $TTS_CACHE_ROUTER_API_KEY" ``` ## What is cached - Each request as a whole: the same body again, to the same model and endpoint, replays the same audio. A request is cached when its `contents` is one text, as the SDK sends a string; anything else, and `streamGenerateContent` without `alt=sse`, goes to Google uncached. - A reply from the cache carries the audio only: the same audio, in events that each carry some of it, the last with `finishReason: "STOP"`. Its usage counts zero tokens (Google spent none on it), and it has no `responseId`. - With the 3.8 models, a request as LiveKit’s Gemini plugin sends it shares cached sentences with the router’s own [/v1/stream](https://docs.ttscacherouter.dev/clients/stream.md) and our plugin, when the settings are the same. ## Errors Errors come in Google’s shape. A wrong router key gets Google’s own refusal, HTTP 400 `API_KEY_INVALID`, and no key at all HTTP 403 `PERMISSION_DENIED`. An org with no Gemini key stored gets the same 400, with a message that links to the dashboard, and an org out of credit HTTP 402. Errors from Google, such as a voice the model doesn’t have, reach you as Google sent them. ```http HTTP/1.1 400 Bad Request {"error": {"code": 400, "message": "API key not valid. Please pass a valid API key.", "status": "INVALID_ARGUMENT", "details": [{"@type": "type.googleapis.com/google.rpc.ErrorInfo", "reason": "API_KEY_INVALID", …}, …]}} ``` --- # Authentication > API keys, where each surface takes them, and what a missing or wrong key gets back. ## API keys Every request to the router carries one of your org’s API keys: `tcr_` followed by 43 characters. - Sign-up creates one; create more in the dashboard under [API keys](https://app.ttscacherouter.dev/app/api-keys). A writer or admin of your org can reveal and copy a key again there at any time. - Every key in an org shares its cache, provider keys and retention. Use one per agent or environment, so you can revoke one alone. - A new key works at once. A deleted key is refused within a few seconds, including on connections already open. - Keep keys server-side, in your agent’s secrets. Never ship one in a browser or a mobile app. To check a key, list your stored provider keys with it: ```shell curl https://api.ttscacherouter.dev/v1/provider-keys \ -H "Authorization: Bearer $TTS_CACHE_ROUTER_API_KEY" ``` ## Where to send it The router's own endpoints take it as a bearer token. The drop-ins take it wherever the provider's API takes its key, so the provider's clients work unchanged. | Endpoint | Header | | --- | --- | | /v1/stream (WebSocket) | `Authorization: Bearer tcr_…` | | /v1/audio/speech | `Authorization: Bearer tcr_…` | | /v1/provider-keys | `Authorization: Bearer tcr_…` | | /sarvam/… | `api-subscription-key: tcr_…` | | /raya/… | `X-API-Key: tcr_…` | | /elevenlabs/… | `xi-api-key: tcr_…` | | /openai/… | `Authorization: Bearer tcr_…` | | /deepgram/… | `Authorization: Token tcr_…` | | /xai/… | `Authorization: Bearer tcr_…` | | /soniox/… | `Authorization: Bearer tcr_…` | | /smallestai/… | `Authorization: Bearer tcr_…` | | /cartesia/… | `X-API-Key: tcr_…` or `Authorization: Bearer tcr_…` | | /gemini/… | `x-goog-api-key: tcr_…` or the `?key=tcr_…` query parameter | `GET /v1/models`, `GET /openapi.json` and `GET /healthz` need no key. In the LiveKit plugin, the key is `api_key=` or `TTS_CACHE_ROUTER_API_KEY`; in a provider’s own plugin or SDK, it goes where the provider key went. ## Provider keys Your Sarvam, Raya, ElevenLabs, OpenAI, Deepgram, xAI, Soniox, Smallest AI, Cartesia and Gemini keys stay with your org, not in your requests. Store one key per provider under [Provider keys](https://app.ttscacherouter.dev/app/provider-keys), or with `PUT /v1/provider-keys/{provider}` (see the [REST API](https://docs.ttscacherouter.dev/reference/api.md#provider-keys)). The router uses it to call that provider for your org’s requests only. It’s stored encrypted and never returned, only its last four characters. Saving doesn’t check it with the provider: a wrong key surfaces as the provider’s refusal on your first request. ## When a key is refused | Where | A missing, wrong or deleted API key gets | | --- | --- | | /v1/stream | HTTP 403 at the handshake. A key deleted while a connection is open: an unauthorized error at its next start. | | /v1/audio/speech | HTTP 401, `unauthorized`. | | /v1/provider-keys | HTTP 401, `unauthenticated`. | | Sarvam drop-in | Sarvam's own refusal: HTTP 403 invalid_api_key_error, at the WebSocket handshake too. | | Raya drop-in | Raya's own refusal: HTTP 401 Unauthorized. | | ElevenLabs drop-in | ElevenLabs' own refusal: HTTP 400 invalid_api_key (401 with no key at all); on a WebSocket, that error message and close 1008. | | OpenAI drop-in | OpenAI's own refusal: HTTP 401, code invalid_api_key (null with no key at all). | | Deepgram drop-in | Deepgram's own refusal: HTTP 401 INVALID_AUTH, at the WebSocket handshake too. | | xAI drop-in | xAI's own refusal: HTTP 400 (401 with no key at all), at the WebSocket handshake too. | | Soniox drop-in | Soniox's own refusal: HTTP 401 unauthenticated; on the WebSocket, that error message right after the connection opens, then a close. | | Smallest AI drop-in | Smallest AI's own refusal: HTTP 401 unauthorized, at the WebSocket handshake too. | | Cartesia drop-in | Cartesia's own refusal: HTTP 401 Unauthorized, in the shape of your Cartesia-Version, at the WebSocket handshake too. | | Gemini drop-in | Google's own refusal: HTTP 400 API_KEY_INVALID (403 PERMISSION_DENIED with no key at all). | An org with no key stored for the provider gets `provider_key_missing`, or on the drop-ins the provider’s own refusal with a message that links to the dashboard. See [Errors](https://docs.ttscacherouter.dev/reference/errors.md). --- # Models, voices and formats > Every provider, model, voice, language, sample rate, format and option the router accepts. Each setting is a field of `start` on the WebSocket and a top-level field of `/v1/audio/speech`, with the LiveKit plugin’s name and default beside it. The router checks models, formats, sample rates and options against these lists; voices and languages go to the provider as given, so a wrong one gets the provider’s own error. The drop-ins take whatever the provider’s own API takes. The same list as JSON, with no authentication: ```shell curl https://api.ttscacherouter.dev/v1/models ``` ## Sarvam [Sarvam API reference](https://docs.sarvam.ai/api-reference/text-to-speech/stream) Protocol `"provider": "sarvam"` LiveKit plugin `tcr.TTS(model="sarvam/…")` - Models: bulbul:v3, bulbul:v4-flash - Languages: 11 - Speed: 0.5 – 2.0 - Sample rates: 8, 16, 22.05, 24, 32, 44.1, 48 kHz - Formats: pcm, wav, mp3, aac, mulaw, alaw ### Models and voices `bulbul:v3` voices (37): `shubh`, `aditya`, `ritu`, `priya`, `neha`, `rahul`, `pooja`, `rohan`, `simran`, `kavya`, `amit`, `dev`, `ishita`, `shreya`, `ratan`, `varun`, `manan`, `sumit`, `roopa`, `kabir`, `aayan`, `ashutosh`, `advait`, `anand`, `tanya`, `tarun`, `sunny`, `mani`, `gokul`, `vijay`, `shruti`, `suhani`, `mohit`, `kavitha`, `rehan`, `soham`, `rupali` `bulbul:v4-flash` voices (222): `aayan_hi_conversational`, `amit_hi_conversational`, `ashutosh_hi_conversational`, `kabir_hi_conversational`, `kavya_hi_conversational`, `manan_hi_conversational`, `rahul_hi_conversational`, `sumit_hi_conversational`, `arnab_bn_conversation`, `roopa_bn_conversational`, `aditi_en_stories`, `aparna_en_companion`, `aparna_en_edtech`, `ashwin_en_sports`, `ashwin_en_sports_energetic`, `chandrika_en_stories`, `dev_en_recovery`, `dev_en_conversational`, `deven_en_conversation`, `ishita_en_customer`, `ishita_en_medical`, `ishita_en_numbers`, `ishita_en_social`, `ishita_en_stories`, `kalpit_en_edtech`, `nachiket_en_ads`, `neha_en_customer`, `neha_en_latenight`, `nupur_en_kids`, `ojas_en_social`, `ritu_en_edtech`, `ritu_en_latenight`, `ritu_en_medical`, `ritu_en_reels`, `rohan_en_recovery`, `roopa_en_conversational`, `rustom_en_suspense`, `sanchita_en_companion`, `sanchita_en_insurance`, `sanchita_en_recovery`, `sanchita_en_market`, `sanchita_en_social`, `shabana_en_edtech`, `shalini_en_companion`, `shalini_en_customer`, `shubh_en_narration`, `shubh_en_numbers`, `shubh_en_ads`, `shubh_en_recovery`, `shubh_en_audiobook`, `shubh_en_narration_gentle`, `shubh_en_sports`, `simran_en_narration`, `simran_en_automobile`, `simran_en_conversation`, `simran_en_customer`, `simran_en_edtech`, `simran_en_edtech_bot`, `simran_en_sales`, `simran_en_recovery`, `simran_en_ads`, `simran_en_therapist`, `sunny_en_social`, `varun_en_ads`, `varun_en_suspense`, `zarina_en_conversation`, `ishita_enhi_companion`, `ishita_enhi_customer`, `ishita_enhi_customer_expressive`, `sanchita_enhi_companion`, `shalini_enhi_companion`, `shalini_enhi_customer`, `shubh_enhi_companion`, `shubh_enhi_ads`, `shubh_enhi_banking`, `simran_enhi_companion`, `simran_enhi_customer`, `simran_enhi_banking_expressive`, `sunny_enhi_customer`, `bhavik_gu_conversation`, `pooja_gu_conversational`, `pooja_gu_customer`, `aditya_hi_conversational`, `aditya_hi_sales`, `anand_hi_documentary`, `anand_hi_news`, `aparna_hi_customer`, `aparna_hi_kyc`, `ashok_hi_character`, `ashok_hi_news`, `chhavi_hi_kids`, `ishita_hi_ads`, `ishita_hi_edtech`, `ishita_hi_banking`, `ishita_hi_ads_informal`, `ishita_hi_devotional`, `ishita_hi_numbers`, `ishita_hi_social`, `kunal_hi_kids`, `mahesh_hi_documentary`, `mani_hi_devotional`, `mani_hi_conversational`, `mohit_hi_conversational`, `nachiket_hi_devotional`, `priya_hi_recovery`, `ratan_hi_latenight`, `ratan_hi_customer_expressive`, `ratan_hi_documentary`, `ratan_hi_devotional`, `ratan_hi_recovery`, `ratan_hi_social`, `ratan_hi_sports`, `ratan_hi_latenight_warm`, `rehan_hi_social`, `ritu_hi_customer_utility`, `ritu_hi_kids`, `ritu_hi_conversation`, `ritu_hi_customer`, `ritu_hi_edtech`, `ritu_hi_ads_formal`, `ritu_hi_banking`, `ritu_hi_ads_informal`, `ritu_hi_insurance`, `ritu_hi_edtech_bot`, `ritu_hi_medical`, `ritu_hi_sales`, `ritu_hi_reels`, `ritu_hi_social`, `ritu_hi_customer_warm`, `ritu_hi_social_lively`, `roopa_hi_companion`, `roopa_hi_narration`, `roopa_hi_recovery`, `roopa_hi_market`, `roopa_hi_conversational`, `sanchita_hi_assistant`, `sanchita_hi_edtech`, `sanchita_hi_banking`, `sanchita_hi_feedback`, `sanchita_hi_ads_formal`, `sanchita_hi_ads_informal`, `sanchita_hi_interview`, `sanchita_hi_romantic`, `sanchita_hi_market`, `sanchita_hi_social`, `sanchita_hi_kyc`, `sarika_hi_conversation`, `shalini_hi_companion`, `shalini_hi_social`, `shreya_hi_conversational`, `shreya_hi_news`, `shruti_hi_edtech`, `shubh_hi_customer`, `shubh_hi_ecomm`, `shubh_hi_stories_mixed`, `shubh_hi_devotional`, `shubh_hi_ads`, `shubh_hi_recovery`, `shubh_hi_stories_dramatic`, `simran_hi_assistant`, `simran_hi_narration`, `simran_hi_automobile`, `simran_hi_conversation`, `simran_hi_news_breaking`, `simran_hi_social_energetic`, `simran_hi_social_excited`, `simran_hi_latenight`, `simran_hi_news`, `simran_hi_recovery`, `simran_hi_sales`, `suchitra_hi_ecomm`, `suhani_hi_social`, `sunny_hi_ads`, `sunny_hi_reels`, `tarun_hi_conversational`, `tarun_hi_sales`, `chaitra_hi_customer`, `shilpa_hi_narration`, `tanya_hi_narration`, `chaitra_kn_conversation`, `chaitra_kn_narration`, `chetan_kn_conversation`, `suchitra_kn_narration`, `ishita_mr_conversational`, `mrunal_mr_narration`, `neha_mr_narration`, `nilesh_mr_conversation`, `ritu_mr_insurance`, `ritu_mr_narration`, `rupali_mr_stories`, `soham_mr_narration`, `anand_pa_conversation`, `anand_pa_customer`, `harpreet_pa_narration`, `jaspal_pa_banking`, `gokul_ta_narration`, `aravind_ta_ads`, `aravind_ta_suspense`, `vijay_ta_narration`, `kavitha_te_conversation`, `kavitha_te_narration`, `pooja_te_conversation`, `tarun_te_narration`, `amelia_en_conversational`, `sophia_en_conversational`, `bimal_bn_suspense`, `girish_en_documentary`, `girish_en_devotional`, `payal_en_edtech`, `sarang_en_narration`, `aarti_hi_customer`, `advait_hi_character`, `aryaman_hi_ads`, `chirag_hi_social`, `girish_hi_devotional`, `mukul_hi_ads`, `mukul_hi_suspense`, `suman_hi_companion`, `vaibhav_hi_social`, `vandana_hi_ecomm`, `vipul_hi_social`, `mukul_mr_stories` ### Languages - Bangla `bn-IN` - English `en-IN` - Gujarati `gu-IN` - Hindi `hi-IN` - Kannada `kn-IN` - Malayalam `ml-IN` - Marathi `mr-IN` - Odia `od-IN` - Punjabi `pa-IN` - Tamil `ta-IN` - Telugu `te-IN` ### Settings Fields of the [start message](https://docs.ttscacherouter.dev/clients/stream.md#start) (top-level fields on [/v1/audio/speech](https://docs.ttscacherouter.dev/clients/speech.md)). The LiveKit plugin’s `TTS` and the Pipecat service’s `Settings` take them in the same names, with `model` as `"sarvam/"` and the options in `options`. | Field | Type | Required or default | Values | Description | | --- | --- | --- | --- | --- | | `model` | string | Required | bulbul:v3, bulbul:v4-flash | Which model synthesizes the segment. | | `voice` | string | Required | | A voice of the chosen model, passed to Sarvam as given. See [models and voices](https://docs.ttscacherouter.dev/reference/models.md#sarvam-models). | | `language` | string | Required | | A Sarvam language code from [languages](https://docs.ttscacherouter.dev/reference/models.md#sarvam-languages), passed through as given. | | `speed` | number | Default `1.0` | 0.5 – 2.0 | Speech speed. 1.0 is normal speed. | | `sample_rate` | integer | Required | 8000, 16000, 22050, 24000 | Of the audio you receive, in Hz. /v1/audio/speech also takes 32000, 44100, 48000. | | `format` | string | Default `"pcm"` | pcm, wav, mp3, aac, mulaw, alaw | The audio format, produced by Sarvam itself. `wav` is `response_format` on [/v1/audio/speech](https://docs.ttscacherouter.dev/clients/speech.md) only. | | `options.temperature` | number | Default `0.6` | 0.01 – 1.0 | Sampling temperature: lower is steadier, higher is more expressive. | | `options.dict_id` | string \| null | Default `null` | | A Sarvam pronunciation dictionary to apply. | | `options.bitrate` | string | Default `"128k"` | 32k, 64k, 96k, 128k, 192k | Bitrate of `mp3` and `aac` audio. | | `options.pitch` | number | `bulbul:v4-flash` only. Default `0.0` | -0.5 – 0.5 | Pitch: lower is deeper, higher is sharper. | | `options.loudness` | number | `bulbul:v4-flash` only. Default `1.0` | 0.1 – 2.5 | Loudness: lower is quieter, higher is louder. | | `options.enable_preprocessing` | boolean | `bulbul:v4-flash` only. Default `false` | | Normalize numbers and English words in the text before synthesis. | | `options.min_buffer_size` | integer \| null | Default `null` | 30 – 200 | Characters Sarvam buffers before it starts synthesizing. Null: the router's choice, the least Sarvam allows. | | `options.max_chunk_length` | integer \| null | Default `null` | 50 – 500 | The longest text Sarvam synthesizes in one piece; it splits longer sentences. Null: the router's choice, the most Sarvam allows, so each sentence is synthesized whole. | **LiveKit plugin** ```python from livekit.plugins import tts_cache_router as tcr tts = tcr.TTS( model="sarvam/bulbul:v3", voice="shubh", language="bn-IN", speed=1.0, options={"temperature": 0.6, "dict_id": None, "min_buffer_size": None, "max_chunk_length": None}, sample_rate=24000, ) ``` **start message** ```json { "type": "start", "segment_id": "c0ffee", "provider": "sarvam", "model": "bulbul:v3", "voice": "shubh", "language": "bn-IN", "speed": 1.0, "sample_rate": 24000, "format": "pcm", "options": {"temperature": 0.6, "dict_id": null, "min_buffer_size": null, "max_chunk_length": null} } ``` ## Raya [Raya API reference](https://docs.litwizlabs.com/api-reference/text-to-speech/text-to-speech-stream) Protocol `"provider": "raya"` LiveKit plugin `tcr.TTS(model="raya/…")` - Models: m1, standard - Languages: 14 - Speed: 0.5 – 1.5 - Sample rates: 8, 16, 22.05, 24 kHz - Formats: pcm, wav, mulaw ### Models and voices m1 Voices are per account: [GET /v1/voices](https://docs.ttscacherouter.dev/reference/voices.md) lists yours, as does [Raya’s voice catalog](https://docs.litwizlabs.com/api-reference/voices/list-voices) standard Voices are per account: [GET /v1/voices](https://docs.ttscacherouter.dev/reference/voices.md) lists yours, as does [Raya’s voice catalog](https://docs.litwizlabs.com/api-reference/voices/list-voices) ### Languages - Hindi `hi` - Marathi `mr` - Telugu `te` - Kannada `kn` - Bangla `bn` - Assamese `as` - Gujarati `gu` - Nepali `ne` - Malayalam `ml` - Tamil `ta` - English (India) `en-in` - English (United States) `en-us` - Englishm1 only `en` - Punjabim1 only `pa` ### Settings Fields of the [start message](https://docs.ttscacherouter.dev/clients/stream.md#start) (top-level fields on [/v1/audio/speech](https://docs.ttscacherouter.dev/clients/speech.md)). The LiveKit plugin’s `TTS` and the Pipecat service’s `Settings` take them in the same names, with `model` as `"raya/"` and the options in `options`. Raya takes no `options`: send `{}` or leave it out. | Field | Type | Required or default | Values | Description | | --- | --- | --- | --- | --- | | `model` | string | Required | m1, standard | Which model synthesizes the segment. | | `voice` | string | Required | | A voice of the chosen model, passed to Raya as given. See [models and voices](https://docs.ttscacherouter.dev/reference/models.md#raya-models). | | `language` | string | Required | | A Raya language code from [languages](https://docs.ttscacherouter.dev/reference/models.md#raya-languages), passed through as given. | | `speed` | number | Default `1.0` | 0.5 – 1.5 | Speech speed. 1.0 is normal speed. | | `sample_rate` | integer | Required | 8000, 16000, 22050, 24000 | Of the audio you receive, in Hz. | | `format` | string | Default `"pcm"` | pcm, wav, mulaw | The audio format, produced by Raya itself. `wav` is `response_format` on [/v1/audio/speech](https://docs.ttscacherouter.dev/clients/speech.md) only. | **LiveKit plugin** ```python from livekit.plugins import tts_cache_router as tcr tts = tcr.TTS( model="raya/m1", voice="", language="hi", speed=1.0, sample_rate=24000, ) ``` **start message** ```json { "type": "start", "segment_id": "c0ffee", "provider": "raya", "model": "m1", "voice": "", "language": "hi", "speed": 1.0, "sample_rate": 24000, "format": "pcm", "options": {} } ``` ## ElevenLabs [ElevenLabs API reference](https://elevenlabs.io/docs/api-reference/text-to-speech/v-1-text-to-speech-voice-id-multi-stream-input) Protocol `"provider": "elevenlabs"` LiveKit plugin `tcr.TTS(model="elevenlabs/…")` - Models: eleven_flash_v2_5, eleven_turbo_v2_5, eleven_multilingual_v2, eleven_flash_v2, eleven_turbo_v2, eleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo - Languages: 94 - Speed: set in voice_settings - Sample rates: 8, 16, 22.05, 24, 32, 44.1, 48 kHz - Formats: pcm, wav, mp3, mulaw, alaw ### Models and voices eleven_flash_v2_5 Voices are per account: [GET /v1/voices](https://docs.ttscacherouter.dev/reference/voices.md) lists yours, as does [ElevenLabs’s voice catalog](https://elevenlabs.io/docs/api-reference/voices/search) eleven_turbo_v2_5 Voices are per account: [GET /v1/voices](https://docs.ttscacherouter.dev/reference/voices.md) lists yours, as does [ElevenLabs’s voice catalog](https://elevenlabs.io/docs/api-reference/voices/search) eleven_multilingual_v2 Voices are per account: [GET /v1/voices](https://docs.ttscacherouter.dev/reference/voices.md) lists yours, as does [ElevenLabs’s voice catalog](https://elevenlabs.io/docs/api-reference/voices/search) eleven_flash_v2 Voices are per account: [GET /v1/voices](https://docs.ttscacherouter.dev/reference/voices.md) lists yours, as does [ElevenLabs’s voice catalog](https://elevenlabs.io/docs/api-reference/voices/search) eleven_turbo_v2 Voices are per account: [GET /v1/voices](https://docs.ttscacherouter.dev/reference/voices.md) lists yours, as does [ElevenLabs’s voice catalog](https://elevenlabs.io/docs/api-reference/voices/search) eleven_v3 Voices are per account: [GET /v1/voices](https://docs.ttscacherouter.dev/reference/voices.md) lists yours, as does [ElevenLabs’s voice catalog](https://elevenlabs.io/docs/api-reference/voices/search) eleven_v3_conversational Voices are per account: [GET /v1/voices](https://docs.ttscacherouter.dev/reference/voices.md) lists yours, as does [ElevenLabs’s voice catalog](https://elevenlabs.io/docs/api-reference/voices/search) eleven_v4 Voices are per account: [GET /v1/voices](https://docs.ttscacherouter.dev/reference/voices.md) lists yours, as does [ElevenLabs’s voice catalog](https://elevenlabs.io/docs/api-reference/voices/search) eleven_v4_turbo Voices are per account: [GET /v1/voices](https://docs.ttscacherouter.dev/reference/voices.md) lists yours, as does [ElevenLabs’s voice catalog](https://elevenlabs.io/docs/api-reference/voices/search) ### Languages - English `en` - Japaneseeleven_flash_v2_5, eleven_turbo_v2_5, eleven_multilingual_v2, eleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `ja` - Chineseeleven_flash_v2_5, eleven_turbo_v2_5, eleven_multilingual_v2, eleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `zh` - Germaneleven_flash_v2_5, eleven_turbo_v2_5, eleven_multilingual_v2, eleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `de` - Hindieleven_flash_v2_5, eleven_turbo_v2_5, eleven_multilingual_v2, eleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `hi` - Frencheleven_flash_v2_5, eleven_turbo_v2_5, eleven_multilingual_v2, eleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `fr` - Koreaneleven_flash_v2_5, eleven_turbo_v2_5, eleven_multilingual_v2, eleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `ko` - Portugueseeleven_flash_v2_5, eleven_turbo_v2_5, eleven_multilingual_v2, eleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `pt` - Italianeleven_flash_v2_5, eleven_turbo_v2_5, eleven_multilingual_v2, eleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `it` - Spanisheleven_flash_v2_5, eleven_turbo_v2_5, eleven_multilingual_v2, eleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `es` - Russianeleven_flash_v2_5, eleven_turbo_v2_5, eleven_multilingual_v2, eleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `ru` - Indonesianeleven_flash_v2_5, eleven_turbo_v2_5, eleven_multilingual_v2, eleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `id` - Dutcheleven_flash_v2_5, eleven_turbo_v2_5, eleven_multilingual_v2, eleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `nl` - Turkisheleven_flash_v2_5, eleven_turbo_v2_5, eleven_multilingual_v2, eleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `tr` - Filipinoeleven_flash_v2_5, eleven_turbo_v2_5, eleven_multilingual_v2, eleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `fil` - Polisheleven_flash_v2_5, eleven_turbo_v2_5, eleven_multilingual_v2, eleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `pl` - Swedisheleven_flash_v2_5, eleven_turbo_v2_5, eleven_multilingual_v2, eleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `sv` - Bulgarianeleven_flash_v2_5, eleven_turbo_v2_5, eleven_multilingual_v2, eleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `bg` - Romanianeleven_flash_v2_5, eleven_turbo_v2_5, eleven_multilingual_v2, eleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `ro` - Arabiceleven_flash_v2_5, eleven_turbo_v2_5, eleven_multilingual_v2, eleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `ar` - Czecheleven_flash_v2_5, eleven_turbo_v2_5, eleven_multilingual_v2, eleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `cs` - Greekeleven_flash_v2_5, eleven_turbo_v2_5, eleven_multilingual_v2, eleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `el` - Finnisheleven_flash_v2_5, eleven_turbo_v2_5, eleven_multilingual_v2, eleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `fi` - Croatianeleven_flash_v2_5, eleven_turbo_v2_5, eleven_multilingual_v2, eleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `hr` - Malayeleven_flash_v2_5, eleven_turbo_v2_5, eleven_multilingual_v2, eleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `ms` - Slovakeleven_flash_v2_5, eleven_turbo_v2_5, eleven_multilingual_v2, eleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `sk` - Danisheleven_flash_v2_5, eleven_turbo_v2_5, eleven_multilingual_v2, eleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `da` - Tamileleven_flash_v2_5, eleven_turbo_v2_5, eleven_multilingual_v2, eleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `ta` - Ukrainianeleven_flash_v2_5, eleven_turbo_v2_5, eleven_multilingual_v2, eleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `uk` - Hungarianeleven_flash_v2_5, eleven_turbo_v2_5, eleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `hu` - Norwegianeleven_flash_v2_5, eleven_turbo_v2_5, eleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `no` - Vietnameseeleven_flash_v2_5, eleven_turbo_v2_5, eleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `vi` - Afrikaanseleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `af` - Armenianeleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `hy` - Assameseeleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `as` - Azerbaijanieleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `az` - Belarusianeleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `be` - Banglaeleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `bn` - Bosnianeleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `bs` - Catalaneleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `ca` - Cebuanoeleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `ceb` - Nyanjaeleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `ny` - Estonianeleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `et` - Galicianeleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `gl` - Georgianeleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `ka` - Gujaratieleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `gu` - Hausaeleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `ha` - Hebreweleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `he` - Icelandiceleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `is` - Irisheleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `ga` - Javaneseeleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `jv` - Kannadaeleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `kn` - Kazakheleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `kk` - Kyrgyzeleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `ky` - Latvianeleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `lv` - Lingalaeleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `ln` - Lithuanianeleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `lt` - Luxembourgisheleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `lb` - Macedonianeleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `mk` - Malayalameleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `ml` - Marathieleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `mr` - Nepalieleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `ne` - Pashtoeleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `ps` - Persianeleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `fa` - Punjabieleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `pa` - Serbianeleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `sr` - Sindhieleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `sd` - Slovenianeleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `sl` - Somalieleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `so` - Swahilieleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `sw` - Telugueleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `te` - Thaieleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `th` - Urdueleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `ur` - Welsheleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo only `cy` - Amhariceleven_v4, eleven_v4_turbo only `am` - Asturianeleven_v4, eleven_v4_turbo only `ast` - Burmeseeleven_v4, eleven_v4_turbo only `my` - Cantoneseeleven_v4, eleven_v4_turbo only `yue` - Fulaeleven_v4, eleven_v4_turbo only `ff` - Kambaeleven_v4, eleven_v4_turbo only `kam` - Laoeleven_v4, eleven_v4_turbo only `lo` - Gandaeleven_v4, eleven_v4_turbo only `lg` - Malteseeleven_v4, eleven_v4_turbo only `mt` - Māorieleven_v4, eleven_v4_turbo only `mi` - Mongolianeleven_v4, eleven_v4_turbo only `mn` - Occitaneleven_v4, eleven_v4_turbo only `oc` - Odiaeleven_v4, eleven_v4_turbo only `or` - Shonaeleven_v4, eleven_v4_turbo only `sn` - Central Kurdisheleven_v4, eleven_v4_turbo only `ckb` - Tajikeleven_v4, eleven_v4_turbo only `tg` - Uzbekeleven_v4, eleven_v4_turbo only `uz` - Wolofeleven_v4, eleven_v4_turbo only `wo` - Yorubaeleven_v4, eleven_v4_turbo only `yo` - Zulueleven_v4, eleven_v4_turbo only `zu` ### Settings Fields of the [start message](https://docs.ttscacherouter.dev/clients/stream.md#start) (top-level fields on [/v1/audio/speech](https://docs.ttscacherouter.dev/clients/speech.md)). The LiveKit plugin’s `TTS` and the Pipecat service’s `Settings` take them in the same names, with `model` as `"elevenlabs/"` and the options in `options`. | Field | Type | Required or default | Values | Description | | --- | --- | --- | --- | --- | | `model` | string | Required | eleven_flash_v2_5, eleven_turbo_v2_5, eleven_multilingual_v2, eleven_flash_v2, eleven_turbo_v2, eleven_v3, eleven_v3_conversational, eleven_v4, eleven_v4_turbo | Which model synthesizes the segment. | | `voice` | string | Required | | A voice of the chosen model, passed to ElevenLabs as given. See [models and voices](https://docs.ttscacherouter.dev/reference/models.md#elevenlabs-models). | | `language` | string \| null | Optional | | A ElevenLabs language code from [languages](https://docs.ttscacherouter.dev/reference/models.md#elevenlabs-languages), passed through as given. Leave it out and ElevenLabs detects the language. | | `sample_rate` | integer | Required | 8000, 16000, 22050, 24000, 32000, 44100, 48000 | Of the audio you receive, in Hz. | | `format` | string | Default `"pcm"` | pcm, wav, mp3, mulaw, alaw | The audio format, produced by ElevenLabs itself. `wav` is `response_format` on [/v1/audio/speech](https://docs.ttscacherouter.dev/clients/speech.md) only. | | `options.voice_settings` | object \| null | Default `null` | | Any of stability, similarity_boost and style (0–1), speed (0.7–1.2) and use_speaker_boost; one left out keeps the voice's stored setting. Null: the voice's stored settings. Dialogue models (eleven_v3, eleven_v4) use stability only. | | `options.apply_text_normalization` | string | Default `"auto"` | auto, on, off | Spell out numbers, dates and the like before speaking. | | `options.apply_language_text_normalization` | boolean \| null | Default `null` | | Language-specific normalization (Japanese); adds latency. | | `options.enable_ssml_parsing` | boolean | Default `false` | | Parse SSML tags in the text. | | `options.enable_logging` | boolean | Default `true` | | false: ElevenLabs' zero-retention mode (enterprise accounts). | | `options.pronunciation_dictionary_locators` | array \| null | Default `null` | | Up to 3 {pronunciation_dictionary_id, version_id}. | | `options.bitrate` | string \| null | Default `null` | 32k, 48k, 64k, 96k, 128k, 192k | Bitrate of `mp3`: 32k at 22050 Hz, 48k at 24000 Hz, 32k–192k at 44100 Hz (default 128k). | | `options.sync_alignment` | boolean | Default `true` | | /v1/stream: send each sentence's word timings (`words` messages). | | `options.preferred_alignment` | string \| null | Default `null` | normalized, original | /v1/stream: words of the text as sent (`original`) or as ElevenLabs normalized it. Null: `original` for Japanese, Korean and Chinese, else `normalized`. | **LiveKit plugin** ```python from livekit.plugins import tts_cache_router as tcr tts = tcr.TTS( model="elevenlabs/eleven_flash_v2_5", voice="", options={"apply_text_normalization": "auto", "apply_language_text_normalization": None, "enable_ssml_parsing": False, "enable_logging": True, "sync_alignment": True, "preferred_alignment": None}, sample_rate=24000, ) ``` **start message** ```json { "type": "start", "segment_id": "c0ffee", "provider": "elevenlabs", "model": "eleven_flash_v2_5", "voice": "", "sample_rate": 24000, "format": "pcm", "options": {"voice_settings": null, "apply_text_normalization": "auto", "apply_language_text_normalization": null, "enable_ssml_parsing": false, "enable_logging": true, "pronunciation_dictionary_locators": null, "sync_alignment": true, "preferred_alignment": null} } ``` ## OpenAI [OpenAI API reference](https://developers.openai.com/api/reference/resources/audio/subresources/speech/methods/create) Protocol `"provider": "openai"` LiveKit plugin `tcr.TTS(model="openai/…")` - Models: gpt-4o-mini-tts, gpt-4o-mini-tts-2025-12-15, tts-1, tts-1-hd - Languages: 57 - Speed: 0.25 – 4.0 - Sample rates: 24 kHz - Formats: pcm, wav, mp3, aac ### Models and voices `gpt-4o-mini-tts` voices (13): `alloy`, `ash`, `ballad`, `coral`, `echo`, `fable`, `nova`, `onyx`, `sage`, `shimmer`, `verse`, `marin`, `cedar` `gpt-4o-mini-tts-2025-12-15` voices (13): `alloy`, `ash`, `ballad`, `coral`, `echo`, `fable`, `nova`, `onyx`, `sage`, `shimmer`, `verse`, `marin`, `cedar` `tts-1` voices (9): `alloy`, `ash`, `coral`, `echo`, `fable`, `nova`, `onyx`, `sage`, `shimmer` `tts-1-hd` voices (9): `alloy`, `ash`, `coral`, `echo`, `fable`, `nova`, `onyx`, `sage`, `shimmer` ### Languages OpenAI takes no language setting: it speaks the language of the text. - Afrikaans `af` - Arabic `ar` - Armenian `hy` - Azerbaijani `az` - Belarusian `be` - Bosnian `bs` - Bulgarian `bg` - Catalan `ca` - Chinese `zh` - Croatian `hr` - Czech `cs` - Danish `da` - Dutch `nl` - English `en` - Estonian `et` - Finnish `fi` - French `fr` - Galician `gl` - German `de` - Greek `el` - Hebrew `he` - Hindi `hi` - Hungarian `hu` - Icelandic `is` - Indonesian `id` - Italian `it` - Japanese `ja` - Kannada `kn` - Kazakh `kk` - Korean `ko` - Latvian `lv` - Lithuanian `lt` - Macedonian `mk` - Malay `ms` - Marathi `mr` - Māori `mi` - Nepali `ne` - Norwegian `no` - Persian `fa` - Polish `pl` - Portuguese `pt` - Romanian `ro` - Russian `ru` - Serbian `sr` - Slovak `sk` - Slovenian `sl` - Spanish `es` - Swahili `sw` - Swedish `sv` - Filipino `tl` - Tamil `ta` - Thai `th` - Turkish `tr` - Ukrainian `uk` - Urdu `ur` - Vietnamese `vi` - Welsh `cy` ### Settings Fields of the [start message](https://docs.ttscacherouter.dev/clients/stream.md#start) (top-level fields on [/v1/audio/speech](https://docs.ttscacherouter.dev/clients/speech.md)). The LiveKit plugin’s `TTS` and the Pipecat service’s `Settings` take them in the same names, with `model` as `"openai/"` and the options in `options`. | Field | Type | Required or default | Values | Description | | --- | --- | --- | --- | --- | | `model` | string | Required | gpt-4o-mini-tts, gpt-4o-mini-tts-2025-12-15, tts-1, tts-1-hd | Which model synthesizes the segment. | | `voice` | string | Required | | A voice of the chosen model, passed to OpenAI as given. See [models and voices](https://docs.ttscacherouter.dev/reference/models.md#openai-models). | | `speed` | number | Default `1.0` | 0.25 – 4.0 | Speech speed. 1.0 is normal speed. | | `sample_rate` | integer | Required | 24000 | Of the audio you receive, in Hz. | | `format` | string | Default `"pcm"` | pcm, wav, mp3, aac | The audio format, produced by OpenAI itself. `wav` is `response_format` on [/v1/audio/speech](https://docs.ttscacherouter.dev/clients/speech.md) only. | | `options.instructions` | string \| null | Default `null` | | How to speak: tone, accent, emotion, pace. gpt-4o-mini-tts follows them; tts-1 and tts-1-hd ignore them. | **LiveKit plugin** ```python from livekit.plugins import tts_cache_router as tcr tts = tcr.TTS( model="openai/gpt-4o-mini-tts", voice="alloy", speed=1.0, options={"instructions": None}, sample_rate=24000, ) ``` **start message** ```json { "type": "start", "segment_id": "c0ffee", "provider": "openai", "model": "gpt-4o-mini-tts", "voice": "alloy", "speed": 1.0, "sample_rate": 24000, "format": "pcm", "options": {"instructions": null} } ``` ## Deepgram [Deepgram API reference](https://developers.deepgram.com/reference/text-to-speech/speak-request) Protocol `"provider": "deepgram"` LiveKit plugin `tcr.TTS(model="deepgram/…")` - Models: aura-2, aura-1, flux - Languages: 7 - Speed: 0.7 – 1.5 - Sample rates: 8, 16, 22.05, 24, 32, 44.1, 48 kHz - Formats: pcm, wav, mulaw, alaw, mp3 ### Models and voices `aura-2` voices (90): `agathe-fr`, `agustina-es`, `alvaro-es`, `ama-ja`, `amalthea-en`, `andromeda-en`, `antonia-es`, `apollo-en`, `aquila-es`, `arcas-en`, `aries-en`, `asteria-en`, `athena-en`, `atlas-en`, `aurelia-de`, `aurora-en`, `beatrix-nl`, `callista-en`, `carina-es`, `celeste-es`, `cesare-it`, `cinzia-it`, `cora-en`, `cordelia-en`, `cornelia-nl`, `daphne-nl`, `delia-en`, `demetra-it`, `diana-es`, `dionisio-it`, `draco-en`, `ebisu-ja`, `elara-de`, `electra-en`, `elio-it`, `estrella-es`, `fabian-de`, `flavio-it`, `fujin-ja`, `gloria-es`, `harmonia-en`, `hector-fr`, `helena-en`, `hera-en`, `hermes-en`, `hestia-nl`, `hyperion-en`, `iris-en`, `izanami-ja`, `janus-en`, `javier-es`, `julius-de`, `juno-en`, `jupiter-en`, `kara-de`, `lara-de`, `lars-nl`, `leda-nl`, `livia-it`, `luciano-es`, `luna-en`, `maia-it`, `mars-en`, `melia-it`, `minerva-en`, `neptune-en`, `nestor-es`, `odysseus-en`, `olivia-es`, `ophelia-en`, `orion-en`, `orpheus-en`, `pandora-en`, `phoebe-en`, `pluto-en`, `rhea-nl`, `roman-nl`, `sander-nl`, `saturn-en`, `selena-es`, `selene-en`, `silvia-es`, `sirio-es`, `thalia-en`, `theia-en`, `uzume-ja`, `valerio-es`, `vesta-en`, `viktoria-de`, `zeus-en` `aura-1` voices (12): `angus-en`, `arcas-en`, `asteria-en`, `athena-en`, `helios-en`, `hera-en`, `luna-en`, `orion-en`, `orpheus-en`, `perseus-en`, `stella-en`, `zeus-en` `flux` voices (36): `alexis-en`, `bree-en`, `brittany-en`, `brooke-en`, `bruce-en`, `cliff-en`, `cole-en`, `colin-en`, `conor-en`, `donovan-en`, `drew-en`, `elise-en`, `gemma-en`, `haley-en`, `hannah-en`, `heather-en`, `jack-en`, `kai-en`, `kelsey-en`, `kit-en`, `maeve-en`, `marcelo-en`, `marcus-en`, `meena-en`, `meghan-en`, `miles-en`, `naveen-en`, `paige-en`, `priya-en`, `rufus-en`, `sean-en`, `sharon-en`, `sienna-en`, `tanner-en`, `wade-en`, `wes-en` ### Languages Deepgram takes no language setting: it speaks the language of the text. - English `en` - Spanishaura-2 only `es` - Germanaura-2 only `de` - Frenchaura-2 only `fr` - Italianaura-2 only `it` - Japaneseaura-2 only `ja` - Dutchaura-2 only `nl` ### Settings Fields of the [start message](https://docs.ttscacherouter.dev/clients/stream.md#start) (top-level fields on [/v1/audio/speech](https://docs.ttscacherouter.dev/clients/speech.md)). The LiveKit plugin’s `TTS` and the Pipecat service’s `Settings` take them in the same names, with `model` as `"deepgram/"` and the options in `options`. | Field | Type | Required or default | Values | Description | | --- | --- | --- | --- | --- | | `model` | string | Required | aura-2, aura-1, flux | Which model synthesizes the segment. | | `voice` | string | Required | | A voice of the chosen model, passed to Deepgram as given. See [models and voices](https://docs.ttscacherouter.dev/reference/models.md#deepgram-models). | | `speed` | number | Optional | 0.7 – 1.5 | Speech speed, aura-2 only. 1.0 is normal speed; left out, the provider's default. | | `sample_rate` | integer | Required | 8000, 16000, 22050, 24000, 32000, 44100, 48000 | Of the audio you receive, in Hz. | | `format` | string | Default `"pcm"` | pcm, wav, mulaw, alaw, mp3 | The audio format, produced by Deepgram itself. `wav` is `response_format` on [/v1/audio/speech](https://docs.ttscacherouter.dev/clients/speech.md) only. | | `options.mip_opt_out` | boolean | Default `false` | | Opt out of Deepgram's Model Improvement Program. | | `options.bit_rate` | integer \| null | `aura-2` , `aura-1` only. Default `null` | 32000 – 48000 | MP3's bit rate in bits per second: 32000 or 48000. Null: Deepgram's default (48000). | **LiveKit plugin** ```python from livekit.plugins import tts_cache_router as tcr tts = tcr.TTS( model="deepgram/aura-2", voice="agathe-fr", options={"mip_opt_out": False}, sample_rate=24000, ) ``` **start message** ```json { "type": "start", "segment_id": "c0ffee", "provider": "deepgram", "model": "aura-2", "voice": "agathe-fr", "sample_rate": 24000, "format": "pcm", "options": {"mip_opt_out": false} } ``` ## xAI [xAI API reference](https://docs.x.ai/developers/model-capabilities/audio/text-to-speech) Protocol `"provider": "xai"` LiveKit plugin `tcr.TTS(model="xai/…")` - Models: xai-tts - Languages: 21 - Speed: 0.7 – 1.5 - Sample rates: 8, 16, 22.05, 24, 44.1, 48 kHz - Formats: pcm, wav, mp3, mulaw, alaw ### Models and voices `xai-tts` voices (28): `altair`, `ara`, `atlas`, `aurora`, `carina`, `castor`, `celeste`, `cosmo`, `eve`, `helios`, `helix`, `iris`, `kepler`, `leo`, `liora`, `lumen`, `luna`, `lux`, `naksh`, `orion`, `perseus`, `rex`, `rigel`, `sal`, `sirius`, `ursa`, `zagan`, `zenith` ### Languages - Detected `auto` - English `en` - Arabic (Egypt) `ar-EG` - Arabic (Saudi Arabia) `ar-SA` - Arabic (United Arab Emirates) `ar-AE` - Bangla `bn` - Chinese `zh` - French `fr` - German `de` - Hindi `hi` - Indonesian `id` - Italian `it` - Japanese `ja` - Korean `ko` - Portuguese (Brazil) `pt-BR` - Portuguese (Portugal) `pt-PT` - Russian `ru` - Spanish (Mexico) `es-MX` - Spanish (Spain) `es-ES` - Turkish `tr` - Vietnamese `vi` ### Settings Fields of the [start message](https://docs.ttscacherouter.dev/clients/stream.md#start) (top-level fields on [/v1/audio/speech](https://docs.ttscacherouter.dev/clients/speech.md)). The LiveKit plugin’s `TTS` and the Pipecat service’s `Settings` take them in the same names, with `model` as `"xai/"` and the options in `options`. | Field | Type | Required or default | Values | Description | | --- | --- | --- | --- | --- | | `model` | string | Required | xai-tts | Which model synthesizes the segment. | | `voice` | string | Required | | A voice of the chosen model, passed to xAI as given. See [models and voices](https://docs.ttscacherouter.dev/reference/models.md#xai-models). | | `language` | string \| null | Optional | | A xAI language code from [languages](https://docs.ttscacherouter.dev/reference/models.md#xai-languages), passed through as given. Leave it out and xAI detects the language. | | `speed` | number | Optional | 0.7 – 1.5 | Speech speed. 1.0 is normal speed; left out, the provider's default. | | `sample_rate` | integer | Required | 8000, 16000, 22050, 24000, 44100, 48000 | Of the audio you receive, in Hz. | | `format` | string | Default `"pcm"` | pcm, wav, mp3, mulaw, alaw | The audio format, produced by xAI itself. `wav` is `response_format` on [/v1/audio/speech](https://docs.ttscacherouter.dev/clients/speech.md) only. | | `options.optimize_streaming_latency` | integer \| null | Default `null` | 0 – 2 | 0: best quality (xAI's default); 1 and 2: a smaller first chunk for lower time to first audio, with a quality tradeoff at chunk boundaries. Null: xAI's default. | | `options.text_normalization` | boolean \| null | Default `null` | | Normalize written forms (numbers, abbreviations, symbols) into spoken forms before synthesis. Null: xAI's default (false). | | `options.bit_rate` | integer \| null | Default `null` | 32000 – 192000 | MP3's bit rate in bits per second: 32000, 64000, 96000, 128000 or 192000. Null: xAI's default (128000). | **LiveKit plugin** ```python from livekit.plugins import tts_cache_router as tcr tts = tcr.TTS( model="xai/xai-tts", voice="altair", options={"optimize_streaming_latency": None, "text_normalization": None}, sample_rate=24000, ) ``` **start message** ```json { "type": "start", "segment_id": "c0ffee", "provider": "xai", "model": "xai-tts", "voice": "altair", "sample_rate": 24000, "format": "pcm", "options": {"optimize_streaming_latency": null, "text_normalization": null} } ``` ## Soniox [Soniox API reference](https://soniox.com/docs/api-reference/tts/websocket-api) Protocol `"provider": "soniox"` LiveKit plugin `tcr.TTS(model="soniox/…")` - Models: tts-rt-v2 - Languages: 63 - Speed: 0.7 – 1.3 - Sample rates: 8, 16, 24, 32, 44.1, 48 kHz - Formats: pcm, wav, mp3, mulaw, alaw ### Models and voices `tts-rt-v2` voices (200): `Daniel`, `Nina`, `Emma`, `Adrian`, `Grace`, `Owen`, `Mina`, `Kenji`, `Rafael`, `Mateo`, `Lucia`, `Oliver`, `Arthur`, `Isla`, `Victoria`, `Cooper`, `Mason`, `Ruby`, `Arjun`, `Rohan`, `Priya`, `Bryce`, `Trevor`, `Evan`, `Nathan`, `Karan`, `Wesley`, `Curtis`, `Miles`, `Preston`, `Walter`, `Russell`, `Tunde`, `Nigel`, `Shane`, `Silas`, `Reid`, `Emilio`, `Alistair`, `Dominic`, `Emerson`, `Harlan`, `Bennett`, `Elliot`, `Sebastian`, `Haruto`, `Freddie`, `Logan`, `Poppy`, `Harper`, `Cordelia`, `Reese`, `Hazel`, `Juliet`, `Piper`, `Reyna`, `Kayla`, `Sloane`, `Iris`, `Freya`, `Brooke`, `Bonnie`, `Imogen`, `Arabella`, `Colleen`, `Margo`, `Nora`, `Bianca`, `Sari`, `Kabir`, `Kavya`, `Aarav`, `Dev`, `Nikhil`, `Ashok`, `Ishita`, `Aman`, `Shalini`, `Manav`, `Nisha`, `Karthik`, `Meena`, `Vani`, `Surya`, `Deepa`, `Sundar`, `Gurpreet`, `Madhura`, `Venkat`, `Rajan`, `Lekha`, `Sneha`, `Bhavana`, `Mohan`, `Divya`, `Camila`, `Santiago`, `Damian`, `Facundo`, `Valentina`, `Alonso`, `Renata`, `Mariana`, `Lorenzo`, `Emiliano`, `Paulina`, `Alvaro`, `Rodrigo`, `Guillermo`, `Marta`, `Sergio`, `Hector`, `Javier`, `Carmen`, `Thiago`, `Juliana`, `Larissa`, `Vinicius`, `Bruna`, `Leandro`, `Diogo`, `Rui`, `Nuno`, `Bernardo`, `Matilde`, `Duarte`, `Julien`, `Genevieve`, `Olivier`, `Antoine`, `Elodie`, `Camille`, `Manon`, `Mathieu`, `Celine`, `Baptiste`, `Noemie`, `Jonas`, `Konrad`, `Matthias`, `Florian`, `Annika`, `Tobias`, `Lena`, `Reinhard`, `Stefan`, `Marlene`, `Marco`, `Giulia`, `Riccardo`, `Valeria`, `Massimo`, `Alessandro`, `Davide`, `Giulio`, `Taras`, `Andrei`, `Yana`, `Bohdan`, `Vadim`, `Kirill`, `Marek`, `Szymon`, `Faisal`, `Layla`, `Hind`, `Rania`, `Yousef`, `Karim`, `Wei`, `Meilin`, `Haoran`, `Zihan`, `Aoi`, `Yuki`, `Sota`, `Riku`, `Hikari`, `Nanami`, `Yuto`, `Sakura`, `Rina`, `Jihoon`, `Minjun`, `Seojun`, `Daeho`, `Jiwoo`, `Hyunwoo`, `Junseo`, `Aiman`, `Nurul`, `Linh`, `Bayu`, `Rizky`, `Wahyu`, `Gilang`, `Maricel`, `Budi`, `Mai`, `Huong` ### Languages - Afrikaans `af` - Arabic `ar` - Azerbaijani `az` - Belarusian `be` - Bulgarian `bg` - Bangla `bn` - Bosnian `bs` - Catalan `ca` - Czech `cs` - Welsh `cy` - Danish `da` - German `de` - Greek `el` - English `en` - Spanish `es` - Estonian `et` - Basque `eu` - Persian `fa` - Finnish `fi` - French `fr` - Galician `gl` - Gujarati `gu` - Hebrew `he` - Hindi `hi` - Croatian `hr` - Hungarian `hu` - Indonesian `id` - Icelandic `is` - Italian `it` - Japanese `ja` - Kazakh `kk` - Kannada `kn` - Korean `ko` - Lithuanian `lt` - Latvian `lv` - Macedonian `mk` - Malayalam `ml` - Marathi `mr` - Malay `ms` - Dutch `nl` - Norwegian `no` - Punjabi `pa` - Polish `pl` - Portuguese `pt` - Romanian `ro` - Russian `ru` - Slovak `sk` - Slovenian `sl` - Albanian `sq` - Serbian `sr` - Sundanese `su` - Swedish `sv` - Swahili `sw` - Tamil `ta` - Telugu `te` - Thai `th` - Filipino `tl` - Turkish `tr` - Ukrainian `uk` - Urdu `ur` - Uzbek `uz` - Vietnamese `vi` - Chinese `zh` ### Settings Fields of the [start message](https://docs.ttscacherouter.dev/clients/stream.md#start) (top-level fields on [/v1/audio/speech](https://docs.ttscacherouter.dev/clients/speech.md)). The LiveKit plugin’s `TTS` and the Pipecat service’s `Settings` take them in the same names, with `model` as `"soniox/"` and the options in `options`. | Field | Type | Required or default | Values | Description | | --- | --- | --- | --- | --- | | `model` | string | Required | tts-rt-v2 | Which model synthesizes the segment. | | `voice` | string | Required | | A voice of the chosen model, passed to Soniox as given. See [models and voices](https://docs.ttscacherouter.dev/reference/models.md#soniox-models). | | `language` | string | Required | | A Soniox language code from [languages](https://docs.ttscacherouter.dev/reference/models.md#soniox-languages), passed through as given. | | `speed` | number | Default `1.0` | 0.7 – 1.3 | Speech speed. 1.0 is normal speed. | | `sample_rate` | integer | Required | 8000, 16000, 24000, 32000, 44100, 48000 | Of the audio you receive, in Hz. | | `format` | string | Default `"pcm"` | pcm, wav, mp3, mulaw, alaw | The audio format, produced by Soniox itself. `wav` is `response_format` on [/v1/audio/speech](https://docs.ttscacherouter.dev/clients/speech.md) only. | | `options.bitrate` | integer \| null | Default `null` | 32000 – 320000 | MP3's bit rate in bits per second: 32000, 64000, 96000, 128000, 192000, 256000 or 320000. Null: Soniox's default (128000). | | `options.reduce_silence` | boolean \| null | Default `null` | | Shorten the pauses between words. Null: Soniox's default (false). | **LiveKit plugin** ```python from livekit.plugins import tts_cache_router as tcr tts = tcr.TTS( model="soniox/tts-rt-v2", voice="Daniel", language="af", speed=1.0, options={"reduce_silence": None}, sample_rate=24000, ) ``` **start message** ```json { "type": "start", "segment_id": "c0ffee", "provider": "soniox", "model": "tts-rt-v2", "voice": "Daniel", "language": "af", "speed": 1.0, "sample_rate": 24000, "format": "pcm", "options": {"reduce_silence": null} } ``` ## Smallest AI [Smallest AI API reference](https://docs.smallest.ai/api-reference/models/text-to-speech/synthesize-speech-sse) Protocol `"provider": "smallestai"` LiveKit plugin `tcr.TTS(model="smallestai/…")` - Models: lightning_v3.1, lightning_v3.1_pro - Languages: 33 - Speed: 0.5 – 2.0 - Sample rates: 8, 16, 24, 44.1 kHz - Formats: pcm, wav, mp3, mulaw, alaw ### Models and voices `lightning_v3.1` voices (234): `lakshya`, `bellatrix`, `srishti`, `chinmayi`, `rupali`, `kiran`, `anuja`, `harsh`, `diego`, `nirav`, `khushi`, `alec`, `rajan`, `samarth`, `gabriela`, `reva`, `shreyas`, `kanik`, `balwinder`, `amaira`, `aadya`, `kaustubh`, `sayali`, `chloe`, `sudarshan`, `ishan`, `vedant`, `virat`, `soumya`, `gurpreet`, `yuvan`, `navdeep`, `sridhar`, `sanjay`, `devansh`, `avery`, `myra`, `imogen`, `niharika`, `karan`, `malcolm`, `albus`, `nirupma`, `debashis`, `nilesh`, `yash`, `prachi`, `priyanka`, `maithili`, `chirag`, `luther`, `erica`, `zorin`, `felix`, `dhruv`, `noah`, `wasim`, `tanvi`, `alistair`, `ryan`, `rishika`, `avni`, `parth`, `kavya`, `atharv`, `isla`, `manjunath`, `arnab`, `liam`, `aditi`, `flynn`, `quinn`, `poppy`, `gargi`, `veer`, `ram`, `christine`, `john`, `sanket`, `gauri`, `neel`, `william`, `mia`, `sakshi`, `sunidhi`, `siddharth`, `elizabeth`, `ganya`, `aneer`, `zoya`, `krish`, `saanvi`, `sienna`, `simran`, `chandana`, `aanya`, `rehan`, `harleen`, `srikanth`, `nikita`, `abir`, `julia`, `kevin`, `ilsa`, `ronald`, `chinmay`, `andrea`, `radhika`, `maya`, `anika`, `kiara`, `shruthi`, `pritam`, `kartik`, `vikram`, `meera`, `blofeld`, `kyle`, `nyah`, `mughda`, `harjot`, `ranbir`, `robert`, `sophia`, `sandra`, `vivaan`, `daniel`, `arjun`, `advika`, `aisha`, `edward`, `vanessa`, `lucas`, `anitha`, `raju`, `brooke`, `yuvika`, `ishani`, `hitesh`, `vaibhav`, `gaurav`, `rachel`, `nicole`, `dina`, `sana`, `ethan`, `hannah`, `lauren`, `magnus`, `olivia`, `kunal`, `jose`, `daniella`, `mariana`, `luis`, `lucia`, `miguel`, `javier`, `aarush`, `aarushi`, `shivangi`, `tara`, `shrihari`, `cooper`, `divya`, `brian`, `ella`, `dhruvit`, `alex`, `carlos`, `camilla`, `mohit`, `deepashri`, `jeevan`, `padmaja`, `rajeshwari`, `shibi`, `mihir`, `harper`, `navya`, `sameera`, `emiliano`, `fernando`, `souvik`, `girish`, `prashanth`, `nagaraj`, `vinay`, `freya`, `freddie`, `theo`, `rosie`, `alejandro`, `sofía`, `carmen`, `camila`, `tyler`, `natasha`, `alonso`, `david`, `marcos`, `isabella`, `emilio`, `rodrigo`, `irisha`, `siya`, `jessica`, `jordan`, `sandhya`, `johnny`, `vaisakh`, `pranav`, `balaji`, `harish`, `arun`, `venkat`, `aadhav`, `ashwin`, `nithya`, `surya`, `reyan`, `jaspal`, `nandini`, `mahesh`, `teja`, `lokesh`, `lavanya`, `alice`, `vaishnavi`, `naina`, `nerea`, `paulina`, `alba`, `harshita` `lightning_v3.1_pro` voices (269): `ben`, `kristian`, `joanna`, `tabitha`, `irene`, `ximena`, `gael`, `candela`, `carla`, `pablo`, `kimberly`, `marco`, `valeria`, `chelsea`, `dante`, `layla`, `adam`, `dylan`, `hazel`, `bryce`, `vivian`, `nora`, `silas`, `daisy`, `roman`, `eli`, `sasha`, `mila`, `miles`, `cole`, `aria`, `jasper`, `june`, `beau`, `wes`, `kai`, `manasi`, `mrunal`, `mandar`, `ketaki`, `sravani`, `tejaswini`, `tamilselvi`, `sreenath`, `spoorthi`, `mathan`, `tamizh`, `barath`, `malar`, `jasleen`, `ankita`, `vishnu`, `naveen`, `dinesh`, `sahana`, `aravindan`, `sasmita`, `subrat`, `unni`, `prabhu`, `kavin`, `ezhil`, `rashmi`, `kishore`, `varsha`, `nila`, `smruti`, `rafael`, `carolina`, `natalia`, `sakthi`, `sofia`, `oscar`, `parvathy`, `murugan`, `regina`, `isabela`, `alejandra`, `julieta`, `rakshith`, `rajdeep`, `lakshmi`, `manmeet`, `adrian`, `hugo`, `ivan`, `charan`, `rakesh`, `gagandeep`, `lorenzo`, `cesar`, `swathi`, `maximiliano`, `grant`, `dean`, `sasank`, `manohar`, `arturo`, `tucker`, `mit`, `bhaskar`, `piper`, `gopal`, `tanmoy`, `yogesh`, `gowtham`, `tejinder`, `arghya`, `shankar`, `basava`, `jignesh`, `debasish`, `sambit`, `pratik`, `indranil`, `krupa`, `keval`, `riddhi`, `francisco`, `petra`, `max`, `markus`, `dimitris`, `katerina`, `elise`, `nikolai`, `manon`, `silvia`, `concetta`, `luca`, `thiago`, `irina`, `andrei`, `vasilis`, `maksim`, `mika`, `matti`, `ewa`, `helmi`, `venla`, `timo`, `espen`, `solveig`, `tomasz`, `aino`, `marit`, `jakub`, `astrid`, `ebba`, `saga`, `alva`, `sanne`, `erik`, `daan`, `anton`, `lars`, `nils`, `femke`, `bram`, `fenna`, `stijn`, `nolan`, `spencer`, `kelsey`, `callum`, `bethany`, `claudia`, `olga`, `gustavo`, `leticia`, `ishita`, `misha`, `suri`, `pari`, `reeva`, `aashi`, `tanya`, `shaan`, `ansh`, `kian`, `atharva`, `ishaan`, `aryan`, `aayan`, `rajib`, `subhro`, `bruno`, `davide`, `zariya`, `vyom`, `reyansh`, `zoravar`, `elowen`, `caspian`, `autumn`, `leah`, `leo`, `amrit`, `brooks`, `asher`, `maverick`, `willow`, `hunter`, `wesley`, `mateo`, `mishka`, `ekaterina`, `benedict`, `austin`, `jenna`, `rupert`, `winston`, `ottilie`, `seraphina`, `kate`, `arabella`, `kaitlyn`, `henry`, `molly`, `tushar`, `juliana`, `juliette`, `nicolas`, `martina`, `finn`, `amelie`, `ahan`, `anastasia`, `catarina`, `maxime`, `gabriel`, `arianna`, `sukhdeep`, `louis`, `blake`, `amelia`, `ruby`, `lea`, `hanna`, `finley`, `leonardo`, `athina`, `dimitra`, `raphael`, `lucie`, `meher`, `inaaya`, `cressida`, `aarini`, `colton`, `cormac`, `everett`, `aviraj`, `savannah`, `skylar`, `saira`, `kareena`, `rhea`, `reagan`, `kennedy`, `sierra`, `luke`, `fiona`, `jack`, `zoe`, `ellie`, `sophie`, `sam`, `owen` ### Languages - Detected `auto` - English `en` - Hindi `hi` - Marathi `mr` - Kannada `kn` - Tamil `ta` - Bangla `bn` - Gujarati `gu` - German `de` - French `fr` - Spanish `es` - Italian `it` - Polish `pl` - Dutch `nl` - Russian `ru` - Arabic `ar` - Hebrewlightning_v3.1 only `he` - Swedish `sv` - Malayalam `ml` - Telugu `te` - Portuguese `pt` - Punjabi `pa` - Odia `or` - Greeklightning_v3.1_pro only `el` - Finnishlightning_v3.1_pro only `fi` - Norwegianlightning_v3.1_pro only `no` - Chineselightning_v3.1_pro only `zh` - Indonesianlightning_v3.1_pro only `id` - Japaneselightning_v3.1_pro only `ja` - Koreanlightning_v3.1_pro only `ko` - Malaylightning_v3.1_pro only `ms` - Turkishlightning_v3.1_pro only `tr` - Vietnameselightning_v3.1_pro only `vi` ### Settings Fields of the [start message](https://docs.ttscacherouter.dev/clients/stream.md#start) (top-level fields on [/v1/audio/speech](https://docs.ttscacherouter.dev/clients/speech.md)). The LiveKit plugin’s `TTS` and the Pipecat service’s `Settings` take them in the same names, with `model` as `"smallestai/"` and the options in `options`. Smallest AI takes no `options`: send `{}` or leave it out. | Field | Type | Required or default | Values | Description | | --- | --- | --- | --- | --- | | `model` | string | Required | lightning_v3.1, lightning_v3.1_pro | Which model synthesizes the segment. | | `voice` | string | Required | | A voice of the chosen model, passed to Smallest AI as given. See [models and voices](https://docs.ttscacherouter.dev/reference/models.md#smallestai-models). | | `language` | string | Default `"en"` | | A Smallest AI language code from [languages](https://docs.ttscacherouter.dev/reference/models.md#smallestai-languages), passed through as given. | | `speed` | number | Default `1.0` | 0.5 – 2.0 | Speech speed. 1.0 is normal speed. | | `sample_rate` | integer | Required | 8000, 16000, 24000, 44100 | Of the audio you receive, in Hz. | | `format` | string | Default `"pcm"` | pcm, wav, mp3, mulaw, alaw | The audio format, produced by Smallest AI itself. `wav` is `response_format` on [/v1/audio/speech](https://docs.ttscacherouter.dev/clients/speech.md) only. | **LiveKit plugin** ```python from livekit.plugins import tts_cache_router as tcr tts = tcr.TTS( model="smallestai/lightning_v3.1", voice="lakshya", language="en", speed=1.0, sample_rate=24000, ) ``` **start message** ```json { "type": "start", "segment_id": "c0ffee", "provider": "smallestai", "model": "lightning_v3.1", "voice": "lakshya", "language": "en", "speed": 1.0, "sample_rate": 24000, "format": "pcm", "options": {} } ``` ## Cartesia [Cartesia API reference](https://docs.cartesia.ai/api-reference/tts/websocket) Protocol `"provider": "cartesia"` LiveKit plugin `tcr.TTS(model="cartesia/…")` - Models: sonic-3.6, sonic-3.5, sonic-3 - Languages: 44 - Speed: 0.6 – 1.5 - Sample rates: 8, 16, 22.05, 24, 44.1, 48 kHz - Formats: pcm, wav, mp3, mulaw, alaw ### Models and voices sonic-3.6 Voices are per account: [GET /v1/voices](https://docs.ttscacherouter.dev/reference/voices.md) lists yours, as does [Cartesia’s voice catalog](https://docs.cartesia.ai/api-reference/voices/list) sonic-3.5 Voices are per account: [GET /v1/voices](https://docs.ttscacherouter.dev/reference/voices.md) lists yours, as does [Cartesia’s voice catalog](https://docs.cartesia.ai/api-reference/voices/list) sonic-3 Voices are per account: [GET /v1/voices](https://docs.ttscacherouter.dev/reference/voices.md) lists yours, as does [Cartesia’s voice catalog](https://docs.cartesia.ai/api-reference/voices/list) ### Languages - English `en` - French `fr` - German `de` - Spanish `es` - Portuguese `pt` - Chinese `zh` - Japanese `ja` - Hindi `hi` - Italian `it` - Korean `ko` - Dutch `nl` - Polish `pl` - Russian `ru` - Swedish `sv` - Turkish `tr` - Filipino `tl` - Bulgarian `bg` - Romanian `ro` - Arabic `ar` - Czech `cs` - Greek `el` - Finnish `fi` - Croatian `hr` - Malay `ms` - Slovak `sk` - Danish `da` - Tamil `ta` - Ukrainian `uk` - Hungarian `hu` - Norwegian `no` - Vietnamese `vi` - Bangla `bn` - Thai `th` - Hebrew `he` - Georgian `ka` - Indonesian `id` - Telugu `te` - Gujarati `gu` - Kannada `kn` - Malayalam `ml` - Marathi `mr` - Punjabi `pa` - Odiasonic-3.6 only `or` - Urdusonic-3.6 only `ur` ### Settings Fields of the [start message](https://docs.ttscacherouter.dev/clients/stream.md#start) (top-level fields on [/v1/audio/speech](https://docs.ttscacherouter.dev/clients/speech.md)). The LiveKit plugin’s `TTS` and the Pipecat service’s `Settings` take them in the same names, with `model` as `"cartesia/"` and the options in `options`. | Field | Type | Required or default | Values | Description | | --- | --- | --- | --- | --- | | `model` | string | Required | sonic-3.6, sonic-3.5, sonic-3 | Which model synthesizes the segment. | | `voice` | string | Required | | A voice of the chosen model, passed to Cartesia as given. See [models and voices](https://docs.ttscacherouter.dev/reference/models.md#cartesia-models). | | `language` | string | Default `"en"` | | A Cartesia language code from [languages](https://docs.ttscacherouter.dev/reference/models.md#cartesia-languages), passed through as given. | | `speed` | number | Optional | 0.6 – 1.5 | Speech speed. 1.0 is normal speed; left out, the provider's default. | | `sample_rate` | integer | Required | 8000, 16000, 22050, 24000, 44100, 48000 | Of the audio you receive, in Hz. | | `format` | string | Default `"pcm"` | pcm, wav, mp3, mulaw, alaw | The audio format, produced by Cartesia itself. `wav` is `response_format` on [/v1/audio/speech](https://docs.ttscacherouter.dev/clients/speech.md) only. | | `options.volume` | number \| null | Default `null` | 0.5 – 2.0 | Loudness relative to the voice's own. Null: Cartesia's default (1.0). | | `options.emotion` | string \| null | Default `null` | | Guide the delivery (English): neutral, calm, angry, content, sad, scared, and the others Cartesia lists. Null: Cartesia reads the emotion from the text. | | `options.pronunciation_dict_id` | string \| null | Default `null` | | A Cartesia pronunciation dictionary's id. Cached takes keep the dictionary as it was when they were synthesized, until they expire. | | `options.bit_rate` | integer \| null | Default `128000` | 32000 – 192000 | MP3's bit rate in bits per second: 32000, 64000, 96000, 128000 or 192000. | | `options.word_timestamps` | boolean | Default `true` | | /v1/stream: send each sentence's word timings (`words` messages). Not with mp3. | **LiveKit plugin** ```python from livekit.plugins import tts_cache_router as tcr tts = tcr.TTS( model="cartesia/sonic-3.6", voice="", language="en", options={"volume": None, "emotion": None, "pronunciation_dict_id": None, "word_timestamps": True}, sample_rate=24000, ) ``` **start message** ```json { "type": "start", "segment_id": "c0ffee", "provider": "cartesia", "model": "sonic-3.6", "voice": "", "language": "en", "sample_rate": 24000, "format": "pcm", "options": {"volume": null, "emotion": null, "pronunciation_dict_id": null, "word_timestamps": true} } ``` ## Gemini [Gemini API reference](https://ai.google.dev/gemini-api/docs/speech-generation) Protocol `"provider": "gemini"` LiveKit plugin `tcr.TTS(model="gemini/…")` - Models: gemini-3.8-flash-tts, gemini-3.8-flash-lite-tts, gemini-3.1-flash-tts-preview, gemini-2.5-flash-preview-tts, gemini-2.5-pro-preview-tts - Languages: 149 - Speed: ask in instructions - Sample rates: 8, 24 kHz - Formats: pcm, wav, mulaw, alaw ### Models and voices gemini-3.8-flash-tts The 30 voices of the models below, and Gemini's voice library: about 2,100 voices, with ids like en-us-tutor-1. [GET /v1/voices](https://docs.ttscacherouter.dev/reference/voices.md) lists them all. gemini-3.8-flash-lite-tts The 30 voices of the models below, and Gemini's voice library: about 2,100 voices, with ids like en-us-tutor-1. [GET /v1/voices](https://docs.ttscacherouter.dev/reference/voices.md) lists them all. `gemini-3.1-flash-tts-preview` voices (30): `Zephyr`, `Puck`, `Charon`, `Kore`, `Fenrir`, `Leda`, `Orus`, `Aoede`, `Callirrhoe`, `Autonoe`, `Enceladus`, `Iapetus`, `Umbriel`, `Algieba`, `Despina`, `Erinome`, `Algenib`, `Rasalgethi`, `Laomedeia`, `Achernar`, `Alnilam`, `Schedar`, `Gacrux`, `Pulcherrima`, `Achird`, `Zubenelgenubi`, `Vindemiatrix`, `Sadachbia`, `Sadaltager`, `Sulafat` `gemini-2.5-flash-preview-tts` voices (30): `Zephyr`, `Puck`, `Charon`, `Kore`, `Fenrir`, `Leda`, `Orus`, `Aoede`, `Callirrhoe`, `Autonoe`, `Enceladus`, `Iapetus`, `Umbriel`, `Algieba`, `Despina`, `Erinome`, `Algenib`, `Rasalgethi`, `Laomedeia`, `Achernar`, `Alnilam`, `Schedar`, `Gacrux`, `Pulcherrima`, `Achird`, `Zubenelgenubi`, `Vindemiatrix`, `Sadachbia`, `Sadaltager`, `Sulafat` `gemini-2.5-pro-preview-tts` voices (30): `Zephyr`, `Puck`, `Charon`, `Kore`, `Fenrir`, `Leda`, `Orus`, `Aoede`, `Callirrhoe`, `Autonoe`, `Enceladus`, `Iapetus`, `Umbriel`, `Algieba`, `Despina`, `Erinome`, `Algenib`, `Rasalgethi`, `Laomedeia`, `Achernar`, `Alnilam`, `Schedar`, `Gacrux`, `Pulcherrima`, `Achird`, `Zubenelgenubi`, `Vindemiatrix`, `Sadachbia`, `Sadaltager`, `Sulafat` ### Languages Gemini takes no language setting: it speaks the language of the text. - Acehnesegemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `ace-Arab` - Afrikaans `af` - Akangemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `ak` - Amharic `am` - Armenian `hy` - Assamesegemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `as` - Awadhigemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `awa` - Balinesegemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `ban` - Bangla `bn` - Banjar (Arabic)gemini-3.8-flash-tts only `bjn-Arab` - Banjar (Latin)gemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `bjn-Latn` - Bashkirgemini-3.8-flash-tts only `ba` - Basque `eu` - Belarusian `be` - Bembagemini-3.8-flash-tts only `bem` - Bhojpurigemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `bho` - Bosniangemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `bs` - Buginesegemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `bug` - Bulgarian `bg` - Burmesegemini-3.8-flash-tts, gemini-3.1-flash-tts-preview, gemini-2.5-flash-preview-tts, gemini-2.5-pro-preview-tts only `my` - Cantonesegemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `yue` - Catalan `ca` - Cebuano `ceb` - Central Kurdishgemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `ckb` - hnegemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `hne` - Chinese (Simplified)gemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `zh-Hans` - Chinese (Traditional)gemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `zh-Hant` - Crimean Tatargemini-3.8-flash-tts only `crh` - Croatian `hr` - Czech `cs` - Danish `da` - Dutch `nl` - Dyulagemini-3.8-flash-tts only `dyu` - Dzongkhagemini-3.8-flash-tts only `dz` - Egyptian Arabicgemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `arz` - English `en` - Estonian `et` - Filipino `fil` - Finnishgemini-3.8-flash-tts, gemini-3.1-flash-tts-preview, gemini-2.5-flash-preview-tts, gemini-2.5-pro-preview-tts only `fi` - French `fr` - Galician `gl` - Gandagemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `lg` - Georgian `ka` - German `de` - Greek `el` - Guaranigemini-3.8-flash-tts only `gn` - Gujarati `gu` - Haitian Creole `ht` - Mongoliangemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `khk` - Hausagemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `ha` - Hebrew `he` - Hindi `hi` - Hungarian `hu` - Icelandic `is` - Igbogemini-3.8-flash-tts only `ig` - Ilokogemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `ilo` - Indonesian `id` - Persiangemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `pes` - Italian `it` - Japanese `ja` - Javanese `jv` - Kabylegemini-3.8-flash-tts only `kab` - Kambagemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `kam` - Kannada `kn` - Kashmiri (Arabic)gemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `ks-Arab` - Kashmiri (Devanagari)gemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `ks-Deva` - Kazakhgemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `kk` - Khmergemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `km` - Kikuyugemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `ki` - Kinyarwandagemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `rw` - Kongogemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `kg` - Korean `ko` - Kyrgyzgemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `ky` - Lao `lo` - Latgaliangemini-3.8-flash-tts only `ltg` - Lingalagemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `ln` - Lithuaniangemini-3.8-flash-tts, gemini-3.1-flash-tts-preview, gemini-2.5-flash-preview-tts, gemini-2.5-pro-preview-tts only `lt` - Luxembourgishgemini-3.8-flash-tts, gemini-3.1-flash-tts-preview, gemini-2.5-flash-preview-tts, gemini-2.5-pro-preview-tts only `lb` - Macedonian `mk` - Magahigemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `mag` - Maithili `mai` - Malayalam `ml` - Maltesegemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `mt` - Manipurigemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `mni` - Marathi `mr` - Minangkabau (Arabic)gemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `min-Arab` - Minangkabau (Latin)gemini-3.8-flash-tts only `min-Latn` - Mizogemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `lus` - Nepaligemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `npi` - fuvgemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `fuv` - Azerbaijanigemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `azj` - Northern Sothogemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `nso` - Uzbekgemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `uzn` - Norwegian Bokmål `nb` - Norwegian Nynorsk `nn` - Nyanjagemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `ny` - Occitangemini-3.8-flash-tts only `oc` - Odiagemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `ory` - Pangasinangemini-3.8-flash-tts only `pag` - Persian (Afghanistan)gemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `prs` - Polish `pl` - Portuguese `pt` - Punjabi `pa` - Romanian `ro` - Russian `ru` - Santaligemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `sat` - Serbian `sr` - Sindhigemini-3.8-flash-tts, gemini-3.1-flash-tts-preview, gemini-2.5-flash-preview-tts, gemini-2.5-pro-preview-tts only `sd` - Sinhala `si` - Slovak `sk` - Sloveniangemini-3.8-flash-tts, gemini-3.1-flash-tts-preview, gemini-2.5-flash-preview-tts, gemini-2.5-pro-preview-tts only `sl` - Somaligemini-3.8-flash-tts only `so` - azbgemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `azb` - pbtgemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `pbt` - Southern Sothogemini-3.8-flash-tts only `st` - Spanish `es` - Arabic (Arabic)gemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `arb-Arab` - Arabic (Latin)gemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `arb-Latn` - Latviangemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `lvs` - Malaygemini-3.8-flash-tts, gemini-3.8-flash-lite-tts only `zsm` - Swahiligemini-3.8-flash-tts only `swh` - Swatigemini-3.8-flash-tts only `ss` - Swedishgemini-3.8-flash-tts, gemini-3.1-flash-tts-preview, gemini-2.5-flash-preview-tts, gemini-2.5-pro-preview-tts only `sv` - Tajikgemini-3.8-flash-tts only `tg` - Tamil `ta` - Telugu `te` - Thaigemini-3.8-flash-tts, gemini-3.1-flash-tts-preview, gemini-2.5-flash-preview-tts, gemini-2.5-pro-preview-tts only `th` - Tigrinyagemini-3.8-flash-tts only `ti` - Albaniangemini-3.8-flash-tts only `als` - Turkish `tr` - Uyghurgemini-3.8-flash-tts only `ug` - Vietnamese `vi` - Arabicgemini-3.1-flash-tts-preview, gemini-2.5-flash-preview-tts, gemini-2.5-pro-preview-tts only `ar` - Konkanigemini-3.1-flash-tts-preview, gemini-2.5-flash-preview-tts, gemini-2.5-pro-preview-tts only `kok` - Latingemini-3.1-flash-tts-preview, gemini-2.5-flash-preview-tts, gemini-2.5-pro-preview-tts only `la` - Latviangemini-3.1-flash-tts-preview, gemini-2.5-flash-preview-tts, gemini-2.5-pro-preview-tts only `lv` - Malagasygemini-3.1-flash-tts-preview, gemini-2.5-flash-preview-tts, gemini-2.5-pro-preview-tts only `mg` - Ukrainiangemini-3.1-flash-tts-preview, gemini-2.5-flash-preview-tts, gemini-2.5-pro-preview-tts only `uk` - Malaygemini-3.1-flash-tts-preview, gemini-2.5-flash-preview-tts, gemini-2.5-pro-preview-tts only `ms` - Mongoliangemini-3.1-flash-tts-preview, gemini-2.5-flash-preview-tts, gemini-2.5-pro-preview-tts only `mn` - Albaniangemini-3.1-flash-tts-preview, gemini-2.5-flash-preview-tts, gemini-2.5-pro-preview-tts only `sq` - Nepaligemini-3.1-flash-tts-preview, gemini-2.5-flash-preview-tts, gemini-2.5-pro-preview-tts only `ne` - Azerbaijanigemini-3.1-flash-tts-preview, gemini-2.5-flash-preview-tts, gemini-2.5-pro-preview-tts only `az` - Odiagemini-3.1-flash-tts-preview, gemini-2.5-flash-preview-tts, gemini-2.5-pro-preview-tts only `or` - Pashtogemini-3.1-flash-tts-preview, gemini-2.5-flash-preview-tts, gemini-2.5-pro-preview-tts only `ps` - Persiangemini-3.1-flash-tts-preview, gemini-2.5-flash-preview-tts, gemini-2.5-pro-preview-tts only `fa` - Chinesegemini-3.1-flash-tts-preview, gemini-2.5-flash-preview-tts, gemini-2.5-pro-preview-tts only `cmn` - Swahiligemini-3.1-flash-tts-preview, gemini-2.5-flash-preview-tts, gemini-2.5-pro-preview-tts only `sw` - Urdugemini-3.1-flash-tts-preview, gemini-2.5-flash-preview-tts, gemini-2.5-pro-preview-tts only `ur` ### Settings Fields of the [start message](https://docs.ttscacherouter.dev/clients/stream.md#start) (top-level fields on [/v1/audio/speech](https://docs.ttscacherouter.dev/clients/speech.md)). The LiveKit plugin’s `TTS` and the Pipecat service’s `Settings` take them in the same names, with `model` as `"gemini/"` and the options in `options`. | Field | Type | Required or default | Values | Description | | --- | --- | --- | --- | --- | | `model` | string | Required | gemini-3.8-flash-tts, gemini-3.8-flash-lite-tts, gemini-3.1-flash-tts-preview, gemini-2.5-flash-preview-tts, gemini-2.5-pro-preview-tts | Which model synthesizes the segment. | | `voice` | string \| null | Required, unless `options.speakers` | | A voice of the chosen model, passed to Gemini as given. See [models and voices](https://docs.ttscacherouter.dev/reference/models.md#gemini-models). With `options.speakers`, left out or null. | | `sample_rate` | integer | Required | 8000, 24000 | Of the audio you receive, in Hz. By format: pcm and wav at 24000 Hz from every model; mulaw and alaw at 8000 Hz, from gemini-3.8-flash-tts and gemini-3.8-flash-lite-tts only. | | `format` | string | Default `"pcm"` | pcm, wav, mulaw, alaw | The audio format, produced by Gemini itself. `wav` is `response_format` on [/v1/audio/speech](https://docs.ttscacherouter.dev/clients/speech.md) only. pcm and wav at 24000 Hz from every model; mulaw and alaw at 8000 Hz, from gemini-3.8-flash-tts and gemini-3.8-flash-lite-tts only. | | `options.instructions` | string \| null | Default `null` | | How to speak: style, tone, accent, pace. Null: for gemini-2.5 and gemini-3.1, LiveKit's plugin's default ("Say the text with a proper tone, don't omit or add any words"), which keeps them reading the text; for gemini-3.8, none. | | `options.speakers` | object \| null | `gemini-3.8-flash-tts` , `gemini-3.8-flash-lite-tts` only. Default `null` | | Two speakers' names, each with a voice (a multi-speaker voice config); replaces voice. Requires speaker. | | `options.speaker` | string \| null | `gemini-3.8-flash-tts` , `gemini-3.8-flash-lite-tts` only. Default `null` | | Which of speakers voices this request. | **LiveKit plugin** ```python from livekit.plugins import tts_cache_router as tcr tts = tcr.TTS( model="gemini/gemini-3.8-flash-tts", voice="Zephyr", options={"instructions": None, "speaker": None}, sample_rate=24000, ) ``` **start message** ```json { "type": "start", "segment_id": "c0ffee", "provider": "gemini", "model": "gemini-3.8-flash-tts", "voice": "Zephyr", "sample_rate": 24000, "format": "pcm", "options": {"instructions": null, "speakers": null, "speaker": null} } ``` --- # Voices > GET /v1/voices: every voice your org can speak with at a provider, your account's own included, in one shape. ## Request `GET https://api.ttscacherouter.dev/v1/voices` with your router API key as `Authorization: Bearer tcr_…`. It lists the voices your org can speak with at one provider, your provider account’s own included, read live with the provider key stored under [Provider keys](https://app.ttscacherouter.dev/app/provider-keys). **All voices** ```shell curl "https://api.ttscacherouter.dev/v1/voices?provider=elevenlabs" \ -H "Authorization: Bearer $TTS_CACHE_ROUTER_API_KEY" ``` **One model** ```shell curl "https://api.ttscacherouter.dev/v1/voices?provider=deepgram&model=flux" \ -H "Authorization: Bearer $TTS_CACHE_ROUTER_API_KEY" ``` - `provider` (`string`, Required): `sarvam`, `raya`, `elevenlabs`, `openai`, `deepgram`, `xai`, `soniox`, `smallestai`, `cartesia` or `gemini`. - `model` (`string`, Optional): Only the voices this model takes, by the router’s model id without the provider (`bulbul:v4-flash`, `aura-2`, `m1`). See [Models and options](https://docs.ttscacherouter.dev/reference/models.md). The answer is the whole list: where a provider pages its list, the router reads every page. Nothing is cached, so a voice you add at the provider is listed on your next request. Only voices the router can speak with are listed. ## Response One shape for every provider. Each voice's id is what voice takes, with any of its models, on every surface. ```json { "voices": [ { "id": "JBFqnCBsd6RMkjVDRZzb", "name": "George - Warm, Captivating Storyteller", "provider": "elevenlabs", "models": ["eleven_flash_v2_5", "eleven_turbo_v2_5", "eleven_multilingual_v2", "…"], "languages": ["en", "es", "fr"], "gender": "male", "accent": "british", "age": "middle_aged", "description": "Warm resonance that instantly captivates listeners.", "preview_url": "https://storage.googleapis.com/eleven-public-prod/…/preview.mp3", "category": "builtin", "provider_data": {"voice_id": "JBFqnCBsd6RMkjVDRZzb", "category": "premade", "labels": {"…": "…"}, "…": "…"} } ] } ``` - `id` (`string`): What `voice` takes on `/v1/stream`, `/v1/audio/speech` and the LiveKit plugin. A Deepgram voice is its model id without the model: `thalia-en` with `aura-2` is Deepgram’s `aura-2-thalia-en`. - `name` (`string`): The provider's display name. - `provider` (`string`): The provider asked for. - `models` (`string[]`): The router's model ids the voice works with. Every ElevenLabs voice works with every ElevenLabs model. - `languages` (`string[]`): The languages the voice is made for, in the provider’s codes. A voice that speaks every language of its model lists them all (Sarvam’s bulbul:v3, OpenAI, xAI’s built-in voices, Soniox); an ElevenLabs voice lists its own language, then those ElevenLabs verified it in; a bulbul:v4-flash persona the one it was recorded in; a Cartesia voice its accents’ locales, its own first; a Gemini voice the locale it was made for. Empty when the provider doesn’t say. - `gender` (`string | null`): `male`, `female` or `neutral`, where the provider says. - `accent` (`string | null`): In the provider's own words, where it says. - `age` (`string | null`): In the provider's own words, where it says. - `description` (`string | null`): The provider's description of the voice, where it has one. - `preview_url` (`string | null`): A sample of the voice at the provider's own URL, where it has one (ElevenLabs, Deepgram). Play it straight from there: it needs no key. - `category` (`string`): Where the voice comes from: see [Categories](https://docs.ttscacherouter.dev/reference/voices.md#categories). - `provider_data` (`object | null`): The provider’s own voice object, as it came: ElevenLabs’ `labels`, `verified_languages` and `high_quality_base_model_ids`, Deepgram’s `tags` and `image`, Soniox’s `use_case` and `style`, Smallest AI’s `tags`, Cartesia’s `accents`, Gemini’s `persona` and `pitch`. Null for voices from the router’s catalog (Sarvam, OpenAI), which no provider API lists. ## Categories Where each provider's voices come from, and the categories they can have. - `builtin`: The provider's own stock voices. - `cloned`: Cloned in your provider account from recordings: ElevenLabs instant and professional clones, xAI custom voices, Soniox cloned voices (once ready), Smallest AI voice clones, your own Cartesia voices. - `custom`: Made in your provider account another way: ElevenLabs voice design. - `library`: Added to your provider account from the provider's shared library: ElevenLabs. | provider | Voices from | Categories | Previews | | --- | --- | --- | --- | | sarvam | The router's catalog: bulbul:v3's 37 speakers (with gender) and bulbul:v4-flash's personas | builtin | No | | raya | Raya's voice list; each voice belongs to one model | builtin | No | | elevenlabs | Your ElevenLabs account's voices, every page | builtin, cloned, custom, library | Yes | | openai | The router's catalog | builtin | No | | deepgram | Deepgram's model lists: Aura and Aura-2, and Flux | builtin | Yes | | xai | xAI's voices and your custom voices | builtin, cloned | No | | soniox | Soniox's voices and your project's cloned voices | builtin, cloned | No | | smallestai | Each model's voices, and your cloned voices with the models they were cloned for | builtin, cloned | No | | cartesia | Cartesia's voice library and your own voices | builtin, cloned | No | | gemini | Gemini's voices: the 30 every model speaks, and the voice library the 3.8 models speak | builtin | No | Sarvam’s cloned voices aren’t listed: Sarvam speaks them only through its voice-cloning API, not its Bulbul models. OpenAI lists no custom voices, and the router passes `voice` as a name. ## Speaking with a voice Pass the voice's id as voice, with one of its models, on any surface. **agent.py** ```python from livekit.plugins import tts_cache_router as tcr # A voice from GET /v1/voices?provider=deepgram&model=aura-2 tts = tcr.TTS(model="deepgram/aura-2", voice="thalia-en") ``` See [TTS(...)](https://docs.ttscacherouter.dev/livekit.md#tts) for the plugin, and [/v1/audio/speech](https://docs.ttscacherouter.dev/clients/speech.md) over HTTP. ## Library voices and voice settings ElevenLabs' library and voice settings are on the ElevenLabs drop-in, with your router API key in xi-api-key. **Add a library voice** ```shell # Browse ElevenLabs' voice library curl "https://api.ttscacherouter.dev/elevenlabs/v1/shared-voices?page_size=10&language=hi" \ -H "xi-api-key: $TTS_CACHE_ROUTER_API_KEY" # Add one to your ElevenLabs account: its public_owner_id and voice_id curl -X POST "https://api.ttscacherouter.dev/elevenlabs/v1/voices/add/$PUBLIC_OWNER_ID/$VOICE_ID" \ -H "xi-api-key: $TTS_CACHE_ROUTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"new_name": "Support voice"}' ``` **Voice settings** ```shell curl "https://api.ttscacherouter.dev/elevenlabs/v1/voices/JBFqnCBsd6RMkjVDRZzb/settings" \ -H "xi-api-key: $TTS_CACHE_ROUTER_API_KEY" {"stability": 0.5, "use_speaker_boost": true, "similarity_boost": 0.75, "style": 0.0, "speed": 1.0} ``` A voice you add appears in `GET /v1/voices?provider=elevenlabs` as `library`. A voice’s settings are what a request without `voice_settings` uses. Each provider’s own voice endpoints are on its drop-in page: [ElevenLabs](https://docs.ttscacherouter.dev/clients/elevenlabs.md#http), [Raya](https://docs.ttscacherouter.dev/clients/raya.md#endpoints), [Deepgram](https://docs.ttscacherouter.dev/clients/deepgram.md#http), [xAI](https://docs.ttscacherouter.dev/clients/xai.md#http), [Soniox](https://docs.ttscacherouter.dev/clients/soniox.md#http), [Smallest AI](https://docs.ttscacherouter.dev/clients/smallestai.md#http), [Cartesia](https://docs.ttscacherouter.dev/clients/cartesia.md#http) and [Gemini](https://docs.ttscacherouter.dev/clients/gemini.md#http). ## Errors The router's error body, with its HTTP status. A provider's refusal passes through: its status in provider_status, its body in message. ```http HTTP/1.1 400 Bad Request {"error": {"code": "provider_rejected", "message": "xAI answered HTTP 400: {\"code\":\"Client specified an invalid argument\",\"error\":\"Incorrect API key provided. You can obtain an API key from https://console.x.ai.\"}", "retryable": false, "provider_status": 400}} ``` - `bad_request` (HTTP 400, not retryable): The request failed validation or a message arrived out of order. The message says which field or rule. - `unauthorized` (HTTP 401, not retryable): The API key is missing, wrong or deleted, or its org is disabled. - `insufficient_credits` (HTTP 402, not retryable): Your org is out of credit, so the router refuses new connections and requests until you [buy more](https://app.ttscacherouter.dev/app/billing). Connections already open carry on. - `provider_key_missing` (HTTP 400, not retryable): Your org has no key stored for the provider. Add one under [Provider keys](https://app.ttscacherouter.dev/app/provider-keys). - `provider_rejected` (HTTP 400, not retryable): The provider refused the request, e.g. a wrong stored provider key or an unknown voice. The provider’s message is passed through. - `provider_rate_limited` (HTTP 429, retryable): Your own quota at the provider is exhausted. - `provider_unavailable` (HTTP 502, retryable): The provider returned a server error, or the connection to it failed or closed unexpectedly. - `provider_timeout` (HTTP 504, retryable): The provider took too long to send audio. - `internal` (HTTP 500, retryable): The router failed. A missing or unknown `provider` or `model`, or any other query parameter, is a `bad_request`. An org with no key stored for the provider gets `provider_key_missing`, also for Sarvam and OpenAI, whose voices come from the router’s catalog. Deepgram’s model lists are public, so a wrong Deepgram key still lists its voices; it is refused when you speak. --- # Errors > The router's error codes, what each means, whether to retry, and how each surface reports them. ## Error codes The WebSocket protocol, /v1/audio/speech, the LiveKit plugin and the Pipecat service share these codes. retryable says whether the same request can succeed if you try again. - `bad_request` (HTTP 400, not retryable): The request failed validation or a message arrived out of order. The message says which field or rule. - `unauthorized` (HTTP 401, not retryable): The API key is missing, wrong or deleted, or its org is disabled. - `insufficient_credits` (HTTP 402, not retryable): Your org is out of credit, so the router refuses new connections and requests until you [buy more](https://app.ttscacherouter.dev/app/billing). Connections already open carry on. - `provider_key_missing` (HTTP 400, not retryable): Your org has no key stored for the provider. Add one under [Provider keys](https://app.ttscacherouter.dev/app/provider-keys). - `provider_rejected` (HTTP 400, not retryable): The provider refused the request, e.g. a wrong stored provider key or an unknown voice. The provider’s message is passed through. - `provider_rate_limited` (HTTP 429, retryable): Your own quota at the provider is exhausted. - `provider_unavailable` (HTTP 502, retryable): The provider returned a server error, or the connection to it failed or closed unexpectedly. - `provider_timeout` (HTTP 504, retryable): The provider took too long to send audio. - `internal` (HTTP 500, retryable): The router failed. The HTTP status is what `/v1/audio/speech` answers with. `provider_status`, when present, is the status the provider answered with. ## How each surface reports them **WebSocket (`/v1/stream`).** An `error` message ends the segment; close the connection and open a new one. `segment_id` is `null` for an error before a valid `start`. Two refusals come before the connection opens, as the HTTP status of the handshake: 403 for an API key the router doesn’t accept, and 402 (`insufficient_credits`) when your org is out of credit. ```json {"type": "error", "segment_id": "c0ffee", "code": "provider_rejected", "message": "…", "retryable": false, "provider_status": 403} ``` **`/v1/audio/speech`.** A JSON body with the status above. The status is sent with the first audio, so an error after audio has started can only abort the body: treat a response that ends early as failed. ```http HTTP/1.1 400 Bad Request {"error": {"code": "provider_key_missing", "message": "…", "retryable": false, "provider_status": null}} ``` **Provider drop-ins.** In the provider’s own format and status, as the provider sent them, so your client’s error handling keeps working. The one error of the router’s own, `insufficient_credits`, comes as HTTP 402 in the provider’s error shape (on ElevenLabs’ WebSockets, as an in-band error and close 1008). See [Sarvam](https://docs.ttscacherouter.dev/clients/sarvam.md#errors), [Raya](https://docs.ttscacherouter.dev/clients/raya.md#errors), [ElevenLabs](https://docs.ttscacherouter.dev/clients/elevenlabs.md#errors), [OpenAI](https://docs.ttscacherouter.dev/clients/openai.md#errors), [Deepgram](https://docs.ttscacherouter.dev/clients/deepgram.md#errors), [xAI](https://docs.ttscacherouter.dev/clients/xai.md#errors), [Soniox](https://docs.ttscacherouter.dev/clients/soniox.md#errors), [Smallest AI](https://docs.ttscacherouter.dev/clients/smallestai.md#errors), [Cartesia](https://docs.ttscacherouter.dev/clients/cartesia.md#errors) and [Gemini](https://docs.ttscacherouter.dev/clients/gemini.md#errors). ## In the LiveKit plugin Router errors become LiveKit's API errors, so LiveKit's retries and FallbackAdapter treat them as they treat any TTS's. | What happened | Raised | | --- | --- | | A router error | `APIStatusError`, with the error’s `retryable`; `status_code` is `provider_status` or the status above | | The router refused the API key | `APIStatusError`, status 403, not retryable | | Your org is out of credit | `APIStatusError`, status 402, not retryable | | The connection failed or dropped | `APIConnectionError` | | The router went quiet after all text was sent | `APITimeoutError` | | A malformed `model` or no `voice`, at construction | `ValueError` | ## In the Pipecat service Router errors become Pipecat ErrorFrames, each with the ErrorCategory that matches its code. The [Pipecat service](https://docs.ttscacherouter.dev/pipecat.md#errors) lists the category for each code. A permanent one (`AUTHENTICATION`, `AUTHORIZATION`, `INVALID_REQUEST`) marks the service unusable until its settings change, as Pipecat does for any service. ## REST API errors The provider-keys API, and the dashboard's own API, answer with a code and a message. ```http HTTP/1.1 401 Unauthorized {"error": {"code": "unauthenticated", "message": "invalid API key"}} ``` | code | Status | Meaning | | --- | --- | --- | | unauthenticated | 401 | The API key is missing, wrong or deleted. | | bad_request | 422 | The request failed validation; the message names the field, never its value. | | not_found | 404 | Nothing to delete: no key is stored for that provider. | | internal | 500 | The router failed. | The dashboard’s API adds these, from managing an org’s API keys and members. Each `/api/orgs/…` route also checks your role in the org: `forbidden` (403) when it isn’t enough, `not_found` (404) when you aren’t a member. | code | Status | Meaning | | --- | --- | --- | | secret_not_stored | 404 | The API key was created before keys could be revealed; its secret was shown once, at creation. | | already_member | 409 | The invited email already belongs to a member of the org. | | already_invited | 409 | The invited email already has a pending invite to the org. | | last_admin | 409 | The change would leave the org without an admin: make another member an admin first. | | last_member | 409 | The org's last member can't leave it; orgs are never deleted. | --- # Limits > Sizes and timeouts the router enforces, and the limits that come from your provider plan. ## Router limits Sizes and timeouts the hosted router enforces. There is no cap on connections, requests or sentences per org. | Limit | Value | | --- | --- | | Text per segment, or per `/v1/audio/speech` input | 20,000 characters | | Request body, `/v1/audio/speech` and the HTTP drop-ins | 80,000 bytes | | WebSocket message | 64 KB | | `/v1/stream`: no message while the router waits for input (between segments, or before `flush`) | Closed after 30 minutes | | Sarvam WebSocket drop-in: no message | Closed after 60 seconds, as Sarvam does | | Every client WebSocket | Pinged every 20 seconds; closed if no pong comes within 20 seconds while the router is sending to it | | Provider's first audio for a sentence | 10 seconds (OpenAI: 30, as its LiveKit plugin waits; Gemini: 120, as its 2.5 models send a sentence only once it is complete), then 10 seconds between chunks: provider_timeout | | A whole REST reply from the provider (Sarvam above 24 kHz, the HTTP drop-ins) | 60 seconds | Text over a limit is a `bad_request` (HTTP 400; on the drop-ins, the provider’s own shape). The LiveKit plugin keeps its pooled connections well inside the idle limit. ## Your provider's limits Rate and concurrency limits are your provider plan's, on your provider key, exactly as when you call the provider directly. - Each client connection to the router (a WebSocket, or one HTTP request) uses one connection to the provider and synthesizes one sentence at a time, so the provider counts your connections and concurrent requests as it would without the router. A cache hit makes no provider request. - When the provider throttles your key, you get `provider_rate_limited` (retryable), or the provider’s own error on the drop-ins. - Sarvam limits WebSocket connections and REST requests per minute by plan; Raya publishes no limits; ElevenLabs limits concurrent requests by plan; OpenAI limits requests and tokens per minute by usage tier; Deepgram limits concurrent requests per project; xAI limits requests per second and concurrent WebSocket sessions per team; Soniox limits requests per minute, concurrent streams (3 by default) and open connections per organization; Smallest AI limits concurrent requests per account (1 by default, across REST, SSE and WebSocket) and open WebSocket connections (5); Cartesia limits concurrent requests by plan (2 on the free plan); Gemini limits requests and tokens per minute and requests per day per project, by usage tier. - Per connection, the router uses one Soniox stream, ElevenLabs context, xAI utterance, Cartesia context or Deepgram Flux turn at a time, as the providers’ LiveKit plugins do. --- # REST API > Every HTTP endpoint, from the router's OpenAPI document. ## Provider keys Set, list and delete your org’s provider keys with an API key, as the dashboard does. Write-only: a stored key is never returned. ### GET `/v1/provider-keys` List your provider keys GET https://api.ttscacherouter.dev/v1/provider-keys Which providers have a key stored, with each key's last four characters. #### Responses - 200 Successful Response An array of ProviderKey: - `provider` "sarvam" | "raya" | "elevenlabs" | "openai" | "deepgram" | "xai" | "soniox" | "smallestai" | "cartesia" | "gemini" Required - `last4` string Required: The key's last four characters. The key itself is never returned. - `updated_at` string (date-time) Required - 401 Not signed in, or the session expired - 422 Validation Error ### PUT `/v1/provider-keys/{provider}` Set or replace a provider key PUT https://api.ttscacherouter.dev/v1/provider-keys/{provider} Stored encrypted and never returned. The router uses a new or replaced key within seconds. #### Parameters - `provider` path, "sarvam" | "raya" | "elevenlabs" | "openai" | "deepgram" | "xai" | "soniox" | "smallestai" | "cartesia" | "gemini" Required #### Request body - `api_key` string, min length 1, max length 512 Required: Your key for the provider. Stored encrypted; it can't be read back. **Example** ```json { "api_key": "sk_…" } ``` #### Responses - 200 Successful Response - `provider` "sarvam" | "raya" | "elevenlabs" | "openai" | "deepgram" | "xai" | "soniox" | "smallestai" | "cartesia" | "gemini" Required - `last4` string Required: The key's last four characters. The key itself is never returned. - `updated_at` string (date-time) Required **Example** ```json { "last4": "x9Qa", "provider": "sarvam", "updated_at": "2026-10-10T09:32:00Z" } ``` - 401 Not signed in, or the session expired - 422 The request failed validation ### DELETE `/v1/provider-keys/{provider}` Delete a provider key DELETE https://api.ttscacherouter.dev/v1/provider-keys/{provider} Requests for this provider fail with `provider_key_missing` within seconds. #### Parameters - `provider` path, "sarvam" | "raya" | "elevenlabs" | "openai" | "deepgram" | "xai" | "soniox" | "smallestai" | "cartesia" | "gemini" Required #### Responses - 204 Successful Response - 401 Not signed in, or the session expired - 404 Not found, or not visible to the signed-in user - 422 Validation Error ## Models What the router accepts, per provider. No authentication. ### GET `/v1/models` List providers and models GET https://api.ttscacherouter.dev/v1/models Every provider and model the router accepts, with the voices, languages, speed range, sample rates, formats and options each one takes. No authentication. #### Responses - 200 Successful Response - `providers` Provider\[\] Required **Example** ```json { "providers": [ { "docs_url": "https://docs.sarvam.ai/api-reference/text-to-speech/stream", "formats": [ "pcm", "wav", "mp3", "aac", "mulaw", "alaw" ], "id": "sarvam", "models": [ { "id": "bulbul:v3", "languages": [ "hi-IN", "en-IN" ], "voices": [ "shubh", "aditya", "ritu" ], "voices_url": "https://docs.sarvam.ai/api/getting-started/models/bulbul" } ], "name": "Sarvam", "options": [ { "default": 0.6, "description": "Sampling temperature.", "max": 1, "min": 0.01, "name": "temperature", "nullable": false, "type": "number", "models": null }, { "default": 0, "description": "Pitch: lower is deeper, higher is sharper.", "max": 0.5, "min": -0.5, "models": [ "bulbul:v4-flash" ], "name": "pitch", "nullable": false, "type": "number", "values": null } ], "sample_rates": [ 8000, 16000, 22050, 24000 ], "speech_sample_rates": [ 8000, 16000, 22050, 24000, 32000, 44100, 48000 ], "speed": { "max": 2, "min": 0.5 } } ] } ``` ## Voices The voices your org can speak with at each provider. The guide is [Voices](https://docs.ttscacherouter.dev/reference/voices.md). ### GET `/v1/voices` List voices GET https://api.ttscacherouter.dev/v1/voices The voices your org can speak with at one provider, in one shape: the provider's built-in voices and your provider account's own (cloned, designed, added from a library), read live from the provider with your stored provider key; Sarvam's and OpenAI's, which no provider API lists, from the router's catalog. Only voices the router can speak with are listed. Never cached: a voice you add at the provider is listed on the next request. A provider that paginates is read to its last page, so the answer is the whole list. Each voice's `id` is what `voice` takes on `/v1/stream`, `/v1/audio/speech` and the LiveKit plugin. Provider errors (a rejected key, a rate limit) pass through with the provider's status in `provider_status` and its body in `message`. #### Parameters - `provider` query, "sarvam" | "raya" | "elevenlabs" | "openai" | "deepgram" | "xai" | "soniox" | "smallestai" | "cartesia" | "gemini" Required: The provider whose voices to list. - `model` query, string: Only voices this model takes, by the router's model id (as `GET /v1/models` lists them, without the provider prefix), e.g. `bulbul:v4-flash`, `aura-2`, `m1`. #### Responses - 200 Successful Response - `voices` Voice\[\] Required - 400 \`bad_request\` (a missing or unknown provider or model, an unknown query parameter), \`provider_key_missing\` (no key stored for the provider) or \`provider_rejected\` (the provider refused the request, e.g. a wrong provider key) - 401 A missing or invalid API key (\`unauthorized\`) - 402 The org is out of credit (\`insufficient_credits\`) - 429 The provider rate-limited your provider key (\`provider_rate_limited\`) - 502 The provider failed or answered something unexpected (\`provider_unavailable\`) - 504 The provider didn't answer in time (\`provider_timeout\`) ## Speech Shaped like OpenAI’s. The guide is [/v1/audio/speech](https://docs.ttscacherouter.dev/clients/speech.md). ### POST `/v1/audio/speech` Create speech POST https://api.ttscacherouter.dev/v1/audio/speech Synthesizes `input`, shaped like OpenAI's `audio/speech`, so the OpenAI SDK works with `base_url="https://api.ttscacherouter.dev/v1"` and your `tcr_` key. Provider-specific fields go in the body (`extra_body` in the OpenAI SDK). The input is split into sentences; each one your org has synthesized before with the same settings is served from the cache, the rest by the provider with your stored provider key. Audio streams back as each sentence resolves. An error after audio has started can only abort the body. #### Request body - `model` string Required: `/`: `sarvam/bulbul:v3`, `sarvam/bulbul:v4-flash`, `raya/standard`, `raya/m1`, an ElevenLabs model such as `elevenlabs/eleven_flash_v2_5`, an OpenAI model such as `openai/gpt-4o-mini-tts`, `deepgram/aura-2`, `deepgram/aura-1` or `deepgram/flux`, `xai/xai-tts`, `soniox/tts-rt-v2` (Soniox's deprecated `tts-rt-v1` aliases are refused), `smallestai/lightning_v3.1` or `smallestai/lightning_v3.1_pro`, or `cartesia/sonic-3.6`, `cartesia/sonic-3.5` or `cartesia/sonic-3` (or a dated snapshot of one, e.g. `cartesia/sonic-3.6-2026-08-27`), or `gemini/gemini-3.8-flash-tts`, `gemini/gemini-3.8-flash-lite-tts`, `gemini/gemini-3.1-flash-tts-preview`, `gemini/gemini-2.5-flash-preview-tts` or `gemini/gemini-2.5-pro-preview-tts`; see `GET /v1/models`. - `input` string, min length 1 Required: The text to speak. - `voice` string, min length 1: The provider's voice id, required except with Gemini's `speakers`: a Sarvam speaker, a Raya voice id, an ElevenLabs voice id, an OpenAI voice (e.g. `ash`), a Deepgram voice with its language (e.g. `thalia-en`: with `deepgram/aura-2`, Deepgram's `aura-2-thalia-en`) an xAI voice (e.g. `ara`, or a custom voice id), a Soniox voice (e.g. `Adrian`, or a cloned voice's id), a Cartesia voice id (e.g. `f786b574-daa5-4673-aa0c-cbe3e8534c02`) a Gemini voice (e.g. `Kore`; with the 3.8 models also a library voice such as `en-us-tutor-1`; left out with Gemini's `speakers`), or a Smallest AI voice of the model (e.g. `meher` with `lightning_v3.1_pro`, `sophia` with `lightning_v3.1`, or a cloned voice's id, with the model it was cloned for). - `language` string | null, min length 1: The provider's language code, e.g. `hi-IN` (Sarvam), `hi` (Raya), `en` (ElevenLabs), `en` or `auto` (xAI), `en` (Soniox), `en`, `hi` or `auto` (Smallest AI), `en` or `hi` (Cartesia). Required for Sarvam, Raya and Soniox (which reads the text through that language's phonetics); optional for ElevenLabs, which otherwise detects the language, for xAI (default `auto`: xAI detects it) for Smallest AI (default `en`; `auto`: Smallest AI detects it) and for Cartesia (default `en`); not accepted for OpenAI and Gemini, which speak the language of the text, nor for Deepgram, whose voice names its language. - `response_format` "pcm" | "wav" | "mp3" | "aac" | "mulaw" | "alaw" | "flac" | "opus": `pcm` (s16le mono), `wav`, `mp3`, `aac`, `mulaw` or `alaw`. Raya supports `pcm`, `wav` and `mulaw`; ElevenLabs `pcm`, `wav`, `mp3`, `mulaw` and `alaw`; OpenAI `pcm`, `wav`, `mp3` and `aac`; Deepgram `pcm`, `wav`, `mulaw`, `alaw` and (Aura only) `mp3`; xAI, Soniox, Smallest AI and Cartesia `pcm`, `wav`, `mp3`, `mulaw` and `alaw`; Gemini `pcm` and `wav`, and with its 3.8 models `mulaw` and `alaw`. `flac` and `opus` are not supported. - `speed` number: Sarvam 0.5–2.0, Raya 0.5–1.5, OpenAI 0.25–4.0, Deepgram 0.7–1.5 (`aura-2` only; left out, Deepgram's default), xAI 0.7–1.5 (left out, xAI's default, 1.0), Soniox 0.7–1.3, Smallest AI 0.5–2.0, Cartesia 0.6–1.5 (left out, Cartesia's default, 1.0). Not for ElevenLabs, whose speed is `voice_settings.speed`, nor for Gemini (ask for a pace in `instructions`). - `sample_rate` 8000 | 16000 | 22050 | 24000 | 32000 | 44100 | 48000: 32000, 44100 and 48000 are Sarvam's over its REST API (one request per sentence), where `bitrate` is fixed at 128k, and ElevenLabs' `pcm`. ElevenLabs' `mp3` is 22050, 24000 or 44100 Hz; its `mulaw` and `alaw` 8000 Hz (their default). OpenAI is 24000 Hz only. Deepgram's `pcm` is 8000, 16000, 24000, 32000, 44100 or 48000 Hz, its `mulaw` and `alaw` 8000 (default) or 16000, its `mp3` 22050 (default). xAI is 8000, 16000, 22050, 24000, 44100 or 48000 Hz for every format. Soniox's `pcm` is 8000, 16000, 24000, 44100 or 48000 Hz, its `mp3` 16000, 24000, 32000, 44100 or 48000, its `mulaw` and `alaw` 8000 (their default). Smallest AI is 8000, 16000, 24000 or 44100 Hz for every format. Cartesia is 8000, 16000, 22050, 24000, 44100 or 48000 Hz for every format. Gemini's `pcm` is 24000 Hz only, its `mulaw` and `alaw` 8000 (their default). - `stream_format` "audio": Only `audio`: the body is the audio itself. - `instructions` string | null, min length 1: OpenAI and Gemini: how to speak (tone, accent, emotion, pace). OpenAI's `gpt-4o-mini-tts` follows them; `tts-1` and `tts-1-hd` ignore them. Gemini's 2.5 and 3.1 models take them in the prompt (left out, LiveKit's Gemini plugin's default: "Say the text with a proper tone, don't omit or add any words"), its 3.8 models as the delivery style (left out, none). - `temperature` number | null: Sarvam only: 0.01–1.0, default 0.6. - `dict_id` string | null: Sarvam only: a pronunciation dictionary. - `bitrate` "32k" | "48k" | "64k" | "96k" | "128k" | "192k" | 32000 | 64000 | 96000 | 128000 | 192000 | 256000 | 320000 | null: `mp3` (and Sarvam's `aac`). Sarvam: 32k–192k, default `128k`. ElevenLabs: 32k at 22050 Hz, 48k at 24000 Hz, 32k–192k at 44100 Hz (default 128k). Soniox (`mp3` only): an integer in bits per second, 32000, 64000, 96000, 128000, 192000, 256000 or 320000 (left out, Soniox's default, 128000). - `pitch` number | null: Sarvam `bulbul:v4-flash` only: -0.5 (deeper) to 0.5 (higher), default 0. - `loudness` number | null: Sarvam `bulbul:v4-flash` only: 0.1–2.5, default 1.0. - `enable_preprocessing` boolean | null: Sarvam `bulbul:v4-flash` only: normalize numbers and English words in the text before synthesis. Default false. - `min_buffer_size` integer | null, ≥ 30, ≤ 200: Sarvam only: characters Sarvam buffers before it starts synthesizing, 30–200. Default: the router's choice, the least Sarvam allows. - `max_chunk_length` integer | null, ≥ 50, ≤ 500: Sarvam only: the longest text Sarvam synthesizes in one piece, 50–500; it splits longer sentences. Default: the router's choice, the most Sarvam allows, so each sentence is synthesized whole. - `voice_settings` object | null: ElevenLabs only: any of the voice's settings; one left out keeps its stored setting, and null keeps them all. Dialogue models (`eleven_v3`, `eleven_v4`) use `stability` only. - `apply_text_normalization` "auto" | "on" | "off" | null: ElevenLabs only: `auto` (default), `on` or `off`. - `apply_language_text_normalization` boolean | null: ElevenLabs only: language-specific normalization (Japanese). - `enable_ssml_parsing` boolean | null: ElevenLabs only: parse SSML tags. Default false. - `enable_logging` boolean | null: ElevenLabs only: false for ElevenLabs' zero-retention mode (enterprise accounts). Default true. - `pronunciation_dictionary_locators` object\[\] | null: ElevenLabs only: up to 3 pronunciation dictionary versions. - `mip_opt_out` boolean: Deepgram only: opt out of Deepgram's Model Improvement Program. - `bit_rate` 32000 | 48000 | 64000 | 96000 | 128000 | 192000 | null: `mp3` only: the bit rate in bits per second. Deepgram: 32000 or 48000 (left out, Deepgram's default, 48000). xAI: 32000, 64000, 96000, 128000 or 192000 (left out, xAI's default, 128000). Cartesia: MP3's bit rate, 32000, 64000, 96000, 128000 (default) or 192000. - `optimize_streaming_latency` 0 | 1 | 2 | null: xAI only: 0 for the best quality (xAI's default), 1 or 2 for a smaller first chunk and lower time to first audio, with a quality tradeoff at chunk boundaries. Left out, xAI's default. - `text_normalization` boolean | null: xAI only: normalize written forms (numbers, abbreviations, symbols) into spoken forms before synthesis. Left out, xAI's default (false). - `reduce_silence` boolean | null: Soniox only: shorten the pauses between words. Left out, Soniox's default (false). - `volume` number, ≥ 0.5, ≤ 2: Cartesia: loudness relative to the voice's own. Left out, Cartesia's default (1.0). - `emotion` string: Cartesia: guide the delivery (English), e.g. `calm`, `sad` or `angry`; Cartesia lists the rest. Left out, Cartesia reads the emotion from the text. - `pronunciation_dict_id` string: Cartesia: a pronunciation dictionary's id, part of the cache key: a dictionary edited in place reaches cached entries only when they expire. - `speakers` object: Gemini's 3.8 models: two speakers' names, each with a voice (a multi-speaker voice config); replaces `voice`. Requires `speaker`. - `speaker` string, min length 1: Gemini: which of `speakers` voices this request. **Example** ```json { "input": "नमस्ते, मैं सिटी हॉस्पिटल से प्रिया बोल रही हूँ।", "language": "hi-IN", "model": "sarvam/bulbul:v3", "response_format": "mp3", "voice": "shubh" } ``` #### Responses - 200 The audio, streamed. - 400 Invalid request, or the provider refused it - 401 Missing or invalid API key - 402 The org is out of credit (\`insufficient_credits\`): buy credit in the dashboard - 429 Your provider quota is exhausted - 502 The provider failed - 504 The provider timed out ## Provider drop-ins Mirrors of each provider's own endpoints. The WebSockets aren't in OpenAPI; each provider's drop-in page lists them. ### Sarvam drop-in ### POST `/sarvam/text-to-speech/stream` Sarvam: text to speech (HTTP stream) POST https://api.ttscacherouter.dev/sarvam/text-to-speech/stream Mirrors `POST https://api.sarvam.ai/text-to-speech/stream` (\[Sarvam's reference\](https://docs.sarvam.ai/api-reference/text-to-speech/convert-stream)): the same body, response and errors. Send your `tcr_` key in `api-subscription-key`; the request goes to Sarvam verbatim with your stored Sarvam key. Each request is one cache entry; a hit returns the stored audio with the codec's Content-Type. An org out of credit is refused with HTTP 402 in the provider's error shape, code `insufficient_credits`. #### Request body #### Responses - 200 Successful Response ### POST `/sarvam/text-to-speech` Sarvam: text to speech (REST) POST https://api.ttscacherouter.dev/sarvam/text-to-speech Mirrors `POST https://api.sarvam.ai/text-to-speech` (\[Sarvam's reference\](https://docs.sarvam.ai/api-reference/text-to-speech/convert)): the same body, response and errors. Send your `tcr_` key in `api-subscription-key`; the request goes to Sarvam verbatim with your stored Sarvam key. Each request is one cache entry; a hit returns `{"request_id": null, "audios": [...]}`. An org out of credit is refused with HTTP 402 in the provider's error shape, code `insufficient_credits`. #### Request body #### Responses - 200 Successful Response ### Raya drop-in ### POST `/raya/v1/text-to-speech` Raya: text to speech (REST) POST https://api.ttscacherouter.dev/raya/v1/text-to-speech Mirrors `POST https://hub.getraya.app/v1/text-to-speech` (\[Raya's reference\](https://docs.litwizlabs.com/api-reference/text-to-speech/text-to-speech)): the same body, response and errors. Send your `tcr_` key in `X-API-Key`; the request goes to Raya verbatim with your stored Raya key. Each request is one cache entry; a hit returns the stored bytes with Raya's headers. An org out of credit is refused with HTTP 402 in the provider's error shape, code `insufficient_credits`. #### Request body #### Responses - 200 Successful Response ### POST `/raya/v1/text-to-speech/stream` Raya: text to speech (SSE stream) POST https://api.ttscacherouter.dev/raya/v1/text-to-speech/stream Mirrors `POST https://hub.getraya.app/v1/text-to-speech/stream` (\[Raya's reference\](https://docs.litwizlabs.com/api-reference/text-to-speech/text-to-speech-stream)): the same body, response and errors. Send your `tcr_` key in `X-API-Key`; the request goes to Raya verbatim with your stored Raya key. Each request is one cache entry; a hit replays the stored samples as `chunk` events (without `step_time`), then `done`. An org out of credit is refused with HTTP 402 in the provider's error shape, code `insufficient_credits`. #### Request body #### Responses - 200 Successful Response ### GET `/raya/v1/voices` Raya: list voices GET https://api.ttscacherouter.dev/raya/v1/voices Mirrors `GET https://hub.getraya.app/v1/voices` (\[Raya's reference\](https://docs.litwizlabs.com/api-reference/voices/list-voices)): the same query, response and errors. Send your `tcr_` key in `X-API-Key`; the request goes to Raya verbatim with your stored Raya key. Not cached. An org out of credit is refused with HTTP 402 in the provider's error shape, code `insufficient_credits`. #### Responses - 200 Successful Response ### ElevenLabs drop-in ### POST `/elevenlabs/v1/text-to-speech/{voice_id}` ElevenLabs: text to speech POST https://api.ttscacherouter.dev/elevenlabs/v1/text-to-speech/{voice_id} Mirrors `POST https://api.elevenlabs.io/v1/text-to-speech/{voice_id}` (\[ElevenLabs' reference\](https://elevenlabs.io/docs/api-reference/text-to-speech/convert)): the same query, body, response and errors. Send your `tcr_` key in `xi-api-key`; the request goes to ElevenLabs verbatim with your stored ElevenLabs key. Each request is one cache entry; a hit replays the stored response. An org out of credit is refused with HTTP 402 in the provider's error shape, code `insufficient_credits`. #### Parameters - `voice_id` path, string Required #### Request body #### Responses - 200 Successful Response ### POST `/elevenlabs/v1/text-to-speech/{voice_id}/stream` ElevenLabs: text to speech (stream) POST https://api.ttscacherouter.dev/elevenlabs/v1/text-to-speech/{voice_id}/stream Mirrors `POST https://api.elevenlabs.io/v1/text-to-speech/{voice_id}/stream` (\[ElevenLabs' reference\](https://elevenlabs.io/docs/api-reference/text-to-speech/stream)): the same query, body, response and errors. Send your `tcr_` key in `xi-api-key`; the request goes to ElevenLabs verbatim with your stored ElevenLabs key. Each request is one cache entry; a hit replays the stored response. An org out of credit is refused with HTTP 402 in the provider's error shape, code `insufficient_credits`. #### Parameters - `voice_id` path, string Required #### Request body #### Responses - 200 Successful Response ### POST `/elevenlabs/v1/text-to-speech/{voice_id}/with-timestamps` ElevenLabs: text to speech with timestamps POST https://api.ttscacherouter.dev/elevenlabs/v1/text-to-speech/{voice_id}/with-timestamps Mirrors `POST https://api.elevenlabs.io/v1/text-to-speech/{voice_id}/with-timestamps` (\[ElevenLabs' reference\](https://elevenlabs.io/docs/api-reference/text-to-speech/convert-with-timestamps)): the same query, body, response and errors. Send your `tcr_` key in `xi-api-key`; the request goes to ElevenLabs verbatim with your stored ElevenLabs key. Each request is one cache entry; a hit replays the stored response. An org out of credit is refused with HTTP 402 in the provider's error shape, code `insufficient_credits`. #### Parameters - `voice_id` path, string Required #### Request body #### Responses - 200 Successful Response ### POST `/elevenlabs/v1/text-to-speech/{voice_id}/stream/with-timestamps` ElevenLabs: text to speech with timestamps (stream) POST https://api.ttscacherouter.dev/elevenlabs/v1/text-to-speech/{voice_id}/stream/with-timestamps Mirrors `POST https://api.elevenlabs.io/v1/text-to-speech/{voice_id}/stream/with-timestamps` (\[ElevenLabs' reference\](https://elevenlabs.io/docs/api-reference/text-to-speech/stream-with-timestamps)): the same query, body, response and errors. Send your `tcr_` key in `xi-api-key`; the request goes to ElevenLabs verbatim with your stored ElevenLabs key. Each request is one cache entry; a hit replays the stored response. An org out of credit is refused with HTTP 402 in the provider's error shape, code `insufficient_credits`. #### Parameters - `voice_id` path, string Required #### Request body #### Responses - 200 Successful Response ### POST `/elevenlabs/v1/text-to-dialogue/stream` ElevenLabs: text to dialogue (stream) POST https://api.ttscacherouter.dev/elevenlabs/v1/text-to-dialogue/stream Mirrors `POST https://api.elevenlabs.io/v1/text-to-dialogue/stream` (\[ElevenLabs' reference\](https://elevenlabs.io/docs/api-reference/text-to-dialogue/stream)): the same query, body, response and errors. Send your `tcr_` key in `xi-api-key`; the request goes to ElevenLabs verbatim with your stored ElevenLabs key. Each request is one cache entry; a hit replays the stored response. An org out of credit is refused with HTTP 402 in the provider's error shape, code `insufficient_credits`. #### Request body #### Responses - 200 Successful Response ### GET `/elevenlabs/v1/voices` ElevenLabs: list voices GET https://api.ttscacherouter.dev/elevenlabs/v1/voices Mirrors `GET https://api.elevenlabs.io/v1/voices` (\[ElevenLabs' reference\](https://elevenlabs.io/docs/api-reference/voices/search)): the same query, response and errors. Send your `tcr_` key in `xi-api-key`; the request goes to ElevenLabs verbatim with your stored ElevenLabs key. Not cached. An org out of credit is refused with HTTP 402 in the provider's error shape, code `insufficient_credits`. #### Responses - 200 Successful Response ### GET `/elevenlabs/v2/voices` ElevenLabs: list voices (v2) GET https://api.ttscacherouter.dev/elevenlabs/v2/voices Mirrors `GET https://api.elevenlabs.io/v2/voices` (\[ElevenLabs' reference\](https://elevenlabs.io/docs/api-reference/voices/search)): the same query (`page_size`, `next_page_token`, `search`, `category`, `voice_type`, …), response and errors. Send your `tcr_` key in `xi-api-key`; the request goes to ElevenLabs verbatim (path, query and body) with your stored ElevenLabs key. Not cached. An org out of credit is refused with HTTP 402 in the provider's error shape, code `insufficient_credits`. #### Responses - 200 The provider's response ### GET `/elevenlabs/v1/voices/{voice_id}/settings` ElevenLabs: get a voice's settings GET https://api.ttscacherouter.dev/elevenlabs/v1/voices/{voice_id}/settings Mirrors `GET https://api.elevenlabs.io/v1/voices/{voice_id}/settings` (\[ElevenLabs' reference\](https://elevenlabs.io/docs/api-reference/voices/settings/get)): the voice's stored `stability`, `similarity_boost`, `style`, `speed` and `use_speaker_boost`, which a request without `voice_settings` uses. ElevenLabs answers an unknown voice with 400 `voice_not_found`. Send your `tcr_` key in `xi-api-key`; the request goes to ElevenLabs verbatim (path, query and body) with your stored ElevenLabs key. Not cached. An org out of credit is refused with HTTP 402 in the provider's error shape, code `insufficient_credits`. #### Parameters - `voice_id` path, string Required #### Responses - 200 The provider's response ### GET `/elevenlabs/v1/shared-voices` ElevenLabs: list library voices GET https://api.ttscacherouter.dev/elevenlabs/v1/shared-voices Mirrors `GET https://api.elevenlabs.io/v1/shared-voices` (\[ElevenLabs' reference\](https://elevenlabs.io/docs/api-reference/voices/voice-library/get-shared)): the voice library, with the same query (`page`, `page_size`, `search`, `language`, `gender`, `accent`, `sort`, …), response and errors. Add one to your ElevenLabs account with `POST /elevenlabs/v1/voices/add/{public_user_id}/{voice_id}`. Send your `tcr_` key in `xi-api-key`; the request goes to ElevenLabs verbatim (path, query and body) with your stored ElevenLabs key. Not cached. An org out of credit is refused with HTTP 402 in the provider's error shape, code `insufficient_credits`. #### Responses - 200 The provider's response ### POST `/elevenlabs/v1/voices/add/{public_user_id}/{voice_id}` ElevenLabs: add a library voice POST https://api.ttscacherouter.dev/elevenlabs/v1/voices/add/{public_user_id}/{voice_id} Mirrors `POST https://api.elevenlabs.io/v1/voices/add/{public_user_id}/{voice_id}` (\[ElevenLabs' reference\](https://elevenlabs.io/docs/api-reference/voices/voice-library/share)): adds a voice from the library (`GET /elevenlabs/v1/shared-voices`: its `public_owner_id` and `voice_id`) to your ElevenLabs account, with the same body (`new_name`, required; `bookmarked`), response (`{"voice_id"}`) and errors. It then appears in `GET /v1/voices` as `library`. Send your `tcr_` key in `xi-api-key`; the request goes to ElevenLabs verbatim (path, query and body) with your stored ElevenLabs key. Not cached. An org out of credit is refused with HTTP 402 in the provider's error shape, code `insufficient_credits`. #### Parameters - `public_user_id` path, string Required - `voice_id` path, string Required #### Request body - `new_name` string Required - `bookmarked` boolean #### Responses - 200 The provider's response ### OpenAI drop-in ### POST `/openai/v1/audio/speech` OpenAI: create speech POST https://api.ttscacherouter.dev/openai/v1/audio/speech Mirrors `POST https://api.openai.com/v1/audio/speech` (\[OpenAI's reference\](https://developers.openai.com/api/reference/resources/audio/subresources/speech/methods/create)): the same body, response and errors. Set the OpenAI SDK's (or LiveKit's OpenAI plugin's) `base_url` to `https://api.ttscacherouter.dev/openai/v1` and its API key to your `tcr_` key (`Authorization: Bearer`); the request goes to OpenAI verbatim with your stored OpenAI key. Each request is one cache entry; a hit replays the stored audio, or with `stream_format: "sse"` the same `speech.audio.delta` events, then `speech.audio.done` with zero usage (no tokens were spent) and `[DONE]`. An org out of credit is refused with HTTP 402 in the provider's error shape, code `insufficient_credits`. #### Request body - `input` string Required: The text to speak. - `model` string Required: `gpt-4o-mini-tts`, `gpt-4o-mini-tts-2025-12-15`, `tts-1` or `tts-1-hd`. - `voice` string Required: An OpenAI voice, e.g. `ash`. - `instructions` string: How to speak; `gpt-4o-mini-tts` only. - `response_format` "mp3" | "opus" | "aac" | "flac" | "wav" | "pcm" - `speed` number, ≥ 0.25, ≤ 4 - `stream_format` "audio" | "sse": `sse` streams `speech.audio.delta` events; `tts-1` and `tts-1-hd` answer with the audio instead. #### Responses - 200 The audio, or with \`stream_format: "sse"\` an event stream. - 400 OpenAI refused the request - 401 Missing or invalid API key, or no OpenAI key stored for the org (\`invalid_api_key\`) - 402 The org is out of credit (\`insufficient_credits\`) - 429 Your OpenAI quota is exhausted - 502 OpenAI failed - 504 OpenAI timed out ### Deepgram drop-in ### POST `/deepgram/v1/speak` Deepgram: Aura text to speech (REST) POST https://api.ttscacherouter.dev/deepgram/v1/speak Mirrors `POST https://api.deepgram.com/v1/speak` (\[Deepgram's reference\](https://developers.deepgram.com/reference/text-to-speech/speak-request)): the same query, body, response and errors. Set the Deepgram SDK's base URL (or LiveKit's Deepgram plugin's `base_url`) to `https://api.ttscacherouter.dev/deepgram/v1/speak` and its API key to your `tcr_` key (`Authorization: Token`); the request goes to Deepgram verbatim with your stored Deepgram key. Each request is one cache entry: a hit replays the stored audio with its `Content-Type` (the `dg-*` headers come only with a miss). A request whose response type the router can't tell from its query (a repeated parameter, a `callback`) is forwarded and not cached. An org out of credit is refused with HTTP 402 in Deepgram's error shape, `err_code` `insufficient_credits`. The WebSocket at the same path is a drop-in for Deepgram's streaming API; see the docs. #### Parameters - `model` query, string: Deepgram's model id with its voice, e.g. `aura-2-thalia-en` (`/v1/speak`) or `flux-alexis-en` (`/v2/speak`). - `encoding` query, "linear16" | "mulaw" | "alaw" | "mp3" | "opus" | "flac" | "aac": Deepgram's default is `mp3`. - `container` query, "none" | "wav" | "ogg" - `sample_rate` query, integer - `bit_rate` query, integer - `speed` query, number - `mip_opt_out` query, boolean #### Request body - `text` string Required: The text to speak. #### Responses - 200 The audio, in the encoding and container the query asks for. - 400 Deepgram refused the request - 401 A wrong or missing \`tcr_\` key (\`INVALID_AUTH\`), or no Deepgram key stored for the org - 402 The org is out of credit (\`insufficient_credits\`) - 403 Your Deepgram project has no access to the model - 429 Your Deepgram quota is exhausted - 502 Deepgram failed - 504 Deepgram timed out ### POST `/deepgram/v2/speak` Deepgram: Flux text to speech (batch) (REST) POST https://api.ttscacherouter.dev/deepgram/v2/speak Mirrors `POST https://api.deepgram.com/v2/speak` (\[Deepgram's reference\](https://developers.deepgram.com/reference/speak/v-2/audio/generate)): the same query, body, response and errors. Set the Deepgram SDK's base URL (or LiveKit's Deepgram plugin's `base_url`) to `https://api.ttscacherouter.dev/deepgram/v2/speak` and its API key to your `tcr_` key (`Authorization: Token`); the request goes to Deepgram verbatim with your stored Deepgram key. Each request is one cache entry: a hit replays the stored audio with its `Content-Type` (the `dg-*` headers come only with a miss). A request whose response type the router can't tell from its query (a repeated parameter, a `callback`) is forwarded and not cached. An org out of credit is refused with HTTP 402 in Deepgram's error shape, `err_code` `insufficient_credits`. The WebSocket at the same path is a drop-in for Deepgram's streaming API; see the docs. #### Parameters - `model` query, string: Deepgram's model id with its voice, e.g. `aura-2-thalia-en` (`/v1/speak`) or `flux-alexis-en` (`/v2/speak`). - `encoding` query, "linear16" | "mulaw" | "alaw" | "mp3" | "opus" | "flac" | "aac": Deepgram's default is `mp3`. - `container` query, "none" | "wav" | "ogg" - `sample_rate` query, integer - `bit_rate` query, integer - `speed` query, number - `mip_opt_out` query, boolean #### Request body - `text` string Required: The text to speak. #### Responses - 200 The audio, in the encoding and container the query asks for. - 400 Deepgram refused the request - 401 A wrong or missing \`tcr_\` key (\`INVALID_AUTH\`), or no Deepgram key stored for the org - 402 The org is out of credit (\`insufficient_credits\`) - 403 Your Deepgram project has no access to the model - 429 Your Deepgram quota is exhausted - 502 Deepgram failed - 504 Deepgram timed out ### GET `/deepgram/v1/models` Deepgram: list models (Aura) GET https://api.ttscacherouter.dev/deepgram/v1/models Mirrors `GET https://api.deepgram.com/v1/models` (\[Deepgram's reference\](https://developers.deepgram.com/reference/manage/models/list)): its `tts` lists the Aura and Aura-2 voices (`canonical_name` `aura-2-thalia-en` is the router's model `aura-2` and voice `thalia-en`). Send your `tcr_` key as `Authorization: Token` (or `Bearer`); the request goes to Deepgram with your query and your stored Deepgram key. Not cached. #### Responses - 200 The provider's response ### GET `/deepgram/v2/models` Deepgram: list models (Flux) GET https://api.ttscacherouter.dev/deepgram/v2/models Mirrors `GET https://api.deepgram.com/v2/models`, which Deepgram doesn't document: its `tts` lists the Flux voices (`flux-alexis-en` is the router's model `flux` and voice `alexis-en`). Send your `tcr_` key as `Authorization: Token` (or `Bearer`); the request goes to Deepgram with your query and your stored Deepgram key. Not cached. #### Responses - 200 The provider's response ### xAI drop-in ### POST `/xai/v1/tts` xAI: text to speech (REST) POST https://api.ttscacherouter.dev/xai/v1/tts Mirrors `POST https://api.x.ai/v1/tts` (\[xAI's reference\](https://docs.x.ai/developers/rest-api-reference/inference/voice)): the same body, response and errors. Send your `tcr_` key as `Authorization: Bearer`; the request goes to xAI verbatim with your stored xAI key. Each request is one cache entry: a hit replays the stored response with its `Content-Type` (`x-trace-id` comes only with a miss). A body whose response type the router can't tell (an unknown `output_format.codec`) is forwarded and not cached. An org out of credit is refused with HTTP 402 in xAI's error shape, `code` `insufficient_credits`. The WebSocket at the same path is a drop-in for xAI's streaming API; see the docs. #### Request body - `text` string Required: The text to speak. - `voice_id` string: An xAI voice, e.g. `eve` (xAI's default) or `ara`, or a custom voice id. - `language` string Required: A BCP-47 code, e.g. `en`, or `auto`. - `output_format` object: Left out, MP3 at 24 kHz and 128 kbps. - `speed` number, ≥ 0.7, ≤ 1.5 - `optimize_streaming_latency` 0 | 1 - `text_normalization` boolean - `with_timestamps` boolean: Answer JSON with base64 audio and character timings. - `replace` object: Pronunciation replacements, phrase to spoken substitution. #### Responses - 200 The audio in the requested codec, or with \`with_timestamps\` a JSON envelope. - 400 xAI refused the request, a wrong \`tcr_\` key (\`Incorrect API key provided.\`), or no xAI key stored for the org - 401 No API key - 402 The org is out of credit (\`insufficient_credits\`) - 404 Unknown voice - 429 Your xAI quota is exhausted ### GET `/xai/v1/tts/voices` xAI: list voices GET https://api.ttscacherouter.dev/xai/v1/tts/voices Mirrors `GET https://api.x.ai/v1/tts/voices` (\[xAI's reference\](https://docs.x.ai/developers/rest-api-reference/inference/voice)): the same response and errors. Send your `tcr_` key as `Authorization: Bearer`; the request goes to xAI with your stored xAI key. Not cached. An org out of credit is refused with HTTP 402 in xAI's error shape, `code` `insufficient_credits`. #### Responses - 200 xAI's response - 400 A wrong \`tcr_\` key, or no xAI key stored for the org - 401 No API key - 402 The org is out of credit (\`insufficient_credits\`) ### GET `/xai/v1/tts/voices/{voice_id}` xAI: get a voice GET https://api.ttscacherouter.dev/xai/v1/tts/voices/{voice_id} Mirrors `GET https://api.x.ai/v1/tts/voices/{voice_id}` (\[xAI's reference\](https://docs.x.ai/developers/rest-api-reference/inference/voice)): the same response and errors. Send your `tcr_` key as `Authorization: Bearer`; the request goes to xAI with your stored xAI key. Not cached. #### Parameters - `voice_id` path, string Required #### Responses - 200 xAI's response - 400 A wrong \`tcr_\` key, or no xAI key stored for the org - 401 No API key - 402 The org is out of credit (\`insufficient_credits\`) ### Soniox drop-in ### POST `/soniox/tts` Soniox: generate speech (REST) POST https://api.ttscacherouter.dev/soniox/tts Mirrors `POST https://tts-rt.soniox.com/tts` (\[Soniox's reference\](https://soniox.com/docs/api-reference/tts/generate_tts)): the same body, response and errors. Send your `tcr_` key as `Authorization: Bearer`; the request goes to Soniox verbatim with your stored Soniox key. Each request is one cache entry: a hit replays the stored audio with the `Content-Type` Soniox answers that `audio_format` with. A body whose response type the router can't tell (an unknown `audio_format`) is forwarded and not cached. An org out of credit is refused with HTTP 402 in Soniox's error shape, `error_type` `insufficient_credits`. The real-time WebSocket, `/soniox/tts-websocket`, is a drop-in for Soniox's; see the docs. #### Request body - `model` string Required: A Soniox model, e.g. `tts-rt-v2`. - `language` string Required: The language the text is read in, e.g. `en`. - `voice` string Required: A Soniox voice, e.g. `Adrian`, or a cloned voice's id. - `audio_format` "pcm_s16le" | "pcm_f32le" | "pcm_mulaw" | "pcm_alaw" | "wav" | "mp3" | "aac" | "opus" | "flac" Required - `text` string Required: The text to speak. - `sample_rate` integer | null: Left out, the format's default (24000; 8000 for `pcm_mulaw` and `pcm_alaw`). - `bitrate` integer | null: Lossy formats only, in bits per second. - `speed` number | null, ≥ 0.7, ≤ 1.3 - `reduce_silence` boolean | null - `client_reference_id` string | null, max length 256 #### Responses - 200 The audio in the requested format, chunked. - 400 Soniox refused the request (e.g. an unknown voice or language) - 401 A missing or wrong \`tcr_\` key, or no Soniox key stored for the org (\`unauthenticated\`) - 402 The org is out of credit (\`insufficient_credits\`), or your Soniox balance is exhausted - 429 Your Soniox rate or concurrency limit ### GET `/soniox/v1/voices` Soniox: list cloned voices GET https://api.ttscacherouter.dev/soniox/v1/voices Mirrors `GET https://api.soniox.com/v1/voices` (\[Soniox's reference\](https://soniox.com/docs/api-reference/tts/voices/get_voices)): your Soniox project's cloned voices, with the same query (`limit`, `cursor`), response and errors. Send your `tcr_` key as `Authorization: Bearer`; the request goes to Soniox (`api.soniox.com`) with your query and your stored Soniox key. Not cached. #### Responses - 200 The provider's response ### GET `/soniox/v1/tts-models` Soniox: list models GET https://api.ttscacherouter.dev/soniox/v1/tts-models Mirrors `GET https://api.soniox.com/v1/tts-models` (\[Soniox's reference\](https://soniox.com/docs/api-reference/tts/get_tts_models)): the text-to-speech models with their languages and built-in voices. Send your `tcr_` key as `Authorization: Bearer`; the request goes to Soniox (`api.soniox.com`) with your query and your stored Soniox key. Not cached. #### Responses - 200 The provider's response ### Smallest AI drop-in ### POST `/smallestai/waves/v1/tts` Smallest AI: synthesize speech (REST) POST https://api.ttscacherouter.dev/smallestai/waves/v1/tts Mirrors `POST https://api.smallest.ai/waves/v1/tts` (\[Smallest AI's reference\](https://docs.smallest.ai/api-reference/models/text-to-speech/synthesize-speech)): the same body, response and errors. Send your `tcr_` key as `Authorization: Bearer`; the request goes to Smallest AI verbatim with your stored Smallest AI key. Each request is one cache entry: a hit replays the stored audio with the `Content-Type` its `output_format` implies (`application/octet-stream` for `pcm`, `audio/mpeg`, `audio/wav`, `audio/basic` for `ulaw` and `alaw`). A body with `pronunciation_dicts` is cached by the dictionaries' ids: a dictionary edited in place reaches entries cached before only when they expire. #### Request body - `text` string Required: The text to speak. - `voice_id` string Required: A Smallest AI voice of the model, e.g. `meher` (`lightning_v3.1_pro`) or `sophia` (`lightning_v3.1`), or a cloned voice's id, with the model it was cloned for. - `model` string: `lightning_v3.1` (Smallest AI's default) or `lightning_v3.1_pro`. - `sample_rate` 8000 | 16000 | 24000 | 44100: Left out, Smallest AI's default (44100). - `speed` number, ≥ 0.5, ≤ 2 - `language` string: A language code of the model, e.g. `en` or `hi`, or `auto`. Left out, Smallest AI's default. - `output_format` "pcm" | "mp3" | "wav" | "ulaw" | "alaw" - `pronunciation_dicts` string\[\]: Pronunciation dictionary ids, part of the cache key: a dictionary edited in place reaches cached entries only when they expire. #### Responses - 200 The audio in the requested format. - 400 Smallest AI refused the request (e.g. a voice of another model, or an unsupported language or sample rate) - 401 A missing or wrong \`tcr_\` key (\`unauthorized\`), or no Smallest AI key stored for the org (with a \`message\` that links to the dashboard) - 402 The org is out of credit (\`insufficient_credits\`) ### POST `/smallestai/waves/v1/tts/live` Smallest AI: synthesize speech (SSE) POST https://api.ttscacherouter.dev/smallestai/waves/v1/tts/live Mirrors `POST https://api.smallest.ai/waves/v1/tts/live` (\[Smallest AI's reference\](https://docs.smallest.ai/api-reference/models/text-to-speech/synthesize-speech-sse)): the same body, server-sent events (`audio` events with base64 audio, then `done`) and errors. Send your `tcr_` key as `Authorization: Bearer`; the request goes to Smallest AI verbatim with your stored Smallest AI key. Each request is one cache entry: a hit replays the stored audio as Smallest AI's events. A body with `pronunciation_dicts` is cached by the dictionaries' ids: a dictionary edited in place reaches entries cached before only when they expire. The WebSocket at the same path, `wss://api.ttscacherouter.dev/smallestai/waves/v1/tts/live`, is a drop-in for Smallest AI's; see the docs. #### Request body - `text` string Required: The text to speak. - `voice_id` string Required: A Smallest AI voice of the model, e.g. `meher` (`lightning_v3.1_pro`) or `sophia` (`lightning_v3.1`), or a cloned voice's id, with the model it was cloned for. - `model` string: `lightning_v3.1` (Smallest AI's default) or `lightning_v3.1_pro`. - `sample_rate` 8000 | 16000 | 24000 | 44100: Left out, Smallest AI's default (44100). - `speed` number, ≥ 0.5, ≤ 2 - `language` string: A language code of the model, e.g. `en` or `hi`, or `auto`. Left out, Smallest AI's default. - `output_format` "pcm" | "mp3" | "wav" | "ulaw" | "alaw" - `pronunciation_dicts` string\[\]: Pronunciation dictionary ids, part of the cache key: a dictionary edited in place reaches cached entries only when they expire. #### Responses - 200 Server-sent events: \`audio\` events, each \`{"audio": \, "done": false}\`, then \`{"done": true}\`. - 400 Smallest AI refused the request (e.g. a voice of another model, or an unsupported language or sample rate) - 401 A missing or wrong \`tcr_\` key (\`unauthorized\`), or no Smallest AI key stored for the org (with a \`message\` that links to the dashboard) - 402 The org is out of credit (\`insufficient_credits\`) ### GET `/smallestai/waves/v1/{model}/get_voices` Smallest AI: list voices GET https://api.ttscacherouter.dev/smallestai/waves/v1/{model}/get_voices Mirrors `GET https://api.smallest.ai/waves/v1/{model}/get_voices` (\[Smallest AI's reference\](https://docs.smallest.ai/api-reference/models/text-to-speech/get-waves-voices)): a model's voices, with the same response and errors. Send your `tcr_` key as `Authorization: Bearer`; the request goes to Smallest AI. Not cached. #### Parameters - `model` path, string Required: The model's id in Smallest AI's voice paths: `lightning-v3.1` or `lightning-v3.1-pro`. #### Responses - 200 Smallest AI's response ### GET `/smallestai/waves/v1/voice-cloning` Smallest AI: list cloned voices GET https://api.ttscacherouter.dev/smallestai/waves/v1/voice-cloning Mirrors `GET https://api.smallest.ai/waves/v1/voice-cloning` (\[Smallest AI's reference\](https://docs.smallest.ai/api-reference/models/text-to-speech/list-voice-clones)): your Smallest AI account's voice clones, with the same response and errors. Send your `tcr_` key as `Authorization: Bearer`; the request goes to Smallest AI with your stored Smallest AI key. Not cached. #### Responses - 200 Smallest AI's response ### Cartesia drop-in ### POST `/cartesia/tts/bytes` Cartesia: synthesize speech (bytes) POST https://api.ttscacherouter.dev/cartesia/tts/bytes Mirrors `POST https://api.cartesia.ai/tts/bytes` (\[Cartesia's reference\](https://docs.cartesia.ai/api-reference/tts/bytes)): the same body, response and errors. Send your `tcr_` key as `X-API-Key` or `Authorization: Bearer`; the request goes to Cartesia verbatim with your stored Cartesia key. Each request is one cache entry (the body and the API version): a hit replays the stored audio with the `Content-Type` its `output_format.container` implies (`audio/pcm`, `audio/wav`, `audio/mpeg`). #### Parameters - `Cartesia-Version` header, string Required: Cartesia's API version, as Cartesia takes it. Errors are plain `Title: Message` text before 2026-03-01 and JSON from it on, as Cartesia's. #### Request body - `transcript` string Required: The text to speak. - `model_id` string Required: A Cartesia model, e.g. `sonic-3.6`, `sonic-3` or a dated snapshot. - `voice` any Required: A voice id, or `{"mode": "id", "id": …}` as older API versions take it. - `output_format` object Required: `{"container": "raw" | "wav" | "mp3", "encoding", "sample_rate", "bit_rate"}`, as Cartesia takes it. - `language` string - `generation_config` object: `speed`, `volume` and `emotion`, as Cartesia takes them. - `pronunciation_dict_id` string: Part of the cache key: a dictionary edited in place reaches cached entries only when they expire. #### Responses - 200 The audio in the requested format. - 400 Cartesia refused the request (e.g. an unsupported model, voice or format) - 401 A missing or wrong \`tcr_\` key, or no Cartesia key stored for the org - 402 The org is out of credit ### POST `/cartesia/tts/sse` Cartesia: synthesize speech (SSE) POST https://api.ttscacherouter.dev/cartesia/tts/sse Mirrors `POST https://api.cartesia.ai/tts/sse` (\[Cartesia's reference\](https://docs.cartesia.ai/api-reference/tts/sse)): the same body, events and errors. Each request is one cache entry (the body minus `context_id`, and the API version): a hit replays the same `chunk`, `timestamps` and `done` events. #### Parameters - `Cartesia-Version` header, string Required: Cartesia's API version, as Cartesia takes it. Errors are plain `Title: Message` text before 2026-03-01 and JSON from it on, as Cartesia's. #### Request body - `transcript` string Required: The text to speak. - `model_id` string Required: A Cartesia model, e.g. `sonic-3.6`, `sonic-3` or a dated snapshot. - `voice` any Required: A voice id, or `{"mode": "id", "id": …}` as older API versions take it. - `output_format` object Required: `{"container": "raw" | "wav" | "mp3", "encoding", "sample_rate", "bit_rate"}`, as Cartesia takes it. - `language` string - `generation_config` object: `speed`, `volume` and `emotion`, as Cartesia takes them. - `pronunciation_dict_id` string: Part of the cache key: a dictionary edited in place reaches cached entries only when they expire. - `add_timestamps` boolean: Word timestamps as `timestamps` events. - `context_id` string: Echoed in the events; not part of the cache key. #### Responses - 200 Cartesia's events. - 400 Cartesia refused the request - 401 A missing or wrong \`tcr_\` key, or no Cartesia key stored for the org - 402 The org is out of credit ### GET `/cartesia/voices` Cartesia: list voices GET https://api.ttscacherouter.dev/cartesia/voices Mirrors `GET https://api.cartesia.ai/voices` (\[Cartesia's reference\](https://docs.cartesia.ai/api-reference/voices/list)): the same query, response and errors, with your stored Cartesia key. Not cached. #### Responses - 200 Cartesia's response ### GET `/cartesia/voices/{id}` Cartesia: get a voice GET https://api.ttscacherouter.dev/cartesia/voices/{id} Mirrors `GET https://api.cartesia.ai/voices/{id}` (\[Cartesia's reference\](https://docs.cartesia.ai/api-reference/voices/get)), with your stored Cartesia key. Not cached. #### Parameters - `id` path, string Required: The voice's id. #### Responses - 200 Cartesia's response ### Gemini drop-in ### POST `/gemini/v1beta/models/{model}:streamGenerateContent` Gemini: generate speech (streamed) POST https://api.ttscacherouter.dev/gemini/v1beta/models/{model}:streamGenerateContent Mirrors `POST https://generativelanguage.googleapis.com/v1beta/models/{model}:streamGenerateContent` (\[Gemini's reference\](https://ai.google.dev/api/generate-content#method:-models.streamgeneratecontent)): the same body, events and errors. Send your `tcr_` key as `x-goog-api-key` or `?key=`; the request goes to Gemini verbatim with your stored Gemini key. With `alt=sse`, each request is one cache entry: a hit replays the stored audio as Gemini's events (each with `inlineData` parts, the last with `finishReason: STOP` and zero usage). Without it, the request is forwarded and not cached. #### Parameters - `model` path, string Required: A Gemini model, passed through (any model Gemini serves). - `alt` query, "sse": `sse`, as Google's SDK sends it. #### Request body - `contents` object\[\] Required: As Gemini takes them. A request is cached when it has one content of one text part (what Google's SDK sends for a string): that text, and the rest of the body, are its cache key. - `generationConfig` object: `responseModalities: ["AUDIO"]`, `speechConfig` and, for the 3.8 models, `response_format`, as Gemini takes them. #### Responses - 200 Gemini's events: each \`data:\` line a response whose parts carry base64 audio. - 400 Gemini refused the request (e.g. an unknown voice or format), or a wrong \`tcr_\` key or no Gemini key stored for the org (\`API_KEY_INVALID\`) - 402 The org is out of credit - 403 No \`tcr_\` key (\`PERMISSION_DENIED\`) ### POST `/gemini/v1beta/models/{model}:generateContent` Gemini: generate speech POST https://api.ttscacherouter.dev/gemini/v1beta/models/{model}:generateContent Mirrors `POST https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent` (\[Gemini's reference\](https://ai.google.dev/api/generate-content#method:-models.generatecontent)): the same body, response and errors, with your stored Gemini key. Each request is one cache entry: a hit replays the stored audio as one response with zero usage. #### Parameters - `model` path, string Required: A Gemini model, passed through (any model Gemini serves). #### Request body - `contents` object\[\] Required: As Gemini takes them. A request is cached when it has one content of one text part (what Google's SDK sends for a string): that text, and the rest of the body, are its cache key. - `generationConfig` object: `responseModalities: ["AUDIO"]`, `speechConfig` and, for the 3.8 models, `response_format`, as Gemini takes them. #### Responses - 200 Gemini's response: its part carries the base64 audio. - 400 Gemini refused the request (e.g. an unknown voice or format), or a wrong \`tcr_\` key or no Gemini key stored for the org (\`API_KEY_INVALID\`) - 402 The org is out of credit - 403 No \`tcr_\` key (\`PERMISSION_DENIED\`) ### GET `/gemini/v1beta/voices` Gemini: list voices GET https://api.ttscacherouter.dev/gemini/v1beta/voices Mirrors `GET https://generativelanguage.googleapis.com/v1beta/voices` (\[Gemini's reference\](https://ai.google.dev/gemini-api/docs/speech-generation#voices)): the same query, response and errors, with your stored Gemini key. Not cached. #### Responses - 200 Gemini's response ### GET `/gemini/v1beta/models` Gemini: list models GET https://api.ttscacherouter.dev/gemini/v1beta/models Mirrors `GET https://generativelanguage.googleapis.com/v1beta/models` (\[Gemini's reference\](https://ai.google.dev/api/models#method:-models.list)): the same query, response and errors, with your stored Gemini key. Not cached. #### Responses - 200 Gemini's response ### GET `/gemini/v1beta/models/{model}` Gemini: get a model GET https://api.ttscacherouter.dev/gemini/v1beta/models/{model} Mirrors `GET https://generativelanguage.googleapis.com/v1beta/models/{model}` (\[Gemini's reference\](https://ai.google.dev/api/models#method:-models.get)): the same query, response and errors, with your stored Gemini key. Not cached. #### Parameters - `model` path, string Required: A Gemini model, passed through (any model Gemini serves). #### Responses - 200 Gemini's response ## OpenAPI document The whole HTTP API as OpenAPI 3.1, for code generators and API clients. The WebSocket protocol has [its own page](https://docs.ttscacherouter.dev/clients/stream.md). ```shell curl https://api.ttscacherouter.dev/openapi.json ``` It also lists the dashboard’s session endpoints (`/api/…`), which take the dashboard’s sign-in cookie rather than an API key and aren’t meant for your code. --- # The dashboard > Connect, usage, billing, API keys, provider keys, members and roles, retention and your account. Sign in at [app.ttscacherouter.dev](https://app.ttscacherouter.dev/app). Everything in it belongs to your **org**: its API keys, provider keys, cache and usage. You get an org of your own when you sign up, and can be a member of others. Switch orgs from the name at the top; the dashboard remembers your choice. ## Connect The dashboard's home: your API key, your provider's key, and the code to connect your agent. - **Your API key** is at the top, ready to copy. Sign-up creates one, named `Default`. Readers see its name and first characters; a writer or admin can copy it. - Pick your provider, then add your key for it right there; the provider’s tab shows whether it’s saved. - Connect a LiveKit agent with our plugin or with the provider’s own LiveKit plugin, or a Pipecat bot with our [Pipecat service](https://docs.ttscacherouter.dev/pipecat.md): the install command, the environment variable (with your key filled in) and the code, with the changed lines marked. The LiveKit ones are the snippets on [Sarvam](https://docs.ttscacherouter.dev/livekit/sarvam.md), [Raya](https://docs.ttscacherouter.dev/livekit/raya.md), [ElevenLabs](https://docs.ttscacherouter.dev/livekit/elevenlabs.md), [OpenAI](https://docs.ttscacherouter.dev/livekit/openai.md), [Deepgram](https://docs.ttscacherouter.dev/livekit/deepgram.md), [xAI](https://docs.ttscacherouter.dev/livekit/xai.md), [Soniox](https://docs.ttscacherouter.dev/livekit/soniox.md), [Smallest AI](https://docs.ttscacherouter.dev/livekit/smallestai.md), [Cartesia](https://docs.ttscacherouter.dev/livekit/cartesia.md) and [Gemini with LiveKit](https://docs.ttscacherouter.dev/livekit/gemini.md). ## Usage What the cache saved: sentences served from it, against those your provider synthesized and billed. - The hit rate, and the characters kept off your provider’s bill, over the last 24 hours or the last 7, 30 or 90 days. - A column per hour or per day, cached under synthesized, and the same split per provider. - Hours are shown in your time zone and days are UTC days. Counts arrive within about a minute. A sentence counts once the provider has finished it, including one that finished after your agent was interrupted; a failed one doesn’t count. - On the drop-in endpoints that cache whole requests (REST and HTTP stream), each request counts as one sentence. ## Billing Your org's credit: what it has, buying more, and where it went. - The balance, and about how many characters served from the cache it covers. - Writers and admins buy credit packs here, paid through Dodo Payments; the credit appears within seconds. Readers see the balance and history. - The history lists the free credit, every purchase and refund, and usage summed per UTC day. - Below $1 every dashboard page warns you; at zero, new connections are refused until you buy more. Prices, refunds and what happens at zero are in [Pricing and billing](https://docs.ttscacherouter.dev/pricing.md). ## API keys The keys your agents authenticate with. - Create a key and name it after where it’s used, then copy it into your agent’s secrets. - The list shows each key’s name, its first characters and when it was created. Writers and admins can reveal any key and copy it again later, except one created before keys could be revealed: it was shown once, at creation, and is marked “shown once”. - Deleting a key stops it within a few seconds, including on connections already open. Your cache stays. More in [Authentication](https://docs.ttscacherouter.dev/reference/authentication.md). ## Provider keys Your own key for each provider, used only to call that provider for your org's requests. - Set, replace or delete one key per provider. A change is used within a few seconds. - Keys are stored encrypted and never shown again, only their last four characters. - Saving doesn’t check a key with the provider: a wrong one surfaces as the provider’s refusal on your first request. Without a key, that provider’s requests fail with `provider_key_missing`. The same actions are in the [REST API](https://docs.ttscacherouter.dev/reference/api.md#provider-keys), with an API key. ## Members and roles Who can use your org's dashboard, under Settings. - **Reader** sees every page and changes nothing. **Writer** also creates, copies and deletes API keys, sets provider keys, changes settings and buys credit. **Admin** also invites and removes members and sets their roles. You are the admin of the org you get at sign-up. - Admins invite by email with a role. Someone who already has an account joins right away; anyone else joins the first time they sign in with that email, and gets an org of their own too. No email is sent, so tell them. - Anyone can leave an org from their own row. An org always keeps an admin and a member: its only admin has to make someone else an admin before leaving or stepping down, and its last member can’t leave. Orgs are never deleted. - API keys belong to the org, not to whoever created them: removing a member leaves their keys working. ## Cache retention How long cached audio is kept, under Settings. Pick 1, 7, 30, 90 or 365 days (the default), any whole number of days up to 3,650, or off. What each choice does to audio already cached is in [How long it is kept](https://docs.ttscacherouter.dev/index.md#retention). Settings also has your org’s name and its ID, which never changes. ## Your account Who you're signed in as, under Settings. To delete your account, write to from the address you sign in with: we erase your account, and every org where you are the only member, with its keys and cached audio. Orgs you share keep running without you. Delete your API keys first to stop all traffic at once. --- # Pricing and billing > What the router charges for, the free credit, buying more, refunds, and what happens at zero. ## The price You pay for what the cache serves, nothing else. - **$0.002 per 1,000 characters served from the cache**, counted on the same characters the dashboard’s [Usage](https://app.ttscacherouter.dev/app/usage) page shows. - **Characters your provider synthesizes are free here.** Your provider already bills you for them, on your own key, at your own rates. - No subscription, no minimum and no per-request fee. A 50-character sentence served from the cache costs $0.0001. Usage is taken from your credit about once a minute, so the balance in the dashboard can trail your traffic by a minute. ## Free credit Every new org starts with **$10 of credit**, enough for 5 million characters served from the cache. You don’t need a card to sign up. ## Buying credit Writers and admins of an org buy prepaid packs on the dashboard’s [Billing](https://app.ttscacherouter.dev/app/billing) page. Each pack adds exactly its price in credit. **Credit never expires.** | Pack | Characters served from cache | | --- | --- | | $10 | 5 million | | $50 | 25 million | | $100 | 50 million | | $500 | 250 million | | $1,000 | 500 million | | $5,000 | 2.5 billion | Checkout is hosted by Dodo Payments, our merchant of record: it takes the payment, handles any taxes that apply where you are, and issues the invoice. The credit appears on the Billing page within seconds of paying. ## Running out The router never cuts off a call in progress. When your balance reaches zero, it refuses **new** connections and requests with `insufficient_credits` (HTTP 402) until you buy more; connections already open carry on until they close, so the balance can dip slightly below zero. The next purchase settles that. The dashboard warns you below $1. See [Errors](https://docs.ttscacherouter.dev/reference/errors.md#codes) for how each surface reports the refusal. > **Plan for it** > > With the LiveKit plugin, a refusal is a non-retryable `APIStatusError` with status 402. A `FallbackAdapter` with your provider’s own plugin as the fallback keeps an agent speaking either way. ## Refunds - Credit you bought and haven’t used is refundable on request within 30 days of the purchase, to the original payment method, through Dodo Payments. - For a pack you’ve partly used, the refund covers the unused part. - Credit already used, and the $10 free credit, aren’t refundable. Write to from your account’s email with the payment’s date. A refund removes the refunded credit from your balance and shows in its history. The full policy is in the [terms](https://ttscacherouter.dev/terms#refunds). ## Payments and invoices Dodo Payments emails the receipt and invoice for every purchase. The Billing page lists every purchase, refund and day of usage against your credit. --- # Security and data handling > How keys are stored, what the cache holds, what is logged, and where data lives. ## Keys - **Provider keys** are encrypted with AES-256-GCM, bound to your org so they can’t be moved to another, and decrypted only in the router’s memory to call that provider for your org’s requests. They are never returned (only their last four characters) and never logged. - **API keys** are stored encrypted the same way, bound to your org and the key, so a writer or admin of your org can reveal one again; readers can’t. The router authenticates requests by a SHA-256 hash of each key and never decrypts them to do it. - Everything is served over TLS, and the dashboard’s actions are protected against cross-site requests. ## Text and audio - Each sentence that isn’t cached goes to the provider your request names, with your key, on your account. A sentence served from the cache reaches no provider. - The cache stores the provider’s audio under a one-way hash of the sentence and its settings, one object per sentence under your org’s own prefix. **The text itself is never stored.** Orgs never share cached audio. - Cached audio is deleted within your org’s [retention](https://docs.ttscacherouter.dev/index.md#retention) plus one day of its last use; set it to off to cache nothing. - Your text and audio are never used to train models, and never sold. - What the provider keeps is between you and the provider. With ElevenLabs, `enable_logging=False` asks for its zero-retention mode (enterprise accounts). ## Logs The router’s logs and metrics never contain sentence text, provider keys or API keys. They record your org, the provider and model, lengths, timings, errors and IP addresses. The router enforces this in its types: text and keys print only their length or a redaction. ## Where data lives - The router, your account and org data, logs and metrics: DigitalOcean, San Francisco, United States. - Cached audio: DigitalOcean Spaces, San Francisco, United States. - Synthesis: Sarvam and Raya in India; ElevenLabs, OpenAI, Deepgram, xAI and Gemini (Google) in the regions they operate; Soniox in the United States; Smallest AI in India or the United States, whichever it routes the request to; Cartesia in the region it routes the request to. ## Deleting data - Delete an API key or a provider key in the dashboard; it stops working within a few seconds. - Turn your retention off to delete all cached audio within about two days. Lowering it deletes older audio, but audio still in use stays cached. - To delete your account, org, keys and cached audio, write to . The [privacy policy](https://ttscacherouter.dev/privacy) is the full statement. The code is open source, so each of these is checkable. To report a vulnerability, write to .