Module 1: LLM API Mastery · Lesson 7 of 7 · 25 min

Beyond Text: Images, PDFs & Files

Real agent tasks aren't text-only: screenshots in bug reports, invoices as PDFs, documents to extract from. Multimodal input is just more content-block types in the same messages array.

Everything in this module so far sent content as a string. The full truth: content is a list of typed blocks, and text is only one block type. Add an image or document block and the same stateless, resend-the-array machinery now carries screenshots and PDFs. No new endpoint, no new mental model.

Images: cost and placement

images: base64 or URL blocks in a user message
import base64

img_b64 = base64.standard_b64encode(open("error.png", "rb").read()).decode()

resp = client.messages.create(
    model="claude-sonnet-5", max_tokens=1024,
    messages=[{
        "role": "user",
        "content": [
            {"type": "image",
             "source": {"type": "base64",
                        "media_type": "image/png", "data": img_b64}},
            # or, if it's already hosted:
            # {"type": "image", "source": {"type": "url", "url": "https://..."}},
            {"type": "text", "text": "What's the error in this screenshot, and the likely fix?"},
        ],
    }],
)
Put media blocks before the text that asks about them. Images are billed as input tokens, and the count scales with resolution.

Do the image-token math out loud when asked — it's a resolution question, not a flat fee. Claude tiles an image into 28×28-pixel patches and bills one visual token per patch (⌈width/28⌉ × ⌈height/28⌉), so cost scales directly with pixel count — the table below shows what that looks like at real sizes. At $3/MTok even the expensive end is under 2¢ per image, but three things compound it: images ride in the history and get re-billed every turn of the conversation, they eat context-window budget, and at 10K images/day the gap between the cheap and expensive rows is real money. The lever is client-side resizing: downsample to the smallest resolution that preserves what the model needs (reading a screenshot's error text needs far less than reading a dense schematic), and pull the image out of history once it's been discussed.

Image sizeStandard-tier tokensHigh-res-tier tokens
200×200 (thumbnail)6464
1000×1000 (1 MP)1,2961,296
1920×1080 (2 MP)~1,560 (downscaled cap)2,691
Full high-res (~2,576px long edge)~1,560 (downscaled cap)4,784

OpenAI counts image tokens two different ways, and knowing which family a model is in is the whole trick:

Patch-based — the 32×32-pixel patch method, now in two tiers. The efficiency models (gpt-5-mini/-nano, gpt-5.4-mini/-nano, gpt-4.1-mini/-nano, o4-mini) count ⌈w/32⌉ × ⌈h/32⌉ patches, cap at 1,536 (proportional downscale when over), then multiply by a per-model factor — 1.62× mini · 2.46× nano · 1.72× o4-mini. The newest flagships (gpt-5.4, gpt-5.5, gpt-5.6) use the same patches but a far larger, detail-controlled budget: high allows up to 2,500 patches (2048px max side), original up to 10,000 (6000px, no forced resize) — and on 5.5 / 5.6 the default is original. The fixed 1.62 / 2.46 multipliers are a mini/nano thing; flagship image tokens track the resized patch count under whichever cap detail selects.

Tile-based — the base gpt-5, gpt-4o, gpt-4.1, and the o1/o3 line (note the split: base gpt-5 is tile-based, but the gpt-5.4+ flagships above moved to patches). Shrink the image to fit a 2048×2048 box, scale the shortest side to 768px, count 512×512 tiles, then bill base + tiles × per-tile. The lever this family hands you that Claude doesn't: detail: low pays only the flat base and skips tiles entirely — one cheap fixed cost per image when you don't need fine detail.

OpenAI modelMethodImage tokens
gpt-5-mini / -nano, gpt-5.4-mini / -nano, gpt-4.1-mini / -nano, o4-miniPatch-based (efficiency)⌈w/32⌉×⌈h/32⌉ patches (max 1,536) × factor — 1.62 mini · 2.46 nano · 1.72 o4-mini
gpt-5.4, gpt-5.5, gpt-5.6 (flagship)Patch-based (large budget)same 32×32 patches, detail-capped — high: ≤2,500 (2048px) · original: ≤10,000 (6000px, no resize; default on 5.5–5.6)
gpt-5 (base)Tile-baseddetail high: 70 base + 140 / 512px tile · detail low: 70 flat
gpt-4o, gpt-4.1Tile-baseddetail high: 85 base + 170 / 512px tile · detail low: 85 flat
gpt-4o-miniTile-baseddetail high: 2,833 base + 5,667 / tile — inflated on purpose so an image costs ≈ gpt-4o despite mini's cheaper text rate

PDFs: text and layout together

PDFs work the same way with a document block — the model sees both the text layer and the rendered pages, so tables, stamps, and layout survive. This is the workhorse for extraction jobs: invoices, contracts, reports.

