1:1 mentoring with Big Tech AI engineers
Back to blog
By AgenticPrep TeamUpdated 11 min read

Claude API Tutorial: Tools, Thinking, Streaming and Caching

One endpoint does everything. Tools, thinking, caching and structured output are fields on the same call, and several shapes you remember from 2025 now return a 400.

claude-apitool-useprompt-cachingtutorial
Key idea

The Claude API is one endpoint, POST /v1/messages. Tools, thinking, caching, structured output and streaming are all fields on that one request, and the response is a list of typed blocks plus a stop_reason. Read the blocks by type and branch on stop_reason, and most production bugs never happen.

One endpoint: every feature is a request field, every result a typed block
REQUEST · POST /v1/messages RESPONSE · one Message modelwhich model, by ID messagesfull history, every call toolsfunctions it may call output_config.efforthow hard it thinks cache_controlreuse the stable prefix output_config.formatschema-valid JSON out streamevents while it writes one call content[] · typed blocks thinkingoften first, text omitted textthe answer tool_userun it, send a result stop_reasonbranch on this first usagetokens and cache hits Features are request fields. Results are typed blocks. Read by type, branch on stop_reason.

Pick a model by ID

List prices per million tokens, read from the pricing page on 21 October 2026. Every current model has a 1M-token context window and up to 128K output tokens, and output costs five times input on all of them.

ModelIDIn / out
Fable 5.1claude-fable-5-1$10 / $50
Opus 5.5claude-opus-5-5$4 / $20
Sonnet 5.5claude-sonnet-5-5$2 / $10
Haiku 5.5claude-haiku-5-5$0.10 / $0.50

Haiku 5.5's price holds for prompts up to 100,000 tokens; above that every rate is five times higher. IDs are complete as written: no date suffix.

Start with Opus 5.5, which the models page recommends for most workloads. Move up to Fable 5.1 only when evals at higher effort still fall short, and down to Haiku 5.5 for classification, extraction and routing (see tiered model routing). Two older names are on their way out: Haiku 4.5 ($1 / $5, 200K context) is still active, and Sonnet 4.5's retirement date is 30 November 2026, with claude-sonnet-5-5 as its replacement.

5×output price over input, every current model
~30%more tokens for the same text on the newer tokenizer
1Mcontext on Fable 5.1, Opus 5.5, Sonnet 5.5 and Haiku 5.5

The tokenizer point matters when you compare bills: the 4.7-and-later models count about 30% more tokens than Sonnet 4.6 and earlier for the same text, so a per-token price comparison across generations understates the difference.

Make the first request, and read it by type

import anthropic

client = anthropic.Anthropic()          # reads ANTHROPIC_API_KEY

response = client.messages.create(
    model="claude-opus-5-5",
    max_tokens=16000,
    system="Answer support questions from the policy text only.",
    messages=[{"role": "user", "content": "Can I return an opened item?"}],
)

if response.stop_reason == "end_turn":
    answer = "".join(b.text for b in response.content if b.type == "text")
    print(answer)
else:
    print("stopped early:", response.stop_reason)

response.content is a list, not a string. On current models adaptive thinking is on by default, so the first block is often a thinking block and content[0].text raises. Join the text blocks, and only after you have checked why the model stopped:

stop_reasonWhat your code does
end_turnfinished: read the text blocks
tool_userun every tool_use block, send the results, call again
max_tokenscut off: raise the cap and stream; never run a half-written tool call
pause_turna server tool paused: send the assistant turn back unchanged
refusaldeclined: surface it and read stop_details

The field-by-field reference, errors and retries are in the Messages API.

Know what returns a 400 now

If you learned this API in 2025, several shapes that used to work are now rejected. Each has a direct replacement:

temperature, top_p, top_k
400 on current models. Steer with output_config.effort and say what you want in the prompt.
budget_tokens
400. Use thinking: {"type": "adaptive"} plus effort.
Assistant prefill
400. Use structured output, or an instruction.
Forced tool_choice
any or a named tool is a 400 on Opus 5.5, Sonnet 5.5 and Fable 5.1. Use auto, a strict tool, and say when to call it.
output_format
Deprecated at the top level. Use output_config.format.
Thinking disabled
400 on Sonnet 5.5 (send between_tools); Opus 5.5 always thinks.

Haiku 5.5 still accepts a forced tool_choice, but the response then starts with the tool call and has no thinking block. Sources: the Sonnet 5.5 and Haiku 5.5 migration guides.

Set effort instead of temperature

