Anthropic's API: Messages, Thinking, and Context Tools
Reading Anthropic's Messages API docs end-to-end, the thing that clicks is the shape. A request is a list of messages. Each message has a role and a content field, and that content field is always an array of typed blocks rather than a plain string. Every interesting thing the API does, extended thinking, tool calls, images, cached prefixes, lives inside that array as its own block type.
That's the durable idea in this post. I'm calling it the content-block model: every turn, in every direction, is an ordered list of typed blocks. Streaming adds blocks to that list one at a time. Caching marks a position in that list. Thinking inserts a new kind of block into that list. Once you see this, every Anthropic feature stops looking like a different feature and starts looking like another entry in the block-type enum.
The problem this layer solves
A chat API has to carry more than text. It has to carry tool calls the model emitted, tool results the host ran, images the user pasted in, thoughts the model did internally, citations the model attached. And those things have to survive a round-trip: whatever the model produced has to come back to it on the next turn in a form it recognizes.
OpenAI's original chat API solved this by adding fields next to the message text: tool_calls sits alongside content, function_call used to sit alongside both, images were a special string format embedded in content. Every new capability meant a new sibling field on the message object. The surface grew wide.
Anthropic made a different call in early 2024. Instead of growing sideways, they made content itself a list. Everything becomes a block: text, tool_use, tool_result, image, thinking, document, search_result. The message stays the same shape forever, and new capabilities slot in as new block types. That's the bet the Messages API is built on, and so far it has aged well.
Prereqs
This one sits in Arc 11, so a lot has been built up already. The post assumes you already have:
- Function calling as structured generation from the function-calling post. You know
tool_useis the model emitting a schema-matching JSON object, not running any code. - The tool loop from the-agent-loop. You know the
model → runtime → tool → resumecycle and that the host is always the one executing tool calls. - Transcript formats from transcript-formats. You've seen Harmony-style channels and understand that each provider has its own on-the-wire representation of a tool-using turn.
MCP isn't a hard prereq for this post, but if you've read the MCP post you'll recognize how cleanly a provider-specific transcript format sits under a provider-agnostic tool protocol. Anthropic's content blocks are one such on-the-wire format. MCP rides on top.
The request and the response
At the wire level, a Messages request is small. A top-level object with model, max_tokens, system, tools, messages, and a handful of knobs. The load-bearing field is messages, an ordered list of {role, content} objects, where content is always an array of blocks.
Every block has a type field. The rest of the fields depend on the type:
The block types are the interesting part. Here are the five you'll see constantly:
{
"type": "text",
"text": "The forecast is clear."
}type. The host routes it by that field. New block types slot into the same array without changing the shape of the request.Every block type has the same envelope (a 'type' field) and different payload shapes. Click or let it autoplay through each type to see the JSON. New block types (search_result, web_search_tool_use, document) slot into this same array without changing the request shape.
The response you get back is the same shape. The model emits an assistant message whose content is an array of blocks, and those are exactly the blocks you hand back to it on the next turn, interleaved with any tool_result blocks you've generated in between. The on-the-wire format for "what the model said" is identical to "what you send the model," which is a nice symmetry and ends up mattering for replay and debugging later.
The id on a tool_use block and the tool_use_id on a tool_result block are how the pairing happens. Get the ids wrong, or fail to include every tool_use's matching tool_result in the next message, and the API will reject the request. The same id game shows up in OpenAI's world too, but because Anthropic puts the id directly on a typed block instead of nested under a tool_calls array, it's usually easier to reason about.
Streaming, caching, thinking
Three runtime concerns. Each of them is a specific move on the block array.
SSE streaming of individual blocks
When you stream a Messages response, you don't get a token stream. You get a block-level event stream over server-sent events. The outer envelope is message_start and message_stop. Inside, for each block the model produces, you see content_block_start (with the type), some number of content_block_delta events (the contents arriving), and a content_block_stop.
The type of the block is announced before its deltas arrive. That is the detail that matters.
Block-level SSE on the left: the client knows the type of each block before any content arrives, so tool_use deltas go straight to the tool-call handler and text deltas go straight to the UI. On the right, a flatter chunk stream where the client has to inspect accumulating fields to figure out what kind of thing it's looking at.
In practice this means your streaming parser is a tiny state machine: content_block_start switches on the block type, the deltas get appended into whatever buffer that type wants, content_block_stop flushes it. No lookahead, no re-inspection of accumulated state. Text goes to the UI as it arrives. Tool-use deltas accumulate as a JSON string that you parse at content_block_stop. Thinking deltas go wherever you want to show reasoning, if anywhere.
Prompt caching: cache breakpoints
Prompt caching is where Anthropic's API gets noticeably cheaper, but it's opt-in. It is not automatic. You opt in by putting cache_control: { type: "ephemeral" } on a block, and everything up to and including that block becomes cacheable. I think of these as cache breakpoints: you are literally drawing a line in the message array and saying "everything above this line, cache it."
Cache hits are billed at roughly 10% of the base input rate. Cache writes are billed at 1.25×. The default cache TTL is five minutes; a one-hour TTL is available if you want it. So the economics are: if you reuse a prefix more than twice within the TTL, you come out ahead; if you reuse it dozens of times, you come out enormously ahead.
20k-token shared prefix (system prompt + a big PDF), six turns, each adding 250 new tokens. Without cache_control (red), every turn bills the full prefix again. With a cache breakpoint on the prefix (green), turn one pays a 1.25× write premium, and every subsequent turn pays the 0.1× cache-hit rate. The savings stack fast.
The placement of the breakpoint is the decision that matters. If your system prompt and tools definition are stable across turns, put the breakpoint after them. If your RAG retrieval varies per turn, put the breakpoint before the retrieved chunks, not after. You can have up to four breakpoints in a single request, which lets you cache at multiple granularities. The mental move is: walk the message array from the top, and ask "up to which block is everything identical across the N requests I'm about to send?"
Extended thinking: reasoning as a block type
Extended thinking is Anthropic's name for the mode where the model produces an internal scratchpad before its answer. The scratchpad shows up in the response as one or more thinking blocks, interleaved with the rest of the blocks like any other content. You control it with a thinking parameter on the request, of which the load-bearing field is budget_tokens: the maximum number of tokens the model may spend on reasoning before it has to produce user-visible output.
budget_tokens trades off answer quality against latency and billed output tokens. Most of the quality gain lands by mid-budget; past that it's diminishing returns. Every thinking block comes back with a signature: a cryptographic hash Anthropic uses to detect tampering when you send the block back on the next turn.
Two things about thinking blocks worth flagging.
First, they bill as output tokens at the normal output rate. There is no secret "thinking is free" discount. If you ask for a 32k thinking budget on a hard problem, you pay for however many thinking tokens the model ends up using, same as if it had produced those tokens as text.
Second, signed thoughts. Every thinking block that comes back in a response carries a signature field. It's an opaque string, and its job is integrity: if you send the block back on the next turn (which you must, for multi-turn reasoning to work), Anthropic checks the signature. If it doesn't match, the thinking block gets rejected. This is the API's answer to the question of "what stops someone from hand-crafting a 'thinking' block that says anything and feeding it back to the model?" The signature is what stops that. I've started using "signed thoughts" as the shorthand handle for this.
Context tools: server-side help
There's one more class of feature that doesn't fit cleanly into "block type" but uses the block array as its input surface: the context tools. These are server-side tools Anthropic implements on your behalf, whose job is to manage your own context window as the conversation grows.
The two worth knowing are the memory tool, which the model can invoke to persist and retrieve structured notes across turns, and context management (specifically tool_result clearing), which you enable in the request to let the API drop stale tool_result blocks from the window as it gets full.
An agent loop pulls in big tool_result blocks until the window is pressured. The memory tool summarizes older exchanges into a small summary block; context_management evicts the stale tool_results. The model resumes from a much smaller effective window without the host having to implement any compaction logic.
Context management used to be entirely the host's problem. You'd write summarization logic, track token counts, decide when to drop or compress. The context-tools class says "I'll help you do this," and pushes the implementation behind the API. You still decide the policy (which blocks are clearable, whether to use the memory tool at all) but you don't implement the compaction.
Worked example
Let me walk through a single multi-turn conversation where the user asks for weather in two cities, the model reasons about it, makes two parallel tool calls, and returns with an answer. I want to show the whole block array, because seeing it end-to-end is where "the content-block model" stops being an abstraction.
A 2-tool turn rendered in Anthropic's content-block format. Notice the ids on tool_use blocks and the tool_use_id on tool_result blocks. Those pairings are how the model knows which result answers which call. Step through with the scrubber or let it autoplay.
Now layer thinking on top. Before any tool_use block appears, the model emits a thinking block with its plan ("two parallel calls, one per city"). That block carries a signature. When the host sends the next turn (the two tool_result blocks), it also sends back every previous assistant block unchanged, including the thinking block with its signature intact. That's the full round trip:
turn 1 request:
messages: [
{role: user, content: [text("What's the weather in Tokyo and Paris?")]},
]
turn 1 response:
content: [
thinking(sig=WaUj..., "Two parallel calls..."),
text("Let me check both cities."),
tool_use(id=toolu_A1, name=get_weather, input={city: Tokyo}),
tool_use(id=toolu_A2, name=get_weather, input={city: Paris}),
]
stop_reason: tool_use
turn 2 request:
messages: [
{role: user, content: [text("What's the weather...")]},
{role: assistant, content: [ # echoed back unchanged
thinking(sig=WaUj..., "Two parallel calls..."),
text("Let me check both cities."),
tool_use(id=toolu_A1, ...),
tool_use(id=toolu_A2, ...),
]},
{role: user, content: [
tool_result(tool_use_id=toolu_A1, content="18C, cloudy"),
tool_result(tool_use_id=toolu_A2, content="11C, light rain"),
]},
]
turn 2 response:
content: [
text("Tokyo is 18C and cloudy. Paris is 11C with light rain."),
]
stop_reason: end_turn
A few observations that will carry over when we compare providers in later posts.
The assistant's content from turn 1 gets echoed back verbatim in turn 2's messages. You don't summarize it, re-serialize it, or reformat it. You paste it back, including the thinking block with its signature. The API expects the exact bytes, because that's how the signature check works.
The tool_result blocks go under a user-role message, not an assistant one. This is different from OpenAI, where tool results have their own role: "tool". In Anthropic's model, a tool_result is something the user (the host, acting on the model's behalf) is handing to the model. Role is about direction of flow; block type is about content kind. Keep those separate.
And the whole exchange is still just blocks. A thinking block, some text, two tool calls, two tool results, a final text. The same representation works for persistence (write it to a DB), for streaming (append as blocks arrive), for replay (feed it back in), and for debugging (pretty-print the array). One shape everywhere. That's the content-block model earning its keep.
Implementation sketch
Here is the whole thing in the official Python SDK. One request with caching, tool use, and extended thinking enabled.
# pip install anthropic
import anthropic
client = anthropic.Anthropic()
# Tool schema looks the same as anywhere else: JSON schema for input.
tools = [
{
"name": "get_weather",
"description": "Get the current weather in a given city.",
"input_schema": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
}
]
# A big shared prefix we want cached across turns.
BIG_SYSTEM_PROMPT = (
"You are a helpful travel assistant. " * 200 # pretend this is 20k tokens of instructions + docs
)
resp = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=2048,
# system accepts a list of text blocks; cache_control marks a breakpoint.
system=[
{
"type": "text",
"text": BIG_SYSTEM_PROMPT,
"cache_control": {"type": "ephemeral"}, # cache this prefix
}
],
tools=tools,
# Extended thinking: give the model a scratchpad before it answers.
thinking={"type": "enabled", "budget_tokens": 4000},
messages=[
{
"role": "user",
"content": "What's the weather in Tokyo and Paris right now?",
},
],
)
# Every block is typed. Walk the array and dispatch on type.
for block in resp.content:
if block.type == "thinking":
print("[thinking]", block.thinking[:80], "... sig:", block.signature[:12])
elif block.type == "text":
print("[text]", block.text)
elif block.type == "tool_use":
print("[tool_use]", block.name, block.input, "id:", block.id)
print("stop_reason:", resp.stop_reason)
print("usage:", resp.usage) # includes cache_read_input_tokensThe cache_control marker on the system prompt is the one word standing between "this costs full price every turn" and "this costs 10% after the first hit." The thinking parameter is opt-in; without it you get no thinking blocks at all. And resp.content is a list you iterate by dispatching on block.type, so the whole consumer side of the content-block model is basically one for loop with a match inside.
Misconceptions
"Thinking is free because it's internal reasoning." It is not. Thinking tokens bill at the output rate. If you set budget_tokens: 32000 and the model uses 20k of it, you pay for 20k output tokens before any user-visible text exists. The reason to use extended thinking is because the quality gain is worth the cost on hard tasks, not because it's off the meter. The usage object in every response breaks out output_tokens and you can see exactly what thinking cost on a given call.
"Cache breakpoints are automatic." They are not. If you never put cache_control on a block, nothing is cached, no matter how many times you send the same prefix. Anthropic's API won't guess. You have to draw the line explicitly. The upside is that caching is predictable: you know exactly what's cached, for how long (5 minutes by default, 1 hour optional), and what the breakpoint boundaries are. The downside is that forgetting to set it is the easiest mistake to make.
"tool_result is an assistant message." No. It comes back as role: user. The user role is "input into the model," and a tool result is exactly that: input the model requested via a tool_use. Assistant messages are output from the model. Getting this wrong produces errors that look like "message role mismatch." Block type describes the kind of content. Role describes the direction.
"Signatures are an anti-abuse feature." Not primarily. Signatures are an integrity check to make sure the thinking block the model sees on turn 2 is bit-for-bit identical to the one it produced on turn 1. They protect against accidental mutation (a proxy re-serializing the content, a bad JSON library reordering keys) as much as deliberate tampering. The model is trained with the guarantee that its own prior reasoning is intact; the signature is how that guarantee gets enforced over the wire.
What's next
This post covered the content-block model, the runtime behavior of streaming and caching, extended thinking with signed thoughts and a budget knob, and the server-side context tools that let the provider help manage your own window. The handles to walk away with: the content-block model (every turn is a typed block array), cache breakpoints (you explicitly mark what's cacheable), and signed thoughts (integrity-verified thinking blocks that have to round-trip intact).
The next post covers Google's Gemini API: Function Calling and Thought Signatures, where the content-block idea gets a third variant, the function-calling interface looks flatter than either OpenAI's or Anthropic's, and thought signatures show up in a completely different form.
Additional reading (and watching)
- Anthropic. (2024). Messages API — Overview. Canonical reference for the request/response shape and the full list of supported block types.
- Anthropic. (2024). Streaming Messages. The SSE event taxonomy:
message_start,content_block_start,content_block_delta,content_block_stop,message_delta,message_stop. - Anthropic. (2024). Prompt Caching. Breakpoint semantics, 5-minute and 1-hour TTLs, cache-read and cache-write pricing multipliers,
usage.cache_read_input_tokens. - Anthropic. (2024). Extended Thinking. The
thinkingrequest parameter,budget_tokens, thinking blocks in responses, and the billing model. - Anthropic. (2024). Extended Thinking — Preserving thinking blocks. Explains the signature field and why thinking blocks must be echoed back verbatim on subsequent turns.
- Anthropic. Python SDK. The official
anthropicpackage; themessages.createentry point maps 1:1 onto the Messages request body. - Willison, S. (2024–2025). Notes on Anthropic's Messages API. Ongoing independent commentary on Messages, caching, and thinking; useful for sanity-checking the API's behaviour against a working practitioner's read.
- AWS. (2024). Anthropic Claude models on Amazon Bedrock. Documents Bedrock's near-verbatim adoption of the Anthropic Messages request shape, including content blocks and tool use.