Tools with Claude and OpenAI APIs
Both Anthropic's Claude API and OpenAI's API support tool use, but their request/response formats and feature parity differ. Claude's tool system is more flexible (supports streaming, vision integration, prompt caching) and uses JSON Schemas natively. OpenAI calls them "functions" and structures them slightly differently. This article teaches you to build agents that work with both APIs, understand the trade-offs, and migrate between them without rewriting your agent logic.
Choosing the right API depends on your needs: Claude excels at nuanced reasoning and long-context tasks; OpenAI's GPT-4 is fast and cost-effective for straightforward tool calling. Many production systems use both, routing requests to the best model for each use case.
API Comparison: Claude vs. OpenAI
Here's how tool definitions differ between the two APIs:
Claude (Anthropic):
import anthropic
tools_claude = [
{
"name": "get_weather",
"description": "Get the current weather",
"input_schema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City name"
}
},
"required": ["location"]
}
}
]
client_claude = anthropic.Anthropic()
response = client_claude.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
tools=tools_claude,
messages=[{"role": "user", "content": "What's the weather in Paris?"}]
)
OpenAI (GPT-4):
from openai import OpenAI
tools_openai = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City name"
}
},
"required": ["location"]
}
}
}
]
client_openai = OpenAI()
response = client_openai.chat.completions.create(
model="gpt-4o",
max_tokens=1024,
tools=tools_openai,
messages=[{"role": "user", "content": "What's the weather in Paris?"}]
)
Key differences:
- Tool structure: Claude tools are bare objects; OpenAI wraps each in a
{"type": "function", "function": {...}}wrapper - Parameters key: Claude uses
input_schema; OpenAI usesparameters - Stop reason: Claude uses
response.stop_reason; OpenAI usesresponse.choices[0].finish_reason - Tool calls: Claude embeds tool calls in
response.content; OpenAI usesresponse.choices[0].message.tool_calls
These differences make it useful to abstract tool handling behind a unified interface.
Building a Model-Agnostic Agent
Create a wrapper that handles both APIs transparently:
from typing import Literal
import anthropic
from openai import OpenAI
class UnifiedAgent:
"""Agent that works with Claude or OpenAI APIs."""
def __init__(self, provider: Literal["claude", "openai"], api_key: str = None):
self.provider = provider
if provider == "claude":
self.client = anthropic.Anthropic(api_key=api_key)
self.model = "claude-3-5-sonnet-20241022"
elif provider == "openai":
self.client = OpenAI(api_key=api_key)
self.model = "gpt-4o"
else:
raise ValueError("Unknown provider")
def format_tools(self, tools: list) -> list:
"""Convert tools to the format expected by the current provider."""
if self.provider == "claude":
return tools # Claude uses them directly
elif self.provider == "openai":
# Wrap each tool in OpenAI's format
return [
{
"type": "function",
"function": {
"name": tool["name"],
"description": tool["description"],
"parameters": tool["input_schema"]
}
}
for tool in tools
]
def call_model(self, messages: list, tools: list) -> tuple:
"""Call the model and return (response, tool_calls)."""
formatted_tools = self.format_tools(tools)
if self.provider == "claude":
response = self.client.messages.create(
model=self.model,
max_tokens=2048,
tools=formatted_tools,
messages=messages
)
stop_reason = response.stop_reason
tool_calls = []
for block in response.content:
if block.type == "tool_use":
tool_calls.append({
"id": block.id,
"name": block.name,
"arguments": block.input
})
# Get text response if present
text = next(
(block.text for block in response.content if hasattr(block, 'text')),
None
)
return {
"stop_reason": "tool_use" if tool_calls else "end_turn",
"text": text,
"tool_calls": tool_calls
}, response
elif self.provider == "openai":
response = self.client.chat.completions.create(
model=self.model,
max_tokens=2048,
tools=formatted_tools,
messages=messages
)
finish_reason = response.choices[0].finish_reason
tool_calls = []
if finish_reason == "tool_calls":
for tool_call in response.choices[0].message.tool_calls:
tool_calls.append({
"id": tool_call.id,
"name": tool_call.function.name,
"arguments": json.loads(tool_call.function.arguments)
})
# Get text response
text = response.choices[0].message.content
return {
"stop_reason": "tool_use" if tool_calls else "end_turn",
"text": text,
"tool_calls": tool_calls
}, response
Now your agent loop is model-agnostic:
def run_unified_agent(provider: str, user_message: str, tools: list) -> str:
"""Run an agent with any supported provider."""
agent = UnifiedAgent(provider)
messages = [{"role": "user", "content": user_message}]
for iteration in range(10):
result, raw_response = agent.call_model(messages, tools)
if result["stop_reason"] == "end_turn":
return result["text"] or "No response"
# Handle tool calls
messages.append({"role": "assistant", "content": result["text"] or ""})
tool_results = []
for tool_call in result["tool_calls"]:
tool_result = execute_tool(tool_call["name"], tool_call["arguments"])
if agent.provider == "claude":
tool_results.append({
"type": "tool_result",
"tool_use_id": tool_call["id"],
"content": tool_result
})
elif agent.provider == "openai":
tool_results.append({
"type": "tool",
"tool_call_id": tool_call["id"],
"content": tool_result
})
if agent.provider == "claude":
messages.append({"role": "user", "content": tool_results})
elif agent.provider == "openai":
messages.append({"role": "tool", "content": tool_results})
return "Max iterations"
# Use with either provider
result = run_unified_agent("claude", "What's 25 * 4?", tools)
result = run_unified_agent("openai", "What's 25 * 4?", tools)
This abstraction lets you switch providers without rewriting your agent loop.
Cost and Performance Trade-offs
Claude and OpenAI have different pricing and performance characteristics:
| Metric | Claude (Sonnet) | GPT-4o |
|---|---|---|
| Input cost (1M tokens) | $3 | $5 |
| Output cost (1M tokens) | $15 | $15 |
| Context window | 200k | 128k |
| Tool use latency | ~1-3s | ~0.5-1.5s |
| Reasoning complexity | Excellent (nuance) | Excellent (speed) |
| Hallucination rate | Lower | Lower |
Use Claude when: Tasks require deep reasoning, long context, or integration with vision/documents. Cost per token is lower for input-heavy workloads.
Use OpenAI when: You need speed (user-facing chat), have shorter contexts, or want ecosystem integration (GPT plugins, integrations).
For cost optimization, route requests at runtime:
def select_provider(task_type: str, context_length: int) -> str:
"""Choose a provider based on task characteristics."""
if context_length > 128000:
return "claude" # Only Claude supports 200k context
if task_type == "real-time-chat":
return "openai" # Faster latency
if task_type == "deep-analysis":
return "claude" # Better reasoning
return "openai" # Default: cheaper
Streaming and Iterative Responses
Claude supports streaming tool calls, letting you process tool results as soon as they arrive:
def run_streaming_agent(user_message: str, tools: list) -> str:
"""Agent with streaming support (Claude only)."""
client = anthropic.Anthropic()
messages = [{"role": "user", "content": user_message}]
with client.messages.stream(
model="claude-3-5-sonnet-20241022",
max_tokens=2048,
tools=tools,
messages=messages
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
response = stream.get_final_message()
# Handle tool calls from the final response
# ... (same as non-streaming)
Streaming is useful for user-facing applications where latency matters. OpenAI also supports streaming, but handles it differently (streaming message deltas rather than structured content).
Key Takeaways
- Claude and OpenAI have different tool definition formats and response structures, but both support the same core capability
- Build a provider-agnostic wrapper to make agent logic reusable across APIs
- Choose providers based on task type: Claude for deep reasoning and long context, OpenAI for speed and cost
- Route requests dynamically based on context length and task complexity to optimize cost and latency
- Claude supports streaming and prompt caching; consider these for production scaling
Frequently Asked Questions
Can I mix Claude and OpenAI calls in a single agent?
Yes. Have different tools use different providers (Claude for analysis, OpenAI for real-time queries), or route based on task type. Keep tool definitions in a provider-agnostic format and convert at call time.
Do both APIs support vision in tool calls?
Claude does (tools can inspect images). OpenAI's function calling doesn't directly use vision. You can manually extract image descriptions and pass them as text to tools.
Which is cheaper for long-running agents?
Claude's lower input cost wins if your agent makes many API calls with long context history. OpenAI wins for short, fast interactions. Calculate per your token usage.
Can I use tool results to refine the model choice?
Yes. If a tool result is unexpected or large, you might switch to a more capable model for the next iteration. This is called "model cascading."