AI EngineeringZero to ProductionHome·About·Contact
Anthropic API in Practice · Part 3

Files API, vision & PDF

Claude reads pictures and PDFs, not just text. This cookbook shows the API mechanics: image and document content blocks, inline base64 vs the upload-once Files API, document Q&A with citations, and the cost and cleanup discipline production needs.

⏱️ ~1.5 hours🧪 5 recipes🎯 Beginner→Tech-lead

Learning objectives

  • Send an image to Claude as an inline base64 image content block.
  • Send a PDF as a document block and ask questions about it.
  • Upload a file once with the Files API and reference it by file_id across many calls.
  • Decide when to inline bytes vs upload to the Files API.
  • Turn on citations so answers point back to the source document.
  • Budget image/document token cost and clean up files in production.

1 · Two ways to give Claude a file essential

Claude can read images and PDFs, not just text. There are exactly two ways to hand it a file, and everything in this lesson is a variation on one of them. Inline: you put the raw bytes (base64-encoded) directly inside the message as a content block — one self-contained request. Files API: you upload the file once, get back a file_id, and then reference that id in as many messages as you like — the bytes are stored on Anthropic's side and never re-sent.

The message body is the same shape either way. A user message's content is a list of blocks. For a picture you send an {"type": "image", ...} block; for a PDF or text file you send an {"type": "document", ...} block; and you usually add a {"type": "text", ...} block with your question. The only thing that changes between the two paths is the block's source: raw base64 bytes inline, versus a {"type": "file", "file_id": ...} reference.

Your file image / PDF Inline base64 in the block messages.create one request Claude reads it
🗺️ How to read this diagram

The top diagram is the inline path — the whole file rides inside one request.

  • Your file is a picture or PDF on your disk.
  • Inline base64 — you encode the raw bytes as base64 text and drop them straight into a content block. The bytes become part of the message.
  • messages.create — a single API call carries the block (bytes and all) up to Anthropic.
  • Claude reads it and answers. Simple, self-contained — but the bytes travel on every call you make.

In short: One file, one question, one request. Everything is in that request; nothing is stored afterward.

Your file image / PDF files.upload once file_id reuse many messages.create reference id Claude reads it
🗺️ How to read this diagram

The bottom diagram is the Files API path — upload the bytes once, then pass a tiny id forever after.

  • files.upload sends the bytes to Anthropic one time and hands back a file_id — a short string that names the stored file.
  • file_id is all you keep. It's a reference, not the bytes.
  • many messages.create — each later call includes the file_id instead of the file. The bytes never travel again.
  • Claude reads the stored file on every call, just as if you'd inlined it — but you only paid to move the bytes once.

In short: Compare the two pictures: the top re-sends bytes each call; the bottom sends them once. That difference is the whole reason the Files API exists.

Read the two diagrams together: the top one uploads the bytes on every call; the bottom one uploads once and then passes a small id string forever after. Same picture reaching Claude — very different network and cost profile when you ask more than one question.

2 · Recipe: an image as inline base64 essential

The most direct way to send a picture: read the file, base64-encode the bytes, and put them in an image block whose source.type is "base64". You must tell the API the media_type (e.g. image/png, image/jpeg, image/gif, image/webp). This block is REAL SDK code — it opens a real file and calls the API.

Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
Python · image content block, inline base64 — ▶ needs API key + network
image_inline.py# needs: pip install anthropic + ANTHROPIC_API_KEY (+ a real image/PDF file)
import base64
import anthropic

client = anthropic.Anthropic()   # reads ANTHROPIC_API_KEY from the environment

with open("chart.png", "rb") as f:
    image_b64 = base64.standard_b64encode(f.read()).decode("utf-8")

resp = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1024,
    messages=[{
        "role": "user",
        "content": [
            {
                "type": "image",
                "source": {
                    "type": "base64",
                    "media_type": "image/png",   # match the actual file type
                    "data": image_b64,
                },
            },
            {"type": "text", "text": "What trend does this chart show?"},
        ],
    }],
)
print(next(b.text for b in resp.content if b.type == "text"))
▶ How this works

