Skip to Content

Vision

Vision-capable models accept images alongside text. Loomflow carries image input through a single value type, Image, and each provider adapter folds it into the multimodal shape that provider’s API expects — OpenAI’s image_url data-URI, Anthropic’s image base64 source block. You attach images to a run; the framework handles the wire format.

Images are optional everywhere. A run with no images is an ordinary text run, unchanged.

Quick start

Images travel on the run’s metadata under the _loom_images key. Encode the image bytes as base64, wrap them in Image, and pass a list:

import base64 from pathlib import Path from loomflow import Agent, Image agent = Agent("You describe images carefully.", model="claude-opus-4-7") raw = Path("diagram.png").read_bytes() b64 = base64.b64encode(raw).decode("ascii") result = await agent.run( "What's in this image?", metadata={ "_loom_images": [Image(data=b64, media_type="image/png")], }, ) print(result.value)

The agent seeds a USER message from the prompt and attaches the images to it. The adapter for whichever model you configured converts the Image into that provider’s multimodal content format before the call.

The Image type

Image is a frozen value object exported from the top level:

from loomflow import Image Image(data=b64_string, media_type="image/jpeg")
FieldTypeDefaultNotes
datastrrequiredThe raw image bytes, base64-encoded as a string.
media_typestr"image/png"MIME type. Supported: image/png, image/jpeg, image/gif, image/webp.

data is the base64 string, not the raw bytes — encode them yourself with base64.b64encode(...).decode() as in the example above.

Attaching images to a run

The _loom_images metadata key accepts a list whose entries are either Image objects or plain dicts with the same fields:

# Image objects metadata={"_loom_images": [Image(data=b64, media_type="image/jpeg")]} # Equivalent dict form — coerced to Image, media_type defaults to image/png metadata={"_loom_images": [{"data": b64, "media_type": "image/jpeg"}]}

A dict without a data key is skipped. Multiple images in one run are attached to the same user message in list order:

result = await agent.run( "Compare these two screenshots.", metadata={ "_loom_images": [ Image(data=before_b64, media_type="image/png"), Image(data=after_b64, media_type="image/png"), ], }, )

metadata is the same free-form bag tools and hooks read via get_run_context().metadata, so vision input rides alongside any other per-run context you already pass.

How it maps per provider

The Image is folded into the provider’s multimodal content format at adapter time. The user message becomes a list of content parts: the text first, then one part per image.

ProviderWire shape
OpenAIAn image_url part whose url is a data-URI: data:<media_type>;base64,<data>.
AnthropicAn image block with a source of {type: "base64", media_type, data}.

For OpenAI the user turn’s content becomes [{"type": "text", ...}, {"type": "image_url", "image_url": {"url": "data:image/png;base64,..."}}]. For Anthropic it becomes [{"type": "text", ...}, {"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": "..."}}]. When the prompt text is empty the text part is omitted and only the image parts are sent.

A user message with no images skips all of this and sends content as a plain string, so text-only callers and adapters are unaffected.

The model has to support vision. Attaching an Image formats it correctly for the provider, but the model itself must be vision-capable for the image to be read. Sending an image to a text-only model is a model-level limitation, not a framework one — check the provider’s model card for image support.

See also

Last updated on