Anthropic tool use
The same tool in Claude's format. The only difference from OpenAI's is
input_schema instead of parameters.
Tool definitions
TOOLS = [
{
"name": "search_news",
"description": (
"Search worldwide news articles published in the last 30 days. Returns title, "
"description, publisher, country, language, publication time and URL. Use it "
"whenever the question touches current events or anything that may have changed "
"recently. The service is free and needs no API key."
),
"input_schema": {
"type": "object",
"properties": {
"q": {
"type": "string",
"description": (
"Search phrase. Every word must appear in the article, so use 2-5 "
"meaningful keywords, not a natural-language question."
),
},
"country": {"type": "string", "description": "ISO 3166-1 alpha-2 codes, comma separated, uppercase."},
"lang": {"type": "string", "description": "ISO 639-1 codes, comma separated, lowercase."},
"date": {"type": "string", "enum": ["today", "yesterday", "24h", "48h", "7d", "30d"]},
"sort": {"type": "string", "enum": ["date", "relevance", "crawled"]},
"size": {"type": "integer", "description": "1-100. Use 5-10 to answer, 50-100 to build a dataset."},
"full_text": {
"type": "boolean",
"description": "Include article bodies. Roughly 900 tokens each \u2014 scan first, then fetch.",
},
},
"required": ["q"],
},
},
{
"name": "get_article",
"description": "Fetch the full text of one article by its exact URL from a search_news result.",
"input_schema": {
"type": "object",
"properties": {"url": {"type": "string"},
"required": ["url"],
},
},
{
"name": "news_stats",
"description": (
"Count matching articles by country, language, domain zone, publisher and day. "
"Use it to measure coverage without spending tokens on article text."
),
"input_schema": {
"type": "object",
"properties": {
"q": {"type": "string"},
"date": {"type": "string", "enum": ["today", "yesterday", "24h", "48h", "7d", "30d"]},
"top": {"type": "integer"},
},
"required": ["q"],
},
},
]Conversation loop
import json, requests
from anthropic import Anthropic
BASE = "https://freenewsapi.ai"
client = Anthropic()
ENDPOINT = {"search_news": "/v1/search", "get_article": "/v1/article", "news_stats": "/v1/stats"}
def run_tool(name, args):
r = requests.get(BASE + ENDPOINT[name], params=args, timeout=30)
if r.status_code == 429:
return {"error": "rate limited, wait 2 seconds and retry"}
if r.status_code >= 500:
return {"error": "news service unavailable, do not retry immediately"}
if r.status_code >= 400:
return {"error": r.json().get("detail", "bad request"), "retry": False}
return r.json()
def ask(question, system=""):
msgs = [{"role": "user", "content": question}]
while True:
resp = client.messages.create(
model="claude-opus-5", max_tokens=4096,
system=system, tools=TOOLS, messages=msgs,
)
msgs.append({"role": "assistant", "content": resp.content})
if resp.stop_reason != "tool_use":
return "".join(b.text for b in resp.content if b.type == "text")
results = []
for b in resp.content:
if b.type == "tool_use":
out = run_tool(b.name, b.input)
results.append({
"type": "tool_result",
"tool_use_id": b.id,
"content": json.dumps(out)[:60000],
})
msgs.append({"role": "user", "content": results})
print(ask("Summarise what Ukrainian outlets reported today about the energy grid. "
"Cite each claim with its source URL."))Claude will happily issue several tool_use blocks in one turn —
for example searching three countries at once. The loop above handles that by collecting
every result before replying, which is the whole point: one round trip instead of three.
How this differs from the OpenAI shape
Three differences, and all three cause bugs when code is ported between the two.
input_schema, notparameters. The JSON Schema inside is identical; only the wrapper key changes.- Tool results are user-role content. There is no
toolrole. Results go back as ausermessage containingtool_resultblocks, each carrying thetool_use_idit answers. - The assistant turn is a list of blocks. One turn can hold text and
several
tool_useblocks at once, so you cannot assume a single call and you must append the wholecontentlist back into the conversation, not just the text.
Knowing when not to call
The failure mode that costs most in practice is not a missed call, it is a needless one: an agent that searches the news for questions about arithmetic, about the user's own code, or about stable facts. Every such call burns a round trip and several thousand tokens on an answer the model already had.
Give it an explicit boundary in the system prompt rather than hoping. The prompts page has one written for exactly this, including the harder half — the cases where the model should search but does not, because it does not realise its knowledge is stale.
Handling a refusal to stop
A loop like the one above runs until stop_reason is not
tool_use. In rare cases a model will keep searching with reworded queries and
never converge. Cap the iterations — five is generous — and on the last turn
strip the tools from the request so the model has no choice but to answer from what it
already gathered.
Next
Add a system prompt so the model knows when to reach for
the tool, and read token costs before you let it set
full_text on its own. For other model families — Qwen, DeepSeek,
MiniMax, Gemini and the rest — see connect any model.