Google Gemini
- In one line
- Dispatches unified requests to Google's
streamGenerateContentendpoint with anx-goog-api-keyheader. - You need
- The
provider-geminifeature and an API key. There is no keyless mode. - Read this if
- You route requests through
ProviderEndpoint::GoogleGemini, handle tool calls against Gemini, or set Gemini thinking budgets and levels.
This adapter speaks Gemini's own contents and parts protocol. Against the OpenAI page it differs in four ways:
- The API key is required and travels in
x-goog-api-key. - The model id is part of the URL path, and the route adds the
/v1betasegment. - The stream has no end marker; it ends with the connection.
- Token usage is not read.
Endpoint
| Fact | Value |
|---|---|
| Feature flag | provider-gemini |
ProviderEndpoint variant | GoogleGemini |
| Default base URL | https://generativelanguage.googleapis.com, used when the client's base URL is empty |
| Route | POST {base_url}/v1beta/models/{model}:streamGenerateContent?alt=sse |
The base URL has no /v1 suffix. The adapter appends the /v1beta segment, the model id and the method, and trims a trailing / from the base URL first.
Authentication
x-goog-api-key: <api_key>is sent on every request.- With no API key, dispatch fails with
CucaError::Config("gemini requires an api key"). - Google OAuth for service accounts is not implemented.
Request translation
The request body is built from a UnifiedRequest. The model is not in the body.
system_instructionis{parts: [{text}]}built from everyTextblock of everySystemmessage, joined with a newline. Non-text system blocks are dropped, and the key is omitted with no system messages.generationConfigcarriestemperatureandmaxOutputTokensonly when set. The key is omitted when neither is set.contentshas one entry per non-system message. The role is"user"forUserandToolmessages and"model"forAssistantmessages.toolsis one entry whosefunctionDeclarationsarray holds everyUnifiedRequest::toolsdefinition as{name, description, parameters}, withparametersholding theinput_schema. The key is omitted when the request declares no tool, and notoolConfigis sent.- Content blocks map to
partsas follows:Textbecomes{text}.ImageBase64becomes{inline_data: {mime_type, data}}.Thinkingbecomes{thought: true, text}; the signature is dropped.ToolCallbecomes{functionCall: {name, args}}; the call id is dropped.ToolResultbecomes{functionResponse: {name, response: {output}}}.
- Gemini's
functionResponsenames the function, not the call.nameis the message'snameannotation, falling back to the block'stool_call_idwhennameis unset.
Response stream
Each frame's candidates[0].content.parts maps to blocks, in order:
- A part with
thought: truebecomesMessageContentBlock::Thinkingwith no signature. - A part with
textbecomesMessageContentBlock::Text. - A part with
functionCallbecomesMessageContentBlock::ToolCall. The arguments are theargsobject as sent, andnullwhenargsis missing. Theidis read when the frame has one and is an empty string otherwise. - A frame without a candidate or parts, such as the final
usageMetadataframe, yields no blocks. - Gemini sends no
[DONE]marker. The stream ends when the byte stream ends.
Token usage
- The adapter ignores
usageMetadata.UnifiedResponse::usageisNone,prompt_tokensis0, andcompletion_tokenscounts one perText,ThinkingandToolCallblock. - No prompt cache counters are read.
Thinking
A thinkingConfig key is sent when req.thinking is set. The first matching rule applies:
- A disabled config sends
{"includeThoughts": false}. ThinkingParams::Gemini { thinking_budget: Some(n), .. }sends{"thinkingBudget": n}.ThinkingParams::Gemini { thinking_level: Some(level), .. }sends{"thinkingLevel": level}with the string as given.- A unified effort sends
{"thinkingLevel": ...}from the table below. - Otherwise it sends
{"includeThoughts": true}.
Unified ThinkingEffort | thinkingLevel |
|---|---|
Minimal | "LOW" |
Low | "LOW" |
Medium | "MEDIUM" |
High | "HIGH" |
XHigh | "HIGH" |
Errors
| Condition | Error |
|---|---|
| Feature not compiled | CucaError::ProviderNotEnabled("provider-gemini") |
| No API key | CucaError::Config |
| Non-2xx response | CucaError::Http { status, body } with the captured body |
| Connection or body-read failure | CucaError::Transport { message } |
| Frame is not valid JSON | CucaError::Json |
Frame carries {"error": {"message": ...}} | CucaError::Provider with provider: GoogleGemini |
| Frame bytes are not valid UTF-8 | CucaError::SseParse |
A malformed frame or an error frame ends the stream after the error.
Limits
- No cap on request or response size, and the client sets no request timeout.
- The SSE parser keeps one reusable buffer that starts at 8192 bytes. Its high-water mark is the largest single frame plus one partial line.
maxOutputTokensis sent only when the request setsmax_tokens. There is no adapter default.- Non-streaming
generateContentis not implemented.
Example
The smallest streaming turn: the GoogleGemini variant and the required API key.
cargo add cuca --path /path/to/cuca --features provider-gemini
cargo add tokio --features rt,macros
cargo add tokio-streamexport GEMINI_API_KEY="..."$env:GEMINI_API_KEY = "..."use std::io::{Write, stdout};
use cuca::types::{MessageContentBlock, ProviderEndpoint};
use cuca::{CucaClient, UnifiedRequest};
use tokio_stream::StreamExt;
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = CucaClient::builder()
.with_provider(ProviderEndpoint::GoogleGemini)
.with_api_key(std::env::var("GEMINI_API_KEY")?)
.build()?;
let request = UnifiedRequest::new("gemini-2.5-flash")
.add_system_message("You are concise.")
.add_user_message("Say hello.")
.set_max_tokens(128);
let mut stream = client.generate_stream(request).await?;
let mut text_blocks = 0usize;
let mut thinking_blocks = 0usize;
while let Some(block) = stream.next().await {
match block? {
MessageContentBlock::Text(text) => {
print!("{text}");
stdout().flush()?;
text_blocks += 1;
}
MessageContentBlock::Thinking { .. } => thinking_blocks += 1,
_ => {}
}
}
println!("\nblocks: {text_blocks} text, {thinking_blocks} thinking");
Ok(())
}cargo run
The provider-gemini feature comes from the cargo add line above.