Skip to content

Image Providers

Image generation is its own provider surface (RFC §25), not a mode of the conversational response. The model that holds a conversation is rarely one that draws, so an agent conversing through Anthropic draws through Gemini exactly as it transcribes through Deepgram.

ImageProvider

Bases: ABC

Generates images from a prompt, decoupled from the conversation (RFC §25).

name property

name

Provider name (e.g. 'OpenAIImageProvider').

model_name abstractmethod property

model_name

Model identifier (e.g. 'gpt-image-2', 'gemini-3-pro-image').

supports_editing property

supports_editing

Whether reference_images is honoured. False refuses them outright.

available_models classmethod

available_models()

Offline metadata for the image models this provider can describe.

Deliberately not folded into :meth:~roomkit.providers.ai.base.AIProvider.available_models. The two catalogs are disjoint sets — gpt-image-2 is not a conversational model and no chat id draws — so merging them saves no maintenance, the entries being written once either way, while obliging every consumer of the conversational catalog to filter out a class of models it can never use. Entries carry :data:IMAGE_GEN_CAPABILITY for a consumer that merges the lists on purpose.

The base returns an empty list; providers override it.

catalog_entry

catalog_entry()

The offline :class:ModelInfo for the active model, if the catalog has it.

generate abstractmethod async

generate(prompt, *, size=None, n=1, reference_images=None)

Generate n images from prompt.

Parameters:

Name Type Description Default
prompt str

What to draw.

required
size str | None

Geometry as "WIDTHxHEIGHT" (e.g. "1024x1024"), or None for the model's default. A provider whose API speaks aspect ratios translates; the caller never has to know which form its vendor wants. A size the model cannot produce raises rather than silently becoming another one.

None
n int

How many images. Must be at least 1. A provider whose API has no batch parameter issues concurrent calls — the vendor bills per image either way.

1
reference_images list[AIImagePart] | None

Images to edit or draw from. Non-empty makes this an edit; a provider that reports supports_editing as False raises rather than quietly generating from the prompt alone.

None

Returns:

Type Description
list[ImageResult]

Exactly n results, or an exception. Never fewer without error:

list[ImageResult]

a caller discovers a short list by indexing it, in production.

Raises:

Type Description
ProviderError

The vendor call failed.

ValueError

The request is one this provider cannot express.

close async

close()

Release resources. Override in subclasses that hold connections.

ImageResult

Bases: BaseModel

One generated image.

Attributes:

Name Type Description
data str

The image as a data:<mime_type>;base64,<payload> URI — always, never bare base64 and never a remote URL. A field documented as "one or the other" makes every consumer sniff the value before it can use it, and one consumer sniffs it wrong. The invariant is also what makes a result immediately usable: a data URI is what MediaContent.url and :class:AIImagePart already accept, so a generated image enters a room, or comes back as a reference for the next edit, without conversion.

mime_type str

Media type of the payload. Always equal to the one spelled in :attr:data; carried separately so a caller can branch on it without parsing the URI.

revised_prompt str | None

The prompt as the model rewrote it, where the vendor reports one — None otherwise. Never a copy of the caller's prompt: echoing the input back would conceal exactly the divergence this field exists to reveal.

usage dict[str, Any]

Token counters for the call, disjoint by construction — input_tokens (text in), input_image_tokens (reference images), output_tokens (text out, where billed) and output_image_tokens (the generated image). Priced by :meth:~roomkit.providers.ai.base.ModelPricing.cost_for. Empty when the vendor reports nothing.

decoded

decoded()

The raw image bytes.

Raises:

Type Description
ValueError

If the payload is not valid base64 — a corrupted result is worth an error at the point it is read, not a truncated file on disk.

to_image_part

to_image_part()

The result as a message part — an AI input, or the next edit's reference.

MockImageProvider

MockImageProvider(images=None, *, model='mock-image', mime_type='image/png', supports_editing=True, revised_prompt=None)

Bases: ImageProvider

Image provider that returns canned images.

Attributes:

Name Type Description
calls list[tuple[str, str | None, int, list[AIImagePart]]]

