ai / claude
Preferred guidelines for how I use Claude via API. I have used Ruby or Go in practice, but examples here use raw HTTP/JSON.
Model selection
- Haiku (
claude-haiku-4-5): short summarization, scoring, classification. - Sonnet (
claude-sonnet-5): default for nuanced extraction and writing tasks. - Opus (
claude-opus-5): deep research and highest-stakes tasks. - Use model aliases, not dated snapshots.
- Pin server tool types as shared constants. They are dated and not guaranteed compatible across model bumps, so re-check them when changing models.
Get structured output from a tool call
Every call that needs structured data forces the model to answer by
calling one client-defined output tool. The answer arrives as the
tool_use block's input, which is native structured arguments.
curl https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{
"model": "claude-haiku-4-5",
"max_tokens": 512,
"messages": [
{
"role": "user",
"content": "Write a YC-style company headline for an AI email triage app."
}
],
"tools": [
{
"name": "record_result",
"description": "Record the result in the structured fields.",
"strict": true,
"input_schema": {
"type": "object",
"properties": {
"headline": {
"type": "string",
"description": "A Y Combinator-style company headline, 80 characters or less."
}
},
"required": ["headline"],
"additionalProperties": false
}
}
],
"tool_choice": {
"type": "tool",
"name": "record_result",
"disable_parallel_tool_use": true
}
}'
I used to send output_config.format with a json_schema instead. That
decoder returns the object as text, and it could fail to close a
free-form string and then serialize the following fields into that
string. A tool_use input cannot fail that way, so I moved every
structured path to a forced tool call and now send no output_config at
all.
disable_parallel_tool_use keeps the response to a single output tool
call, so there is one block to read.
Keep output schemas inside the strict subset
strict: true is what makes the arguments conform to the schema. It
accepts only a subset of JSON Schema:
type, properties, required, additionalProperties, items, enum, const,
description, title, anyOf, $ref, $defs, definitions
Anything else (maxLength, minLength, pattern, format, minimum,
maximum, minItems, maxItems, default) is a 400 at request time.
Express the constraint in the field description instead, and check
schemas in a test so an unsupported keyword fails CI rather than a live
job.
Adaptive thinking is for prose calls only
Enable adaptive thinking for complex Sonnet/Opus work that returns prose:
"thinking": {"type": "adaptive"}
Do not use it with Haiku. The API also refuses to combine extended
thinking with a strict, forced tool_choice, so a structured call runs
without thinking. Reject the combination in your own client rather than
dropping one of the two silently.
Research in two passes
Pass 1 runs an unconstrained web search and returns prose, so the search loop is free to run. Pair web search with code execution to filter and aggregate retrieved data before it consumes context.
{
"model": "claude-opus-5",
"thinking": { "type": "adaptive" },
"tools": [
{
"type": "web_search_20260209",
"name": "web_search",
"max_uses": 5,
"allowed_callers": ["direct"]
},
{ "type": "code_execution_20260521", "name": "code_execution" }
]
}
Pass 2 shapes that prose into a schema with a separate forced tool call.
When server tools ship alongside an output tool in one request, use
tool_choice: {"type": "auto", "disable_parallel_tool_use": true}.
Forcing the output tool makes the model call it on turn one, before any
search, and return placeholder fields. Steer it with a clear tool
description, then retry once: replay the model's own text as an
assistant turn, add a user turn asking it to call the tool, and force the
tool on that second attempt. Ending on a user turn keeps it as ordinary
multi-turn input, since a conversation ending on an assistant message is
a prefill and prefill does not combine with tool use.
Pin hosted tools to direct callers
Set allowed_callers: ["direct"] on web_search and web_fetch.
Anthropic does not support calling them from the code execution
container, so leaving the caller unset buys nothing, and an unpinned
hosted tool makes the request eligible for programmatic tool calling.
The API refuses to combine that with disable_parallel_tool_use or with
a strict: true tool, so the request 400s.
Bound server-side fetch loops
Give web_fetch a max_uses and a max_content_tokens:
{
"type": "web_fetch_20260209",
"name": "web_fetch",
"max_uses": 5,
"max_content_tokens": 20000,
"allowed_callers": ["direct"]
}
Without a content cap, the loop accumulates whole pages turn after turn until the request passes the context window and the API rejects it with "prompt is too long." Divide the input headroom left after the prompt across the fetches the loop is allowed.
Prompt boundary rule
Use the system prompt only to separate instructions from untrusted input (scraped pages, user text, raw threads). Otherwise, keep instructions in the user prompt.
Hallucination guardrails
Apply Anthropic's guidance:
- Restrict answers to provided data.
- Allow explicit uncertainty (
"Insufficient data"). - Require inline citations (
[n]) for research claims. - Pass structured context as JSON.
Handle "Insufficient data" before database writes.
Context window budgeting
Compute max prompt size:
(context_window - max_output_tokens - buffer) * 4 chars/token
Budget per model context window and output cap. Example defaults: 64k output for Haiku/Sonnet, 128k for Opus, and a 5k system-prompt buffer. Sonnet supports a 1M window natively, but I keep it at 200k until I deliberately raise the per-request input cap.
Retries
Two policies, at the HTTP layer and above it.
Transient status codes: the usual 502, 503, and 504, plus 429 (rate limited), 529 (overloaded), and 500. Anthropic returns intermittent 500s under load, and retrying recovers most of them.
Successful responses that carry no usable answer get at most one more attempt. A response cut off by the output budget is worth one retry at a doubled budget. A response with no tool call is worth one retry only if you change something about the request, since re-posting the identical body re-rolls the same dice.
Timeouts differ by call type. A single generation needs seconds. A call whose server-side search or fetch loop runs first needs minutes: a 10-search Opus run has been observed around 250s. Hold the client timeout inside the caller's deadline so the failure comes from the client, which can say more about what went wrong.