Anthropic Tool Use Function Calling 8 min read

Anthropic Claude Tool Use Errors — Fix tool_result, input_schema & Streaming

Troubleshoot Claude function calling errors: tool_use block must be followed by tool_result, wrong input_schema format, parallel tool calls, streaming input_json_delta accumulation, and tool_choice configuration.

Anthropic API live status

Anthropic API — live status

Updated every 5 minutes · Full incident history →

Full status →

Fix 1 — tool_use block must be followed by tool_result

The multi-turn tool loop structure

When Claude's response has stop_reason: "tool_use", you must: (1) append the full assistant message to your messages array, (2) execute the tool, (3) add a new user message containing a tool_result block. Sending any other user message in this position causes the validation error.

import anthropic, json

client = anthropic.Anthropic()

tools = [{
    "name": "get_weather",
    "description": "Get current weather for a city.",
    "input_schema": {
        "type": "object",
        "properties": {
            "city": {"type": "string", "description": "City name"}
        },
        "required": ["city"]
    }
}]

messages = [{"role": "user", "content": "What is the weather in Paris?"}]

# --- agentic loop ---
while True:
    response = client.messages.create(
        model="claude-opus-4-8",
        max_tokens=1024,
        tools=tools,
        messages=messages,
    )

    # Always append the assistant turn first
    messages.append({"role": "assistant", "content": response.content})

    if response.stop_reason == "end_turn":
        # No more tool calls — print final text and exit
        for block in response.content:
            if block.type == "text":
                print(block.text)
        break

    if response.stop_reason == "tool_use":
        # Collect ALL tool_result blocks for this turn
        tool_results = []
        for block in response.content:
            if block.type == "tool_use":
                # Execute your tool here
                result = {"temperature": "18C", "condition": "Partly cloudy"}
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": json.dumps(result),
                })
        # Send all results back in one user message
        messages.append({"role": "user", "content": tool_results})
Common mistake: forgetting to append {"role": "assistant", "content": response.content} before the tool_result message. The assistant turn must exist in the history or the API returns a 400 validation error.

Fix 2 — input_schema format (not OpenAI parameters)

JSON Schema under input_schema, not parameters

Anthropic tool definitions use input_schema (JSON Schema draft-07) — not the OpenAI parameters / function wrapper.

# WRONG — OpenAI-style format
tools_wrong = [{
    "type": "function",
    "function": {
        "name": "search",
        "parameters": {
            "type": "object",
            "properties": {"query": {"type": "string"}}
        }
    }
}]

# CORRECT — Anthropic format
tools_correct = [{
    "name": "search",
    "description": "Search the web for recent information.",
    "input_schema": {
        "type": "object",
        "properties": {
            "query": {
                "type": "string",
                "description": "Search query string"
            },
            "max_results": {
                "type": "integer",
                "description": "Maximum number of results (1-10)",
                "default": 5
            }
        },
        "required": ["query"]
    }
}]
  • No type: function wrapper: Claude tools are flat objects with name, description, input_schema — not nested under a function key.
  • description is required: missing description causes a validation error and also degrades Claude's ability to choose the right tool.
  • input_schema.type must be "object": top-level type must always be "object" — you cannot pass an array or primitive directly as tool input.

Fix 3 — content must be a list / string shorthand

"content must be a list" — use content block arrays

Once your conversation includes tool_result blocks, every content field must be an array. The Python SDK accepts a string shorthand for simple text, but mixing string and array forms in the same conversation causes a validation error when tool blocks appear.

# WRONG — string shorthand breaks in agentic loops
messages = [
    {"role": "user", "content": "Search for AI news"},  # string ok here
    {"role": "assistant", "content": response.content},
    # SDK may auto-convert to list but manual construction may not
    {"role": "user", "content": "Thanks"},             # ERROR if mixed
]

# CORRECT — always use content block arrays in agentic loops
messages = [
    {"role": "user", "content": [{"type": "text", "text": "Search for AI news"}]},
    {"role": "assistant", "content": response.content},
    {"role": "user", "content": [
        {"type": "tool_result", "tool_use_id": tool_use_id, "content": result}
    ]},
]

Fix 4 — streaming tool calls (input_json_delta accumulation)

Accumulate input_json_delta before parsing

In streaming mode, tool input arrives as a series of input_json_delta events, each containing a partial_json fragment. Do not try to parse each fragment individually — concatenate them all and parse once after the content_block_stop event.

import anthropic, json

client = anthropic.Anthropic()
tool_input_accumulator = {}  # tool_use_id -> partial json string