This is the most direct recipe: read an image file, base64-encode it, and send it as an image block. It hits the real API, so it needs a key and network — the ▶ needs API key + network label flags that.

  1. anthropic.Anthropic() builds the client; it picks up ANTHROPIC_API_KEY from the environment, so the key never appears in code.
  2. base64.standard_b64encode(f.read()).decode("utf-8") turns the raw image bytes into the base64 text the API expects.
  3. The content is a list of blocks: an image block (with source.type == "base64" and the matching media_type) followed by a text block holding your question.
  4. next(b.text for b in resp.content if b.type == "text") pulls the first text block out of the reply — the reply is a list of blocks too.

Try this: Point open("chart.png") at a real image you have and change the question. The media_type must match the file — use image/jpeg for a .jpg.

You can also point at a URLInstead of base64 bytes, an image block accepts "source": {"type": "url", "url": "https://…/chart.png"}. Anthropic fetches the image for you — handy when the file already lives on a public URL and you don't want to download-then-re-upload it yourself.

3 · See the block shape — offline essential

Before spending a single token, it helps to see what an image content block actually is: a plain Python dict. The helper below takes raw bytes and a media type, base64-encodes them with the standard library, and builds the exact block the SDK sends. It uses a tiny hardcoded PNG byte literal so it needs no file, no key, and no network — it just prints the block's shape. This is the one recipe on the page you can run right now.

Python · build an image block from bytes & print its shape (runs offline)
block_shape.pyimport base64

# A tiny hardcoded PNG (the 1x1 transparent pixel) — no file needed.
PNG_BYTES = (
    b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR"
    b"\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06"
)

def image_block(raw: bytes, media_type: str) -> dict:
    """Build the inline base64 image content block the SDK would send."""
    b64 = base64.standard_b64encode(raw).decode("utf-8")
    return {
        "type": "image",
        "source": {"type": "base64", "media_type": media_type, "data": b64},
    }

block = image_block(PNG_BYTES, "image/png")
print("type:        ", block["type"])
print("source.type: ", block["source"]["type"])
print("media_type:  ", block["source"]["media_type"])
print("data length: ", len(block["source"]["data"]), "base64 chars")
print("data preview:", block["source"]["data"][:16], "...")
type:         image
source.type:  base64
media_type:   image/png
data length:  36 base64 chars
data preview: iVBORw0KGgoAAAAN ...
▶ How this works

This is the one recipe you can run right now — pure standard library, no key, no network. It shows that an image content block is just a Python dict, using a tiny hardcoded PNG so there's no file to find.

  1. PNG_BYTES is a hardcoded byte literal (the start of a 1x1 PNG). Because it's baked in, the script needs no file on disk.
  2. image_block(raw, media_type) base64-encodes the bytes and assembles the exact dict the SDK sends: a type: "image" with a source holding type, media_type, and data.
  3. The print lines pull each field back out so you can see the shape — the same shape every image recipe on this page produces.

What the output means: You see the block's fields printed: type: image, source.type: base64, the media type, and the base64 data length plus a short preview. That dict is all the SDK ever sends over the wire.

Try this: Swap in your own tiny bytes literal, or change the media_type, and re-run. The data length changes with the input; the structure never does.

This dict is all the SDK sendsEvery image you send — from any of the recipes here — ends up as this exact block structure on the wire. The SDK doesn't do anything magic; it serializes the dict you built. Seeing it once demystifies the rest of the lesson.

4 · Recipe: a PDF document & document Q&A intermediate

PDFs use a document block instead of an image block, with media_type: "application/pdf". Claude reads both the text and the visual layout of each page — tables, charts, signatures — so you can ask questions that depend on how the page looks, not just its extracted text. Put the document block before your question text. This is REAL SDK code.

Python · PDF document block, inline base64 — ▶ needs API key + network
pdf_inline.py# needs: pip install anthropic + ANTHROPIC_API_KEY (+ a real image/PDF file)
import base64
import anthropic

client = anthropic.Anthropic()

with open("contract.pdf", "rb") as f:
    pdf_b64 = base64.standard_b64encode(f.read()).decode("utf-8")

resp = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1024,
    messages=[{
        "role": "user",
        "content": [
            {
                "type": "document",
                "source": {
                    "type": "base64",
                    "media_type": "application/pdf",
                    "data": pdf_b64,
                },
            },
            {"type": "text", "text": "What is the termination clause? Quote it."},
        ],
    }],
)
print(next(b.text for b in resp.content if b.type == "text"))
▶ How this works

