# 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", …}, …]}}
```
