Image generation lives on a parallel surface to the text APIs.
%ALLM.ImageRequest{} and %ALLM.ImageResponse{} mirror the
Request/Response shape; the engine has a separate :image_adapter
slot; and the entry points (ALLM.generate_image/3 and
ALLM.edit_image/4) take the same engine and return image responses.
This guide covers what each entry point does, the parallel adapter
slot, OpenAI vs Gemini coverage, and the FakeImages adapter for
deterministic testing.
Two operations
| Operation | Function | What it does |
|---|---|---|
| Generate | ALLM.generate_image/3 | Produces a new image from a text prompt |
| Edit (inpaint) | ALLM.edit_image/4 | Modifies an existing image, optionally masked |
Each returns {:ok, %ALLM.ImageResponse{}} with :images (list of
%ALLM.Image{}) and :usage (provider-reported counts).
The image-adapter engine slot
An engine has six adapter slots — :adapter for chat, :image_adapter
for images, and :embed_adapter, :moderation_adapter,
:speech_adapter, and :transcription_adapter for the other
capabilities. Set whichever you need:
engine = ALLM.Engine.new(
adapter: ALLM.Providers.OpenAI, # for chat, optional here
image_adapter: ALLM.Providers.OpenAI.Images,
model: "gpt-image-2"
)The image model comes from the engine's :model field, or from a
per-call model: opt (ALLM.generate_image(engine, prompt, model: "gpt-image-2"))
which overrides it.
If you only generate images (no chat), the :adapter slot can stay
unset.
Generating an image
iex> engine = ALLM.Engine.new(
...> image_adapter: ALLM.Providers.FakeImages,
...> adapter_opts: [
...> image_script: [
...> {:ok, [%ALLM.Image{source: {:bytes, <<137, 80, 78, 71>>}, mime_type: "image/png"}]}
...> ]
...> ]
...> )
iex> {:ok, %ALLM.ImageResponse{images: [%ALLM.Image{} = img]}} =
...> ALLM.generate_image(engine, "a watercolor kestrel")
iex> img.mime_type
"image/png"ALLM.generate_image/3 accepts opts:
:model— override the engine's default.:size—"1024x1024", a{w, h}tuple, or:auto. Supported sizes differ per provider and model.gpt-image-2and later accept any size whose sides are multiples of 16, with an aspect ratio between 1:3 and 3:1, up to 3840x2160.:n— number of images to generate.:quality—:low,:medium,:high, or:autoon OpenAI's GPT image models;gpt-image-2.5-sunburstandgpt-image-2.5-flarealso take:xhighand:max.:background—:transparentor:opaque(GPT image models).:response_format—:binary(the default),:base64, or:url. OpenAI's GPT image models only return base64, so they reject:url.:options— provider-specific extras. The OpenAI adapter forwards:output_format("png","jpeg","webp"),:output_compression(0–100),:moderation("low"or"auto", generate only) and:input_fidelity("high"or"low", edit only) when present.:request_timeout— HTTP receive timeout in milliseconds.
Editing an image (inpaint)
ALLM.edit_image/4 takes the engine, the base image, the prompt, and
optionally a mask:
base = File.read!("base.png")
mask = File.read!("mask.png") # white = paint here, transparent = keep
{:ok, response} = ALLM.edit_image(engine, base, "add a fountain", mask: mask)The base and mask can be raw bytes, a file path
({:file, "/path/to/x.png"}), or an %ALLM.Image{}.
Provider coverage
| Operation | OpenAI | Gemini |
|---|---|---|
Generate (generate_image/3) | yes (gpt-image-2) | yes (gemini-3.1-flash-image) |
Edit (edit_image/4) | yes (gpt-image-2) | yes |
The OpenAI adapter treats every gpt-image-* model as a GPT image model
(currently gpt-image-1, gpt-image-1-mini, gpt-image-1.5,
gpt-image-2, gpt-image-2.5-sunburst, gpt-image-2.5-flare, and
their dated snapshots), so a new snapshot works without a library
update. OpenAI removes gpt-image-1-mini, gpt-image-1.5, and
chatgpt-image-latest on 2026-12-01.
OpenAI has retired dall-e-2 and dall-e-3. The OpenAI adapter still
recognises both names — a request naming one is gated before any HTTP
call — but new code should use a GPT image model.
Anthropic does not ship an image adapter — set :image_adapter to
OpenAI's or Gemini's even when your chat adapter is Anthropic.
Materializing the result
A %ALLM.Image{} carries a :source (either {:bytes, binary} or
{:url, string}) and a :mime_type. To get raw bytes regardless of
source:
{:ok, bytes} = ALLM.Image.to_binary(image)This handles the URL fetch transparently if needed.
To write to disk:
{:ok, bytes} = ALLM.Image.to_binary(image)
File.write!("output.png", bytes)Testing with FakeImages
ALLM.Providers.FakeImages is the canonical test vehicle for image
flows — same idea as ALLM.Providers.Fake for chat. Build a scripted
response and assert against it:
iex> engine = ALLM.Engine.new(
...> image_adapter: ALLM.Providers.FakeImages,
...> adapter_opts: [
...> image_script: [
...> {:ok, [%ALLM.Image{source: {:bytes, <<137, 80, 78, 71, 0, 0>>}, mime_type: "image/png"}]}
...> ]
...> ]
...> )
iex> {:ok, %ALLM.ImageResponse{images: images}} =
...> ALLM.generate_image(engine, "anything")
iex> length(images)
1Fake replies are deterministic, async-test-safe (per-process cursor), and require no network or API key.
Common patterns
Generate + persist
{:ok, %ALLM.ImageResponse{images: [image]}} =
ALLM.generate_image(engine, prompt, size: "1024x1024")
{:ok, bytes} = ALLM.Image.to_binary(image)
File.write!(target_path, bytes)Edit with progress
generate_image/3 and friends are non-streaming. Long generations
block until the provider returns the bytes. Pass a longer per-call
:request_timeout (milliseconds) if needed:
ALLM.generate_image(engine, prompt, request_timeout: 180_000)Multi-tenant key resolution
Image-adapter calls go through the same ALLM.Keys resolution chain as
chat calls. Pass :api_key per-call for BYOK SaaS:
ALLM.generate_image(engine, prompt, api_key: tenant.openai_key)Where to next
vision.md— sending images TO the model, vs generating new ones.examples/10_generate_image.exs— runnable smoke test.examples/11_edit_image.exs— inpaint with mask.