response = client.messages.create(
    model="claude-opus-5-5",
    max_tokens=16000,
    thinking={"type": "adaptive", "display": "summarized"},
    output_config={"effort": "high"},     # low | medium | high | xhigh | max
    messages=messages,
)
for block in response.content:
    if block.type == "thinking":
        log.debug("summary: %s", block.thinking)   # empty unless display is set
    elif block.type == "text":
        print(block.text)

Where you once turned temperature, you now choose how hard the model works. effort lives inside output_config, not at the top level. The default is high on most models, medium on Opus 5.5 and Haiku 5.5.

low
routing, extraction, subagents that look things up
medium / high
most product traffic; measure before moving
xhigh
the usual pick for coding and long agent runs
max
when a wrong answer costs more than the tokens

Thinking text is omitted by default: thinking blocks arrive with an empty thinking field and a signature. On Opus 5.5 the short notes the model writes between tool calls also come back as thinking blocks, so an app that streams progress goes quiet until it sets display: "summarized". Pass thinking blocks back unchanged when you continue the conversation. More in extended thinking.

Follow one tool call on the wire

A tool is a name, a description and a JSON schema. The model never calls your API: it asks, your code calls, and you send the result back. These are the six messages, with their real payloads:

  1. Requesttools + messages
  2. tool_usestop_reason pauses
  3. Your codecalls the real API
  4. 200 OKthe real API answers
  5. tool_resultsent back in one user turn
  6. end_turnthe final text

Interactive: step through the six messages of one tool call and read each payload.

Put strict: true on the tool definition, with required and additionalProperties: false, and the input is guaranteed to match the schema. That is also the replacement for forced tool_choice on the 5.5 models. The description does most of the selection work, especially the sentence that says when not to use the tool (see tool surface design).

Run the loop, and return every result in one message

def run(messages: list, max_steps: int = 20):
    for _ in range(max_steps):                      # a budget, never while True
        resp = client.messages.create(
            model="claude-opus-5-5", max_tokens=16000,
            tools=TOOLS, messages=messages)
        messages.append({"role": "assistant", "content": resp.content})
        if resp.stop_reason != "tool_use":          # end_turn, max_tokens, refusal...
            return resp
        results = []
        for b in resp.content:
            if b.type != "tool_use":
                continue
            try:
                out = str(TOOL_FNS[b.name](**b.input))
                results.append({"type": "tool_result", "tool_use_id": b.id, "content": out})
            except Exception as e:                  # errors go back as results
                results.append({"type": "tool_result", "tool_use_id": b.id,
                                "content": f"{type(e).__name__}: {e}", "is_error": True})
        messages.append({"role": "user", "content": results})   # ONE message
    raise RuntimeError("max_steps reached")

One assistant turn can hold several tool_use blocks, because parallel tool use is on by default. Send all their results back in a single user message. Split them across messages and the model learns to stop calling in parallel: the agent gets slower and nothing errors. A failed tool still gets a tool_result, with is_error: true; a missing one makes the next request fail.

You rarely need to hand-write this. The SDK's Tool Runner (client.beta.messages.tool_runner) runs the same loop for decorated functions, and the Claude Agent SDK gives you the whole Claude Code harness. Which one to pick is who owns the loop; the loop itself, with every guard, is the agent loop.

Stream anything long

  1. message_startid, model, empty content
  2. content_block_deltatext, tool input JSON, thinking
  3. content_block_stopone block done
  4. message_deltastop_reason and usage
  5. message_stopthe end
with client.messages.stream(
    model="claude-opus-5-5",
    max_tokens=64000,
    messages=messages,
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)
    final = stream.get_final_message()     # the assembled Message

print(final.stop_reason, final.usage.output_tokens)

Up to 128K output tokens will not finish inside an HTTP timeout, so the SDK expects you to stream large max_tokens values. A sensible default is about 16K non-streaming and 64K streaming. A low cap does not save money: you pay for what is generated, and a truncated answer costs a retry. Tool inputs stream as input_json_delta fragments; parse them once the block stops. See streaming.

Cache the stable prefix

Cache from the front: one moving byte early costs the whole prefix
PREFIX MATCH, IN A FIXED ORDER: TOOLS → SYSTEM → MESSAGES read from cache at 0.05x on Sonnet 5.5 STABLE tools system prompt messages (new each turn) cache_control breakpoint datetime.now() VOLATILE tools system …system messages everything after the change: full price, every call If cache_read_input_tokens stays at 0, find the byte that moves.
response = client.messages.create(
    model="claude-sonnet-5-5", max_tokens=16000,
    system=[{"type": "text", "text": FROZEN_INSTRUCTIONS,
             "cache_control": {"type": "ephemeral"}}],    # breakpoint after system
    messages=messages,
)
u = response.usage
print(u.cache_read_input_tokens)       # served from cache
print(u.cache_creation_input_tokens)   # written to cache this call
print(u.input_tokens)                  # after the breakpoint, full price

