Step-by-step guide to switching from OpenAI GPT to Anthropic Claude in Python. Side-by-side API differences, model mapping, and a drop-in wrapper class.
Switching from OpenAI to Claude requires adapting to a different SDK shape — but the concepts map cleanly. This guide walks through every difference and provides a drop-in wrapper so you can migrate incrementally.
| OpenAI Python SDK | Anthropic Python SDK | |
|---|---|---|
| Install | pip install openai | pip install anthropic |
| Auth | OPENAI_API_KEY | ANTHROPIC_API_KEY |
| Client | OpenAI() | anthropic.Anthropic() |
| Create message | client.chat.completions.create() | client.messages.create() |
| Model param | model="gpt-4o" | model="claude-sonnet-4-6" |
| System prompt | Role "system" in messages | Separate system= param |
| Max tokens | max_tokens= | max_tokens= (required, no default) |
| Read output | response.choices[0].message.content | response.content[0].text |
| Usage | response.usage.prompt_tokens | response.usage.input_tokens |
from openai import OpenAI
client = OpenAI() # reads OPENAI_API_KEY
response = client.chat.completions.create(
model="gpt-4o",
max_tokens=512,
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "What is the capital of France?"}
]
)
print(response.choices[0].message.content)
import anthropic
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=512,
system="You are a helpful assistant.", # system prompt moves here
messages=[
{"role": "user", "content": "What is the capital of France?"}
]
)
print(response.content[0].text)
| OpenAI model | Claude equivalent | Notes |
|---|---|---|
| gpt-4o | claude-sonnet-4-6 | Similar quality, lower price/token |
| gpt-4o-mini | claude-haiku-4-5-20251001 | Haiku is faster and cheaper |
| o1 / o3 (reasoning) | claude-opus-4-7 | Opus with extended thinking for step-by-step reasoning |
| gpt-4-turbo | claude-opus-4-7 | Long context + top quality |
Migrate one call site at a time with this compatibility shim:
import anthropic
MODEL_MAP = {
"gpt-4o": "claude-sonnet-4-6",
"gpt-4o-mini": "claude-haiku-4-5-20251001",
"gpt-4-turbo": "claude-opus-4-7",
"gpt-4": "claude-opus-4-7",
"gpt-3.5-turbo": "claude-haiku-4-5-20251001",
}
class OpenAICompatClient:
"""Thin shim mapping chat.completions.create() → client.messages.create()."""
def __init__(self):
self._client = anthropic.Anthropic()
self.chat = self
self.completions = self
def create(self, *, model, messages, max_tokens=1024, **kwargs):
claude_model = MODEL_MAP.get(model, model)
# Extract system prompt if passed in messages list (OpenAI style)
system = None
user_messages = []
for m in messages:
if m["role"] == "system":
system = m["content"]
else:
user_messages.append(m)
resp = self._client.messages.create(
model=claude_model,
max_tokens=max_tokens,
system=system or anthropic.NOT_GIVEN,
messages=user_messages
)
# Return an OpenAI-shaped response object
return _CompatResponse(resp)
class _CompatChoice:
def __init__(self, text):
self.message = type("M", (), {"content": text, "role": "assistant"})()
class _CompatUsage:
def __init__(self, resp):
self.prompt_tokens = resp.usage.input_tokens
self.completion_tokens = resp.usage.output_tokens
self.total_tokens = resp.usage.input_tokens + resp.usage.output_tokens
class _CompatResponse:
def __init__(self, resp):
self.choices = [_CompatChoice(resp.content[0].text)]
self.usage = _CompatUsage(resp)
self.model = resp.model
# Usage — zero changes to existing call site:
client = OpenAICompatClient()
response = client.chat.completions.create(
model="gpt-4o",
max_tokens=256,
messages=[
{"role": "system", "content": "Be concise."},
{"role": "user", "content": "What is Claude?"}
]
)
print(response.choices[0].message.content)
# OpenAI function calling
functions = [{
"name": "get_weather",
"description": "Get current weather",
"parameters": {
"type": "object",
"properties": {"location": {"type": "string"}},
"required": ["location"]
}
}]
# Claude tool use — same JSON schema, different key names
tools = [{
"name": "get_weather",
"description": "Get current weather",
"input_schema": { # ← "input_schema" not "parameters"
"type": "object",
"properties": {"location": {"type": "string"}},
"required": ["location"]
}
}]
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=256,
tools=tools,
messages=[{"role": "user", "content": "What's the weather in Paris?"}]
)
# Read tool call result
for block in response.content:
if block.type == "tool_use":
print(block.name, block.input) # "get_weather" {"location": "Paris"}
For a full cost comparison before committing to migration, use the Claude API Cost Calculator and compare your current OpenAI spend against Claude's equivalent models.