🔍 Data Verification
Claude models and pricing verified: Oct 28 2026. The AI tool landscape changes rapidly. Always check official sources for the most current pricing and features:
Part 1: Using vs. Building With AI
When you use Claude in claude.ai, you type a message and get a response. The interface handles everything else: the conversation history, the system context, the display. Building a product with the Claude API means you are responsible for all of that yourself.
You decide what's in the system prompt — and it applies to every user of your product. You decide how much conversation history to send with each request — and pay for every token. You decide when to use streaming, what to do when the model returns JSON that doesn't parse, and how to handle rate limits at 3am when you're not watching.
These are engineering problems, not prompting problems. The prompting skills from the rest of this site still apply — but they're now embedded in an application with real users, real costs, and real reliability requirements.
When you use AI, you are the context — you know the goal, the constraints, what you've tried. When you build with AI, your product must provide all of that context programmatically, for every user, on every request. System prompts are code, not suggestions.
Which Model to Use
The current Claude models (as of Oct 2026, per Anthropic's models overview):
- claude-opus-5-5 — Anthropic's recommended starting point for most workloads (launched September 22, 2026). Long-running agentic coding and knowledge work. 1M-token context, adaptive thinking always on. $4 / $20 per million input / output tokens.
- claude-fable-5-1 — The most capable model, for demanding reasoning and long-horizon agentic work when Opus 5.5 at higher effort still falls short. Thinking is always on and the raw chain of thought is never returned. $10 / $50 per million tokens.
- claude-sonnet-5-5 — Best combination of speed and intelligence, and the cost-efficient default for most product use cases (launched September 28, 2026). Structured output, coding, document processing. 1M-token context. $2 / $10 per million tokens.
- claude-haiku-5-5 — Fastest current model for high-volume classification, extraction and routing (launched October 7, 2026). 1M-token context. $0.10 / $0.50 per million input / output tokens for prompts up to 100,000 tokens; $0.50 / $2.50 above that threshold. The entire prompt, including cache reads and writes, determines the price tier for each request.
Every dateless ID from the 4.6 generation on is a pinned snapshot, so claude-sonnet-5-5 is safe to use in production without a date suffix. Older models, among them Claude Opus 5, Sonnet 5, Fable 5, Opus 4.8, Sonnet 4.6 and Haiku 4.5, are still served; Sonnet 4.5 retires from the Claude API on November 30, 2026. The code below targets the current generation.
Some request fields now return a 400 error. With API version 2026-10-21, which the current SDKs send, any temperature, top_p or top_k is rejected on every model, even at its default value; earlier API versions already rejected non-default values on Claude 4.7 and later. Every model from the 4.6 generation on rejects a prefilled assistant turn at the end of messages. Opus 5.5, Sonnet 5.5 and Fable 5.1 also reject forced tool use (tool_choice any or tool) and thinking: {"type": "disabled"}. Thinking runs by default, so a response can begin with a thinking block — read text by block type rather than from content[0], and remember that max_tokens covers thinking plus the answer. On Sonnet 5.5, thinking: {"type": "between_tools"} turns off up-front thinking; on Opus 5.5 you lower the effort level instead.
The practical rule: use Opus 5.5 as the initial quality baseline, following Anthropic's model-selection guidance, then benchmark Sonnet 5.5 and Haiku 5.5 on your actual task to see whether they meet the same threshold at lower cost. Haiku 5.5 enables adaptive thinking by default; omit manual budget_tokens and sampling parameters when migrating. The short summarization example below explicitly disables Haiku thinking so that its small output budget is reserved for the answer.
Part 2: Your First API Call
The Anthropic SDK handles authentication, retries, and response parsing. Install it and make one call before building anything more complex.
# Python
pip install anthropic
# Node.js / TypeScript
npm install @anthropic-ai/sdk
These commands install the newest major versions (anthropic 2.x and @anthropic-ai/sdk 1.x), which send API version 2026-10-21 rather than 2023-06-01 and no longer accept temperature, top_p or top_k. If you are upgrading an existing integration, inventory the requests that version rejects first with the Claude API version 2026-10-21 migration guide.
import anthropic
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY from environment
message = client.messages.create(
model="claude-sonnet-5-5",
max_tokens=1024,
messages=[
{"role": "user", "content": "Summarize this in one sentence: [text]"}
]
)
# Read text blocks by type: with thinking on, content[0] can be a thinking block
print("".join(b.text for b in message.content if b.type == "text"))
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic(); // reads ANTHROPIC_API_KEY from environment
const message = await client.messages.create({
model: "claude-sonnet-5-5",
max_tokens: 1024,
messages: [
{ role: "user", content: "Summarize this in one sentence: [text]" }
],
});
// Read text blocks by type: with thinking on, content[0] can be a thinking block
console.log(
message.content.filter((b) => b.type === "text").map((b) => b.text).join("")
);
Set your API key as an environment variable — never hardcode it in your source files:
# .env (never commit this file)
ANTHROPIC_API_KEY=sk-ant-...
# Load it with python-dotenv (Python) or dotenv (Node)
# The SDK reads it automatically if the env var is set
Thinking counts toward max_tokens. Sonnet 5.5 defaults to high effort and Opus 5.5 and Haiku 5.5 to medium, so a small max_tokens can end with stop_reason: "max_tokens" before the answer is complete. For short, simple tasks, lower the effort with output_config={"effort": "low"}; otherwise leave room for thinking and check stop_reason as shown in Part 8.
Understanding the Response
# The response has these key fields:
message.id # unique request ID
message.model # model used
message.content # list of content blocks
message.content # thinking, text, tool_use ... blocks — filter by block.type
message.usage.input_tokens # tokens you sent (what you pay for)
message.usage.output_tokens # tokens in the response (what you pay for)
message.stop_reason # "end_turn" | "max_tokens" | "tool_use" | "refusal" | "pause_turn"
Log usage.input_tokens and usage.output_tokens on every request in development. It's the fastest way to understand your token budget and catch prompt engineering mistakes — a prompt that's unexpectedly long will immediately show up in the token count.
Part 3: System Prompt Design
The system prompt is the most important engineering decision in an AI-powered product. It defines what your product does, how it behaves, and what it refuses to do — for every user, on every request.
What Goes in a System Prompt
A production system prompt typically has five components:
- Role and context. Who Claude is in this product and what it knows about the product domain.
- Task definition. What Claude should do. Be specific — "help users" is not a task definition.
- Output format. What the response should look like. If you need JSON, say so. If you need a specific structure, describe it exactly.
- Constraints and guardrails. What Claude should not do. Topics to avoid, things to refuse, scope limits.
- Examples. One or two examples of ideal input/output pairs for complex or nuanced tasks. These are the most reliable way to communicate the quality standard you expect.
You are a code review assistant for a team that builds TypeScript applications.
Your task is to review code changes and provide feedback in the following categories:
- Correctness: bugs, logic errors, type safety issues
- Security: SQL injection, XSS, secrets in code, missing auth checks
- Performance: obvious N+1 queries, unnecessary re-renders, sync operations that should be async
- Maintainability: unclear names, missing error handling, code duplication
Output format — respond with a JSON object:
{
"summary": "one sentence overall assessment",
"issues": [
{
"severity": "critical" | "major" | "minor" | "suggestion",
"category": "correctness" | "security" | "performance" | "maintainability",
"location": "filename:line",
"description": "what the issue is",
"suggestion": "how to fix it"
}
],
"approved": boolean
}
Rules:
- Only flag real issues — don't invent problems to seem thorough
- "critical" severity is for security vulnerabilities and definite bugs only
- If there are no issues in a category, don't mention that category
- "approved" is true only if there are no critical or major issues
- Do not explain your output format or add commentary outside the JSON
System Prompt as Code
Your system prompt is code — it changes how your product behaves, and those changes affect all users. Treat it accordingly:
- Store it in a file in your repository, not hardcoded in an API call
- Version control it with the same discipline as application code
- Test changes against a fixed set of inputs before deploying
- Use environment variables if you need different prompts for dev/staging/prod
import json
from pathlib import Path
SYSTEM_PROMPT = Path("prompts/code_review.txt").read_text()
def review_code(diff: str) -> dict:
message = client.messages.create(
model="claude-sonnet-5-5",
max_tokens=2048,
system=SYSTEM_PROMPT,
messages=[{"role": "user", "content": diff}]
)
# Part 7 shows how to make the API enforce this JSON shape with output_config.format
return json.loads("".join(b.text for b in message.content if b.type == "text"))
Part 4: Context Management
The context window is the total number of tokens Claude can see in one request — your system prompt, the conversation history, and the current message. Claude Opus 5.5, Sonnet 5.5 and Haiku 5.5 have a 1,000,000-token context window (the older Haiku 4.5 has 200,000), which sounds large until you're building a product that processes long documents or maintains long conversations.
The Context Window Budget
Think of the context window as a fixed budget. Every token you spend on history is a token you can't spend on documents. Every token the system prompt consumes is a token the user can't use. Most products allocate the budget something like:
- System prompt: 500–2000 tokens (keep it tight)
- Retrieved context / documents: variable, often 10,000–50,000 tokens
- Conversation history: 5–20 messages, pruned to fit
- Current user message: whatever the user sends
- Max output tokens: set explicitly, typically 1,024–4,096
Managing Conversation History
For a chat product, you need to decide how much conversation history to include with each request. Sending the entire history gets expensive quickly. Three common strategies:
def build_messages(history: list[dict], new_message: str, max_history: int = 10) -> list[dict]:
"""Keep only the last N messages of history."""
recent = history[-max_history:] if len(history) > max_history else history
return recent + [{"role": "user", "content": new_message}]
# Count with the API, not with a third-party tokenizer:
# Claude's tokenizer differs from tiktoken, and it changed again in the 4.7 generation.
def count_tokens(text: str) -> int:
return client.messages.count_tokens(
model="claude-sonnet-5-5",
messages=[{"role": "user", "content": text}],
).input_tokens
def build_messages_within_budget(
history: list[dict],
new_message: str,
token_budget: int = 10_000
) -> list[dict]:
"""Add messages from newest to oldest until budget is used."""
messages = [{"role": "user", "content": new_message}]
tokens_used = count_tokens(new_message)
for msg in reversed(history):
msg_tokens = count_tokens(msg["content"])
if tokens_used + msg_tokens > token_budget:
break
messages.insert(0, msg)
tokens_used += msg_tokens
return messages
def summarize_old_history(history: list[dict]) -> str:
"""Summarize early conversation to a compact representation."""
early_history = history[:-10] # everything before last 10 messages
response = client.messages.create(
model="claude-haiku-5-5", # lower-cost model; benchmark on your task
max_tokens=512,
thinking={"type": "disabled"},
messages=[{
"role": "user",
"content": f"Summarize this conversation in 2-3 sentences, keeping the key facts:\n\n{format_history(early_history)}"
}]
)
return "".join(b.text for b in response.content if b.type == "text")
Use Haiku for the summarization step — it's cheap and fast for this kind of task, and you're compressing old history that doesn't need deep understanding.
Retrieval-Augmented Generation (RAG)
For products that answer questions about a document corpus, you don't send all documents in every request. You embed the user's question, retrieve the most relevant chunks, and send only those:
def answer_question(question: str, vector_db) -> str:
# 1. Retrieve relevant chunks (using embeddings + vector search)
relevant_chunks = vector_db.search(question, top_k=5)
# 2. Build context from retrieved chunks
context = "\n\n---\n\n".join(
f"[Source: {chunk.source}]\n{chunk.text}"
for chunk in relevant_chunks
)
# 3. Ask Claude with the retrieved context
response = client.messages.create(
model="claude-sonnet-5-5",
max_tokens=1024,
system="""Answer questions using only the provided context.
If the answer is not in the context, say so — do not guess.
Cite the source for each claim.""",
messages=[{
"role": "user",
"content": f"Context:\n{context}\n\nQuestion: {question}"
}]
)
return "".join(b.text for b in response.content if b.type == "text")
Part 5: Cost Optimization
At scale, prompt costs are real engineering costs. A product with 10,000 daily active users making 5 API calls each needs these numbers to work. The main levers: model selection, prompt caching, and prompt efficiency.
Prompt Caching
Prompt caching is the single most impactful cost optimization for most products. When a request starts with a long prefix that's identical to a recent request (your system prompt, a large document, a reference corpus), Claude can reuse the cached computation instead of reprocessing it.
The cache TTL is 5 minutes by default and is refreshed each time the prefix is read; a 1-hour TTL costs more to write. The prefix must also reach the model's minimum cacheable length, 512 tokens on Opus 5.5, Sonnet 5.5, Haiku 5.5 and Fable 5.1. A shorter prefix is processed without caching and no error is returned, so check the usage fields below. For a product with a fixed, long system prompt, caching cuts the cached part of the input price by 90% or more.
import anthropic
client = anthropic.Anthropic()
# Mark the system prompt for caching
# Cache hits cost 10% of base input on most models: 5% on Opus 5.5 and Sonnet 5.5, 2.5% on Fable 5.1
response = client.messages.create(
model="claude-sonnet-5-5",
max_tokens=1024,
system=[
{
"type": "text",
"text": LONG_SYSTEM_PROMPT, # your stable system prompt
"cache_control": {"type": "ephemeral"} # marks this for caching
}
],
messages=[{"role": "user", "content": user_message}]
)
# Check whether the cache was hit or missed
print(f"Cache read tokens: {response.usage.cache_read_input_tokens}")
print(f"Cache creation tokens: {response.usage.cache_creation_input_tokens}")
# Both 0 means nothing was cached, usually because the prefix is below the minimum length
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const response = await client.messages.create({
model: "claude-sonnet-5-5",
max_tokens: 1024,
system: [
{
type: "text",
text: LONG_SYSTEM_PROMPT,
cache_control: { type: "ephemeral" },
},
],
messages: [{ role: "user", content: userMessage }],
});
// usage.cache_read_input_tokens > 0 means cache hit
console.log("Cache hit:", (response.usage.cache_read_input_tokens ?? 0) > 0);
A 5-minute cache write costs 1.25× a regular input token; a cache read costs 10% of one on most models, and less on Opus 5.5 ($0.20 per million), Sonnet 5.5 ($0.10 per million) and Fable 5.1 ($0.25 per million), checked October 27, 2026. A prefix that is reused even once within the cache lifetime already costs less than sending it twice uncached. For a fixed system prompt served to many users, the payback is essentially immediate.
Model Selection by Task
Not every request in your product needs the same model. A common pattern:
def get_model_for_task(task_type: str) -> str:
"""Select the right model based on what we need to do."""
routing = {
# High quality needed
"complex_reasoning": "claude-opus-5-5",
"creative_writing": "claude-opus-5-5",
# Good balance for most tasks
"code_review": "claude-sonnet-5-5",
"document_analysis": "claude-sonnet-5-5",
"chat_response": "claude-sonnet-5-5",
# Simple tasks where speed and cost matter
"classification": "claude-haiku-5-5",
"summarization": "claude-haiku-5-5",
"data_extraction": "claude-haiku-5-5",
"intent_detection": "claude-haiku-5-5",
}
return routing.get(task_type, "claude-sonnet-5-5")
Prompt Efficiency
Shorter prompts cost less. Every word in your system prompt costs money on every request. Audit your system prompts periodically — AI is good at making them shorter without losing meaning:
This is my system prompt. It currently uses about 800 tokens. Make it as short as possible while preserving every behavioral constraint — I don't care about tone, only that the AI behaves identically. Then tell me how many tokens the optimized version uses and what you removed.
[paste your current system prompt]
Part 6: Streaming
Streaming lets you display Claude's response as it's generated rather than waiting for the full response. For user-facing features, streaming dramatically improves perceived performance — users see output in under a second instead of waiting 5–10 seconds for a complete response.
import anthropic
client = anthropic.Anthropic()
with client.messages.stream(
model="claude-sonnet-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Write a short story about a robot."}]
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
# After the stream:
message = stream.get_final_message()
print(f"\n\nTokens: {message.usage.input_tokens} in, {message.usage.output_tokens} out")
// app/api/chat/route.ts
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
export async function POST(req: Request) {
const { message } = await req.json();
const stream = client.messages.stream(
{
model: "claude-sonnet-5-5",
max_tokens: 4096, // thinking counts toward max_tokens
messages: [{ role: "user", content: message }],
},
{ signal: req.signal }, // a closed tab or Stop aborts the upstream request
);
// Return as Server-Sent Events for the browser to consume
const encoder = new TextEncoder();
const readable = new ReadableStream({
async start(controller) {
for await (const event of stream) {
if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
controller.enqueue(
encoder.encode(`data: ${JSON.stringify({ text: event.delta.text })}\n\n`)
);
}
}
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
controller.close();
},
cancel() {
stream.abort();
},
});
return new Response(readable, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}
"use client";
import { useState } from "react";
export function ChatInput() {
const [response, setResponse] = useState("");
async function sendMessage(message: string) {
setResponse("");
const res = await fetch("/api/chat", {
method: "POST",
body: JSON.stringify({ message }),
});
if (!res.ok || !res.body) throw new Error(`Chat request failed: ${res.status}`);
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true }); // keeps a split "å" intact
const events = buffer.split("\n\n");
buffer = events.pop() ?? ""; // an event can arrive split across reads
for (const event of events) {
if (!event.startsWith("data: ") || event === "data: [DONE]") continue;
const { text } = JSON.parse(event.slice(6));
setResponse((prev) => prev + text);
}
}
}
return (
<div>
<button onClick={() => sendMessage("Tell me something interesting")}>
Ask
</button>
<p>{response}</p>
</div>
);
}
Part 7: Tool Use (Function Calling)
Tool use lets Claude call functions in your application — look up data, run calculations, trigger actions. Claude decides when to use a tool based on the user's request, calls it with structured arguments, and uses the result to generate its final response.
This is the pattern behind AI assistants that can actually do things: look up a user's order status, check a flight, query a database, send a notification.
Defining a Tool
tools = [
{
"name": "get_order_status",
"description": "Look up the current status of a customer order by order ID. Use this when the user asks about their order.",
"input_schema": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "The order ID (format: ORD-XXXXXXXX)"
}
},
"required": ["order_id"]
}
},
{
"name": "list_customer_orders",
"description": "Get a list of recent orders for the authenticated customer.",
"input_schema": {
"type": "object",
"properties": {
"limit": {
"type": "integer",
"description": "Maximum number of orders to return (default 5, max 20)"
}
}
}
}
]
The Tool Use Loop
import json
def run_agent(user_message: str, customer_id: str) -> str:
messages = [{"role": "user", "content": user_message}]
while True:
response = client.messages.create(
model="claude-sonnet-5-5",
max_tokens=1024,
system=f"You are a customer support agent for Acme Store. The authenticated customer ID is {customer_id}.",
tools=tools,
messages=messages,
)
# If Claude finished (no tool call), return the text
if response.stop_reason == "end_turn":
return "".join(b.text for b in response.content if b.type == "text")
# If Claude wants to use a tool, run it
if response.stop_reason == "tool_use":
# Add Claude's response (including the tool call) to history
messages.append({"role": "assistant", "content": response.content})
# Find and execute each tool call
tool_results = []
for block in response.content:
if block.type == "tool_use":
result = execute_tool(block.name, block.input, customer_id)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result)
})
# Add tool results to history and continue
messages.append({"role": "user", "content": tool_results})
continue
# max_tokens, refusal, pause_turn ...: stop instead of looping forever
raise RuntimeError(f"Unexpected stop_reason: {response.stop_reason}")
def execute_tool(name: str, inputs: dict, customer_id: str) -> dict:
"""Dispatch tool calls to the appropriate function."""
if name == "get_order_status":
return order_service.get_status(inputs["order_id"], customer_id)
elif name == "list_customer_orders":
limit = inputs.get("limit", 5)
return order_service.list_orders(customer_id, limit=limit)
else:
return {"error": f"Unknown tool: {name}"}
Claude decides which tool to call based on the user's message. But the authorization check must happen in your tool function — not in the system prompt. Always verify that the authenticated user has permission to access the resource before returning data. Never rely on Claude to enforce authorization.
Getting Structured JSON Output (Without Tool Use)
If you just need structured output and don't need Claude to call external functions, ask the API to enforce a JSON schema with output_config.format. When the response ends normally, its text is valid JSON that matches the schema, so you never need a "fix the JSON" retry. Check stop_reason first: a max_tokens or refusal stop can return output that is incomplete or off-schema. (The old pattern of prefilling an assistant turn to coerce output returns a 400 error on current models.)
import json
def extract_contact_info(text: str) -> dict:
response = client.messages.create(
model="claude-sonnet-5-5",
max_tokens=512,
system="Extract contact information from the provided text. If a field is not present, use null.",
messages=[{"role": "user", "content": text}],
output_config={
"effort": "low", # a simple extraction; leaves max_tokens for the answer
"format": {
"type": "json_schema",
"schema": {
"type": "object",
"properties": {
"name": {"type": ["string", "null"]},
"email": {"type": ["string", "null"]},
"phone": {"type": ["string", "null"]},
"company": {"type": ["string", "null"]},
},
"required": ["name", "email", "phone", "company"],
"additionalProperties": False,
}
}
},
)
if response.stop_reason != "end_turn":
# max_tokens or refusal: the JSON may be incomplete or off-schema
raise ValueError(f"No complete structured output: {response.stop_reason}")
text = next(block.text for block in response.content if block.type == "text")
return json.loads(text)
# With Pydantic, client.messages.parse(..., output_format=ContactInfo) returns
# response.parsed_output as a validated model instance instead.
Part 8: Production Patterns
A few patterns that distinguish production-grade API integrations from prototype-grade ones.
Retry on Rate Limits and Transient Errors
The Anthropic SDK retries connection errors, rate limits and server errors twice by default. Configure it explicitly, and if your application already has its own retry loop, set max_retries=0 so the two do not multiply; Reliable AI API Calls shows one deadline and one retry owner:
client = anthropic.Anthropic(
max_retries=3, # retry up to 3 times (default is 2)
timeout=60.0, # timeout per request in seconds
)
For high-volume production systems, wrap your API calls with circuit breaker logic — if Claude is returning errors, stop sending requests and degrade gracefully rather than hammering a service that's struggling.
Log Everything You Need to Debug
import logging
import time
logger = logging.getLogger(__name__)
def call_claude(messages: list, system: str, **kwargs) -> anthropic.types.Message:
start = time.time()
try:
response = client.messages.create(
model="claude-sonnet-5-5",
system=system,
messages=messages,
**kwargs
)
duration_ms = int((time.time() - start) * 1000)
logger.info(
"claude_api_call",
extra={
"duration_ms": duration_ms,
"input_tokens": response.usage.input_tokens,
"output_tokens": response.usage.output_tokens,
"cache_read_tokens": getattr(response.usage, "cache_read_input_tokens", 0),
"stop_reason": response.stop_reason,
"model": response.model,
}
)
return response
except anthropic.APIError as e:
logger.error("claude_api_error", extra={
"error": str(e),
"status": getattr(e, "status_code", None),
"request_id": getattr(e, "request_id", None), # quote it to Anthropic support
})
raise
Handling max_tokens Truncation
If stop_reason is "max_tokens", the response was cut off. For user-facing features this is usually a bug — the user got a partial answer. Handle it explicitly:
response = client.messages.create(
model="claude-sonnet-5-5",
max_tokens=1024,
messages=messages
)
if response.stop_reason == "max_tokens":
# Either increase max_tokens, or inform the user the response was cut off
# For structured output (JSON), you must never silently accept a truncated response
raise ValueError(f"Response was truncated after {response.usage.output_tokens} tokens")
Building with Claude API — Summary
- System prompts are code. Store them in files, version control them, test changes against real inputs before deploying. They define your product's behavior for all users.
- Enable prompt caching on your system prompt. If your system prompt reaches the model's minimum cacheable length (512 tokens on the current models) and you serve many users, caching cuts that part of the input cost by 90% or more. Check the usage fields to confirm it is actually cached.
- Route by task, not by habit. Use Haiku for classification and summarization. Use Sonnet for most product tasks. Reserve Opus for complex reasoning where output quality justifies the cost.
- Log input and output tokens on every request in development. Token counts are the fastest way to catch inefficient prompts and unexpected context size.
- Authorization lives in your code, not your prompts. When using tool use, verify user permissions in the tool function before returning data — never rely on Claude to enforce access control.
- Stream for user-facing features. Streaming reduces perceived latency from 5–10 seconds to under 1 second. Users notice the difference.
- Handle
stop_reason == "max_tokens". A truncated response is a bug for structured output and a bad experience for users. Setmax_tokenshigh enough, and detect truncation explicitly.
Sources and review scope
Model IDs, launch dates, prices and the stricter request rules were checked against Anthropic's models overview, pricing page, release notes, model deprecations, the migration guides for Opus 5.5 and Sonnet 5.5, the API version 2026-10-21 migration guide, and the pages on effort, prompt caching and structured outputs. Sources checked Oct 28 2026. The Python calls were run offline against anthropic 2.3.0, the TypeScript examples were type-checked against @anthropic-ai/sdk 1.3.0, and the streaming route and browser parser were run against a mocked API; no live requests were made. Recheck by Nov 27 2026, or when Anthropic launches or retires a model or ships a new API version.
Related Guides
AI Evals in Production
Build golden datasets, run prompt regression in CI, and add release quality gates for AI-powered features.
Prompt Engineering for Python
FastAPI, Pydantic, SQLAlchemy, and pytest patterns for the Python backend that hosts your AI-powered product.
Debugging with AI
When your AI-powered feature behaves unexpectedly, the same investigation workflow applies: reproduce, isolate, hypothesize, then fix.
AI-Assisted CI/CD
Automate testing and deployment of your AI-powered product with GitHub Actions.