OpenAI and Anthropic SDKs
The modern way to call frontier models from Python — the Responses API for OpenAI, the Messages API for Anthropic, side by side with the patterns you actually ship.
What you'll learn
- The current shape of OpenAI and Anthropic SDK calls
- Common arguments — model, messages, max_tokens, temperature, system
- Tool calling at a glance, and the differences between providers
- Error handling and retries with tenacity
Before you start
Two SDKs carry the large majority of production LLM traffic: openai and anthropic. The pip installs are unsurprising; the call sites have drifted just enough since 2023 that copy-pasting an old answer off the internet will quietly fail. So here is the current shape of both, side by side — and the reassuring conclusion up front is that the differences are surface quirks, not different ways of thinking.
OpenAI — the Responses API
chat.completions.create is now treated as legacy. The current entry point is client.responses.create, which unifies plain text, multimodal input, tool calls, and structured outputs behind one method:
from openai import OpenAI
client = OpenAI() # reads OPENAI_API_KEY from the environment
resp = client.responses.create(
model="gpt-5-mini",
input=[
{"role": "user", "content": "Summarize this paragraph in one sentence: ..."}
],
)
print(resp.output_text)
Three things to register. input= has replaced messages= (the role/content shape underneath is the same). resp.output_text is the convenience accessor — under the hood resp.output is a list of typed parts (text, tool calls, images). And you need not pass max_tokens for a trivial call; the API picks a sensible default, and you set it explicitly only when latency or cost matter.
Anthropic — the Messages API
Anthropic’s surface has moved much less — it is still client.messages.create, with system as a top-level argument rather than a message role:
from anthropic import Anthropic
client = Anthropic() # reads ANTHROPIC_API_KEY from the environment
msg = client.messages.create(
model="claude-sonnet-4.6",
max_tokens=1024,
system="You are a helpful expert in data engineering.",
messages=[
{"role": "user", "content": "Explain partitioning in one paragraph."}
],
)
print(msg.content[0].text)
Two differences are worth burning in. max_tokens is required on Anthropic — there is no implicit default. And msg.content is always a list of content blocks (text, tool_use, thinking), so you index into it to reach the text.
The same task, both SDKs
Extracting named entities makes the symmetry obvious — the real call sites on top, a runnable mock below so you can see the response shape:
# OpenAI
def extract_entities_openai(text: str) -> str:
resp = client_openai.responses.create(
model="gpt-5-mini",
input=[
{"role": "system", "content": "Extract named entities. Return JSON: people, orgs, places."},
{"role": "user", "content": text},
],
temperature=0,
)
return resp.output_text
# Anthropic
def extract_entities_anthropic(text: str) -> str:
msg = client_anthropic.messages.create(
model="claude-sonnet-4.6",
max_tokens=512,
temperature=0,
system="Extract named entities. Return JSON: people, orgs, places.",
messages=[{"role": "user", "content": text}],
)
return msg.content[0].text
The bodies are nearly identical. To see the response shapes resolve without a real API key, here is a mock of each SDK’s surface:
# Mock SDK surfaces — they mirror the real response objects.
class _OpenAIResp:
def __init__(self, text): self.output_text = text
class _AnthropicBlock:
def __init__(self, text): self.text = text; self.type = "text"
class _AnthropicMsg:
def __init__(self, text): self.content = [_AnthropicBlock(text)]
class MockOpenAI:
class responses:
@staticmethod
def create(model, input, temperature=1, **kw):
return _OpenAIResp('{"people": ["Ada Lovelace"], "orgs": ["Cambridge"], "places": ["London"]}')
class MockAnthropic:
class messages:
@staticmethod
def create(model, max_tokens, messages, system=None, temperature=1, **kw):
return _AnthropicMsg('{"people": ["Ada Lovelace"], "orgs": ["Cambridge"], "places": ["London"]}')
client_openai = MockOpenAI()
client_anthropic = MockAnthropic()
text = "Ada Lovelace studied mathematics at Cambridge and worked in London."
print("OpenAI ->", extract_entities_openai(text))
print("Anthropic ->", extract_entities_anthropic(text))
OpenAI -> {"people": ["Ada Lovelace"], "orgs": ["Cambridge"], "places": ["London"]}
Anthropic -> {"people": ["Ada Lovelace"], "orgs": ["Cambridge"], "places": ["London"]}
The only real differences are input versus messages, where system lives, and how you reach the text. Pick a provider per task — for latency, price, or reasoning quality — but do not pick a different programming model.
Common arguments you will actually use
| Argument | What it does | Notes |
|---|---|---|
model | Which model to call | gpt-5-mini, gpt-5.1, claude-sonnet-4.6, claude-opus-4.7 |
temperature | Randomness | 0 for extraction/classification, 0.7+ for creative writing |
max_tokens | Cap on output | Required on Anthropic, optional on OpenAI |
system | Behaviour instructions | Set once per conversation, not per turn |
top_p | Nucleus sampling | Use either temperature or top_p, not both |
stop / stop_sequences | Hard stop on a substring | Useful for “stop at ---” patterns |
Tool calling at a glance
Both providers let the model request a function call instead of (or alongside) text. The choreography is the same on either side — you declare tools, the model returns a structured tool-use block, you execute it, and you feed the result back. The Anthropic flavour:
tools = [{
"name": "get_weather",
"description": "Get current weather for a city",
"input_schema": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
}]
msg = client.messages.create(
model="claude-sonnet-4.6",
max_tokens=1024,
tools=tools,
messages=[{"role": "user", "content": "What's the weather in Tokyo?"}],
)
# msg.content includes a tool_use block carrying the args the model chose.
OpenAI’s Responses API uses tools=[{"type": "function", ...}] and round-trips results via previous_response_id, but the pattern is identical: declare, the model picks the arguments, you execute, you respond.
Errors and retries
Rate limits — the provider’s cap on requests-per-minute and tokens-per-minute — and transient 5xx errors will hit you in production. The clean pattern is tenacity with exponential backoff (wait a little, then longer, then longer, so you do not hammer a struggling server) on the specific exception classes the SDK exposes:
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
from openai import RateLimitError, APIConnectionError, APIStatusError
@retry(
retry=retry_if_exception_type((RateLimitError, APIConnectionError, APIStatusError)),
wait=wait_exponential(multiplier=1, min=1, max=30),
stop=stop_after_attempt(5),
)
def call_with_retry(prompt: str) -> str:
resp = client.responses.create(
model="gpt-5-mini",
input=[{"role": "user", "content": prompt}],
)
return resp.output_text
Anthropic exposes the same exceptions — anthropic.RateLimitError, anthropic.APIConnectionError, anthropic.APIStatusError. The one rule that matters: do not retry on BadRequestError. That is your bug, not the provider’s, and the same malformed payload will fail identically every time.
In one breath
- OpenAI’s current entry point is
responses.createwithinput=; Anthropic’s ismessages.createwithmessages=. systemis a message in OpenAI’s input; a top-level argument in Anthropic.max_tokensis optional vs required.temperature=0for extraction/code;~0.7for creative — tune the prompt, not the in-between.- Tool calling is declare → model picks args → you execute → you respond, on both.
- Retry rate limits, connection errors, and
5xxwith exponential backoff; never retry aBadRequestError.
Practice
Quick check
What’s next
You can call the models. Next is making the wait feel instant — streaming the response token by token instead of waiting for the whole thing to finish.
Practice this in an interview
All questionsAn agent is an LLM placed in a loop where it reasons, chooses and calls tools or actions, observes the results, and repeats until a goal is met, rather than producing one response and stopping. The key differences are autonomy, tool use, memory and state, and multi-step control flow driven by the model's own decisions.
MCP is an open protocol from Anthropic that standardizes how LLM applications discover and connect to external tools, data sources, and prompts through a common client-server interface. It replaces bespoke per-integration glue with a single protocol, so any MCP-compatible host can use any MCP server, and has been adopted across the broader ecosystem.
Keep raw credentials outside model context and traces. Let the model propose typed intent, authorize the final action and arguments deterministically, then have a trusted executor inject a short-lived, narrowly scoped, audience-restricted credential for one call. Re-authorize downstream and gate high-impact writes with explicit approval.
Tool calling extends the LLM's output space to include structured function invocations. The model emits a JSON object naming a tool and its arguments; the runtime executes the tool and feeds the result back as a new message. An agent is a loop that repeats this cycle — observe, think, act — until the task is complete or a stopping condition is met.