Say you want to add an AI assistant to a shop website: the customer types an order number and the assistant looks up the delivery status. The demo works nicely. Then it goes live and three things happen. Customers stare at a blank screen for ten seconds or more before any text shows up. The assistant can't look up orders by itself, because it knows nothing about your data. And the API bill at month end is higher than you expected, because every call re-sends a long instruction manual.
Those are the three jobs of this post: how to let the AI call your functions, how to show text as it appears, and how to stop paying repeatedly for the same block of text. I use the Claude API. Model names, parameters and syntax below were checked against the current official SDK docs (October 2026), not written from memory.
How the API works
Everything goes through one door. You send a list of messages (alternating between the user and the AI), optionally with system instructions and a list of tools. You get back a list of content blocks: text, tool requests, thinking summaries. The API remembers nothing between calls, so each time you send the whole conversation again.
Install the official library with pip install anthropic (Python) or npm install @anthropic-ai/sdk (JavaScript). Keep the API key in the ANTHROPIC_API_KEY environment variable, never in code. The library reads it automatically.
Which model
| Model | ID | Good for | Price in / out per million tokens |
|---|---|---|---|
| Claude Opus 5.5 | claude-opus-5-5 |
hard tasks, code, agents; the default choice | $4 / $20 |
| Claude Sonnet 5.5 | claude-sonnet-5-5 |
high volume, faster and cheaper | $2 / $10 |
| Claude Haiku 5.5 | claude-haiku-5-5 |
classification, simple extraction | $0.10 / $0.50 (prompts up to 100K tokens) |
Write the ID string exactly as in the table, with no date suffix. Prices can change, so check the official pricing page when you budget.
Three things differ from older models and often cause 400 errors. On Opus 5.5 the AI always has a "thinking" step that can't be turned off; leave the thinking parameter out. Sending {"type": "disabled"} or budget_tokens is rejected. To go faster and cheaper, lower output_config={"effort": "low"}; the default for this generation is medium. Second, don't send a temperature, top_p or top_k other than the default. Third, forcing the AI to call one specific tool with tool_choice set to any or tool is also rejected on Opus 5.5 and Sonnet 5.5; use auto and say so in the instructions.
Letting the AI call your functions (tool use)
Think of it this way. The AI is a capable employee sitting in a closed room. To look up an order, it writes you a note: "please check order HD-1001". You check, then slide the result under the door. It reads it and answers the customer. The AI never runs your function itself. It asks, you run.
Technically, you describe a tool with a name, a description and a data shape. The AI returns a tool_use block with parameters, you run the function, and you send the result back in a tool_result block. This repeats until the AI stops asking.

Three things to remember. Always validate what the AI sends before running the function, and never assume it matches the shape. If the function fails, return is_error: true with an explanation so the AI can correct itself, and don't silently skip it. And if the AI asks for several things in one turn, run them all and send every result in a single message; splitting them across messages gradually teaches the AI to stop asking in parallel.
Showing text as it appears (streaming)
Normally the API finishes the whole answer before sending it. For a long answer the customer sees a blank screen. Streaming sends pieces of text the moment they're produced, like watching someone type. The library also requires streaming when you allow very long answers.
One detail: on the newer models the "thinking" part isn't returned by default. If you want to show customers a summary, set thinking={"type": "adaptive", "display": "summarized"}.
Not paying twice for the same text (prompt caching)
If every call re-sends the same long manual, mark it so the API remembers it. On later calls the remembered part costs a small fraction of the normal price (around a tenth, or less, depending on the model). The first write costs a little extra, about 1.25 times.
The rule to remember: the cache matches from the start. Change one word near the top and the cache is lost from that point on. Requests are assembled as tools, then system instructions, then messages, so put what rarely changes first and what often changes last. The remembered part also has to be long enough (the docs list a minimum of roughly 512 tokens depending on the model, so double-check); if it's too short the API raises no error, it just doesn't cache. The default memory lasts 5 minutes, and you can set "ttl": "1h".