Same idea as the image recipe, but for a PDF — the block type is document instead of image. This is real API code (▶ needs API key + network).

  1. The media_type is "application/pdf". Claude reads the text and the visual layout of each page, so questions about tables or signatures work.
  2. The document block comes before the text block — put the file first, then the question about it.
  3. Otherwise the request is identical to the image recipe: base64 the bytes, build the block list, read the first text block out of the reply.

Try this: This is document Q&A — swap the question for "list every date mentioned" and Claude answers from the same PDF. Remember inline requests cap at 32 MB total, so a huge scanned PDF needs the Files API instead.

Limit (inline base64)Value
Max request size32 MB total
Max PDF pages600 (100 for 200K-context models)
Image formatsPNG, JPEG, GIF, WebP
Newlines in base64must be stripped (the SDK does this for you)
Inline is capped at 32 MB per requestThe whole request — every block, base64 and all — must fit under 32 MB. A big scanned PDF blows past that fast. When a file is large, or you'll ask about it more than once, upload it with the Files API (next section) instead of inlining it every time.

5 · Recipe: upload once, reuse with the Files API intermediate

The Files API is the answer to "I have one document and twenty questions". You upload the file once and get back a file_id. Every later message references that id via a {"type": "file", "file_id": ...} source — the bytes never travel again. The Files API is beta, so you call it on the client.beta.* namespace and pass betas=["files-api-2025-04-14"] on both the upload and any messages.create that references the file. This is REAL SDK code.

Python · upload once, ask many questions by file_id — ▶ needs API key + network
files_api.py# needs: pip install anthropic + ANTHROPIC_API_KEY (+ a real image/PDF file)
import anthropic

client = anthropic.Anthropic()

# 1. Upload ONCE. The file argument is a (filename, fileobj, media_type) tuple.
uploaded = client.beta.files.upload(
    file=("contract.pdf", open("contract.pdf", "rb"), "application/pdf"),
    betas=["files-api-2025-04-14"],
)
print("file_id:", uploaded.id, "| size:", uploaded.size_bytes, "bytes")

# 2. Reference the SAME file_id across many calls — bytes never re-sent.
questions = [
    "What are the key terms?",
    "What is the termination clause?",
    "Summarize the payment schedule.",
]
for q in questions:
    resp = client.beta.messages.create(
        model="claude-opus-4-8",
        max_tokens=1024,
        messages=[{
            "role": "user",
            "content": [
                {"type": "document", "source": {"type": "file", "file_id": uploaded.id}},
                {"type": "text", "text": q},
            ],
        }],
        betas=["files-api-2025-04-14"],
    )
    answer = next(b.text for b in resp.content if b.type == "text")
    print(f"\nQ: {q}\nA: {answer[:120]}")

# 3. Manage stored files. Uploads count against a per-org storage quota.
for f in client.beta.files.list(betas=["files-api-2025-04-14"]):
    print(f.id, f.filename, f.size_bytes)

# 4. Clean up when you're done with it.
client.beta.files.delete(uploaded.id, betas=["files-api-2025-04-14"])
▶ How this works

This recipe is "one document, many questions" done right: upload once, then reference the stored file by id on every later call. Real API code — and the Files API is beta, so note the client.beta.* calls and the betas=[...] argument.

  1. Step 1client.beta.files.upload(...) sends the bytes once and returns an object whose .id is your file_id.
  2. Step 2 — the loop asks three questions. Each message uses a document block whose source is {"type": "file", "file_id": uploaded.id} — the bytes are never re-sent, only the id.
  3. Step 3client.beta.files.list(...) shows what's stored; uploads count against your organization's storage quota.
  4. Step 4client.beta.files.delete(uploaded.id, ...) cleans up. Uploaded files persist until you delete them, so always pair an upload with a cleanup.

Try this: Note the betas=["files-api-2025-04-14"] on both the upload and each messages.create — the beta header is required on any call that touches the file.

An image works the same wayTo reuse an image instead of a PDF, upload it exactly the same way, then reference the id with an image block: {"type": "image", "source": {"type": "file", "file_id": id}}. The block type must match the file's MIME type — document for PDFs/text, image for pictures.

6 · Inline vs Files API — choosing advanced

Both paths get the same bytes to Claude. The decision is about how many times you'll ask and how big the file is:

QuestionInline base64Files API
How many times you'll askoncemany times
Bytes re-sent per callevery callnever (only the id)
File size ceiling32 MB per request500 MB per file
State to managenonestored files + a quota to clean up
Beta header needednoyes (files-api-2025-04-14)

