Skip to main content

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:

  1. Tool structure: Claude tools are bare objects; OpenAI wraps each in a {"type": "function", "function": {...}} wrapper
  2. Parameters key: Claude uses input_schema; OpenAI uses parameters
  3. Stop reason: Claude uses response.stop_reason; OpenAI uses response.choices[0].finish_reason
  4. Tool calls: Claude embeds tool calls in response.content; OpenAI uses response.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:

MetricClaude (Sonnet)GPT-4o
Input cost (1M tokens)$3$5
Output cost (1M tokens)$15$15
Context window200k128k
Tool use latency~1-3s~0.5-1.5s
Reasoning complexityExcellent (nuance)Excellent (speed)
Hallucination rateLowerLower

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."

Further Reading