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.
AgenticMedia Team
Content Creator

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 underparametersinside afunctionobject — 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_choicecontrols 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_resultdescribing 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
bashandtext_editorthat still execute in your environment. - Server tools —
web_search,web_fetch,code_execution, andtool_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
descriptionfield — 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_resultcontent, 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_choiceto 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.
AgenticMedia Team
Content Creator • @agenticmedia
Writer and technology enthusiast sharing engineering playbooks and digital optimization guides.