Rule of thumb: inline for a one-shot (extract a field from this one receipt), Files API when the same document drives many turns (a contract you'll interrogate across a conversation, or a reference doc shared by a batch of requests). The Files API also lets a big file exceed the 32 MB inline request cap, since the bytes aren't part of the message.

Pair the Files API with prompt caching for big docsFor a large document reused across many requests, upload it once and put a cache_control breakpoint after it so its processed tokens are cached too. The Files API saves re-uploading the bytes; caching (see AP2) saves re-processing them into tokens. Together they cut both bandwidth and cost.

7 · Recipe: citations that point at the source advanced

For document Q&A you often want the answer to cite where it came from — which page, which sentence. Set citations: {"enabled": True} on the document block. Claude then splits its reply into multiple text blocks, and blocks that draw on the document carry a citations array — each citation names the source, the cited text, and a location (a page number for PDFs, a character range for plain text). This is REAL SDK code.

Python · document Q&A with citations — ▶ needs API key + network
citations.py# needs: pip install anthropic + ANTHROPIC_API_KEY (+ a real image/PDF file)
import base64
import anthropic

client = anthropic.Anthropic()

with open("report.pdf", "rb") as f:
    pdf_b64 = base64.standard_b64encode(f.read()).decode("utf-8")

resp = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1024,
    messages=[{
        "role": "user",
        "content": [
            {
                "type": "document",
                "source": {"type": "base64", "media_type": "application/pdf", "data": pdf_b64},
                "title": "Q4 Report",
                "citations": {"enabled": True},   # turn citations on
            },
            {"type": "text", "text": "What were Q4 revenues? Cite the page."},
        ],
    }],
)

for block in resp.content:
    if block.type != "text":
        continue
    print(block.text)
    for c in (block.citations or []):
        # PDF citations carry a page_location; plain text carries char indices.
        print(f'   ↳ cited "{c.cited_text[:50]}" from {c.document_title}')
▶ How this works

This recipe makes the answer traceable: with citations on, Claude tells you which page and sentence each claim came from. Real API code (▶ needs API key + network).

  1. "citations": {"enabled": True} on the document block is the only new ingredient — everything else is the PDF recipe from earlier.
  2. The reply is split into multiple text blocks. A block that drew on the document carries a citations array; a block that didn't has none — hence block.citations or [].
  3. Each citation names the source (document_title), the exact cited_text, and a location — a page number for PDFs, a character range for plain text.

Try this: Ask a question whose answer spans two pages and watch multiple citations come back. Note you can't combine citations with output_config.format (JSON schema) — the API returns a 400 if you try both.

Citations and structured outputs don't mixYou can't combine citations on a document with output_config.format (JSON schema output) — the API returns a 400. Pick one: cite the source, or force a JSON shape. Citations are also all-or-none across a request's documents — enable them on every document block or none.

8 · Cost, limits & production hygiene professional

Images and documents are billed as input tokens — a page or an image costs roughly what its visual complexity works out to, and a full-resolution image can be thousands of tokens. That means the recurring cost lever is how often you re-send the same bytes. The production checklist:

Production checklist for files & vision

  1. Don't re-inline. If a document is used more than once, upload it via the Files API and pass the file_id — you stop paying to ship the bytes each call.
  2. Cache big prefixes. Combine the Files API with cache_control so a large reused document is neither re-uploaded nor re-tokenized (see AP2).
  3. Downsample when you can. A smaller image is fewer input tokens; send the resolution the task actually needs, not the original camera dump.
  4. Mind the quotas. Files API storage is capped per organization (100 GB) and individual files up to 500 MB — track what you upload.
  5. Clean up. Uploaded files persist until you delete them. Delete files you no longer need so storage doesn't creep and stale documents don't linger.
  6. Count before you send. Use token counting (see AP4) on representative files to budget cost before rolling out.
Uploaded files don't expire on their ownUnlike a chat that ends, a Files API upload sits in your org's storage until explicitly deleted. A pipeline that uploads per request without deleting will fill the 100 GB quota and start failing uploads. Always pair an upload with a cleanup path — delete on completion, or run a sweep of old files.list() entries.

9 · Tech-lead — a document-ingestion contract tech-lead