PDFs: document blocks + structured extraction
pdf_b64 = base64.standard_b64encode(open("invoice.pdf", "rb").read()).decode()

resp = client.messages.create(
    model="claude-sonnet-5", max_tokens=2048,
    output_config={"format": {"type": "json_schema", "schema": {
        "type": "object",
        "properties": {
            "line_items": {"type": "array", "items": {
                "type": "object",
                "properties": {
                    "description": {"type": "string"},
                    "qty": {"type": "integer"},
                    "unit_price": {"type": "number"},
                },
                "required": ["description", "qty", "unit_price"],
                "additionalProperties": False,
            }},
        },
        "required": ["line_items"],
        "additionalProperties": False,
    }}},
    messages=[{
        "role": "user",
        "content": [
            {"type": "document",
             "source": {"type": "base64",
                        "media_type": "application/pdf", "data": pdf_b64}},
            {"type": "text", "text": "Extract every line item."},
        ],
    }],
)
Notice the combo: a document block plus structured outputs from Lesson 4 — multimodal extraction with a guaranteed shape is the pattern behind half of real-world document automation. Know the concrete limits: requests cap around 32 MB, PDFs at roughly 600 pages on large-context models (about 100 on smaller ones), and the base64 string must have no newlines. Count tokens on big documents before sending — a long PDF bills both its text layer and its rendered pages.

The Files API: upload once, reuse everywhere

Inlining base64 is fine for one-shot calls, but an agent that consults the same 200-page handbook every session would resend megabytes per call. The Files API fixes that: upload once, get a file_id, reference it by id in any later request.

Files API: upload once, reference forever
# upload once (beta header required at the time of writing)
f = client.beta.files.upload(file=open("handbook.pdf", "rb"))

# reference by id in any later request — no re-upload, no base64
resp = client.beta.messages.create(
    model="claude-sonnet-5", max_tokens=1024,
    betas=["files-api-2025-04-14"],
    messages=[{
        "role": "user",
        "content": [
            {"type": "document", "source": {"type": "file", "file_id": f.id}},
            {"type": "text", "text": "What does the vacation policy say?"},
        ],
    }],
)
The file content still counts as input tokens on every call — the Files API saves upload bandwidth and request size, not token cost (prompt caching handles that part). The table below covers when to reach for each option, on both providers.
MethodBest forNotes
Base64 (inline)One-off images/PDFs already in memorySimplest option; resent — and re-billed as input tokens — every turn it stays in conversation history
URLMedia already hosted (CDN, public URL)Provider fetches it directly; base64-only on Amazon Bedrock and Google Cloud
Files API (file_id)Anything reused across requests or sessionsAnthropic: still beta (anthropic-beta: files-api-2025-04-14 header), files persist until deleted, 500 MB/file cap. OpenAI: GA, purpose="user_data", 50 MB per file/request. Saves re-upload bandwidth, not token cost — pair with prompt caching for that.
Check yourself
Three inputs: (a) a screenshot a user just pasted into your support bot, (b) a product image already on your CDN, (c) a 300-page compliance manual your agent consults on every run. Base64, URL, or Files API for each — and which one still needs prompt caching to be economical?
Interview angle
Multimodal questions are usually cost questions in disguise: "design a pipeline that processes 10K invoices/day." Strong answers combine this lesson's blocks — document input + structured outputs + a cheap model tier + batching — and mention the failure modes: page limits, image token costs, and validating extracted numbers client-side.

Whiteboard drills

Check yourself
Drill: Design the 10K-invoices/day extraction pipeline end-to-end — components, model choice, cost ballpark, and failure handling. This is the capstone question for everything in Module 1.
Check yourself
Drill: A user uploads a 40 MB scanned PDF to your assistant. Walk through what breaks and every option you have.
Key takeaways
  • content is a list of typed blocks — images and PDFs are just more block types in the same stateless array.
  • Media blocks go before the text that references them; images cost resolution-dependent input tokens (~1.6K typical, ~4.8K at full high-res) — resize client-side, and remember they're re-billed every turn they stay in history.
  • OpenAI counts image tokens two ways: patch-based 32×32 patches (mini/nano/o4-mini cap at 1,536 ×factor; the newest gpt-5.4/5.5/5.6 flagships use a bigger detail-capped budget of 2,500 / 10,000 patches) vs tile-based (base gpt-5, gpt-4o, gpt-4.1 — base + 512px tiles, detail: low = flat rate).
  • Concrete limits: ~32 MB per request, ~600 PDF pages on large-context models (~100 on smaller) — split or downsample past them.
  • document blocks + structured outputs = schema-guaranteed extraction, the core document-automation pattern.
  • Base64 for one-offs, URL for hosted media, Files API (file_id) for anything reused — plus caching for repeated token cost.
  • Check per-model limits (request size, page caps) and count tokens before sending large documents.