Skip to content

Gemini (Google AI)

A Gemini native-compatible endpoint so the Google Gemini CLI and the Google AI SDK (@google/genai) connect unchanged.

POST/v1beta/models/{model}:generateContent
POST/v1beta/models/{model}:streamGenerateContent
POST/v1beta/models/{model}:countTokens
GET/v1beta/models

PleumRouter exposes the Gemini native format (generateContent / streamGenerateContent) directly. Tools hardcoded to the Gemini CLI or the Google AI SDK cannot reach an OpenAI-compatible base URL, so this inbound surface is required. Each request is converted to the internal standard (OpenAI-compatible) pipeline so routing, billing, and streaming cores are reused as-is, and the response is serialized back to Gemini format (candidates[] / usageMetadata).

Connecting#

In the Google AI SDK, set baseUrl (HttpOptions(base_url=...) in Python) to the root https://router.pleum.ai. The SDK appends /v1beta/models/{model}:generateContent.

Google Gen AI SDK (Python)
from google import genai

client = genai.Client(
    api_key="plm_...",
    http_options=genai.types.HttpOptions(base_url="https://router.pleum.ai"),
)

response = client.models.generate_content(
    model="gemini-2.5-flash",
    contents="Hello",
)
print(response.text)
Google Gen AI SDK (TypeScript)
import { GoogleGenAI } from "@google/genai";

const client = new GoogleGenAI({
  apiKey: "plm_...",
  httpOptions: { baseUrl: "https://router.pleum.ai" },
});

const response = await client.models.generateContent({
  model: "gemini-2.5-flash",
  contents: "Hello",
});

console.log(response.text);

For the Gemini CLI, set GOOGLE_GEMINI_BASE_URL to the root and your plm_ key as GEMINI_API_KEY.

Gemini CLI
export GEMINI_API_KEY="plm_..."
export GOOGLE_GEMINI_BASE_URL="https://router.pleum.ai"
gemini
Do not append /v1beta to the base URL — the SDK appends it again and the doubled path fails. Use the root https://router.pleum.ai only.

Authenticate with a plm_ API key via the Gemini-standard x-goog-api-key header or the ?key= query parameter. The Authorization: Bearer header is also accepted.

Generate content#

ParameterTypeRequiredDescription
contentsarrayRequired{role, parts} array. role is user or model; parts is an array of text / inline_data blocks.
systemInstructionobject | stringOptionalSystem prompt. Only text in parts is extracted and turned into a system message.
generationConfigobjectOptionalAccepts both camelCase and snake_case — temperature, maxOutputTokens, topP, stopSequences, etc.
toolsarrayOptionalGemini-shaped function declarations (functionDeclarations). Converted both ways.
toolConfigobjectOptionalFunction-calling mode (functionCallingConfig).
safetySettingsarrayOptionalAccepted but not forwarded upstream.
request
curl https://router.pleum.ai/v1beta/models/gemini-2.5-flash:generateContent \
  -H "x-goog-api-key: plm_..." \
  -H "content-type: application/json" \
  -d '{
    "systemInstruction": {"parts": [{"text": "You are a helpful assistant."}]},
    "contents": [{"role": "user", "parts": [{"text": "Hello"}]}],
    "generationConfig": {"temperature": 0.7, "maxOutputTokens": 1024}
  }'

The response follows the Gemini schema. Text arrives in candidates[].content.parts; token usage is reported as usageMetadata.promptTokenCount / candidatesTokenCount / totalTokenCount. Image input can be sent via inline_data (base64).

200 OK
{
  "candidates": [
    {
      "content": {
        "parts": [{"text": "Hello! How can I help you?"}],
        "role": "model"
      },
      "finishReason": "STOP",
      "index": 0
    }
  ],
  "usageMetadata": {
    "promptTokenCount": 5,
    "candidatesTokenCount": 7,
    "totalTokenCount": 12
  }
}

Streaming#

:streamGenerateContent uses the SSE format via the ?alt=sse query. Each data: line is a GenerateContentResponse-shaped JSON chunk; there is no [DONE] marker.

stream
curl "https://router.pleum.ai/v1beta/models/gemini-2.5-flash:streamGenerateContent?alt=sse" \
  -H "x-goog-api-key: plm_..." \
  -H "content-type: application/json" \
  -d '{
    "contents": [{"role": "user", "parts": [{"text": "Hello"}]}]
  }'

# Each SSE data: line is a GenerateContentResponse JSON chunk (no [DONE] marker).
# data: {"candidates":[{"content":{"parts":[{"text":"Hello"}],"role":"model"},"index":0}]}
# data: {"candidates":[{"content":{"parts":[{"text":"! How can I help you?"}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":5,"candidatesTokenCount":7,"totalTokenCount":12}}

Count tokens#

:countTokens accepts the same body as generateContent and returns {"totalTokens": <int>}. It is a heuristic estimate, not the result of the real tokenizer.

count tokens
curl https://router.pleum.ai/v1beta/models/gemini-2.5-flash:countTokens \
  -H "x-goog-api-key: plm_..." \
  -H "content-type: application/json" \
  -d '{
    "contents": [{"role": "user", "parts": [{"text": "Hello"}]}]
  }'

# -> {"totalTokens": 5}

List models#

GET /v1beta/models returns the model list in Gemini format, sharing the same public-visibility rules as GET /v1/models.

list models
curl https://router.pleum.ai/v1beta/models \
  -H "x-goog-api-key: plm_..."

# -> {"models": [{"name": "models/gemini-2.5-flash", "supportedGenerationMethods": [...]}, ...]}
Cost is exposed in the response headers X-Cost-Krw (integer) and X-Cost-Usd (decimal) so the Gemini body is not broken (same pattern as the Anthropic endpoint). Streaming omits the header because the cost is only known at completion.