Every (prompt, size, n, reference_images) this provider was asked for, in order — what a test asserts against.

parse_size

parse_size(size)

Parse a "WIDTHxHEIGHT" size string into its two integers.

Parameters:

Name Type Description Default
size str

Geometry as the :class:ImageProvider surface spells it.

required

Returns:

Type Description
tuple[int, int]

(width, height).

Raises:

Type Description
ValueError

If size is not two positive integers joined by x.

to_data_uri

to_data_uri(data, mime_type)

Encode raw bytes as the data:<mime>;base64,<payload> URI RoomKit carries images in.

parse_data_uri

parse_data_uri(url, *, fallback_mime=None)

Split a data: URI into its media type and its decoded bytes.

The counterpart of :func:to_data_uri, and the one place a payload is validated: every provider that accepts an image has to reject a corrupt one, and each doing it itself is how one of them ends up handing malformed bytes to a vendor and reporting the rejection as a provider failure rather than a caller error.

Whitespace — an encoder that wrapped its lines — and missing padding are repaired, so the bytes, and anything re-encoded from them, are canonical whatever the caller's URI carried. Anything else is refused: a character outside the alphabet, a length no padding can complete.

Parameters:

Name Type Description Default
url str

The URI to split.

required
fallback_mime str | None

Media type to use when the URI declares none. None falls back to image/png.

None

Returns:

Type Description
tuple[str, bytes]

(mime_type, data).

Raises:

Type Description
ValueError

If url is not a data: URI, carries no payload, or its payload is not base64.

sniff_mime_type

sniff_mime_type(data, *, fallback='image/png')

The media type of raw image bytes, read from their magic number.

For providers whose API can answer with bytes but no declared type — an :class:ImageResult must state the type its data URI carries, and labelling a JPEG image/png because a fallback said so is a lie every consumer of the URI then repeats. Only the formats image APIs actually return are recognized; fallback answers for anything else.

Vendor implementations

OpenAIImageProvider

OpenAIImageProvider(config)

Bases: ImageProvider

Image provider using the OpenAI Images API.

generate calls images.generate; a call carrying reference images calls images.edit instead. That split is OpenAI's, not the caller's — RFC §25.4 requires the provider to absorb it.

available_models classmethod

available_models()

Curated, offline catalog of OpenAI image models.

OpenAIImageConfig

Bases: BaseModel

OpenAI image-generation provider configuration (RFC §25).

Separate from :class:OpenAIConfig because it configures a different endpoint with a disjoint model lineup — sampling temperature, reasoning effort and completion caps mean nothing to /v1/images, and an image model means nothing to Chat Completions.

Attributes:

Name Type Description
api_key SecretStr

API key for authentication.

base_url str | None

Custom base URL for an OpenAI-compatible images endpoint. None uses the default OpenAI API.

model str

Image model identifier (e.g. "gpt-image-2"). Required, for the same reason the chat config requires one: upgrading RoomKit must not silently change a caller's cost or output.

quality str | None

"low" | "medium" | "high" | "auto", or None for the model's default. Multiplies both the token count and the latency, so it is a deployment decision rather than a per-call one.

background str | None

"transparent" | "opaque" | "auto". Transparent requires a png or webp output format.

output_format str | None

"png" | "jpeg" | "webp". None leaves the vendor default, which the response reports back and this provider reads rather than assuming.

timeout float

HTTP request timeout in seconds. Higher than the chat default because a high-quality image routinely takes more than 30s.

connect_timeout float

TCP connect timeout in seconds, kept apart from timeout so a host that no longer accepts connections is given up on in seconds rather than after the read budget.

max_retries int

SDK-level retry count. 0 because RoomKit's RetryPolicy handles retries at the right layer.

default_headers class-attribute instance-attribute

default_headers = None

Extra HTTP headers sent on every request, passed to the SDK's default_headers — same role as on :class:OpenAIConfig.

GeminiImageProvider

GeminiImageProvider(config)

Bases: ImageProvider

Image provider using the Gemini Interactions API.

available_models classmethod

available_models()

Curated, offline catalog of Gemini image models.

close async

close()

