---
title: Gemini
description: Call Gemini at half price through its native API with the Google Gen AI SDK or Gemini CLI: generateContent, SSE streaming, thinking and countTokens.
---

Half Off Dev exposes Gemini's own `generateContent`, `streamGenerateContent` and `countTokens` methods under `/v1beta/models/{model}`. Give the Google Gen AI SDK the base URL `https://halfoff.dev` and your Half Off Dev key as its API key, and existing Gemini code keeps working at half the list price. Bodies are forwarded unchanged, so [Google's API reference](https://ai.google.dev/api/generate-content) describes every field.

## Configure the SDK {#connect}

The SDKs authenticate with `x-goog-api-key`. A `?key=` query parameter is accepted too.

```bash title="cURL"
curl "https://halfoff.dev/v1beta/models/gemini-3.1-pro:generateContent" \
  -H "x-goog-api-key: $HALFOFF_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "contents": [{"role": "user", "parts": [{"text": "Summarize what a CDN does for a small blog."}]}]
  }'
```

```python title="Python"
import os
from google import genai
from google.genai import types

client = genai.Client(
    api_key=os.environ["HALFOFF_API_KEY"],
    http_options=types.HttpOptions(base_url="https://halfoff.dev"),
)
response = client.models.generate_content(
    model="gemini-3.1-pro",
    contents="Summarize what a CDN does for a small blog.",
)
print(response.text)
```

```js title="Node.js"
import { GoogleGenAI } from '@google/genai';

const ai = new GoogleGenAI({
  apiKey: process.env.HALFOFF_API_KEY,
  httpOptions: { baseUrl: 'https://halfoff.dev' },
});
const response = await ai.models.generateContent({
  model: 'gemini-3.1-pro',
  contents: 'Summarize what a CDN does for a small blog.',
});
console.log(response.text);
```

Install with `pip install google-genai` or `npm install @google/genai`. Model ids come from [`GET /v1/models`](/docs/models/pricing), and Google's own spellings such as `gemini-3.1-pro-preview` resolve to the same model. For a list in Gemini's response shape, call `GET /v1beta/models`.

## Stream with server-sent events {#streaming}

Request `:streamGenerateContent?alt=sse` to receive SSE. Every event is a partial `GenerateContentResponse`, and the final one holds the complete `usageMetadata`. The SDKs append `alt=sse` on their own.

```bash title="cURL"
curl -N "https://halfoff.dev/v1beta/models/gemini-3-flash:streamGenerateContent?alt=sse" \
  -H "x-goog-api-key: $HALFOFF_API_KEY" \
  -H "content-type: application/json" \
  -d '{"contents": [{"role": "user", "parts": [{"text": "Write a four-line poem about compilers."}]}]}'
```

```python title="Python"
for chunk in client.models.generate_content_stream(
    model="gemini-3-flash",
    contents="Write a four-line poem about compilers.",
):
    print(chunk.text or "", end="", flush=True)
```

```js title="Node.js"
const stream = await ai.models.generateContentStream({
  model: 'gemini-3-flash',
  contents: 'Write a four-line poem about compilers.',
});
for await (const chunk of stream) {
  process.stdout.write(chunk.text ?? '');
}
```

Leave out `alt=sse` and the method streams a single JSON array instead. It works, but SSE is simpler to parse incrementally and is the only mode in which the gateway can deliver a proper error event if the stream stalls. See [Streaming](/docs/models/streaming).

## System instructions and thinking {#system-and-thinking}

`systemInstruction`, `generationConfig`, `safetySettings` and function declarations in `tools` go through exactly as Google documents them. Gemini 3 models think by default; tune that with `thinkingConfig` following [Google's thinking guide](https://ai.google.dev/gemini-api/docs/thinking).

```python title="Python"
response = client.models.generate_content(
    model="gemini-3.1-pro",
    contents="Suggest a weekly study plan for learning Rust in two months.",
    config=types.GenerateContentConfig(
        system_instruction="You are a practical programming coach. Use bullet points.",
        max_output_tokens=4096,
    ),
)
print(response.text)
print(response.usage_metadata)
```

```js title="Node.js"
const response = await ai.models.generateContent({
  model: 'gemini-3.1-pro',
  contents: 'Suggest a weekly study plan for learning Rust in two months.',
  config: {
    systemInstruction: 'You are a practical programming coach. Use bullet points.',
    maxOutputTokens: 4096,
  },
});
console.log(response.text);
console.log(response.usageMetadata);
```

Thinking shows up as `usageMetadata.thoughtsTokenCount` and is billed at the output rate along with `candidatesTokenCount`. As in Google's API, thought summaries come back as parts marked `"thought": true` only if you set `generationConfig.thinkingConfig.includeThoughts: true`; otherwise the response contains no thought parts.

## Count tokens {#count-tokens}

`:countTokens` takes the same `contents` you would generate with and returns `totalTokens`. It is free and runs on a zero balance. Treat the number as approximate: good for checking against the context window, not for predicting the charge to the cent.

```bash title="cURL"
curl "https://halfoff.dev/v1beta/models/gemini-3.1-pro:countTokens" \
  -H "x-goog-api-key: $HALFOFF_API_KEY" \
  -H "content-type: application/json" \
  -d '{"contents": [{"role": "user", "parts": [{"text": "Measure this sentence."}]}]}'
```

```python title="Python"
result = client.models.count_tokens(model="gemini-3.1-pro", contents="Measure this sentence.")
print(result.total_tokens)
```

```js title="Node.js"
const result = await ai.models.countTokens({ model: 'gemini-3.1-pro', contents: 'Measure this sentence.' });
console.log(result.totalTokens);
```

## Gemini CLI {#gemini-cli}

The Gemini CLI takes its endpoint and key from environment variables:

```bash
export GOOGLE_GEMINI_BASE_URL=https://halfoff.dev
export GEMINI_API_KEY="$HALFOFF_API_KEY"
gemini -m gemini-3.1-pro
```

The full walkthrough is in [Gemini CLI](/docs/integrations/gemini-cli).

## Caching {#caching}

Gemini caches repeated prompt prefixes automatically (implicit caching). Cached tokens are reported in `usageMetadata.cachedContentTokenCount` and billed at the cache-read rate. Each conversation is routed back to the same channel so that cache stays warm; see [Prompt caching](/docs/models/caching#gemini).

## Not supported {#not-supported}

These Gemini API features return `404`: `cachedContents` (explicit context caching), file uploads, `embedContent` and `batchEmbedContents`, batch mode, and the Live API.

## Request headers {#headers}

| Header | Behavior |
| --- | --- |
| `x-goog-api-key` | Your Half Off Dev key. `Authorization: Bearer` and `?key=` work as well. |
| `x-halfoff-group` | Optional. Picks the [channel group](/docs/models/channel-groups) for this request, unless the key is already bound to one. |
| `x-session-id` | Optional. Marks requests as one conversation for [cache-aware routing](/docs/models/caching#explicit-session-id). |

Errors use Gemini's shape, `{"error": {"code": 429, "message": "...", "status": "RESOURCE_EXHAUSTED"}}`; see [Errors](/docs/api/errors).