The cache matches a prefix in the fixed order tools, system, messages. Any byte that changes before your breakpoint invalidates everything after it. Take a 20,000-token system prompt on Sonnet 5.5, called 1,000 times inside the cache lifetime:

$40prefix input without caching (1,000 × 20K × $2 per million)
$2.05one write at $2.50 plus 999 hits at $0.10 per million
95%less spent on the prefix

Cache hits are not 0.1x everywhere: Sonnet 5.5 and Opus 5.5 read at 0.05x of input, Fable 5.1 at 0.025x, Haiku 5.5 at 0.1x. The 5-minute write costs 1.25x and the 1-hour write 2x. The minimum cacheable prefix on the 5.5 models is 512 tokens; a shorter one is silently not cached. Usage is the only honest check. Details in prompt caching and the interview answer how prompt caching works.

Constrain the output, don't ask nicely

schema = {
    "type": "object",
    "properties": {
        "category": {"type": "string", "enum": ["billing", "bug", "other"]},
        "summary": {"type": "string"},
    },
    "required": ["category", "summary"],
    "additionalProperties": False,
}
response = client.messages.create(
    model="claude-sonnet-5-5", max_tokens=4000,
    output_config={"format": {"type": "json_schema", "schema": schema}},
    messages=[{"role": "user", "content": ticket_text}],
)
text = next(b.text for b in response.content if b.type == "text")
ticket = json.loads(text)          # valid shape; still check the values

A JSON schema in output_config.format guarantees the shape: schemas need additionalProperties: false, and the SDKs strip numeric and length constraints and check them client-side. It does not guarantee the values, so a well-formed ticket can still carry the wrong category. Keep validating. client.messages.parse() does the parsing and validation for you. See structured output.

Common mistakes

Reading content[0].text. Appending only the answer text instead of the full content, which drops tool calls and thinking blocks. Splitting parallel tool results across messages. Setting max_tokens low to save money. A timestamp or a reordered tool list in front of the cache breakpoint. None of these raise an error; all of them show up in stop_reason and usage.

Interview angle

“Walk me through what happens when Claude calls a tool.” Name the six messages: request with tools, tool_use with stop_reason: "tool_use", your code calls the real system with its own credentials, one user message of tool_result blocks keyed by tool_use_id, then end_turn. Then say what you guard: a step budget, errors as results, strict schemas, and approvals before anything irreversible.

Check yourself
A classifier used tool_choice: {"type": "tool"} to force JSON. After moving from Opus 5 to Opus 5.5 every call returns 400. What do you change?

Forced tool choice is rejected on Opus 5.5. For a classifier, switch to structured output (output_config.format with a JSON schema), which guarantees the shape without a tool. If you keep the tool, use tool_choice: auto, mark the tool strict: true and say in the prompt that it must be called. Haiku 5.5 would accept the forced choice, and is cheaper for classification anyway.

Your system prompt is 300 tokens, marked with cache_control, and cache_creation_input_tokens is always 0. Why?

It is below the 512-token minimum cacheable prefix, so the API silently does not cache it. Move the breakpoint later (after the tools and a longer stable block), or accept that a prefix this small is not worth caching.

The model asks for three tools in one turn and you send each result in its own user message. What goes wrong, and when do you notice?

Nothing errors. Over the following turns the model learns to call tools one at a time, so runs get slower. Send all three tool_result blocks in one user message, ahead of any text, and include failed calls with is_error: true.

Recap
  • One endpoint; tools, thinking, caching, structured output and streaming are request fields.
  • Read content by block type and branch on stop_reason before you read anything.
  • Sampling parameters, budget_tokens, prefill and (on Opus 5.5, Sonnet 5.5, Fable 5.1) forced tool_choice return 400; use effort, adaptive thinking, structured output and strict tools.
  • All tool results for a turn go back in one user message, errors included.
  • Cache a stable prefix; on Sonnet 5.5 a hit costs 0.05x of input, and usage proves it works.

Next: Tool use on Claude — tool choice, parallel calls, server tools and the Tool Runner in depth.

Enjoyed this post? The full curriculum has 149 sections, system design problems, and AI-reviewed practice runs.

See the full guide