Image Generation
The image modality. Vendors live under image/.
image.GenerateImage(ctx, prompt) takes only a prompt — every vendor knob
(size, aspect ratio, quality, response format, style, seed, safety, …) lives
on the vendor's Options and is set at construction. Image generation is
"configure once, prompt many" and vendor request bodies don't share enough
common shape to support a portable per-call surface.
OpenAI
import (
"github.com/joakimcarlsson/ai/image"
imageopenai "github.com/joakimcarlsson/ai/image/openai"
)
client := imageopenai.NewGeneration(
imageopenai.WithAPIKey(os.Getenv("OPENAI_API_KEY")),
imageopenai.WithModel(imageopenai.Models[imageopenai.GPTImage15]),
imageopenai.WithSize(imageopenai.Size1024x1024),
imageopenai.WithQuality(imageopenai.QualityHigh),
imageopenai.WithBackground(imageopenai.BackgroundTransparent),
imageopenai.WithOutputFormat(imageopenai.OutputFormatPNG),
)
resp, err := client.GenerateImage(ctx, "A serene mountain landscape at sunset")
if err != nil {
log.Fatal(err)
}
data, _ := image.DecodeBase64Image(resp.Images[0].ImageBase64)
os.WriteFile("output.png", data, 0644)
Full option set (typed enums — see the package's exported Size, Quality,
Background, Moderation, OutputFormat types):
imageopenai.WithN(int) // 1–10
imageopenai.WithSize(imageopenai.Size1024x1024) // 1024x1024 | 1024x1536 | 1536x1024 | auto
imageopenai.WithQuality(imageopenai.QualityHigh) // low | medium | high | auto
imageopenai.WithBackground(imageopenai.BackgroundAuto) // transparent | opaque | auto — gpt-image-1.5 only (gpt-image-2 rejects)
imageopenai.WithModeration(imageopenai.ModerationAuto) // auto | low
imageopenai.WithOutputFormat(imageopenai.OutputFormatPNG) // png | jpeg | webp
imageopenai.WithOutputCompression(int) // 0–100 — jpeg/webp only
imageopenai.WithUser(string) // end-user identifier
imageopenai.WithStreamingOptions(...) // partial-image count for streaming
Supported models: gpt-image-1.5 and gpt-image-2. DALL-E 2/3 and gpt-image-1
(plus mini) are removed; pricing-registry entries dropped along with the
matching package code paths.
Azure OpenAI
image/azure is to image/openai what llm/azure is to llm/openai — a thin
wrapper that reuses the OpenAI request building and overrides only endpoint and
auth. It resolves in three branches, mirroring llm/azure:
- no endpoint set → plain
image/openai(optionally withWithAPIKey); - an endpoint containing
/openai/v1(the OpenAI-compatible surface) → routed throughimage/openaiwithWithBaseURL; - a classic
https://<resource>.openai.azure.comendpoint → theapi-keyheader plus the?api-version=query param.
import imageazure "github.com/joakimcarlsson/ai/image/azure"
client := imageazure.NewGeneration(
imageazure.WithEndpoint("https://my-resource.openai.azure.com"),
imageazure.WithAPIVersion("2025-04-01-preview"),
imageazure.WithAPIKey(os.Getenv("AZURE_OPENAI_API_KEY")),
imageazure.WithModel(openai.Models[openai.GPTImage2]),
imageazure.WithSize(imageazure.Size1024x1024),
imageazure.WithOutputFormat(imageazure.OutputFormatPNG),
)
resp, err := client.GenerateImage(ctx, "A serene mountain landscape at sunset")
Auth resolution matches llm/azure: a static api-key is used when
WithAPIKey is set; otherwise the client falls back to
DefaultAzureCredential (Entra ID / managed identity) automatically — omit
WithAPIKey and ensure az login or a managed identity is available.
The image/openai enum types and their values are re-exported
(imageazure.Size1024x1024, imageazure.QualityHigh,
imageazure.OutputFormatPNG, …), and the full option set is forwarded:
WithSize, WithQuality, WithN, WithBackground, WithModeration,
WithOutputFormat, WithOutputCompression, WithUser, WithExtraHeaders,
WithStreamingOptions, WithTimeout. Returned clients are tracing-wrapped like
image/openai.
Gemini / Imagen
import imagegemini "github.com/joakimcarlsson/ai/image/gemini"
client := imagegemini.NewGeneration(
imagegemini.WithAPIKey(os.Getenv("GEMINI_API_KEY")),
imagegemini.WithModel(gemini.Models[gemini.Imagen4]),
imagegemini.WithAspectRatio(imagegemini.AspectRatio16x9),
imagegemini.WithN(2),
)
resp, err := client.GenerateImage(ctx, "A cyberpunk cityscape")
for i, img := range resp.Images {
data, _ := image.DecodeBase64Image(img.ImageBase64)
os.WriteFile(fmt.Sprintf("image_%d.png", i), data, 0644)
}
Full option set (Imagen-only fields are ignored when the active model is a Gemini Image variant):
import "google.golang.org/genai"
imagegemini.WithN(int32) // Imagen: 1–4
imagegemini.WithAspectRatio(imagegemini.AspectRatio16x9) // see imagegemini.AspectRatio*
imagegemini.WithNegativePrompt(string) // Imagen only
imagegemini.WithSeed(int32) // Imagen only (requires AddWatermark=false)
imagegemini.WithPersonGeneration(genai.PersonGenerationAllowAdult) // both paths
imagegemini.WithSafetyFilterLevel(genai.SafetyFilterLevelBlockOnlyHigh) // Imagen only
imagegemini.WithLanguage(genai.ImagePromptLanguageEn) // Imagen only
imagegemini.WithEnhancePrompt(bool) // Imagen only
imagegemini.WithImageSize(imagegemini.ImageSize2K) // 1K | 2K | 4K — model-dependent
imagegemini.WithIncludeRAIReason(bool) // Imagen only
imagegemini.WithOutputMIMEType(imagegemini.OutputMIMETypePNG) // image/png | image/jpeg — Imagen only
imagegemini.WithOutputCompressionQuality(int32) // 0–100 — Imagen jpeg only
xAI Grok Imagine
import imagexai "github.com/joakimcarlsson/ai/image/xai"
client := imagexai.NewGeneration(
imagexai.WithAPIKey(os.Getenv("XAI_API_KEY")),
imagexai.WithModel(xai.Models[xai.GrokImagineImage]),
imagexai.WithAspectRatio(imagexai.AspectRatio16x9),
imagexai.WithResolution(imagexai.Resolution2K),
imagexai.WithResponseFormat(imagexai.ResponseFormatBase64),
)
resp, err := client.GenerateImage(ctx, "A neon-lit street market")
Full option set:
imagexai.WithN(int) // 1–10
imagexai.WithAspectRatio(imagexai.AspectRatio16x9) // 14 values — see imagexai.AspectRatio*
imagexai.WithResolution(imagexai.Resolution2K) // 1K | 2K
imagexai.WithResponseFormat(imagexai.ResponseFormatBase64) // url | b64_json
imagexai.WithUser(string) // end-user identifier
OpenRouter
Unlike the TTS and STT OpenRouter packages, image/openrouter is a full
implementation rather than a base-URL wrapper: OpenRouter's image endpoint is
POST /api/v1/images, a different path and body from OpenAI's
/v1/images/generations.
import imageopenrouter "github.com/joakimcarlsson/ai/image/openrouter"
client := imageopenrouter.NewGeneration(
imageopenrouter.WithAPIKey(os.Getenv("OPENROUTER_API_KEY")),
imageopenrouter.WithModel(openrouter.Models[openrouter.Seedream45]),
imageopenrouter.WithAspectRatio(imageopenrouter.AspectRatio16x9),
)
resp, err := client.GenerateImage(ctx, "A red panda astronaut")
data, _ := image.DecodeBase64Image(resp.Images[0].ImageBase64)
fmt.Println(resp.Images[0].MediaType) // image/png
fmt.Println(resp.Usage.Cost) // 0.04 — dollars, as OpenRouter reported them
Full option set:
imageopenrouter.WithN(int) // 1–10, per-model ceiling
imageopenrouter.WithSize("2048x2048") // tier or explicit pixels
imageopenrouter.WithAspectRatio(imageopenrouter.AspectRatio16x9) // 18 values
imageopenrouter.WithResolution(imageopenrouter.Resolution4K) // 1K | 2K | 4K
imageopenrouter.WithQuality(imageopenrouter.QualityHigh) // auto | low | medium | high
imageopenrouter.WithBackground(imageopenrouter.BackgroundTransparent) // auto | transparent | opaque
imageopenrouter.WithOutputFormat(imageopenrouter.OutputFormatWebP) // png | jpeg | webp | svg
imageopenrouter.WithOutputCompression(int) // 0–100, webp/jpeg
imageopenrouter.WithSeed(int64) // where supported
imageopenrouter.WithInputReferences(urls ...string) // image-to-image
imageopenrouter.WithProviderRouting(order []string, allowFallbacks bool)
imageopenrouter.WithRequestJSONField(key string, value any) // escape hatch
imageopenrouter.WithHTTPClient(*http.Client)
imageopenrouter.WithExtraHeaders(map[string]string) // HTTP-Referer, X-Title
imageopenrouter.WithTimeout(time.Duration)
Not every model accepts every knob, and a model advertising a value does not mean
it accepts it in every combination. seedream-4.5 lists 1K, 2K and 4K but
enforces a floor of 3,686,400 output pixels, so 1K and 2K both draw an HTTP
400 at 16:9. Omitting WithResolution lets OpenRouter apply the model's own
default, which is why the examples above do. Query
GET /api/v1/images/models for a model's supported_parameters first.
There is no WithModelFallbacks. OpenRouter documents its models fallback
array for chat completions and /api/v1/messages only; on /images a
nonexistent primary answers 404 No model found rather than falling through,
and the request schema silently drops fields it does not know. Fall back in
caller code with a second client instead. WithProviderRouting is documented
for this endpoint and is wired up.
Using a model the registry does not define
openrouter.Models carries 26 known-good defaults, but
OpenRouter routes more than that and adds new models weekly. You never have to
wait for a release to use one. WithModelID takes any raw OpenRouter id:
client := imageopenrouter.NewGeneration(
imageopenrouter.WithAPIKey(os.Getenv("OPENROUTER_API_KEY")),
imageopenrouter.WithModelID("krea/krea-2-large"),
imageopenrouter.WithAspectRatio(imageopenrouter.AspectRatio4x5),
)
That is shorthand for WithModel(image.GenerationModel{APIModel: id,
Provider: "openrouter"}), so Model() reports the id and provider
and nothing else — no pricing, no supported-value lists. Generation works fine;
what breaks is anything that reads that metadata, such as a cost estimator or a
UI offering the caller a list of aspect ratios.
When you need those, describe the model yourself and it behaves exactly like a
registered entry, DefaultAspectRatio included:
riverflowFast := image.GenerationModel{
ID: "openrouter.riverflow-v2.5-fast",
Name: "OpenRouter – Riverflow V2.5 Fast",
Provider: "openrouter",
APIModel: "sourceful/riverflow-v2.5-fast",
Pricing: map[string]map[string]float64{
"default": {"default": 0.019},
},
MaxPromptTokens: 4000,
SupportedAspectRatios: []string{"1:1", "4:3", "16:9", "21:9", "auto"},
DefaultAspectRatio: "16:9",
SupportedSizes: []string{"1K", "2K"},
DefaultSize: "1K",
SupportedQualities: []string{"default"},
DefaultQuality: "default",
}
client := imageopenrouter.NewGeneration(
imageopenrouter.WithAPIKey(os.Getenv("OPENROUTER_API_KEY")),
imageopenrouter.WithModel(riverflowFast),
)
The real capability and pricing data for any id lives at
GET /api/v1/images/models/<id>/endpoints. stt/openrouter and
tts/openrouter expose the same WithModelID shorthand over
stt.TranscriptionModel and tts.AudioModel.
examples/image/openrouter runs all of this, including both custom-model forms.
GenerateImageStreaming is real here, not ErrStreamingNotSupported: models
whose descriptor reports supports_streaming (the gpt-image-* family today)
deliver EventPartialImage frames before the final EventCompleted.
Per-model capability data — including SupportedAspectRatios — lives on
image.GenerationModel. Inspect it to know what a given model accepts:
Streaming partial images (OpenAI gpt-image-*)
client := imageopenai.NewGeneration(
imageopenai.WithAPIKey(...),
imageopenai.WithModel(openai.Models[openai.GPTImage15]),
imageopenai.WithStreamingOptions(imageopenai.StreamingOptions{PartialImages: 3}),
)
err := client.GenerateImageStreaming(ctx, prompt,
func(event image.StreamEvent) error {
switch event.Type {
case image.EventPartialImage:
data, _ := image.DecodeBase64Image(event.ImageBase64)
os.WriteFile(fmt.Sprintf("partial_%d.png", event.PartialImageIndex), data, 0644)
case image.EventCompleted:
data, _ := image.DecodeBase64Image(event.ImageBase64)
os.WriteFile("final.png", data, 0644)
}
return nil
},
)
Returns image.ErrStreamingNotSupported if the model can't stream.