A provider's SDK: what it does for you, what it hides, and how to read it
Two ways to make the same call
With requests:
r = session.post(
"https://api.openai.com/v1/chat/completions",
headers={"Authorization": f"Bearer {key}"},
json={"model": "gpt-4o-mini", "messages": [{"role": "user", "content": "Hi"}]},
timeout=(5, 120),
)
r.raise_for_status()
text = r.json()["choices"][0]["message"]["content"]With the provider's SDK:
from openai import OpenAI
client = OpenAI() # reads OPENAI_API_KEY from the environment
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "Hi"}],
)
text = resp.choices[0].message.contentThe second is shorter, and it is worth being exact about what the shortness bought.
What the SDK does for you
The session and the headers. The client object is a persistent HTTP client (httpx underneath) with keep-alive, the auth header, the API version header and a User-Agent, set once.
Retries. Two attempts with exponential backoff on 408, 409, 429 and 5xx, honouring Retry-After. You saw last lesson why you should not wrap this in your own loop.
Typed responses. resp.choices[0].message.content is attribute access on an object, so a typo raises AttributeError at that line with the name in it, and your editor can autocomplete the fields. With a raw dict a typo raises KeyError and the editor knows nothing.
Errors as exception classes. A 401 becomes AuthenticationError, a 429 becomes RateLimitError, a 400 becomes BadRequestError, each with the server's message attached. except RateLimitError: reads better than if r.status_code == 429:.
Streaming, files, tools. Each is a parameter or a method rather than a protocol you implement.
Keeping up. When the provider adds a field, the SDK gains it in the next release. With requests you read the changelog yourself.
What it hides
Which URL and which version. The SDK knows the endpoint; you do not see it. When a request fails in a way the error message does not explain, you need to see the bytes that went out, and the SDK's abstraction is in the way. Every SDK has a way through: set OPENAI_LOG=debug or, for Anthropic, ANTHROPIC_LOG=debug, and the client prints each request and response.
Timeouts. The openai client defaults to 600 seconds. That is ten minutes of silent waiting if something wedges. Set it: OpenAI(timeout=60.0), or per call with client.with_options(timeout=30).chat....
Dependency weight. The anthropic package pulls in httpx, pydantic, anyio, typing-extensions and more. On a phone under Termux or a machine with a slow connection this is not nothing. requests is one small package.
Its own bugs. SDKs are code; they have versions and regressions. Pin them like anything else.
One shape, many servers
Here is the fact that matters most for a learner without a card to put on a paid account. The OpenAI request and response format has become a de facto standard, and free servers speak it.
from openai import OpenAI
client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
resp = client.chat.completions.create(
model="llama3.2",
messages=[{"role": "user", "content": "Hi"}],
)That is the same SDK talking to Ollama running a free model on your own laptop. api_key must be non-empty because the client insists; Ollama ignores it. The same trick works for llama.cpp's server, LM Studio, vLLM, and most hosted providers of open models. Code written against the OpenAI shape can move between a free local model for development and a paid one for production by changing two strings.
Anthropic's format is different — messages.create returns content as a list of blocks — so code written against one SDK does not run against the other. If you may need to switch, keep the call in one function of your own and let only that function know which SDK it is using. Module 7 turns that into a class.
Reading the SDK
The single most useful habit with any library: find it and read it.
import openai
print(openai.__file__)
# .../.venv/lib/python3.12/site-packages/openai/__init__.pyOpen that folder. _client.py is the client, _base_client.py has the retry loop with its backoff constants, and resources/chat/completions.py is where create lives, with every parameter documented in the signature. When the documentation site is vague about a default, the code is not.
help(client.chat.completions.create) in the shell prints the docstring without leaving the terminal. inspect.signature(...) prints the parameters and defaults.
Which to use
Use the SDK when one exists and the provider is your main dependency. Use requests when the provider has no SDK, when the SDK is heavier than your whole program, when you need to see exactly what is sent, or when you are teaching yourself what an SDK does — which is what the first two lessons of this module were for.
Whichever you use, construct the client once. A new OpenAI() inside a function called in a loop throws away the session each time and reintroduces every handshake you learned to avoid.
Try this now
Install Ollama (free, runs on CPU), pull a small model with ollama pull qwen2.5:0.5b, and run the base_url example with model="qwen2.5:0.5b". Then set OPENAI_LOG=debug and run it again to see the request the SDK actually sent.
The one thing to keep
An SDK is a Session with the auth header, retries, typed responses and streaming already written; the same OpenAI-shaped client talks to a free local model by changing base_url, and when it misbehaves the source is sitting in your virtual environment to read.
Before you move on
A developer's script using the `openai` package works against OpenAI's API. They change only `base_url="http://localhost:11434/v1"` and `model="llama3.2"` to point it at Ollama running on their laptop, and it works too. What makes this possible?
Pick the one you would defend. Nobody sees your answer.