Close the SDK and the httpx client it was given.

GeminiImageConfig

Bases: BaseModel

Google Gemini image-generation provider configuration (RFC §25).

Separate from :class:GeminiConfig because it configures a disjoint model lineup — the *-image models the chat catalog explicitly excludes — and a different set of knobs: geometry and output encoding rather than temperature and thinking level.

Attributes:

Name Type Description
api_key SecretStr

API key for authentication.

model str

Image model identifier (e.g. "gemini-3-pro-image").

image_size str | None

Default resolution tier — "512" | "1K" | "2K" | "4K". A per-call size wins over it, since the caller asking for specific pixels is more specific than a deployment default. None leaves the model's own default.

output_mime_type str | None

"image/jpeg" to ask for JPEG. None leaves the vendor default (PNG); the response reports what it actually produced and the provider reads that rather than assuming. Gemini offers no other selectable output type here.

timeout float

Read budget in seconds for one interaction. Higher than the chat default because an image is produced whole: nothing streams before it.

connect_timeout float

TCP connect timeout in seconds, kept apart from timeout so a host that no longer accepts connections is given up on in seconds rather than after the read budget.

XAIImageProvider

XAIImageProvider(config)

Bases: ImageProvider

Image provider using xAI's Grok Imagine images API.

supports_editing property

supports_editing

Whether the configured model takes reference images.

Read off the catalog's edit tag. An id the catalog does not know (a model newer than the snapshot) defaults to True rather than refusing a capability the current lineup mostly has — the same permissive default the chat provider uses for vision.

available_models classmethod

available_models()

Curated, offline catalog of Grok Imagine image models.

XAIImageConfig

Bases: BaseModel

xAI (Grok Imagine) image-generation provider configuration (RFC §25).

Separate from :class:XAIConfig because it configures a different endpoint with a disjoint model lineup — sampling temperature and reasoning effort mean nothing to /v1/images, and an image model means nothing to Chat Completions. Distinct from :class:XAIRealtimeConfig for the same reason — same vendor, three protocols.

Attributes:

Name Type Description
api_key SecretStr

xAI API key for authentication.

base_url str

xAI's API endpoint. Override only to point at a proxy.

model str

Grok Imagine image model id — see :mod:roomkit.providers.xai.image_models for the curated catalog. Defaults to the model xAI's own docs recommend for images.

quality str | None

"low" | "medium", or None for the model's default. Only grok-imagine-image-2.0 accepts it; sent only when set.

resolution str | None

Default resolution tier — "1k" | "2k" — applied when a call names no size. None leaves the vendor default. A size passed to generate wins over this.

timeout float

HTTP request timeout in seconds. Higher than the chat default because image synthesis routinely takes more than 30s.

connect_timeout float

TCP connect timeout in seconds, kept apart from timeout so a host that no longer accepts connections is given up on in seconds rather than after the read budget.

max_retries int

SDK-level retry count. 0 because RoomKit's RetryPolicy handles retries at the right layer.

OpenRouterImageProvider

OpenRouterImageProvider(config)

Bases: ImageProvider

Image provider using OpenRouter's Image API.

available_models classmethod

available_models()

Curated, offline slice of OpenRouter image model slugs.

A small representative sample — OpenRouter's public GET /api/v1/images/models is the discovery surface for the full, always-current set, with each model's live parameter constraints.

OpenRouterImageConfig

Bases: OpenAIImageConfig

OpenRouter image-generation provider configuration (RFC §25).

OpenRouter's Image API routes one request shape to every image model it aggregates, so this subclasses :class:OpenAIImageConfig and inherits its request fields (quality, background, output_format, timeout, max_retries, default_headers) — the Image API accepts each of them, forwarding what the routed model understands. Only the endpoint and OpenRouter's app-attribution headers are added on top.

The endpoint is OpenRouter's own (POST {base_url}/images), not the OpenAI images path — see :class:~roomkit.providers.openrouter.image.OpenRouterImageProvider.

base_url class-attribute instance-attribute

base_url = 'https://openrouter.ai/api/v1'

OpenRouter's API root. Override only to point at a self-hosted proxy.

