Generation — Gemini
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.
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:
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:
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:
// 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.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 — the shared option base this engine extends, and the engine whose
edit()404s on some gateways where this one works. - Recipes — an edit-tool that consumes an inbound
Mediaattachment viaGenerationImageInput. - Assembly → Generation batteries — full option/exception tables, all three engines.