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.
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}) {"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 afunctionkey. - description is required: missing
descriptioncauses 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 emitinput_json_delta— always checkevent.delta.typebefore 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: anywith a single extraction tool to force Claude to always return structured JSON instead of prose. - disable_parallel_tool_use: add
"disable_parallel_tool_use": trueinsidetool_choiceto 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.