model instance-attribute

model

OpenRouter image model slug — e.g. "google/gemini-3.1-flash-image" or "x-ai/grok-imagine-image-2.0". Required (the value of OpenRouter is choosing the model). Browse the live set at GET /api/v1/images/models.

site_url class-attribute instance-attribute

site_url = None

Sent as the HTTP-Referer header — same attribution role as on :class:OpenRouterConfig.

app_name class-attribute instance-attribute

app_name = None

Sent as the X-Title header — same attribution role as on :class:OpenRouterConfig.

AzureImageProvider

AzureImageProvider(config)

Bases: OpenAIImageProvider

Image provider using the images endpoint of an Azure OpenAI resource.

Subclasses :class:OpenAIImageProvider the way the chat providers pair up — Azure serves the same gpt-image-* lineup through the same SDK, so request building, the generate/edit split, response mapping and usage accounting are all inherited. Two things are genuinely Azure's: the client (endpoint, key and API version instead of a bearer token), and the catalog (none — deployments are user-named).

available_models classmethod

available_models()

Azure exposes user-named deployments, not a fixed model catalog.

The reason :meth:AzureAIProvider.available_models gives holds here too: deployment names are chosen per Azure resource, so there is no meaningful offline list. Rates for the underlying gpt-image-* models are Azure's own, not OpenAI's, so the OpenAI image catalog is deliberately not inherited either.

AzureImageConfig

Bases: BaseModel

Azure OpenAI image-generation provider configuration (RFC §25).

Configures the images endpoint of an Azure OpenAI resource — the same gpt-image-* lineup :class:~roomkit.providers.openai.config.OpenAIImageConfig reaches on openai.com, deployed under a name the resource owner chose. Separate from :class:AzureAIConfig for the reason the OpenAI configs are separate: a different endpoint, a disjoint model lineup, and none of the chat request fields mean anything to it.

Attributes:

Name Type Description
api_key SecretStr

Azure API key for authentication.

azure_endpoint str

Azure OpenAI resource endpoint URL.

api_version str

Azure API version string. The default is the version Azure's image-generation documentation currently requires; older versions predate gpt-image-1 and reject it.

model str

Deployment name (no default — deployment names are chosen per Azure resource, so there is nothing sensible to guess).

quality str | None

"low" | "medium" | "high" | "auto", or None for the deployment's default.

background str | None

"transparent" | "opaque" | "auto". Transparent requires a png or webp output format.

output_format str | None

"png" | "jpeg". None leaves the vendor default. Azure does not offer webp on this endpoint.

timeout float

HTTP request timeout in seconds. Higher than the chat default because a high-quality image routinely takes more than 30s.

connect_timeout float

TCP connect timeout in seconds, kept apart from timeout so a host that no longer accepts connections is given up on in seconds rather than after the read budget.

max_retries int

SDK-level retry count. 0 because RoomKit's RetryPolicy handles retries at the right layer.

Catalogs and pricing

Each provider ships an offline catalog through available_models(), disjoint from the conversational catalog returned by AIProvider.available_models(): no id draws and converses, so merging the two would only oblige every consumer of the conversational list to filter out models it can never use. Entries carry capabilities=["image_gen", "edit"] for a consumer that combines the lists on purpose.

The usage counters an ImageResult reports — input_tokens, input_image_tokens, output_tokens, output_image_tokens — are disjoint: a token appears under exactly one of them, so summing them counts it once.

Pricing is separate from that report, and optional. The OpenAI and Gemini lineups meter per token, with generated pixels on their own counter at a rate an order of magnitude above text, so ModelPricing carries image_input_per_million and image_output_per_million alongside the text rates and cost_for() applies them. xAI and most of OpenRouter's lineup charge a flat amount per image; their catalog entries carry no pricing rather than a rate in the wrong unit, and OpenRouter reports the billed amount itself, which its provider surfaces as ImageResult.usage["cost"].

entry = next(m for m in provider.available_models() if m.id == provider.model_name)
cost = entry.pricing.cost_for(result.usage)

See the Image Generation guide for the whole path, from a tool call to an image in a room.