Skip to content
4 min read · 851 words

Generation — OpenAI

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

OptionTypeDefaultNotes
modelstring(required)No default anywhere in this domain — always the caller's job.
apiKeystringSent as Authorization: Bearer {apiKey}.
baseURLstringhttps://api.openai.com/v1Injectable — point it at a gateway/LB.
headersRecord<string, string>Merged over the default headers; can override Authorization entirely.
fetchtypeof fetchglobal fetchSwap in a custom transport for tests or non-standard runtimes.
responseFormatMode'auto' | 'send' | 'omit''auto'See below.
sizestringPassed 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).
nnumberPer-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) 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.

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 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

ExceptionStatusFatal?Thrown when
E_INVALID_OPENAI_GENERATION_OPTIONS529YesAdapter options fail openaiGenerationOptionsSchema validation (missing model, unknown key, etc.).
E_OPENAI_GENERATION_HTTP_ERROR502NoThe upstream HTTP call fails and retries are exhausted (or the status isn't retriable). Detail: [status, body].
E_OPENAI_GENERATION_REQUEST_TIMEOUT504NorequestTimeoutMs elapses before the request settles. Detail: [requestTimeoutMs].
E_OPENAI_GENERATION_MALFORMED_RESPONSE502NoA 2xx response has no usable data[].b64_json entries. Detail: [reason].

Where to go next