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

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:

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.

← All articles