To know whether caching works, read the usage numbers (usage). On the first call cache_creation_input_tokens is large; on later calls cache_read_input_tokens is large. If the read number is always 0, something is quietly breaking the cache: the current time inside the instructions, a random ID, or a tool list that changes order every time.
For those who write code
The tool-calling loop in Python (the TypeScript version has the same structure):
TOOLS = [{
"name": "get_order_status",
"description": "Look up an order by its id. Use only when the user gives an order id.",
"input_schema": {
"type": "object",
"properties": {"order_id": {"type": "string", "description": "Order id like HD-12345"}},
"required": ["order_id"],
"additionalProperties": False,
},
}]
def run_with_tools(client, user_input, max_turns=8):
messages = [{"role": "user", "content": user_input}]
for _ in range(max_turns):
response = client.messages.create(
model="claude-opus-5-5", max_tokens=16000, tools=TOOLS, messages=messages)
if response.stop_reason in ("refusal", "max_tokens"):
raise RuntimeError(f"Stopped early: {response.stop_reason}")
if response.stop_reason == "pause_turn": # only with server-side tools
messages.append({"role": "assistant", "content": response.content})
continue
if response.stop_reason != "tool_use":
return "".join(b.text for b in response.content if b.type == "text")
messages.append({"role": "assistant", "content": response.content})
results = []
for block in response.content:
if block.type == "tool_use":
content, is_error = execute_tool(block.name, block.input)
results.append({"type": "tool_result", "tool_use_id": block.id,
"content": content, "is_error": is_error})
messages.append({"role": "user", "content": results}) # ALL results in ONE message
raise RuntimeError("Too many tool turns")
Streaming and caching, the same idea in both languages:
with client.messages.stream(
model="claude-sonnet-5-5", max_tokens=64000,
messages=[{"role": "user", "content": prompt}],
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
final = stream.get_final_message() # full Message, including usage and stop_reason
response = client.messages.create(
model="claude-sonnet-5-5", max_tokens=16000,
system=[{"type": "text", "text": guidelines, # stable part first
"cache_control": {"type": "ephemeral"}}],
messages=[{"role": "user", "content": f"Review this diff:\n{diff}"}], # changing part last
)
u = response.usage
print(u.cache_creation_input_tokens, u.cache_read_input_tokens, u.input_tokens)
const stream = client.messages
.stream({ model: "claude-sonnet-5-5", max_tokens: 64000,
messages: [{ role: "user", content: prompt }] })
.on("text", (text) => process.stdout.write(text));
const final = await stream.finalMessage();
Opus 5.5's safety filter sometimes declines a request (stop_reason: "refusal"). There is a beta parameter fallbacks: "default" with the header server-side-fallback-2026-07-01 that makes the API re-run the declined request on a substitute model. I only checked that function with the TypeScript compiler and a fake server, not against the real API.
Common mistakes
The most common one is ignoring stop_reason. If it's max_tokens the answer was cut off midway; if it's refusal the filter declined and content may be empty. Check it before reading the content. Another is catching every error in one generic handler. Separate them: RateLimitError and 5xx errors deserve a retry (the library already retries twice), while 4xx errors don't, because sending the same thing again fails again. And never leave the API key in the repo or in logs.
What I checked and what I didn't
My machine has no API key, so no real call was made. On 21 Oct 2026 I re-ran three things. First, compiling the Python file (python -m py_compile, with the latest anthropic 1.17.0 installed in a scratch folder). Second, tsc --noEmit with @anthropic-ai/sdk 0.135.0, which accepted the types and parameters, including fallbacks: "default". Third, running both versions against a local fake Messages server: the tool loop sent exactly one tool_use and then exactly one tool_result in a single message, streaming assembled the text correctly, and usage returned the right cache numbers. That checks how my code and the library handle responses; it proves nothing about the real API's behaviour. Before using this for anything billed, run it once with your own key.
What to do next
Start small: one plain call with claude-opus-5-5 or claude-sonnet-5-5, turn on streaming, and only then add tools. Once tools work, move the long instructions to the top, mark them for caching, and read usage to see whether you really save money.
If you need a whole team of AIs rather than a single call, see Handing a Big Job to Several AIs Without Breaking Things. How to word the instructions for each call is in How to Ask an AI So It Gets It Right and Stops Making Things Up.



