---
url: 'https://adk.nht.io/batteries/generation/gemini.md'
description: >-
  The Gemini Generation engine: native generateContent REST over raw fetch,
  response-modality defaults, image-part ordering, refusal surfacing.
---

# Generation — Gemini

## LLM summary — Gemini Generation adapter

* [`GeminiGenerationAdapter`](https://adk.nht.io/api/@nhtio/adk/batteries/generation/gemini/adapter/classes/GeminiGenerationAdapter) — `@nhtio/adk/batteries/generation/gemini`. Raw `fetch` against the
  native `generativelanguage` REST `generateContent` endpoint. No `@google/genai` SDK. `baseURL` default
  `https://generativelanguage.googleapis.com/v1beta`.
* Option shapes re-export-then-extend `../openai/types` — same retry/timeout/`fetch`/`headers` fields,
  same `BaseGenerationAdapterOptions{model}` base. `model` required, no default.
* Auth: `apiKey` → `x-goog-api-key` header (direct-Google caller shape). Gateway/LB callers instead pass
  `headers: { Authorization: 'Bearer ' + token }`, bypassing the adapter's own `apiKey` handling entirely
  — confirmed exactly this pattern in the domain's own live cross-spec.
* `responseModalities: GeminiResponseModality[]` (`'TEXT' | 'IMAGE'`), default `['TEXT', 'IMAGE']` —
  probe-confirmed: omitting `TEXT` from the request can make the model return image-only or refuse
  outright depending on prompt; requesting both is the reliable default.
  `imageConfig.aspectRatio` is only sent when explicitly set (no default aspect ratio is imposed).
  `candidateCount` (`n` on the call) is only sent when `> 1`.
* `edit()` part ordering is a probe-confirmed wire requirement, not a style choice: **image parts first
  (in input order), text prompt last**. Source has it commented verbatim: `// Probe-confirmed ordering:
  image parts first (in input order), text prompt last.` Reordering these can silently degrade output
  quality or cause the model to ignore the images.
* Response parsing tolerates both camelCase `inlineData` and snake\_case `inline_data` on responses (the
  API is inconsistent about this); **requests always send camelCase** (`inlineData`) — there is no
  snake\_case fallback on the way out.
* Refusal surfacing: if a 2xx response contains zero image parts (e.g. a safety refusal returning text
  only), the adapter throws `E_GEMINI_GENERATION_MALFORMED_RESPONSE` with any text parts' content embedded
  in the detail string (`response contained no image parts; text: ${textParts.join(' ')}`) — the refusal
  reason is legible in the thrown error, not silently swallowed.
* **Probe-confirmed live**: both `generate()` and `edit()` work through the LB gateway (unlike the OpenAI
  engine, whose `/v1/images/edits` 404s there). Nano Banana family (`gemini-2.5-flash-image` and
  successors) is the practical default; the separate Imagen `:predict` REST surface is **not** what this
  adapter calls — this engine is `generateContent`-only, so Imagen-only models will not work here.

The Gemini-shaped engine talks to the native `generativelanguage` REST API's `generateContent` endpoint
directly — no `@google/genai` SDK, just `fetch`. Its option surface extends the OpenAI engine's shared base
(`BaseGenerationAdapterOptions`, retry/timeout/`headers`/`fetch`), adding only what Gemini's own request
shape needs.

```ts
import { GeminiGenerationAdapter } from '@nhtio/adk/batteries/generation/gemini'

const generator = new GeminiGenerationAdapter({
  model: 'gemini-2.5-flash-image',
  apiKey: process.env.GEMINI_API_KEY,
})

const [image] = await generator.generate('A watercolor illustration of a lighthouse at dawn')
```

## Options

| Option | Type | Default | Notes |
| --- | --- | --- | --- |
| `model` | `string` | *(required)* | Same base as the OpenAI engine — no default anywhere in this domain. |
| `apiKey` | `string` | — | Sent as `x-goog-api-key`. Use `headers` instead for a Bearer-token gateway. |
| `baseURL` | `string` | `https://generativelanguage.googleapis.com/v1beta` | Injectable. |
| `headers` | `Record<string, string>` | — | Merged over defaults; can fully replace the auth scheme. |
| `fetch` | `typeof fetch` | global `fetch` | Same seam as the OpenAI engine. |
| `responseModalities` | `('TEXT' \| 'IMAGE')[]` | `['TEXT', 'IMAGE']` | See below — don't drop `TEXT`. |
| `aspectRatio` | `string` | — | e.g. `'1:1'`, `'16:9'`. Only sent when set (as `generationConfig.imageConfig.aspectRatio`). |
| `n` | `number` | — | Per-call (`generate`/`edit`). Sent as `generationConfig.candidateCount` **only when `n > 1`**; ignored otherwise. |
| `requestTimeoutMs` | `number` | `0` (disabled) | Same semantics as the OpenAI engine. |
| `retry.*` | — | same defaults as OpenAI | `maxAttempts` 1, `baseDelayMs` 500, `maxDelayMs` 30000, `retriableStatuses` `[429,500,502,503,504]`, `honorRetryAfter` true. |

## Auth: `apiKey` vs gateway `headers`

Direct-to-Google callers set `apiKey`; the adapter sends it as `x-goog-api-key`:

```ts
new GeminiGenerationAdapter({ model: 'gemini-2.5-flash-image', apiKey: process.env.GEMINI_API_KEY })
```

Gateway/LB callers instead skip `apiKey` and pass a `Bearer` token through `headers` — this is the exact
shape used by this domain's own live-testing setup, and it bypasses the adapter's own `apiKey` → header
translation entirely:

```ts
new GeminiGenerationAdapter({
  model: 'gemini-2.5-flash-image',
  baseURL: process.env.LB_BASE_URL,
  headers: { Authorization: `Bearer ${process.env.LB_TOKEN}` },
})
```

## `responseModalities` — why `TEXT` stays in the default

Gemini's image models are natively multimodal-out: a request can ask for text, image, or both back.
`responseModalities` defaults to `['TEXT', 'IMAGE']` rather than `['IMAGE']` alone because omitting `TEXT`
from the request has been probe-confirmed to produce inconsistent results — sometimes an image-only
response, sometimes an outright refusal, depending on the prompt. Requesting both and then discarding the
text (the adapter only ever returns `GeneratedMediaOutput[]` for images) is the reliable path. Override it
only if you have verified your specific model/prompt combination behaves with a narrower modality list.

## `generate()` and `edit()` — image parts first, text last

Both calls build a single `contents[0].parts` array. `generate()` has only the text prompt part.
`edit()` is the one where ordering matters:

```ts
// conceptually, inside edit():
const parts: GeminiRequestPart[] = normalizedInputs.map((input) => ({
  inlineData: { mimeType: input.mimeType, data: toBase64(input.bytes) },
}))
parts.push({ text: prompt }) // Probe-confirmed ordering: image parts first (in input order), text prompt last.
```

```ts
const outputs = await generator.edit(
  [{ bytes: pngBytes, mimeType: 'image/png' }],
  'Recolor the car to matte black',
  { aspectRatio: '1:1' }
)
```

Multiple input images are sent in the order you pass them, all before the text part. This ordering was
determined empirically against the live API (not documented by Google) — do not reorder it in a custom
fork of this adapter.

## Refusal surfacing

A 2xx response with zero image parts — most commonly a safety refusal that comes back as text only —
throws `E_GEMINI_GENERATION_MALFORMED_RESPONSE` rather than silently resolving an empty array. The
refusal's own text is embedded in the exception detail:

```
response contained no image parts; text: I can't create images depicting real public figures.
```

Treat this exception as "the model declined," not just "malformed wire data," when deciding how to surface
the failure to a user.

## Nano Banana family and the Imagen limitation

This adapter calls `generateContent` exclusively. The "Nano Banana" model family
(`gemini-2.5-flash-image` and its successors) is built for exactly this endpoint and is the practical
default for both `generate()` and `edit()`. Google's separate Imagen line is served from a different REST
surface (`:predict`) that this adapter does not call — pointing `model` at an Imagen-only model name will
not work here, regardless of `responseModalities`.

## Exceptions

| Exception | Status | Fatal? | Thrown when |
| --- | --- | --- | --- |
| `E_INVALID_GEMINI_GENERATION_OPTIONS` | 529 | Yes | Adapter options fail `geminiGenerationOptionsSchema` validation. |
| `E_GEMINI_GENERATION_HTTP_ERROR` | 502 | No | The upstream HTTP call fails and retries are exhausted. Detail: `[status, body]`. |
| `E_GEMINI_GENERATION_REQUEST_TIMEOUT` | 504 | No | `requestTimeoutMs` elapses before the request settles. Detail: `[requestTimeoutMs]`. |
| `E_GEMINI_GENERATION_MALFORMED_RESPONSE` | 502 | No | No candidates, or zero image parts (refusal-shaped response). Detail includes any text parts' content. |

## Where to go next

* [OpenAI](./openai) — the shared option base this engine extends, and the engine whose `edit()` 404s on
  some gateways where this one works.
* [Recipes](./recipes) — an edit-tool that consumes an inbound `Media` attachment via `GenerationImageInput`.
* [Assembly → Generation batteries](/assembly/batteries-generation) — full option/exception tables, all three engines.
