Send Multiple Images in a Single Message
On this page
Analyze Multiple Images at Once
Mistral’s Vision API isn’t limited to one image per message. You can send multiple images in a single call to ask the model to compare, synthesize, or analyze them together. The difference between this and two separate calls is critical: in a single call, the model sees both images simultaneously and can reason about their relationships—something no aggregation of individual responses could achieve.
This capability unlocks use cases where the question focuses on the gap rather than the content: before/after comparisons, analysis of document series, or visual compliance checks.
Structure of a Multi-Image Message
The principle is simple: add multiple image_url items to the content array:
message = {
"role": "user",
"content": [
{"type": "text", "text": "Compare these two images and describe the differences."},
{"type": "image_url", "image_url": "https://example.com/image1.jpg"},
{"type": "image_url", "image_url": "https://example.com/image2.jpg"}
]
}
Homogeneity isn’t required: you can mix images by URL and Base64 in the same message. This is common when you need to compare a hosted reference image against a freshly generated local file.
Example: Comparing Two Images
Consider a warehouse receiving inspection. You compare a reference photo of the compliant product against a photo of the received sample, using a system prompt that establishes the inspector role:
from mistralai.client import Mistral
import os
client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])
response = client.chat.complete(
model="mistral-large-latest",
messages=[
{
"role": "system",
"content": "You are a quality control expert. Compare the provided images with precision."
},
{
"role": "user",
"content": [
{
"type": "text",
"text": "Here are two photos of the same product. The first is the reference, the second is the received sample. Identify all differences."
},
{"type": "image_url", "image_url": "https://example.com/reference.jpg"},
{"type": "image_url", "image_url": "https://example.com/sample.jpg"}
]
}
]
)
print(response.choices[0].message.content)
The text explicitly states the order—“the first is the reference”—because the model cannot guess it.
Interleaving Text and Images
This ordering approach quickly hits its limits beyond two images. A more robust method is to label each image just before providing it, alternating text and image_url elements:
response = client.chat.complete(
model="mistral-large-latest",
messages=[
{
"role": "user",
"content": [
{"type": "text", "text": "Here is the original architectural plan:"},
{"type": "image_url", "image_url": "https://example.com/plan-original.png"},
{"type": "text", "text": "And here is the photo of the constructed building:"},
{"type": "image_url", "image_url": "https://example.com/photo-batiment.jpg"},
{"type": "text", "text": "Does the building match the plan? List the discrepancies."}
]
}
]
)
Each image now carries its own label, and the final question arrives only after all material is presented. This format significantly improves the model’s understanding: it knows the plan is the standard and the photo is the object to evaluate. Reversing them would produce an inverted discrepancy report.
Batch Processing with Base64
For a folder of local images, building the content becomes programmatic. You assemble the list in a loop, encoding each file on the fly:
import base64
import os
from pathlib import Path
from mistralai.client import Mistral
client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])
def encode_image(path):
with open(path, "rb") as f:
b64 = base64.standard_b64encode(f.read()).decode("utf-8")
ext = Path(path).suffix.lower()
mime = {"jpg": "jpeg", "jpeg": "jpeg", "png": "png", "webp": "webp"}.get(ext.lstrip("."), "jpeg")
return f"data:image/{mime};base64,{b64}"
# Load all images from a folder
folder = Path("captures/")
images = sorted(folder.glob("*.png"))
# Build the message content
content = [{"type": "text", "text": f"Analyze these {len(images)} screenshots and summarize common elements."}]
for img in images[:5]: # Limit to max 5 images
content.append({"type": "image_url", "image_url": encode_image(img)})
response = client.chat.complete(
model="mistral-small-latest",
messages=[{"role": "user", "content": content}]
)
print(response.choices[0].message.content)
The images[:5] slice is the script’s safety guard. Without it, a folder containing two hundred screenshots would go entirely into a single call—with the consequences described just below.
Limits of Multi-Image Calls
There’s no strict documented limit on the number of images, but two effects combine beyond five to ten: analysis quality degrades as the model’s attention dilutes across images, and token costs rise sharply. Recall the order of magnitude from Lesson 2: each image consumes 800 to 1,500 tokens, so five images already represent 5,000 to 7,500 prompt tokens before your question even starts. The total—images, text, and response—must also fit within the model’s context window, a ceiling that a batch of images reaches much faster than a text document.
This function calculates the bill before the call rather than after:
def estimate_multi_image_cost(num_images, tokens_per_image=1000, text_tokens=100, response_tokens=500):
"""Estimate total tokens for a multi-image call."""
total_input = (num_images * tokens_per_image) + text_tokens
total = total_input + response_tokens
print(f"Images: {num_images} x {tokens_per_image} = {num_images * tokens_per_image} tokens")
print(f"Prompt text: {text_tokens} tokens")
print(f"Estimated response: {response_tokens} tokens")
print(f"Estimated total: {total} tokens")
return total
estimate_multi_image_cost(3)
# Images: 3 x 1000 = 3000 tokens
# Prompt text: 100 tokens
# Estimated response: 500 tokens
# Estimated total: 3600 tokens
Strategy for Large Volumes
Beyond five images, change your approach rather than pushing through. Three paths exist, often combined. Sequential analysis processes images one by one then aggregates results—the cost stays linear and per-image precision is maximal. Batch grouping sends packets of three to five images, preserving some intra-batch comparison. Pre-filtering, finally, uses a lighter model like Small to discard irrelevant images before handing important cases to Large.
The first path looks like this, with a final synthesis that handles only text:
# Sequential analysis with aggregation
results = []
for img_path in images:
response = client.chat.complete(
model="mistral-small-latest",
messages=[{
"role": "user",
"content": [
{"type": "text", "text": "Briefly describe the content of this image."},
{"type": "image_url", "image_url": encode_image(img_path)}
]
}]
)
results.append(response.choices[0].message.content)
# Final synthesis (text only)
synthesis = client.chat.complete(
model="mistral-small-latest",
messages=[{
"role": "user",
"content": f"Here are descriptions of {len(results)} images:\n\n" +
"\n---\n".join(results) +
"\n\nProvide a global synthesis."
}]
)
The trick lies in the second call: it contains no images, so no visual tokens. You synthesize fifty images for the price of a few hundred text tokens.
Key Takeaways
- You can send multiple images in a single message by adding multiple
image_urlelements - Interleave text between images to provide context to the model
- Each image adds 800-1500 tokens—estimate your costs before calling
- Beyond 5 images, prefer a sequential or batched strategy
- URLs and Base64 can be mixed in the same message