with client.messages.stream(
    model="claude-opus-4-8",
    max_tokens=1024,
    tools=[{
        "name": "get_weather",
        "description": "Get weather for a city.",
        "input_schema": {
            "type": "object",
            "properties": {"city": {"type": "string"}},
            "required": ["city"]
        }
    }],
    messages=[{"role": "user", "content": "Weather in Tokyo?"}],
) as stream:
    current_tool_id = None
    for event in stream:
        if event.type == "content_block_start":
            if event.content_block.type == "tool_use":
                current_tool_id = event.content_block.id
                tool_input_accumulator[current_tool_id] = ""
        elif event.type == "content_block_delta":
            if event.delta.type == "input_json_delta":
                # Accumulate — do NOT parse here
                tool_input_accumulator[current_tool_id] += event.delta.partial_json
        elif event.type == "content_block_stop":
            if current_tool_id and tool_input_accumulator.get(current_tool_id):
                # NOW parse the complete JSON
                tool_input = json.loads(tool_input_accumulator[current_tool_id])
                print(f"Tool input ready: {tool_input}")
                current_tool_id = None
  • SDK helper: the Python SDK's stream.get_final_message() handles accumulation automatically — use it when you do not need incremental UI updates during the tool input phase.
  • text_delta vs input_json_delta: text content blocks emit text_delta; tool input blocks emit input_json_delta — always check event.delta.type before accessing fields.

Fix 5 — tool_choice and parallel tool use

tool_choice: auto vs any vs specific tool

The tool_choice parameter controls whether Claude must use a tool:

# auto (default) — Claude decides whether to use a tool
response = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1024,
    tools=tools,
    tool_choice={"type": "auto"},
    messages=messages,
)

# any — Claude must use at least one tool (no plain text response)
response = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1024,
    tools=tools,
    tool_choice={"type": "any"},
    messages=messages,
)

# tool — force a specific tool by name
response = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1024,
    tools=tools,
    tool_choice={"type": "tool", "name": "get_weather"},
    messages=messages,
)
  • Use "any" for structured extraction: combine tool_choice: any with a single extraction tool to force Claude to always return structured JSON instead of prose.
  • disable_parallel_tool_use: add "disable_parallel_tool_use": true inside tool_choice to guarantee at most one tool per response, simplifying your loop logic.

Parallel tool use — return all results in one user message

Claude may return multiple tool_use blocks in a single response. You must execute all of them and return all results in one user message. Sending partial results causes a validation error on the following call.

import asyncio

async def run_tools_parallel(response_content, tool_executor):
    """Execute all tool_use blocks concurrently, return list of tool_result blocks."""
    tool_use_blocks = [b for b in response_content if b.type == "tool_use"]

    async def execute_one(block):
        result = await tool_executor(block.name, block.input)
        return {
            "type": "tool_result",
            "tool_use_id": block.id,
            "content": json.dumps(result),
        }

    return await asyncio.gather(*[execute_one(b) for b in tool_use_blocks])

# Usage:
# tool_results = await run_tools_parallel(response.content, my_executor)
# messages.append({"role": "user", "content": tool_results})

Computer use — beta header required

The computer use tool (computer_20241022, text_editor_20241022, bash_20241022) requires a beta header. Without it the API returns a 400 error.

response = client.beta.messages.create(
    model="claude-opus-4-8",
    max_tokens=1024,
    betas=["computer-use-2024-10-22"],  # required beta header
    tools=[
        {"type": "computer_20241022", "name": "computer", "display_width_px": 1280, "display_height_px": 800},
        {"type": "text_editor_20241022", "name": "str_replace_editor"},
        {"type": "bash_20241022", "name": "bash"},
    ],
    messages=messages,
)

Get an email the next time Anthropic goes down

Outage alerts for Anthropic, straight to your inbox. No account needed, unsubscribe in every email.

Watching more than one service? A free account covers 5 services + a daily digest — and Pro is currently free.

FAQ

What causes "tool_use block must be followed by tool_result"?

This error fires when your message history has an assistant turn ending with a tool_use block but the next user turn does not contain a matching tool_result. Always check stop_reason and branch on "tool_use" before constructing the next user message.

Why does Anthropic API say "content must be a list"?

The messages[].content field must be an array of typed content blocks once you use tool features. Replace any string shorthand with [{"type": "text", "text": "..."}] throughout your agentic loop.

How is Claude tool input_schema different from OpenAI function parameters?

Anthropic uses input_schema (direct JSON Schema object), not parameters or a function wrapper. The tool definition is also flat — no type: "function" key needed.

How do I handle parallel tool calls from Claude?

Collect all tool_use blocks from the response, execute them (ideally concurrently with asyncio.gather), then send all tool_result blocks in a single user message. Never split results across multiple user messages.

Related guides