A lead owns the document-handling policy for the whole system, not just one call site. That means a written contract: which paths inline vs upload (a size/reuse threshold, not per-developer whim); a file lifecycle (who uploads, who deletes, what the retention window is) so the org quota never becomes an incident; a citation requirement for any answer users act on, so claims are traceable to the source page; and a cost model that accounts for image/document input tokens and the caching + Files-API levers that reduce them.

Every mechanic in this lesson maps to one clause of that contract. The inline vs Files API table is the routing rule; the cleanup step is the lifecycle; citations are the traceability requirement; token cost is the budget. A lead makes these decisions once, writes them down, and lets the codebase follow the contract instead of re-litigating "inline or upload?" on every PR.

Exercise AP3.1 — Run the offline block builder, then wire a real call

Context: Knowing exactly what an image content block looks like — before any network call — demystifies vision requests. Matching the offline shape to the real block is the bridge from theory to a working call.

Your task: Run block_shape.py and confirm its printed shape matches the out() block, then point image_inline.py at a real PNG, add ANTHROPIC_API_KEY, and ask Claude what is in the image.

Requirements:

  • First confirm the offline-printed block shape matches the expected output
  • Point image_inline.py at a real PNG you have
  • Set ANTHROPIC_API_KEY in your environment before the real call
  • Get Claude's description of the image back
  • Note which parts of the real block are identical to the offline helper's output

💡 Hint: The base64 data string is the only part that changes between the offline shape and the real call — the surrounding block structure is identical.

Exercise AP3.2 — One document, inline vs Files API

Context: Asking one PDF three questions two ways makes the Files API's payoff concrete: you can watch the bytes-on-the-wire drop while comparing input-token cost. Adding citations shows the answers stay auditable either way.

Your task: Ask a multi-page PDF three questions inline (pdf_inline.py) and again by uploading once and referencing file_id (files_api.py), compare bytes and cost, then enable citations and delete the upload.

Requirements:

  • Run the three questions both ways: inline base64 per call, and one upload referenced three times
  • Compare the bytes sent and the input-token cost between the two approaches
  • Enable citations (citations.py) and confirm each answer names its page
  • Finish by delete-ing the uploaded file

💡 Hint: The inline path re-sends the full base64 on every call; the Files API path sends the bytes once — that difference is exactly what you are measuring.

🪜 Practice ladder beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · Send an image as inline base64Beginner

Context: The simplest way to put an image in front of Claude is to inline it as base64 in an image content block. Getting the block shape right is the foundation for every vision task that follows.

Your task: Read a PNG, base64-encode it, and ask Claude what it shows using an inline image content block.

Requirements:

  • Needs a real API key to run
  • The base64 payload has no embedded newlines
  • Use an image block with a base64 source and matching media_type (e.g. image/png)
  • The image block precedes the text block in the same user message
  • Print the model's text answer describing the image

💡 Hint: Encode the raw bytes with base64.standard_b64encode(...).decode() and make media_type match the actual file type.

Show solution

The canonical inline-image block (base64 must have no newlines):

import base64, anthropic
client = anthropic.Anthropic()

with open("chart.png", "rb") as f:
    data = base64.standard_b64encode(f.read()).decode("utf-8")

resp = client.messages.create(
    model="claude-opus-4-8", max_tokens=512,
    messages=[{"role": "user", "content": [
        {"type": "image", "source": {
            "type": "base64", "media_type": "image/png", "data": data}},
        {"type": "text", "text": "What does this chart show?"},
    ]}],
)
print(next(b.text for b in resp.content if b.type == "text"))

Inline base64 is the simplest path for a one-off image. The image block comes before the text block, and media_type must match the file.

Exercise 2 · Ask a question about a PDF documentIntermediate

Context: PDFs go through a document block rather than an image block, and the API enforces hard size and page limits. Knowing those limits tells you when inlining stops being viable.

Your task: Send a PDF as a base64 document block, ask a question about it, and note the size and page limits.

Requirements:

  • Needs a real API key to run
  • Use a document block with a base64 source and application/pdf media type
  • The document block comes before the text block
  • No beta header is needed for base64 PDF
  • State the limits: 32 MB per request and 600 pages (100 on 200k-context models)
  • Note that a large or reused file should instead go through the Files API

💡 Hint: The block mirrors the inline-image pattern — swap image for document and use the PDF media type.

Show solution

PDF uses a document block, placed before the text:

