Multimodal Observations¶
LLEnvs supports image observations alongside text, enabling evaluation of vision-language models (VLMs) on visual environments.
ImageContent¶
Images are represented as base64-encoded data:
from llenvs.core import ImageContent
img = ImageContent(data="iVBORw0KGgo...", media_type="image/png")
| Field | Type | Default | Description |
|---|---|---|---|
data |
str |
required | Base64-encoded image data |
media_type |
str |
"image/png" |
MIME type (image/png, image/jpeg) |
Observation Images¶
Environments produce image observations via the images field on Observation:
from llenvs.core.state import Observation, ImageContent
obs = Observation(
prompt="What do you see in this image?",
images=(ImageContent(data="iVBOR..."),),
)
Text-only environments leave images as an empty tuple (the default).
Pipeline¶
Images flow through the full pipeline:
Environment.reset() / .step()
→ Observation(prompt="...", images=(...))
→ TrajectoryRunner._build_messages()
→ ChatMessage(role="user", content="...", images=(...))
→ .to_dict() # OpenAI format
→ .to_anthropic_dict() # Anthropic format
OpenAI Format¶
ChatMessage.to_dict() produces the OpenAI vision format when images are present:
msg = ChatMessage(role="user", content="What is this?", images=(img,))
msg.to_dict()
# {
# "role": "user",
# "content": [
# {"type": "text", "text": "What is this?"},
# {"type": "image_url", "image_url": {"url": "data:image/png;base64,iVBOR..."}},
# ]
# }
Without images, to_dict() produces standard format ("content": "text").
Anthropic Format¶
ChatMessage.to_anthropic_dict() produces Anthropic's image format:
msg.to_anthropic_dict()
# {
# "role": "user",
# "content": [
# {"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": "iVBOR..."}},
# {"type": "text", "text": "What is this?"},
# ]
# }
Backend Support¶
| Backend | Vision Support | Notes |
|---|---|---|
| OpenAI | Yes | Via image_url content blocks |
| Anthropic | Yes | Via base64 source blocks |
| OpenRouter | Yes | Via OpenAI format |
| vLLM | Auto-detected | VLMs (LLaVA, Qwen-VL, PaliGemma, etc.) via multi_modal_data |
| HuggingFace | Auto-detected | VLMs via AutoProcessor + AutoModelForVision2Seq |
Check at runtime:
Local VLM Support (vLLM / HuggingFace)¶
Both local backends auto-detect vision-language models by checking model_type against a known list (llava, qwen2_vl, paligemma, internvl_chat, phi3_v, etc.). When a VLM is detected:
capabilities.supports_visionreturnsTrue- Images from
ChatMessage.imagesare decoded from base64 to PIL Images - vLLM: Images are passed via
multi_modal_data={"image": [pil_img, ...]}alongside the text prompt - HuggingFace: Uses
AutoProcessor(combined tokenizer + image processor) andAutoModelForVision2Seqinstead ofAutoModelForCausalLM
No configuration is needed — just use a VLM model path:
from llenvs.inference.backends import VLLMBackend
backend = VLLMBackend(model_path="llava-hf/llava-1.5-7b-hf")
print(backend.capabilities.supports_vision) # True
Batch generation with images works automatically — each conversation in a batch can have different images.
Creating Visual Environments¶
Environments produce images by including ImageContent in their observations:
from llenvs.core.state import ImageContent, Observation
# In your environment's reset() or step():
img = ImageContent(data=base64_encoded_png, media_type="image/png")
obs = Observation(
prompt="Describe what you see and choose an action.",
images=(img,),
)
Multi-Turn Image History¶
For multi-turn environments, images in history messages are preserved. The observation's messages list can include image data:
obs = Observation(
prompt="Initial prompt",
images=(current_frame,),
messages=(
{"role": "assistant", "content": "I see a forest."},
{
"role": "user",
"content": "Now look again.",
"images": [{"data": "iVBOR...", "media_type": "image/png"}],
},
),
)
Memory Management¶
Multi-turn trajectories with images at every step can consume large amounts of memory. Several mechanisms help control this.
Image History Truncation¶
Limit how many images are kept in the message history during evaluation:
from llenvs.evaluation import TrajectoryRunner
runner = TrajectoryRunner(
environment=env,
backend=backend,
max_image_history=5, # Keep only 5 most recent images
)
When max_image_history is set, older images are stripped from messages while preserving the text content. None (default) keeps all images.
Trajectory Image Stripping¶
Remove all images from a completed trajectory (e.g., before serialization or logging):
stripped = trajectory.strip_images()
# Returns a new Trajectory with all Observation.images set to ()
Log Image Stripping¶
Strip images from evaluation logs to reduce JSONL/wandb payload size:
from llenvs.evaluation.logging import LogConfig
log = LogConfig(
targets=["file"],
strip_images=True, # Images stripped before logging
)
Adapters with Image Support¶
| Adapter | Image Mode |
|---|---|
| Craftax | observation_mode="pixels" — renders game frames as PNG |
| Gymnasium | ImageObservationMapper for pixel observations (Atari presets included) |
| AlfWorld | visual=True — AI2-THOR ego-centric RGB frames |
Container Serialization¶
Image observations serialize to JSON naturally (base64 strings). The container protocol handles images in Observation round-trip:
from llenvs.container.serialization import serialize_observation, deserialize_observation
serialized = serialize_observation(obs)
# {"prompt": "...", "images": [{"data": "...", "media_type": "image/png"}]}
restored = deserialize_observation(serialized)
assert restored.images[0].data == obs.images[0].data
Prompt Transformers¶
All prompt transformers preserve images when modifying messages:
ChainOfThoughtWrapperAnswerFormatInjectorContentWrapperRoleMapperPromptTemplateTransformer