Tool use cheatsheet.

What it is

LLM decides to call a function with structured args. Your code runs it; result fed back.

user  LLM  tool_call  your code  tool_result  LLM  response

OpenAI

tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "Get current weather",
        "parameters": {
            "type": "object",
            "properties": {
                "city": {"type": "string"},
                "unit": {"type": "string", "enum": ["c", "f"]},
            },
            "required": ["city"],
        },
    },
}]

response = client.chat.completions.create(
    model="gpt-5",
    messages=[{"role": "user", "content": "Weather in Paris?"}],
    tools=tools,
)

msg = response.choices[0].message

if msg.tool_calls:
    for call in msg.tool_calls:
        args = json.loads(call.function.arguments)
        result = get_weather(**args)
        
        # Send result back
        messages.append(msg)
        messages.append({
            "role": "tool",
            "tool_call_id": call.id,
            "content": json.dumps(result),
        })
    
    final = client.chat.completions.create(model="gpt-5", messages=messages, tools=tools)

Anthropic

tools = [{
    "name": "get_weather",
    "description": "Get current weather",
    "input_schema": {
        "type": "object",
        "properties": {
            "city": {"type": "string"},
            "unit": {"type": "string", "enum": ["c", "f"]},
        },
        "required": ["city"],
    },
}]

response = client.messages.create(
    model="claude-opus-4-7",
    max_tokens=1024,
    tools=tools,
    messages=[{"role": "user", "content": "Weather in Paris?"}],
)

if response.stop_reason == "tool_use":
    for block in response.content:
        if block.type == "tool_use":
            result = get_weather(**block.input)
            
            messages.append({"role": "assistant", "content": response.content})
            messages.append({"role": "user", "content": [{
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": json.dumps(result),
            }]})
    
    final = client.messages.create(model="claude-opus-4-7", max_tokens=1024, tools=tools, messages=messages)

Parallel tool calls

OpenAI / Anthropic can return multiple tool_calls. Execute them in parallel:

import asyncio

async def execute_all(tool_calls):
    tasks = [tool_funcs[c.function.name](**json.loads(c.function.arguments)) for c in tool_calls]
    results = await asyncio.gather(*tasks)
    return list(zip(tool_calls, results))

Loop until done

while True:
    response = client.messages.create(model=..., messages=messages, tools=tools)
    
    if response.stop_reason != "tool_use":
        break
    
    messages.append({"role": "assistant", "content": response.content})
    
    tool_results = []
    for block in response.content:
        if block.type == "tool_use":
            result = run_tool(block.name, block.input)
            tool_results.append({
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": json.dumps(result),
            })
    
    messages.append({"role": "user", "content": tool_results})

# response now has final answer

Tool schema tips

  • Detailed descriptions: LLM picks better tool.
  • Required vs optional: mark clearly.
  • Enums: constrain values.
  • Examples in description: “e.g., ‘Paris, FR’”.
  • One tool, one purpose: don’t bundle.

Common tool ideas

  • search_web(query).
  • read_file(path) / write_file(path, content).
  • run_sql(query).
  • send_email(to, subject, body).
  • get_calendar(date) / create_event(...).
  • query_vector_db(text, k).

Forcing tool use

# OpenAI
tool_choice = {"type": "function", "function": {"name": "get_weather"}}
tool_choice = "auto"     # default
tool_choice = "none"
tool_choice = "required"  # must use any tool

# Anthropic
tool_choice = {"type": "tool", "name": "get_weather"}
tool_choice = {"type": "any"}
tool_choice = {"type": "auto"}

JSON output via tool

Reliable structured output: define a tool whose only purpose is to return data:

tools = [{
    "name": "output_data",
    "description": "Output the result",
    "input_schema": {
        "type": "object",
        "properties": {
            "summary": {"type": "string"},
            "tags": {"type": "array", "items": {"type": "string"}},
        },
        "required": ["summary", "tags"],
    },
}]

response = client.messages.create(
    model="...",
    tools=tools,
    tool_choice={"type": "tool", "name": "output_data"},
    messages=[...],
)
# response.content[0].input has the structured data

Validation

from pydantic import BaseModel, ValidationError

class WeatherArgs(BaseModel):
    city: str
    unit: str = "c"

try:
    args = WeatherArgs(**json.loads(call.function.arguments))
except ValidationError as e:
    return {"error": str(e)}

Don’t trust LLM args; validate.

Error handling

Pass errors back to LLM so it can recover:

try:
    result = tool(**args)
except Exception as e:
    result = {"error": str(e)}

# Pass result back; LLM may retry with corrected args

Loop guards

Cap iterations to prevent infinite tool loops:

for _ in range(10):
    response = call_with_tools()
    if not has_tool_calls(response):
        break
else:
    raise RuntimeError("Too many tool iterations")

Streaming + tools

OpenAI: tool calls appear as deltas. Anthropic: similar.

# Pattern: buffer tool args during stream, execute on complete

Common mistakes

  • Bad schema → LLM hallucinates arg names.
  • Not handling tool errors → loop fails.
  • Mixing async exec with sync agent loop.
  • Tools that take >30s → request timeout.
  • Returning huge tool results → wastes context.

Read this next

If you want my tool-using agent template, it’s at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .