Multimodal AI
Working with images, documents, audio, and generated media.
Introduction
Everything so far has been text in, text out. Real products rarely stay that clean: users upload receipts, scan contracts, record calls, and expect diagrams back. Modern models are multimodal, they accept images and documents alongside text, and dedicated models handle audio and image generation. This lesson shows what a single model can do natively (vision, document understanding), what has to be routed to a specialized service (speech, image generation), and how to wire them into one pipeline.
1. What Is Multimodal?
Modalities and their token cost
2. Vision
Sending images to the model
3. Documents
Native PDF understanding
4. Audio
Speech-to-text and back
5. Image Generation
Routing to an image model
6. Combined Pipeline
Chaining modalities together
7. Cost & Pitfalls
Tokens, latency, and PII
1. What Is Multimodal AI?
A multimodal model accepts more than one kind of input in the same request. Claude and Gemini, for example, take interleaved text and images (and PDFs) in a single message. Crucially, not every modality flows through one model: today Claude reads images and documents but does not hear audio or draw pictures, so a complete product often combines a reasoning model with specialized services.
Multimodal Inputs to One Model
Figure 1: Text, images, and documents can share a single request; the model reasons over all of them together
What an Image Actually Costs
Images are billed as tokens, and the count comes from the pixel dimensions, not the file size. Rather than guess, measure with client.messages.count_tokens(). Here is the same receipt photo sent at six sizes, with a 12-token text prompt subtracted out:
| Long edge | File size | Image tokens | Relative cost |
|---|---|---|---|
| 400px | 3 KB | 168 | 0.04x |
| 800px | 10 KB | 641 | 0.14x |
| 1000px | 15 KB | 975 | 0.21x |
| 1500px | 31 KB | 2,217 | 0.47x |
| 2000px | 52 KB | 3,891 | 0.82x |
| 3000px | 115 KB | 4,743 | 1.00x |
Two things fall out of those numbers. Cost tracks area, so halving the long edge quarters the bill: dropping from 2000px to 1000px is a 75% saving. And the growth flattens at the top, because the API downscales very large uploads server-side. A 3000px image costs 4,743 tokens rather than the ~9,000 the area formula predicts, so sending a huge file buys you nothing except upload time.
2. Vision: Sending Images
You pass an image as a content block with a base64 (or URL) source, alongside a text block that says what to do with it. The model can then describe, classify, compare, extract text from, or reason about the image.
# vision.py - fetch an image, encode it, ask about it
import base64
import anthropic
import httpx
client = anthropic.Anthropic()
# Any reachable image works. Fetch it, then base64-encode the bytes.
url = "https://raw.githubusercontent.com/github/explore/main/topics/python/python.png"
image_data = base64.standard_b64encode(httpx.get(url).content).decode("utf-8")
message = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
messages=[{
"role": "user",
"content": [
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/png",
"data": image_data,
},
},
{"type": "text", "text": "Describe what this image shows in one sentence."},
],
}],
)
print(next(b.text for b in message.content if b.type == "text"))
print(f"input tokens: {message.usage.input_tokens}")
# You can also hand the model a URL and skip the download:
# {"type": "image", "source": {"type": "url", "url": url}}
# It is fetched server-side, so it only works if the host serves that
# request; some CDNs refuse it and the call fails with a 400.Expected Output:
This image shows the official Python programming language logo, featuring two intertwined snake-like shapes in blue (top) and yellow (bottom), each with a small circular "eye," forming the recognizable symbol used to represent Python. input tokens: 144
Describe & classify
Alt text, content moderation, product tagging
Extract text
Read handwriting, signs, screenshots, labels
UI to code
Turn a screenshot or mockup into HTML/JSX
Visual QA
Answer questions about charts, diagrams, photos
3. Document Understanding
Documents are the highest-value multimodal use case in business software: invoices, contracts, forms, statements. Claude accepts a PDF as a document content block and reads both its text layer and its rendered pages, so tables and even image-only scans work without a separate OCR step. Pair it with a schema through structured outputs and what comes back is a validated object rather than JSON you still have to check.
# extract_invoice.py - PDF in, validated Invoice object out
import base64
import json
import anthropic
from pydantic import BaseModel
from reportlab.lib.pagesizes import LETTER
from reportlab.pdfgen import canvas
client = anthropic.Anthropic()
# --- Build an invoice PDF so the example runs with no external file ---
ROWS = [("Espresso beans 1kg", 2, 18.50), ("Paper filters", 3, 4.25),
("Ceramic mug", 1, 14.00)]
c = canvas.Canvas("invoice.pdf", pagesize=LETTER)
c.setFont("Helvetica-Bold", 16)
c.drawString(72, 720, "Blue Bottle Coffee")
c.setFont("Helvetica", 11)
c.drawString(72, 690, "Invoice INV-2044 Date 2026-08-11")
y = 650
for description, qty, unit in ROWS:
c.drawString(72, y, f"{description:<24} {qty:>3} x {unit:>6.2f} = {qty * unit:>7.2f}")
y -= 20
c.drawString(72, y - 20, f"TOTAL {sum(q * u for _, q, u in ROWS):.2f}")
c.save()
class LineItem(BaseModel):
description: str
qty: int
unit_price: float
class Invoice(BaseModel):
invoice_number: str
date: str
vendor: str
total: float
line_items: list[LineItem]
with open("invoice.pdf", "rb") as f:
pdf_data = base64.standard_b64encode(f.read()).decode("utf-8")
# messages.parse applies the schema to the model's output, so what comes
# back is a validated Invoice, not JSON you still have to check.
response = client.messages.parse(
model="claude-sonnet-5",
max_tokens=1024,
messages=[{
"role": "user",
"content": [
{
"type": "document",
"source": {
"type": "base64",
"media_type": "application/pdf",
"data": pdf_data,
},
},
{"type": "text", "text": "Extract this invoice."},
],
}],
output_format=Invoice,
)
invoice = response.parsed_output
print(json.dumps(invoice.model_dump(), indent=2))
# Claude reads the PDF natively, both its text layer AND its rendered
# pages, so tables, columns, and image-only scans work without a
# separate OCR step.Expected Output:
{
"invoice_number": "INV-2044",
"date": "2026-08-11",
"vendor": "Blue Bottle Coffee",
"total": 63.75,
"line_items": [
{
"description": "Espresso beans 1kg",
"qty": 2,
"unit_price": 18.5
},
{
"description": "Paper filters",
"qty": 3,
"unit_price": 4.25
},
{
"description": "Ceramic mug",
"qty": 1,
"unit_price": 14.0
}
]
}INV-2044 and the correct total. Claude looks at the page, it does not just read the embedded text stream.Structured Document Extraction
Figure 2: Extraction is only trustworthy once the output is validated against a schema, retry on failure
4. Audio: Transcribe, Then Reason
Claude has no native audio input or output. Send an audio block and the API rejects it, listing the block types it does accept: text, image, document, and the tool and thinking variants. Nothing for sound. So the pattern is a two-step pipeline: transcribe speech to text with a dedicated speech-to-text (STT) service, then let Claude do what it is best at, reasoning over the text. For the reverse direction, a text-to-speech (TTS) service turns Claude's output back into audio.
# call_summary.py - synthesize a call, transcribe it, then reason over the text
# Claude has no audio input, so speech is transcribed first by a dedicated
# service and Claude reasons over the text.
import anthropic
from openai import OpenAI
audio_api = OpenAI() # STT and TTS live here
claude = anthropic.Anthropic()
SCRIPT = ("Hi, this is Dana from Northwind. Our checkout page has been returning "
"500 errors since this morning and we are losing sales. I will escalate "
"this to engineering now and we will have a fix deployed by Friday.")
# 0. Manufacture a recording, so the example needs no audio file of your own.
# This is also the text-to-speech direction: Claude's output can be spoken
# the same way.
audio_api.audio.speech.create(
model="gpt-4o-mini-tts", voice="alloy", input=SCRIPT,
).write_to_file("support_call.mp3")
# 1. Speech -> text
with open("support_call.mp3", "rb") as audio:
transcript = audio_api.audio.transcriptions.create(
model="gpt-4o-transcribe", # whisper-1 still works; this is the current model
file=audio,
).text
print(f"transcript: {transcript[:70]}...")
# 2. Text -> analysis (this is where Claude excels)
reply = claude.messages.create(
model="claude-sonnet-5",
max_tokens=512,
messages=[{"role": "user", "content": (
"Summarize this support call in one line, then list any commitments "
f"made as short bullets. No headings.\n\n{transcript}"
)}],
)
print(next(b.text for b in reply.content if b.type == "text"))Expected Output:
transcript: Hi, this is Dana from Northwind. Our checkout page has been returning ... Dana from Northwind reported checkout page 500 errors causing lost sales since this morning; support committed to escalating and fixing by Friday. - Escalate the issue to engineering immediately - Deploy a fix by Friday
gpt-4o-transcribe (andgpt-4o-transcribe-diarize when you need speaker labels), Deepgram, and Google Speech-to-Text; whisper-1 still works and is the name you will see in older code. For TTS, ElevenLabs and OpenAI's audio models. Worth knowing that the two-step split is a Claude constraint, not a universal one: OpenAI's gpt-audio and gpt-realtime families take audio directly, which is the better fit for live conversation. When the job is reasoning over a recording, transcribe-then-reason stays the simpler and cheaper shape.5. Image Generation
Generating images is another job to route out: Claude cannot draw, so you dispatch that step to an image model (OpenAI's image API, Google Imagen, Stability, and others). The most effective pattern is a division of labor, Claude writes a precise image prompt, the image model renders it, and Claude's vision then reviews the result. That last step closes the loop: the same model that specified the image can judge whether it got one, which is what makes the pattern automatable.
# icon_pipeline.py - Claude writes the prompt, the image model renders, Claude reviews
# Claude cannot generate images. When a workflow needs one, route that step
# to an image model, exactly the hybrid routing pattern from the
# "Building AI Applications" lesson.
import base64
import anthropic
from openai import OpenAI
claude = anthropic.Anthropic()
image_client = OpenAI()
# 1. Claude turns a vague request into a detailed image prompt.
brief = "an icon for the shipping section of our app"
reply = claude.messages.create(
model="claude-sonnet-5",
max_tokens=200,
messages=[{"role": "user", "content": (
f"Write a single-sentence image generation prompt for: {brief}. "
"Flat vector style, green on white. Return only the prompt."
)}],
)
prompt = next(b.text for b in reply.content if b.type == "text").strip()
print(f"prompt: {prompt}")
# 2. The image model renders it.
result = image_client.images.generate(
model="gpt-image-1.5", # or Google Imagen, Stability, etc.
prompt=prompt,
size="1024x1024",
quality="low",
)
# gpt-image models return base64 only; there is no response_format="url".
image_bytes = base64.b64decode(result.data[0].b64_json)
with open("box.png", "wb") as f:
f.write(image_bytes)
print(f"saved box.png ({len(image_bytes) // 1024} KB)")
# 3. Claude's vision reviews the result and can request a revision.
review = claude.messages.create(
model="claude-sonnet-5",
max_tokens=200,
messages=[{"role": "user", "content": [
{"type": "image", "source": {
"type": "base64", "media_type": "image/png",
"data": base64.b64encode(image_bytes).decode("utf-8")}},
{"type": "text", "text": (
f"Does this image satisfy the brief '{brief}'? "
"Answer in one sentence.")},
]}],
)
print(f"review: {next(b.text for b in review.content if b.type == 'text')}")Expected Output:
prompt: A flat vector icon of a delivery truck in solid green on a white background, minimalist design, clean simple lines, no gradients, no shadows, centered composition, app icon style. saved box.png (127 KB) review: Yes, this green delivery truck icon with motion lines effectively conveys shipping/delivery and would work well for a shipping section icon.
6. A Combined Multimodal Pipeline
Real features chain modalities. Consider an expense app: a user photographs a receipt, and the system extracts the data, checks it against policy, and returns a tidy summary. One vision call feeds a reasoning call feeds a rendered output, each step using the model best suited to it.
Receipt-to-Report Pipeline
Figure 3: Each stage uses the right model, vision to read the receipt, reasoning to apply policy, generation for the summary
7. Cost, Latency & Pitfalls
- Resize/downsample images before sending, resolution drives token cost
- Always validate extracted data against a schema before using it
- Cache results, the same receipt should not be re-processed
- Ask for structured JSON, not prose, when the output feeds code
- Blurry or low-res images produce confidently wrong text
- Images often contain PII (IDs, faces), handle and store them accordingly
- Vision + document calls are slower; stream or show progress
- Generated images can carry watermarks or licensing terms, check them
Key Takeaways
- Multimodal means mixed input in one request - Claude and Gemini take text, images, and PDFs together and reason over all of them.
- Vision is a content block - pass an image (base64 or URL) plus a text instruction; use it to describe, extract, classify, or convert UI to code.
- Document understanding is native - Claude reads a PDF's text and its rendered pages, so even a scan with no text layer extracts correctly; pair it with a schema and you get a validated object.
- Audio is routed out - transcribe with an STT service, reason with Claude, and use TTS for spoken output.
- Image generation is routed out - Claude writes the prompt, an image model renders it, Claude's vision reviews it.
- Chain modalities per stage - use the right model at each step of a pipeline (extract, analyze, generate).
- Image cost scales with area - measured with
count_tokens, one receipt runs 168 tokens at 400px and 4,743 at 3000px; halving the long edge quarters the bill. - Mind latency and PII - validate outputs, cache repeat work, and treat uploaded media as sensitive data.