Skip to content

Image generation

You need a picture: a product mock, a thumbnail variant, an illustration draft, or an edit of an image your agent is already holding. You want to write that once and not rewrite it when you switch providers.

That is the ImageGenerator tag. A prompt goes in, images come out.

import { Effect } from "effect"
import { generate } from "@effect-uai/core/ImageGenerator"
const program = Effect.gen(function* () {
const { images } = yield* generate({
prompt: "A lighthouse at dusk, long exposure",
model: "gpt-image-2",
aspectRatio: "16:9",
resolution: "2K",
})
return images
})

Swap the Layer and the same call runs on a different provider.

Three calls

  • generate — prompt in, images out.
  • edit — prompt plus the images you want changed. Separate, because references are required here and meaningless there, and providers treat them as different endpoints.
  • streamGeneration and streamEdit — preview frames while the image resolves, for when someone is watching a spinner. Both are gated by the ImageStreaming marker, so a provider that cannot preview is a compile error rather than a surprise at runtime.

Ask for a shape, not pixels

readonly aspectRatio?: AspectRatio // "1:1", "16:9", "21:9", …
readonly resolution?: ImageResolution // "1K" | "2K" | "4K"

Pixels don’t port. One provider takes "1536x1024", the next takes 16:9 plus 2K, and a hardcoded pair silently becomes the wrong crop when you switch. Ratio plus tier is what both understand, so adapters derive the dimensions for you. Need exact pixels? Set them on that provider’s typed request and they win.

Not every model accepts every ratio, and the list changes with each release, so rather than ship a table that goes stale the adapter sends what you asked for and hands you the provider’s answer.

Feed the result into the next turn

const { images, usage } = yield* generate({ … })
const [{ image, watermark }] = images

image is an ImageSource, the same type you pass into a multimodal language model, so a picture you just generated goes straight into the next turn with no conversion. Adapters hand back the bytes the provider sent, with its MIME type, and never re-encode. watermark is set only when the provider stamped one, and every usage field is optional since not all providers report.

Editing

Pass the images you want changed alongside the prompt:

const dawn = Effect.gen(function* () {
const dusk = yield* generate({ prompt: "A lighthouse at dusk", model: "gpt-image-2" })
return yield* edit({
prompt: "Make it dawn instead of dusk",
model: "gpt-image-2",
images: [dusk.images[0]!.image],
})
})

Conditioning on references is also how you keep a character or a product consistent across many images: see the storyboard recipe.

Masks stay off the portable request, since only inpainting endpoints have one; same for quality tiers, output encodings and moderation levels. They live on the typed request of providers that support them.

Previews while it renders

streamGeneration emits zero or more PartialImage frames, then exactly one Complete. Complete carries the response fields flat, so it is an ImageResponse: filter for it and pass it on. streamEdit does the same for an edit, which is what makes a conversational editing session feel live rather than frozen.

streamGeneration({ prompt, model, partialImages: 2 }).pipe(Stream.filter(isPartialImage))

See the conversational image edit recipe, which draws those frames straight into your terminal.

When something goes wrong

SituationError
Prompt or output blocked by moderationContentFiltered
Provider returned no imageGenerationFailed
Something the provider structurally cannot doUnsupported, before the request goes out

Asking for more images than an endpoint returns fails rather than degrades: fewer images is a different result, not a smaller one.

Providers

  • OpenAIgpt-image-2, including edits and partial-image streaming.
  • Google — Nano Banana 2, Lite and Pro. Faster and cheaper; no streaming.
  • fal — FLUX.2, Seedream 5, Qwen Image and the open-weights field, including the sub-second tier. One key for models you cannot reach directly; no streaming.