AM
AgenticMedia
SponsoredSponsored Banner
Domain.com - Your Global Address Book. Domains & Sites Starts at $X.XX/domain
agentic-ai
•7 min read

Connecting AI Agents to Real-World APIs: Function Calling Guide

A hands-on developer guide to OpenAI and Claude function calling (tool use) — JSON schemas, response parsing, executing REST APIs in Python, and closing the agent loop.

AT

AgenticMedia Team

Content Creator

Written in Markdown
Diagram showing an AI agent calling external REST APIs through function calling

TL;DR / Quick Summary & Key Takeaways

Function calling (tool use) is the mechanism that turns an LLM from a text generator into an agent that can actually do things — hit your CRM, check inventory, book a calendar slot, or query a weather API. The model never executes code itself; it returns a structured request naming the function and the arguments, your application executes it, and you send the result back for the model to reason over.

  • Both OpenAI and Claude use the same core pattern: define a JSON Schema for each tool → model returns a structured call → your code executes it → you send the result back → model produces the final answer.
  • Claude’s tool schemas live under input_schema; OpenAI’s live under parameters inside a function object — structurally near-identical, syntactically different.
  • Server tools vs. client tools matters. On the Claude API, tools like web search and code execution run on Anthropic’s infrastructure — you get results with no handler code. Custom tools (your REST API) are client tools: your application always executes them.
  • tool_choice controls whether a call is forced, automatic, or disabled — critical for pipelines where a tool call must happen every time (e.g., a lead-enrichment agent that must always query your CRM API).
  • Error handling belongs in the loop, not around it — a failed API call should become a tool_result describing the failure, not a crashed script, so the model can retry or fall back gracefully.
  • This pattern is the backbone of every framework in the Day 1 comparison (LangGraph, CrewAI, AutoGen) — they’re abstractions over exactly this request/execute/respond loop.

Core Technical / Conceptual Deep Dive

The agent loop, conceptually

Every “AI agent calling an API” system, no matter how sophisticated the framework, reduces to this loop:

1. Send the user request + list of available tools to the model
2. Model decides: respond directly, or call a tool?
3. If tool call → model returns structured JSON (tool name + arguments)
4. Your application code executes the actual API call
5. Send the result back to the model as a "tool result"
6. Model either calls another tool, or produces the final answer

This is a client-executed loop for anything that isn’t an Anthropic- or OpenAI-hosted server tool. Your infrastructure — not the model provider — makes the actual HTTP request to Stripe, Salesforce, or your internal microservice. That’s the part developers get wrong most often: the model never touches your API keys or your network. It only ever produces intent.

Claude tool use: client tools vs. server tools

On the Claude API, tools fall into two categories:

  • Client tools — user-defined custom tools (your REST APIs) plus Anthropic-defined schemas like bash and text_editor that still execute in your environment.
  • Server tools — web_search, web_fetch, code_execution, and tool_search, which run entirely on Anthropic’s infrastructure. You get the results back with zero handler code.

For connecting agents to your real-world APIs (the focus of this guide), you’re always writing client tools: you define the input_schema, Claude returns a tool_use block, and your code executes the HTTP call.

OpenAI function calling: the equivalent shape

OpenAI’s Chat Completions / Responses API uses tools with a type: "function" wrapper and a parameters object (standard JSON Schema). The model returns tool_calls with a function.name and a function.arguments string (which you json.loads() yourself — a common source of bugs if the model returns slightly malformed JSON on older models).

The conceptual loop is identical to Claude’s — schema in, structured call out, your code executes, result goes back in — but the object shapes differ enough that framework code is rarely portable without an adapter layer.


Practical Tutorial / Step-by-Step Implementation

Step 1: Define your tool’s JSON Schema (Claude)

import anthropic
import requests
import json

client = anthropic.Anthropic()

tools = [
    {
        "name": "get_order_status",
        "description": "Look up the current shipping status of a customer order by order ID.",
        "input_schema": {
            "type": "object",
            "properties": {
                "order_id": {
                    "type": "string",
                    "description": "The unique order identifier, e.g. ORD-48213"
                }
            },
            "required": ["order_id"]
        }
    }
]

Step 2: Send the request and inspect the response

messages = [{"role": "user", "content": "Where is my order ORD-48213?"}]

response = client.messages.create(
    model="claude-sonnet-5",
    max_tokens=1024,
    tools=tools,
    tool_choice={"type": "auto"},
    messages=messages,
)

# Claude returns a tool_use block when it decides to call the function
tool_use = next(
    (block for block in response.content if block.type == "tool_use"), None
)

if tool_use:
    print(f"Claude wants to call {tool_use.name} with {json.dumps(tool_use.input)}")

Step 3: Execute the real REST API call

This is the step most tutorials skip. Your handler needs to actually hit the external service — and needs to fail gracefully.

def execute_get_order_status(order_id: str) -> str:
    try:
        resp = requests.get(
            f"https://api.yourshop.com/v1/orders/{order_id}/status",
            headers={"Authorization": f"Bearer {YOUR_API_KEY}"},
            timeout=8,
        )
        resp.raise_for_status()
        data = resp.json()
        return f"Status: {data['status']}, ETA: {data.get('eta', 'unknown')}"
    except requests.exceptions.RequestException as e:
        # Return the failure as a result, not a crash — let the model reason about it
        return f"Error: could not retrieve order status ({str(e)})"

Step 4: Send the tool result back and get the final answer

if tool_use:
    result_text = execute_get_order_status(**tool_use.input)

    messages.append({"role": "assistant", "content": response.content})
    messages.append({
        "role": "user",
        "content": [
            {
                "type": "tool_result",
                "tool_use_id": tool_use.id,
                "content": result_text,
            }
        ],
    })

    followup = client.messages.create(
        model="claude-sonnet-5",
        max_tokens=1024,
        tools=tools,
        messages=messages,
    )

    final_text = next(b for b in followup.content if b.type == "text")
    print(final_text.text)

Step 5: The equivalent flow in OpenAI’s API

from openai import OpenAI
client = OpenAI()

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_order_status",
            "description": "Look up the current shipping status of a customer order by order ID.",
            "parameters": {
                "type": "object",
                "properties": {
                    "order_id": {"type": "string", "description": "e.g. ORD-48213"}
                },
                "required": ["order_id"],
            },
        },
    }
]

messages = [{"role": "user", "content": "Where is my order ORD-48213?"}]

response = client.chat.completions.create(
    model="gpt-4o",
    messages=messages,
    tools=tools,
    tool_choice="auto",
)

msg = response.choices[0].message
if msg.tool_calls:
    call = msg.tool_calls[0]
    args = json.loads(call.function.arguments)   # note: manual JSON parse
    result_text = execute_get_order_status(**args)

    messages.append(msg)
    messages.append({
        "role": "tool",
        "tool_call_id": call.id,
        "content": result_text,
    })

    followup = client.chat.completions.create(
        model="gpt-4o", messages=messages, tools=tools
    )
    print(followup.choices[0].message.content)

Step 6: Forcing a tool call (don’t rely on prompting alone)

If your agent must always check inventory before quoting a price, don’t hope the model calls the tool — force it:

# Claude: force a specific tool
response = client.messages.create(
    model="claude-sonnet-5",
    max_tokens=1024,
    tools=tools,
    tool_choice={"type": "tool", "name": "get_order_status"},
    messages=messages,
)
# OpenAI: force a specific function
response = client.chat.completions.create(
    model="gpt-4o",
    messages=messages,
    tools=tools,
    tool_choice={"type": "function", "function": {"name": "get_order_status"}},
)

Step 7: Handling parallel tool calls

Both APIs can return multiple tool calls in a single turn (e.g., “check order status AND check refund eligibility”). Loop over every tool_use/tool_calls entry, execute each independently, and return all results before requesting the follow-up — never assume there’s only one.


Tool / Solution Comparison Table

Feature Claude API (Anthropic) OpenAI API
Schema field name input_schema parameters (nested in function)
Tool definition wrapper Flat object (name, description, input_schema) Wrapped: {"type": "function", "function": {...}}
Model’s call format tool_use content block, .input is already parsed JSON tool_calls[].function.arguments — a string you must json.loads()
Sending results back tool_result block with tool_use_id Message with role: "tool" and tool_call_id
Forcing a specific tool tool_choice: {"type": "tool", "name": "..."} tool_choice: {"type": "function", "function": {"name": "..."}}
Schema strict conformance strict: true on tool definition Native strict mode on function object
Server-executed tools Yes — web search, web fetch, code execution run on Anthropic’s infra Yes — hosted tools available depending on API surface
Parallel tool calls Supported; can disable via disable_parallel_tool_use Supported natively
Best for Long agentic loops, precise schema adherence, native bash/text_editor tools Broadest ecosystem/tooling familiarity, wide framework support

Actionable Checklist / Next Steps

  • Write tool descriptions like documentation, not labels. The model chooses tools based on the description field — vague descriptions cause missed or wrong calls.
  • Always validate arguments before hitting your API. Don’t trust the model’s JSON blindly, even with strict schema mode — treat it like user input.
  • Return failures as tool_result content, never let the handler throw uncaught. A broken loop is worse than a model that knows the call failed and can retry or apologize.
  • Use tool_choice to force calls in pipelines where a step is mandatory (compliance checks, inventory checks) — don’t rely on prompt-level hints alone.
  • Set explicit timeouts on every outbound HTTP call inside a tool handler — a hanging API call will stall your entire agent loop.
  • Log every tool call and result during development — this is your primary debugging surface when an agent “does the wrong thing.”
  • Once you have 3+ tools, consider a framework (LangGraph, CrewAI) to manage the loop, state, and multi-step planning — see our Day 1 framework comparison before hand-rolling orchestration logic.
SponsoredSponsored Feature
Domain.com - Your Global Address Book. Domains & Sites Starts at $X.XX/domain
#Function Calling#Tool Use#Claude API#OpenAI API#AI Agents
AT

AgenticMedia Team

Content Creator • @agenticmedia

Writer and technology enthusiast sharing engineering playbooks and digital optimization guides.