Gemini (Google AI)
A Gemini native-compatible endpoint so the Google Gemini CLI and the Google AI SDK (@google/genai) connect unchanged.
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.
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)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.
export GEMINI_API_KEY="plm_..."
export GOOGLE_GEMINI_BASE_URL="https://router.pleum.ai"
gemini/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#
| Parameter | Type | Required | Description |
|---|---|---|---|
| contents | array | Required | {role, parts} array. role is user or model; parts is an array of text / inline_data blocks. |
| systemInstruction | object | string | Optional | System prompt. Only text in parts is extracted and turned into a system message. |
| generationConfig | object | Optional | Accepts both camelCase and snake_case — temperature, maxOutputTokens, topP, stopSequences, etc. |
| tools | array | Optional | Gemini-shaped function declarations (functionDeclarations). Converted both ways. |
| toolConfig | object | Optional | Function-calling mode (functionCallingConfig). |
| safetySettings | array | Optional | Accepted but not forwarded upstream. |
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).
{
"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.
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.
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.
curl https://router.pleum.ai/v1beta/models \
-H "x-goog-api-key: plm_..."
# -> {"models": [{"name": "models/gemini-2.5-flash", "supportedGenerationMethods": [...]}, ...]}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.