Anthropic Tool Use¶
Anthropic's Claude supports tool use with a slightly different API shape than OpenAI's, but the conceptual pattern is identical: define tools → model requests a call → you execute → you return the result. If you understand OpenAI's tools, this is a 10-minute translation exercise.
Learning objectives¶
- Define tools in Anthropic's format (
input_schemavs OpenAI'sparameters) - Parse
tool_usecontent blocks from Claude's response - Implement the multi-turn tool loop with
tool_resultblocks - Use
tool_choiceto control when Claude calls tools - Know the key API differences between OpenAI and Anthropic
Minimal working example¶
import os
import json
import anthropic
client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
tools = [
{
"name": "get_weather",
"description": "Get current weather for a city.",
"input_schema": { # Anthropic uses input_schema, not parameters
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
},
"required": ["city"]
}
}
]
def get_weather(city: str, unit: str = "celsius") -> dict:
return {"city": city, "temperature": 22, "unit": unit, "condition": "sunny"}
response = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
tools=tools,
messages=[{"role": "user", "content": "What's the weather in Paris?"}]
)
print(f"Stop reason: {response.stop_reason}") # → tool_use
print(f"Content: {response.content}")
# [ToolUseBlock(id='toolu_...', input={'city': 'Paris'}, name='get_weather', type='tool_use')]
API shape comparison¶
| Concept | OpenAI | Anthropic |
|---|---|---|
| Tool list key | tools |
tools |
| Schema key | parameters |
input_schema |
| Stop signal | finish_reason == "tool_calls" |
stop_reason == "tool_use" |
| Tool call location | message.tool_calls |
response.content (list of blocks) |
| Tool call type | ChatCompletionMessageToolCall |
ToolUseBlock |
| Arguments key | function.arguments (JSON string) |
input (dict — already parsed) |
| Result message role | "tool" |
"user" with tool_result block |
Anthropic's input is already a dict
Unlike OpenAI where tool_call.function.arguments is a JSON string you must parse, Anthropic's tool_use_block.input is already a Python dict. Skip the json.loads() call.
Full multi-turn tool loop¶
import os
import anthropic
client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
TOOLS = [
{
"name": "search_database",
"description": "Search the product database for items matching a query.",
"input_schema": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search term"},
"category": {
"type": "string",
"enum": ["electronics", "clothing", "books", "home"],
"description": "Product category filter"
}
},
"required": ["query"]
}
},
{
"name": "calculate_discount",
"description": "Calculate the final price after applying a discount code.",
"input_schema": {
"type": "object",
"properties": {
"original_price": {"type": "number"},
"discount_code": {"type": "string"}
},
"required": ["original_price", "discount_code"]
}
}
]
def search_database(query: str, category: str = None) -> list:
results = [
{"id": "E001", "name": "Bluetooth Speaker", "price": 59.99, "category": "electronics"},
{"id": "E002", "name": "Smart Watch", "price": 199.99, "category": "electronics"},
{"id": "B001", "name": "Python Cookbook", "price": 34.99, "category": "books"},
]
filtered = [r for r in results if query.lower() in r["name"].lower()]
if category:
filtered = [r for r in filtered if r["category"] == category]
return filtered
def calculate_discount(original_price: float, discount_code: str) -> dict:
codes = {"SAVE10": 0.10, "SAVE20": 0.20, "HALFOFF": 0.50}
discount = codes.get(discount_code.upper(), 0)
final_price = original_price * (1 - discount)
return {
"original": original_price,
"discount_pct": discount * 100,
"final_price": round(final_price, 2),
"valid_code": discount > 0
}
FUNCTION_MAP = {
"search_database": search_database,
"calculate_discount": calculate_discount,
}
def chat_with_tools(user_message: str) -> str:
messages = [{"role": "user", "content": user_message}]
while True:
response = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
tools=TOOLS,
messages=messages,
)
if response.stop_reason == "end_turn":
# Extract text from content blocks
text_blocks = [b.text for b in response.content if hasattr(b, "text")]
return " ".join(text_blocks)
if response.stop_reason == "tool_use":
# Add assistant response to history
messages.append({"role": "assistant", "content": response.content})
# Process all tool_use blocks
tool_results = []
for block in response.content:
if block.type != "tool_use":
continue
func_name = block.name
func_args = block.input # Already a dict — no json.loads needed
print(f" → Calling {func_name}({func_args})")
result = FUNCTION_MAP[func_name](**func_args)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result),
})
# Add tool results as user message
messages.append({"role": "user", "content": tool_results})
# Test
import json
print(chat_with_tools("Find me a Bluetooth speaker and apply discount code SAVE20"))
Controlling tool use¶
# Default: Claude decides when to call tools
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
tools=tools,
messages=messages,
)
# Force Claude to call a specific tool
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
tools=tools,
tool_choice={"type": "tool", "name": "extract_entities"},
messages=messages,
)
# Force Claude to call any tool
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
tools=tools,
tool_choice={"type": "any"},
messages=messages,
)
# Prevent tool calls
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
tools=tools,
tool_choice={"type": "none"}, # Note: Anthropic uses dict, not string
messages=messages,
)
Extraction-only pattern¶
import os
import anthropic
client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
EXTRACT_TOOL = {
"name": "extract_job_posting",
"description": "Extract structured data from a job posting.",
"input_schema": {
"type": "object",
"properties": {
"job_title": {"type": "string"},
"company": {"type": "string"},
"location": {"type": "string"},
"salary_min": {"type": "number", "description": "Minimum salary in USD annually"},
"salary_max": {"type": "number", "description": "Maximum salary in USD annually"},
"remote": {"type": "boolean", "description": "Whether remote work is offered"},
"required_skills": {
"type": "array",
"items": {"type": "string"},
"description": "List of required technical skills"
}
},
"required": ["job_title", "company"]
}
}
def extract_job(text: str) -> dict:
response = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=500,
tools=[EXTRACT_TOOL],
tool_choice={"type": "tool", "name": "extract_job_posting"},
messages=[{"role": "user", "content": f"Extract job info from:\n\n{text}"}]
)
for block in response.content:
if block.type == "tool_use":
return block.input # Already a dict
return {}
# Test
posting = """
Senior ML Engineer at TechCorp — San Francisco (Remote OK)
Salary: $180,000–$240,000/year
We need 5+ years Python, experience with PyTorch or TensorFlow, and strong MLOps skills.
"""
result = extract_job(posting)
print(result)
# {'job_title': 'Senior ML Engineer', 'company': 'TechCorp', 'location': 'San Francisco',
# 'salary_min': 180000, 'salary_max': 240000, 'remote': True, 'required_skills': ['Python', 'PyTorch', 'TensorFlow', 'MLOps']}
Handle tool_use blocks in streaming responses
If you use client.messages.stream(), tool_use blocks arrive as content_block_start + content_block_delta + content_block_stop events. The input dict is built incrementally — use block.input only after content_block_stop for that block.