import base64, anthropic
client = anthropic.Anthropic()

with open("report.pdf", "rb") as f:
    pdf = base64.standard_b64encode(f.read()).decode("utf-8")

resp = client.messages.create(
    model="claude-opus-4-8", max_tokens=1024,
    messages=[{"role": "user", "content": [
        {"type": "document", "source": {
            "type": "base64", "media_type": "application/pdf", "data": pdf}},
        {"type": "text", "text": "What are the three key findings?"},
    ]}],
)
print(next(b.text for b in resp.content if b.type == "text"))

Base64 PDF has no beta header. Limits: 32 MB per request and 600 pages (100 on 200k-context models). For a bigger or reused file, upload it once with the Files API instead (next rung).

Exercise 3 · Upload once with the Files API, reuse across questionsAdvanced

Context: When the same document is queried repeatedly, re-inlining its bytes on every call is wasteful. The Files API lets you upload once and reference the file by id, paying the transfer a single time.

Your task: Upload a PDF once, then ask three questions referencing it by file_id without re-uploading, and clean up afterward.

Requirements:

  • Needs a real API key to run
  • Upload via client.beta.files.upload and reference the returned file_id
  • Include the files-api-2025-04-14 beta header on upload and on each message call
  • Reference the document by {"type": "file", "file_id": ...} rather than re-sending bytes
  • Delete the uploaded file at the end (files persist until deleted)
  • Note that uploads/lists/deletes are free but the content still bills as input tokens each use

💡 Hint: Do the upload once outside the question loop; inside the loop only the question text and the file_id reference change.

Show solution

Upload → reference by file_id → clean up. Beta header required on both:

uploaded = client.beta.files.upload(
    file=("contract.pdf", open("contract.pdf", "rb"), "application/pdf"),
)
for q in ["Termination clause?", "Payment schedule?", "Governing law?"]:
    resp = client.beta.messages.create(
        model="claude-opus-4-8", max_tokens=1024,
        betas=["files-api-2025-04-14"],          # required on upload AND here
        messages=[{"role": "user", "content": [
            {"type": "text", "text": q},
            {"type": "document",
             "source": {"type": "file", "file_id": uploaded.id}},
        ]}],
    )
    print(q, "->", next(b.text for b in resp.content if b.type == "text")[:60])

client.beta.files.delete(uploaded.id)            # files persist until deleted

The Files API avoids re-uploading the same bytes for every question — you pay the upload once and reference file_id thereafter. Uploads/lists/deletes are free; the content is billed as input tokens each time it is used.

Exercise 4 · Estimate image token cost before sending (OFFLINE)Expert

Context: Image token cost scales with pixel area, so an oversized image can quietly dominate a request's bill. Estimating the cost before the call lets you trade fidelity for price deliberately.

Your task: Write image_tokens(w, h) and image_cost(w, h) for Opus 4.8 input, then compare a 1568×1568 image against a 784×784 downsize.

Requirements:

  • Pure offline estimator — no API key
  • Estimate tokens as roughly (width × height) / 750
  • Cost is the estimated tokens at the Opus 4.8 input price
  • Report the percentage saved by halving each edge
  • Show that halving both edges cuts cost to about a quarter (~75% saved)
  • Note when to keep full resolution: dense documents or fine detail

💡 Hint: Because area is the driver, halving width and height quarters the pixel count — compute both sizes and diff the costs.

Show solution

Offline estimator — decide fidelity vs cost before the call:

IN = 5.0 / 1e6   # $ per input token, Opus 4.8

def image_tokens(w, h):
    return round(w * h / 750)          # Anthropic's rough estimate

def image_cost(w, h):
    return image_tokens(w, h) * IN

print(image_tokens(1568, 1568), f"${image_cost(1568,1568):.5f}")  # ~3279  $0.01640
print(image_tokens(784, 784),   f"${image_cost(784, 784):.5f}")   # ~820   $0.00410
print(f"downsizing saves {100*(1-image_cost(784,784)/image_cost(1568,1568)):.0f}%")  # 75%

Token cost scales with pixel area, so halving each edge quarters the cost. Downsize client-side when the extra resolution will not change the answer — but keep full resolution for dense documents or fine detail where it will.

Exercise 5 · Citations that point back at the sourceProfessional

Context: For auditable answers, each claim should point back to a locatable span in the source document. Enabling citations turns a document answer into cited blocks you can verify.

