---
url: 'https://adk.nht.io/batteries/generation/openai.md'
description: >-
  The OpenAI-shaped Generation engine: /v1/images/generations + /v1/images/edits
  over raw fetch, responseFormatMode, multipart edits, gateway routing.
---

# Generation — OpenAI

## LLM summary — OpenAI Generation adapter

* [`OpenAIGenerationAdapter`](https://adk.nht.io/api/@nhtio/adk/batteries/generation/openai/adapter/classes/OpenAIGenerationAdapter) — `@nhtio/adk/batteries/generation/openai`. Raw `fetch`, no SDK.
  `generate()` → `POST {baseURL}/images/generations`. `edit()` → `POST {baseURL}/images/edits` as
  `multipart/form-data`. `baseURL` default `https://api.openai.com/v1`.
* `model: string` required, no default — owned by `BaseGenerationAdapterOptions`, the base every other
  engine extends.
* Auth: `apiKey` → `Authorization: Bearer {apiKey}` header. `headers` option can add/override any header
  (e.g. an LB-style gateway auth scheme) — applied after the `Authorization` default, so a caller can
  override it entirely.
* `responseFormatMode: 'auto' | 'send' | 'omit'` (default `'auto'`). `'auto'` sends `response_format:
  'b64_json'` in the request body **only when `model` starts with `'dall-e'`** — `gpt-image-*` models
  reject the `response_format` param outright (they always return `b64_json`). `'send'` always includes
  it; `'omit'` never does.
* `edit()` builds `multipart/form-data` via `FormData`. Image field name is `EDIT_IMAGE_FIELD_NAME =
  'image[]'` (probe-confirmed against the upstream API during WP-0 — not documented in OpenAI's own
  published shape). Deliberately **no manual `Content-Type` header** on multipart requests — `fetch`
  supplies its own boundary; setting it manually breaks the request.
  `EDIT_IMAGE_FIELD_NAME` is exported so a custom transport/mock can match the exact field name.
* Retry/backoff: `retry.maxAttempts` (default 1), `baseDelayMs` (default 500), `maxDelayMs` (default
  30\_000), `retriableStatuses` (default `[429, 500, 502, 503, 504]`), `honorRetryAfter` (default
  `true`). `requestTimeoutMs` (default `0` = disabled) aborts via `AbortController`, throwing
  `E_OPENAI_GENERATION_REQUEST_TIMEOUT`.
* 4 exceptions: `E_INVALID_OPENAI_GENERATION_OPTIONS` (529, fatal), `E_OPENAI_GENERATION_HTTP_ERROR` (502,
  non-fatal, `[status, detail]`), `E_OPENAI_GENERATION_REQUEST_TIMEOUT` (504, non-fatal,
  `[requestTimeoutMs]`), `E_OPENAI_GENERATION_MALFORMED_RESPONSE` (502, non-fatal, `[detail]` — thrown
  when the response has no `data` array or a `data` entry has no `b64_json`).
* **Known gap**: probing this engine's `edit()` against the LB gateway returns a 404 — the gateway does
  not implement OpenAI-shaped image edits, only generations. Live cross-spec only exercises `generate()`
  against the LB (model `gemini-2.5-flash-image`, LB-translated). Use the [Gemini
  engine](./gemini) if you need edits through the same gateway.

The OpenAI-shaped engine talks to the `/v1/images/generations` and `/v1/images/edits` endpoints over a raw
`fetch` call — no `openai` SDK dependency. It is the domain's shared-option owner: `gemini.md` and
`transformers_js.md` both extend the option shapes defined here.

```ts
import { OpenAIGenerationAdapter } from '@nhtio/adk/batteries/generation/openai'

const generator = new OpenAIGenerationAdapter({
  model: 'gpt-image-1',
  apiKey: process.env.OPENAI_API_KEY,
})

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

## Options

| Option | Type | Default | Notes |
| --- | --- | --- | --- |
| `model` | `string` | *(required)* | No default anywhere in this domain — always the caller's job. |
| `apiKey` | `string` | — | Sent as `Authorization: Bearer {apiKey}`. |
| `baseURL` | `string` | `https://api.openai.com/v1` | Injectable — point it at a gateway/LB. |
| `headers` | `Record<string, string>` | — | Merged over the default headers; can override `Authorization` entirely. |
| `fetch` | `typeof fetch` | global `fetch` | Swap in a custom transport for tests or non-standard runtimes. |
| `responseFormatMode` | `'auto' \| 'send' \| 'omit'` | `'auto'` | See below. |
| `size` | `string` | — | Passed through verbatim, e.g. `'1024x1024'`. |
| `quality` | `'low' \| 'medium' \| 'high' \| 'auto'` | — | Constructor validation rejects any other value. |
| `background` | `'transparent' \| 'opaque' \| 'auto'` | — | `gpt-image-*` transparency support. |
| `outputFormat` | `'png' \| 'jpeg' \| 'webp'` | `'png'`¹ | Sets the request's `output_format` (generate) and the returned MIME type + filename extension (both). |
| `n` | `number` | — | Per-call (`generate`/`edit`) — number of images. Sent whenever defined. |

¹ `outputFormat` has no schema default (it's optional at construction), but the adapter resolves it to
`'png'` at call time when neither the per-call option nor the constructor option is set.
| `requestTimeoutMs` | `number` | `0` (disabled) | Aborts in-flight requests past this many ms. |
| `retry.maxAttempts` | `number` | `1` | Total attempts, including the first. |
| `retry.baseDelayMs` | `number` | `500` | Base of the exponential backoff. |
| `retry.maxDelayMs` | `number` | `30000` | Backoff ceiling. |
| `retry.retriableStatuses` | `number[]` | `[429, 500, 502, 503, 504]` | Statuses that trigger a retry. |
| `retry.honorRetryAfter` | `boolean` | `true` | Honor a `Retry-After` response header over the computed backoff. |

## `responseFormatMode` — why it exists

OpenAI's own image API is split across two model families with incompatible `response_format` handling:
`dall-e-*` models accept (and, historically, sometimes require) an explicit `response_format:
'b64_json'`; `gpt-image-*` models reject the parameter outright and always return base64 JSON regardless.
`'auto'` (the default) resolves this by inspecting `model`:

```ts
// conceptually:
const responseFormat =
  responseFormatMode === 'send' ? 'b64_json'
  : responseFormatMode === 'omit' ? undefined
  : model.startsWith('dall-e') ? 'b64_json' : undefined // 'auto'
```

Use `'send'`/`'omit'` to force the behavior explicitly — for example, if you're routing through a gateway
whose model-name prefix doesn't match either OpenAI family.

## `generate()`

```ts
const outputs = await generator.generate('A neon-lit alley in the rain', {
  size: '1024x1024',
  quality: 'high',
})
```

`POST {baseURL}/images/generations` with a JSON body built from the prompt plus any of `size`/`quality`/
`background`/`outputFormat`/the resolved `response_format`. The response's `data[].b64_json` entries are
decoded to `Uint8Array` and returned as `GeneratedMediaOutput[]`; a response with no `data` array, an empty
`data` array, or a `data` entry missing `b64_json` throws `E_OPENAI_GENERATION_MALFORMED_RESPONSE`.

## `edit()` — multipart, not JSON

```ts
const outputs = await generator.edit(
  [{ bytes: pngBytes, mimeType: 'image/png' }],
  'Add a red bicycle leaning against the wall',
  { size: '1024x1024' }
)
```

`edit()` accepts one or more `GenerationImageInput` values (`Uint8Array`, `{ bytes, mimeType? }`, or
anything `Media`-shaped — see [`_shared`](/assembly/batteries-generation#generationimageinput)) and posts
them as `multipart/form-data`:

* Each image is appended under the field name `image[]` (`EDIT_IMAGE_FIELD_NAME`), OpenAI's actual
  probe-confirmed field for multi-image edit requests.
* `mask?`, `size?`, `quality?` (the `OpenAIEditOptions` fields) are appended as additional form fields
  when present.
* **No `Content-Type` header is set manually.** `fetch` computes the multipart boundary itself when given
  a `FormData` body; setting `Content-Type` by hand breaks the boundary and the request fails silently
  server-side. If you write a custom `fetch` override, do not add this header.

::: warning Gateway-dependent
Not every gateway implements OpenAI-shaped image edits. The domain's own live cross-spec found the LB
gateway's `/v1/images/edits` route returns 404 even though `/v1/images/generations` works — i.e. `edit()`
is verified directly against OpenAI, not (yet) through every gateway topology. If your deployment routes
through a gateway, verify `edit()` against it before depending on it; the [Gemini engine](./gemini) is
confirmed to support `edit()` through the same LB.
:::

## Gateway routing

`baseURL` and `headers` together let you point this engine at any OpenAI-compatible gateway/LB without
touching the calling code — the same polyglot-LB pattern used across the LLM chat-completions batteries:

```ts
const generator = new OpenAIGenerationAdapter({
  model: 'gemini-2.5-flash-image', // gateway translates the model name
  baseURL: process.env.LB_BASE_URL,
  headers: { Authorization: `Bearer ${process.env.LB_TOKEN}` },
})
```

## Exceptions

| Exception | Status | Fatal? | Thrown when |
| --- | --- | --- | --- |
| `E_INVALID_OPENAI_GENERATION_OPTIONS` | 529 | Yes | Adapter options fail `openaiGenerationOptionsSchema` validation (missing `model`, unknown key, etc.). |
| `E_OPENAI_GENERATION_HTTP_ERROR` | 502 | No | The upstream HTTP call fails and retries are exhausted (or the status isn't retriable). Detail: `[status, body]`. |
| `E_OPENAI_GENERATION_REQUEST_TIMEOUT` | 504 | No | `requestTimeoutMs` elapses before the request settles. Detail: `[requestTimeoutMs]`. |
| `E_OPENAI_GENERATION_MALFORMED_RESPONSE` | 502 | No | A 2xx response has no usable `data[].b64_json` entries. Detail: `[reason]`. |

## Where to go next

* [Gemini](./gemini) — the engine to reach for when you need `edit()` through a gateway this one 404s on.
* [Recipes](./recipes) — wiring `generate()`/`edit()` into a real `Tool`.
* [Assembly → Generation batteries](/assembly/batteries-generation) — all three engines' option tables side by side.
