---
url: 'https://adk.nht.io/batteries/llm/gemini.md'
description: >-
  Google's native generateContent wire for Gemini: contents[] turns, model
  roles, systemInstruction, tool correlation by name, signed thinking replay,
  and raw wire observability.
---

# Gemini generateContent

## LLM summary — Gemini generateContent

* [`GeminiGenerateContentAdapter`](https://adk.nht.io/api/@nhtio/adk/batteries/llm/gemini_generate_content/adapter/classes/GeminiGenerateContentAdapter) from `@nhtio/adk/batteries/llm/gemini_generate_content`. Drives Google's native `:generateContent` and `:streamGenerateContent` endpoints directly, not an OpenAI-compatible façade.
* Required: `model`. Optional: `apiKey`, `useBearerAuth` (default `false`, sends `x-goog-api-key`), `baseURL` (default `https://generativelanguage.googleapis.com/v1beta`), `stream`, `maxOutputTokens`, `temperature`, `topP`, `topK`, `stopSequences`, `thinkingConfig`, `safetySettings`, `toolConfig`, `thoughtSignatureSentinel`, `timeoutMs`, `retry`, `localToolCallParser`, `onPromptAssembled`, `onRawGeneration`, `helpers`.
* Wire shape: turns are `contents[]` with role `model` (never `assistant`) and `user`. System text lives in top-level `systemInstruction`, not in `contents`. Payloads are heterogeneous `parts[]`.
* Tool calling: model calls are `functionCall` parts on a `model` turn; results are `functionResponse` parts on a `user` turn. Correlation is by declared function name, not call ID. Result payloads must be JSON objects (`{ result: ... }`), never bare strings.
* Thought signatures: Gemini 3+ rejects historical `functionCall` parts without a `thoughtSignature`. `thoughtSignatureSentinel` defaults to `'skip_thought_signature_validator'`; set `false` to opt out.
* Schema sanitization: parameter schemas are automatically stripped of unsupported JSON-Schema keywords (`$ref`, `additionalProperties`, `anyOf`, `default`, etc.) to prevent opaque upstream `INVALID_ARGUMENT` rejections.
* Observability: `onPromptAssembled` inspects the exact native request payload before dispatch; `onRawGeneration` captures raw text, reasoning, and calls.
* Per-iteration overrides via `ctx.stash` use `STASH_KEY` `'geminiGenerateContent'`.
* Exceptions: `E_INVALID_GEMINI_GENERATE_CONTENT_OPTIONS`, `E_GEMINI_{REQUEST_FAILED,MISSING_THOUGHT_SIGNATURE,STREAM_ERROR,INVALID_TOOL_CALL_ARGS}`.

Google is not OpenAI with different header names. The native `generateContent` API has its own wire
grammar: turns are `contents[]`, the assistant role is `model`, system instructions sit outside the
conversation stream, tool calls live on `model` turns, and tool results come back on **`user`** turns.
Correlation between a call and its result happens by declared tool name, because the wire has no concept
of a tool call ID. This battery speaks that native wire directly.

Pointing an OpenAI adapter at a compatibility proxy works until you need to reason about what Google
actually received. Gateways silently merge turns, forge call IDs, and inject sentinels behind your back.
When you need an honest wire-shape audit or an ordering guard that checks reality rather than proxy
behaviour, you speak the native endpoint. This is the doctrine described in
[Which API surface a rule applies to](/batteries/validation/api-surface-scope).

```ts
import { GeminiGenerateContentAdapter } from '@nhtio/adk/batteries/llm/gemini_generate_content'

const adapter = new GeminiGenerateContentAdapter({
  model: 'gemini-2.5-flash-lite',
  apiKey: process.env.GEMINI_API_KEY,
})

// adapter.executor() is the DispatchExecutorFn you hand to TurnRunner.
```

::: tip Routing through an OpenAI proxy?
If you access Gemini through an OpenAI-compatible gateway (LiteLLM, OpenRouter, or a cloud proxy) and
want the gateway to handle turn translation, use the [OpenAI Chat Completions adapter](./openai)
pointed at that gateway's `/v1`. Use this battery when you target Google directly or need unmediated
control over the native wire shape.
:::

## What this wire buys you

* **Unmediated wire observability.** The `onPromptAssembled` hook exposes the exact JSON payload about
  to be POSTed to `:generateContent` (or `:streamGenerateContent`). You see the real `contents[]`, the
  `systemInstruction`, and the sanitized `tools` declarations before dispatch, making wire audits and
  ordering validation authentic.
* **Native tool correlation and formatting.** ADK tools map to `tools[].functionDeclarations[]`. When the
  model invokes a tool, ADK executes it and returns a `functionResponse` part on a `user` turn. Result
  payloads are automatically wrapped in a JSON object (`{ result: ... }`) with untrusted-content tagging,
  because Gemini hard-rejects raw strings.
* **Automatic schema sanitization.** Gemini's OpenAPI parameter parser rejects dozens of standard JSON-Schema
  keywords (`$ref`, `$defs`, `additionalProperties`, `anyOf`, `oneOf`, `default`, bounds constraints). The
  battery's `sanitizeGeminiSchema` helper recursively prunes them before dispatch, and omits empty
  `parameters` objects on zero-argument tools so Google does not fail the call with an opaque `INVALID_ARGUMENT`.
* **Thought-signature handling for Gemini 3+.** Gemini 3+ models require a cryptographic `thoughtSignature`
  on historical `functionCall` parts. When replaying history or running multi-turn loops, the battery
  preserves genuine signatures. For history that originated outside Gemini (model handoffs or restored state),
  `thoughtSignatureSentinel` automatically injects Google's documented portable sentinel
  (`'skip_thought_signature_validator'`) on the first call to prevent a hard 400 rejection.
* **Clean reasoning isolation.** Model thoughts arrive flagged as `thought: true`. The extractor separates
  them into ADK [`Thought`](https://adk.nht.io/api/@nhtio/adk/common/classes/Thought) records rather than concatenating scratchpad reasoning into visible message
  text, preventing prompt leaks across turns.

## The gotchas, because they will get you

**Unmatched tool names yield silent emptiness.** Gemini correlates tool results to tool declarations
strictly by `name` — there is no call ID. If a historical `functionResponse.name` does not match any
tool currently declared in `tools[].functionDeclarations`, Gemini does not throw a 400. It silently
returns an empty candidate (`candidates: []` or a turn with no parts). If a turn finishes instantly with
no output and no error, check whether a tool was renamed or dropped from the active toolset between turns.

**Reasoning models exhaust small token budgets on thoughts.** Models with thinking enabled spend their
output-token budget on reasoning parts before generating any user-visible text. If `maxOutputTokens` is
set to a small value (such as 512 or 1024), the budget can be exhausted entirely inside the thinking phase.
Google returns an HTTP 200 with `finishReason: 'MAX_TOKENS'` and zero text parts. To the caller, the response
looks complete, but has no content. When using thinking models, set a generous `maxOutputTokens` or constrain
`thinkingConfig.thinkingBudget`.

**Gemini 3+ hard-rejects missing thought signatures.** If history contains a `functionCall` part without a
`thoughtSignature` and you set `thoughtSignatureSentinel: false`, Gemini 3+ returns an HTTP 400
`INVALID_ARGUMENT`. The adapter inspects the response and classifies it as
`E_GEMINI_MISSING_THOUGHT_SIGNATURE` rather than a generic transport failure, making the condition actionable.
If you switch models mid-conversation, keep the default sentinel enabled.

**Auth headers differ between Google and gateways.** Google's first-party endpoint requires the
`x-goog-api-key` header (the default when `apiKey` is provided). Gateways fronting Gemini often accept only
`Authorization: Bearer <key>`. Set `useBearerAuth: true` when routing through a bearer-authenticated proxy;
the adapter switches headers rather than sending both.

**Zero-argument tools must omit parameters.** Gemini rejects an empty parameters schema
(`{ type: 'object', properties: {} }`). The adapter's helper strips empty parameter objects entirely,
sending only `name` and `description`. If you provide custom translation helpers via `helpers`, ensure
your `toolsToGeminiTools` preserves this behavior.

**Local tool-call recovery is fallback-only.** If a model outputs tool-call JSON as plain text rather than
structured `functionCall` parts, `localToolCallParser` attempts to extract calls against the turn's offered
tool names. If the provider returned any native `functionCall` parts, the local parser is bypassed entirely;
native calls always win.

Full option and exception details are in [Assembly → LLM batteries](/assembly/batteries-llm).