Your task: Enable citations on a document so each cited answer block carries a source span, and show both the request flag and how to read the response citations.

Requirements:

  • Needs a real API key to run
  • Set citations: {"enabled": True} on the document block
  • Citations are all-or-none across documents in the request
  • Read each text block's citations for cited_text and a location
  • Handle the location type: page for PDFs, character range for plain text
  • Note that citations are incompatible with forcing a JSON output_config.format

💡 Hint: Iterate the response content blocks; each text block exposes a citations list whose entries name the cited text and its page or char range.

Show solution

Set citations.enabled on the document; the response splits into cited blocks:

resp = client.messages.create(
    model="claude-opus-4-8", max_tokens=1024,
    messages=[{"role": "user", "content": [
        {"type": "document",
         "source": {"type": "base64", "media_type": "application/pdf", "data": pdf},
         "title": "Q4 Report",
         "citations": {"enabled": True}},          # all-or-none across documents
        {"type": "text", "text": "What was Q4 revenue? Cite the source."},
    ]}],
)
for block in resp.content:
    if block.type == "text":
        print(block.text)
        for c in (block.citations or []):
            # PDF -> page_location; plain text -> char_location
            print("  cite:", c.cited_text, "p", getattr(c, "start_page_number", None))

Citations make answers auditable: each cited text block carries cited_text plus a location (page for PDFs, char range for text). Note citations are incompatible with output_config.format — you cannot force JSON and cite at the same time.

Exercise 6 · A document-ingestion contract for the platformIndustry scenario

Context: A platform serving many documents needs one rule that decides transport and rejects over-limit files before a request is ever built. Encoding that contract as a gate keeps hard limits from failing deep in the call.

Your task: Write ingest_decision(size_mb, pages, reuse_count) that returns the transport (inline vs Files API) or a rejection.

Requirements:

  • Pure decision logic — no API key needed
  • Reject documents over 32 MB and over 600 pages, fast, before building a request
  • Route reused documents (reuse_count ≥ 2) to the Files API
  • Route one-off documents to inline base64
  • Rejections explain the limit hit and advise splitting or chunking
  • Never silently truncate an over-limit document

💡 Hint: Check the hard limits first and return early on rejection; only then branch on reuse count to pick inline versus Files API.

Show solution

Encode the ingestion contract as a gate (pure logic, no API needed):

def ingest_decision(size_mb, pages, reuse_count):
    if size_mb > 32:
        return "REJECT — exceeds 32 MB request limit; split the document"
    if pages > 600:
        return "REJECT — exceeds 600-page limit; chunk by section"
    if reuse_count >= 2:
        return "Files API — upload once, reference by file_id (avoid re-upload)"
    return "inline base64 — one-off use, simplest path"

print(ingest_decision(2, 40, 1))     # inline base64
print(ingest_decision(2, 40, 5))     # Files API (reused 5x)
print(ingest_decision(50, 40, 1))    # REJECT — too big

The contract fails fast on the hard limits (32 MB, 600 pages) and routes by reuse: one-off → inline, reused → Files API to stop paying repeated upload/transfer. Never silently truncate an over-limit document — reject and chunk it.

✓ Checkpoint — you can move on when you can…

  • Send an image inline as a base64 image content block.
  • Send a PDF as a document block and do document Q&A.
  • Upload a file once and reference it by file_id across many calls.
  • Choose inline vs Files API by reuse count and file size.
  • Enable citations and read the source location back from the response.
  • Explain the token cost of files and the cleanup/quota discipline they demand.

Knowledge check check yourself

✓ Knowledge check

When should you use the Files API instead of inlining base64, and what discipline does the Files API require that inlining does not?

Show answer
Use the Files API when a file exceeds the ~32 MB inline request limit (up to 500 MB) or when the same document drives multiple calls, so you upload once and reference by file_id rather than re-sending bytes. It requires lifecycle discipline: uploaded files persist indefinitely against a 100 GB org quota until you explicitly call files.delete().
✓ Knowledge check

Why can't you combine document citations with output_config.format in the same request, and what does enabling citations change about the response shape?

Show answer
The two are mutually exclusive; requesting both returns an HTTP 400. Enabling citations splits the response into multiple text blocks where citing blocks carry a citations array with document_title, cited_text, and a location (page number for PDFs, character range for text), so answers become traceable to their source.
© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in