π The Request/Response Cycle β Complete End-to-End Flow¶
What goes into a Messages API request, what happens to it on the server, how the response streams back, and how tokens (and money) accumulate across an agent loop.
30-second answer: A request is model + max_tokens + messages (plus optional system, tools, caching markers, thinking/effort settings). The server renders it into the model's prompt format, tokenizes it, runs prefill over all input tokens (reusing cached prefix state if there's a cache hit), then decodes one token at a time until the model ends its turn, asks for a tool, or hits max_tokens. The response is a list of content blocks plus a stop_reason and usage. In an agent loop every tool call means another full request, so input tokens grow every turn and caching is what keeps that affordable.
1. THE PROMPT ASSEMBLY¶
Everything the model sees is decided client-side, before the request is sent.
1.1 The Complete Prompt Structure¶
The API renders a request in a fixed order: tools β system β messages. That order matters for prompt caching, which matches on an exact prefix.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β TOOL DEFINITIONS name, description, input_schema (JSON) β
β (+ a tool-use system prompt the API adds automatically) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β SYSTEM PROMPT role, rules, context, output format β
βββββββββββββ cache breakpoint: everything above is stable βββββββββββ€
β MESSAGES β
β user: "Create a todo API..." β
β assistant: [text, tool_use{id, name, input}] β
β user: [tool_result{tool_use_id, content}] β
β ... repeated per tool round-trip ... β
β user: current message β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βΌ model generates here (output, β€ max_tokens)
When you pass tools, Anthropic documents that the API also injects a tool-use system prompt (a few hundred tokens, model-dependent), billed as input. Tool schemas themselves are input tokens too, which is why large tool sets get expensive and why tool search / deferred loading exists.
1.2 Token Count Breakdown (illustrative agent request)¶
| Component | Tokens | Share of input | Changes per call? |
|---|---|---|---|
| Tool definitions (+ tool-use prompt) | ~2,500 | 14% | No (cacheable) |
| System prompt | ~2,000 | 11% | No (cacheable) |
| Conversation history (text turns) | ~5,000 | 28% | Grows |
| Tool results (file contents, command output) | ~8,000 | 44% | Grows, usually the largest part |
| Current user message | ~500 | 3% | Yes |
| Total input | ~18,000 | 100% | |
Output (max_tokens cap, not pre-charged) |
up to e.g. 16,000 | n/a | You pay only for tokens actually generated |
max_tokens reserves room in the context window (input + output must fit), but you're billed only for generated tokens.
2. THE API REQUEST¶
2.1 HTTP Request Structure¶
POST https://api.anthropic.com/v1/messages
x-api-key: sk-ant-...
anthropic-version: 2023-06-01
content-type: application/json
{
"model": "claude-opus-5-5",
"max_tokens": 16000,
"stream": true,
"output_config": {"effort": "medium"},
"system": [
{
"type": "text",
"text": "You are a coding agent working in a FastAPI repo...",
"cache_control": {"type": "ephemeral"}
}
],
"tools": [
{
"name": "read_file",
"description": "Read a UTF-8 file from the repository. Use before editing.",
"input_schema": {
"type": "object",
"properties": {"path": {"type": "string"}},
"required": ["path"]
}
}
],
"messages": [
{"role": "user", "content": "Create a REST API for a todo app with FastAPI"}
]
}
Corrections to common mistakes:
- Auth is the
x-api-keyheader (OAuth bearer tokens are a separate login path).anthropic-versionis2023-06-01, the current API version; new features arrive viaanthropic-betaheaders, not new version dates. - Don't send
stop_sequences: ["\n\nHuman:"]. That's a leftover from the retired Text Completions API; the Messages API handles turns structurally. - Don't set
temperature/top_p/top_kon current models: models released after Opus 4.6 reject non-default values. Useeffort.
2.2 Request Fields Explained¶
| Field | Required | Purpose |
|---|---|---|
model |
β | Model ID, e.g. claude-opus-5-5 (IDs from the 4.6 generation on are dateless pinned snapshots) |
max_tokens |
β | Cap on generated tokens (thinking included) |
messages |
β | Conversation; must start with a user turn |
system |
β | System prompt (string or list of text blocks, cacheable) |
tools, tool_choice |
β | Tool definitions; auto / none / force a tool. Forcing (any/tool) is rejected on the newest models, so use auto + strict: true tools or structured outputs |
thinking |
β | {"type": "adaptive"} on current models (older models: enabled + budget_tokens) |
output_config |
β | effort (lowβ¦max) and structured-output format (JSON schema) |
cache_control |
β | Top-level automatic caching, or per-block breakpoints (max 4) |
stream |
β | SSE streaming; recommended for long outputs to avoid HTTP timeouts |
stop_sequences |
β | Custom stop strings |
metadata |
β | user_id (an opaque ID for abuse detection) |
temperature, top_p, top_k |
β | Deprecated; only older models accept non-default values |
3. WHAT HAPPENS INSIDE THE LLM¶
Anthropic doesn't document its serving stack or tokenizer internals. The pipeline below is general LLM background, true of any transformer server.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 1. RENDER + TOKENIZE β
β JSON request β model's internal prompt format (role markers, β
β tool schemas, special tokens; not public) β token IDs β
β Subword tokenizer (BPE-style); Claude's vocabulary isn't public β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β 2. PREFILL (parallel over all input tokens) β
β β’ Cache hit? Reuse stored state for the cached prefix, compute β
β only the rest. That's why cached input is cheaper and faster β
β β’ Each layer: causal self-attention + feed-forward β
β β’ Writes K/V for every position into the KV cache β
β β’ Produces logits for the first output token β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β 3. DECODE LOOP (one token per step) β
β β’ Sample a token from the logits β
β β’ Run one forward pass for that token, attending over the KV β
β cache; append its K/V β
β β’ Stream the token's text (or tool-input JSON) to the client β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β 4. STOP CONDITION β stop_reason β
β end_turn model finished its turn β
β tool_use model emitted tool_use block(s), wants resultsβ
β max_tokens hit your cap (output truncated) β
β stop_sequence hit one of your stop strings β
β pause_turn long server-tool turn paused; resend to resumeβ
β refusal safety classifier stopped the response β
β model_context_window_exceeded ran out of context window β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
tool_use isn't decided "after" generation: the model generates the tool call as tokens like any other output, and the server parses them into a structured tool_use block.
Figure: server-side stages from request to stop_reason.
flowchart TD
A["JSON request"] --> B["Render and tokenize"]
B --> C["Prefill (reuse cached prefix if hit)"]
C --> D["Decode loop: sample, forward pass, stream"]
D --> E{"Stop?"}
E -- "no" --> D
E -- "yes" --> F["stop_reason: end_turn / tool_use / max_tokens / ..."]
4. THE STREAMING RESPONSE¶
Why Stream?¶
| Reason | Impact |
|---|---|
| Perceived latency | Text appears as soon as the first token is decoded (time-to-first-token is dominated by prefill, so it grows with prompt size) |
| Long outputs | Non-streaming requests with very large max_tokens can hit HTTP timeouts; the SDKs push you to stream |
| Early tool dispatch | The client sees a complete tool call at content_block_stop, before the message ends |
| Cancellation | Close the connection to stop paying for further output |
Streaming Event Types¶
| Event | Data |
|---|---|
message_start |
Message ID, model, input-side usage (incl. cache read/write tokens) |
content_block_start |
Block index and type (text, thinking, tool_use, server-tool blocks) |
content_block_delta |
text_delta, input_json_delta (partial tool JSON), thinking_delta, signature_delta |
content_block_stop |
Block complete |
message_delta |
stop_reason, cumulative output usage |
message_stop |
Stream ends |
ping / error |
Keep-alive / mid-stream error such as overloaded_error |
5. TOOL CALL HANDLING¶
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Response: β
β content: [ {type: "text", text: "Let me check main.py"}, β
β {type: "tool_use", id: "toolu_01A", name: "read_file", β
β input: {"path": "main.py"}} ] β
β stop_reason: "tool_use" β
β β
β Client: β
β 1. Append the WHOLE assistant content (text, thinking, tool_use) β
β 2. Validate tool name + input (schema, path allow-list) β
β 3. Check permissions / ask the user for risky actions β
β 4. Execute; truncate huge outputs β
β 5. Append ONE user message with ALL tool_result blocks: β
β {type: "tool_result", tool_use_id: "toolu_01A", β
β content: "from fastapi import FastAPI\n...", β
β is_error: false} β
β 6. Resend the complete conversation β
β 7. Model either calls more tools or ends its turn β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Rules that bite in practice:
- Every
tool_useneeds a matchingtool_resultin the very next user message; otherwise you get a 400. - Failed tools return
is_error: truewith the error text so the model can recover. Don't drop them. - If thinking is on, pass thinking blocks back unmodified with the tool results (they're signed).
- Parse tool
inputas JSON; don't string-match it. Escaping can differ between models.
Token Consumption Across Tool Calls¶
Each call re-sends all previous context:
Call 1 (user prompt): 5,000 input + 500 output
Call 2 (after tool result): 6,000 input + 300 output
Call 3 (after tool result): 7,000 input + 400 output
Call 4 (final answer): 8,000 input + 800 output
ββββββββββββββββββββββββββ
Total: 26,000 input + 2,000 output
Three tool calls cost 28,000 tokens, versus ~5,500 if the same answer came in one call. In general, with a fixed prefix P and ~Ξ new tokens per turn, cumulative input after N calls is about NΒ·P + ΞΒ·NΒ²/2: linear per call, quadratic in total. With prompt caching, most of each call's input is billed as cache reads (~10% of the input rate, less on some models), which flattens that curve dramatically.
Figure: the client executes tools and resends the whole conversation each round.
sequenceDiagram
participant C as Client
participant M as Messages API
C->>M: Request (call 1)
M-->>C: text + tool_use, stop_reason tool_use
C->>C: Validate, check permissions, execute
C->>M: Full history + one user message of tool_result blocks
M-->>C: end_turn or more tool_use
6. RESPONSE POST-PROCESSING (CLIENT SIDE)¶
What a well-built client does with the response:
| Step | Why |
|---|---|
Branch on stop_reason before reading content |
max_tokens means truncated (maybe mid tool-JSON); refusal means no usable answer; pause_turn means resend to continue |
Iterate content blocks by type |
content[0] may be a thinking block, not text |
| Validate tool inputs against your schema | Or set strict: true on tools to get schema-valid inputs |
| Render text, show tool activity | UX; Claude Code shows each tool call as it runs |
Record usage |
Cost tracking, cache hit-rate monitoring (cache_read_input_tokens) |
7. COMPLETE END-TO-END EXAMPLE¶
"Add a health check endpoint to the FastAPI app", in a loop with no caching (illustrative counts):
CALL 1
input: tools 2,500 + system 2,000 + history 3,200 + message 8 = 5,708
output: text + tool_use(read_file main.py) = 85
client reads main.py β tool_result = 340
CALL 2
input: 5,708 + 85 (assistant turn) + 340 (tool result) = 6,133
output: tool_use(edit main.py) = 62
client applies edit β tool_result "edit applied" = 62
CALL 3
input: 6,133 + 62 + 62 = 6,257
output: "Added GET /health ..." stop_reason: end_turn = 150
TOTAL input 18,098 output 297
At example rates of $3 / $15 per million input/output tokens: 18,098 Γ $3/M + 297 Γ $15/M β $0.054 + \(0.004 = **\)0.059**. With the 4,500-token tools+system prefix cached (written once at 1.25Γ in call 1, read at 0.1Γ in calls 2 and 3), billed input drops by roughly 40% for this short loop; caching the growing history too, and running more turns, pushes the saving much higher.
Figure: the three calls of the health-check example.
sequenceDiagram
participant C as Client
participant M as Model
C->>M: Call 1 (5,708 input tokens)
M-->>C: tool_use read_file main.py
C->>M: Call 2 (6,133 input tokens, with file contents)
M-->>C: tool_use edit main.py
C->>M: Call 3 (6,257 input tokens, edit applied)
M-->>C: Final text, end_turn
8. KEY TAKEAWAYS¶
| Concept | Why It Matters |
|---|---|
| The API is stateless | Every call resends everything; the client owns conversation state |
| Cost per call grows linearly; per session, quadratically | Cache the prefix, compact or reset long sessions |
| Order is tools β system β messages | Put stable content first so caching works |
| Output is priced at 5Γ input (current Claude lineup) | Control verbosity and effort; thinking tokens bill as output |
stop_reason drives the loop |
tool_use β run tools and continue; end_turn β done; handle the rest explicitly |
| TTFT comes from prefill | Long prompts start slower; caching cuts TTFT on repeated prefixes |
Next: Tokenization & Token Calculation β How tokens are counted, priced, and optimized