Skip to content
beginner Phase 3 · LLM API Fundamentals

OpenAI SDK Essentials

Navigate the chat completions client, parameters, and error types in the OpenAI SDK.

45m
0 problems
Topic Progress 0%

Where the Client Gets Its Configuration

The hand-rolled httpx loop from Your First LLM API Call works, but it already needs three things you have not written yet: credential resolution, retries, and a timeout policy. The SDK ships all three, and the first decision is where its configuration comes from.

from openai import OpenAI

client = OpenAI(            # api_key defaults to $OPENAI_API_KEY
    timeout=30.0,           # budget for one attempt, including reads
    max_retries=2,          # attempts the SDK makes before raising
)

reply = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "Classify: my card was charged twice"}],
)
print(reply.choices[0].message.content)

Input: Classify: my card was charged twice

Output: {"category": "billing", "urgency": "medium"}

Credential resolution is a two-step rule: an explicit api_key argument wins, otherwise the client reads the OPENAI_API_KEY environment variable. If neither is present, the constructor raises immediately with a message naming both options — you find out at startup, not on the first user request. Pass api_key explicitly only in tests that load a fixture key; production code should depend on the environment so no key can be committed by accident.

Three configuration points matter beyond the key:

  • Build one client per process. The client wraps a connection pool. Constructing it inside a request handler throws away keep-alive connections and pays a fresh TLS handshake every call, which shows up as added latency — Latency Profiling shows how to catch it.
  • base_url points the same client at a gateway, proxy, or self-hosted compatible endpoint without touching call sites.
  • default_headers, organization, and project come from client options or the OPENAI_ORG_ID / OPENAI_PROJECT_ID environment variables when one deployment must bill a specific project.

The Retry Loop You No Longer Have to Write

A 429 at 3 a.m. should not wake anyone up. The SDK's retry helper answers three questions on its own: which responses are worth retrying, how long to wait, and when to give up.

attempt 1  ->  429   sleep 0.8s   (honors retry-after when the provider sends one)
attempt 2  ->  429   sleep 1.7s   (exponential backoff with jitter)
attempt 3  ->  200   return       default max_retries=2, so three attempts total

The distinction that saves you from a bad afternoon is which statuses are retryable:

Status SDK behavior Why
429 Retried with backoff Rate or quota limit clears on its own
500, 502, 503 Retried with backoff Provider incident, usually brief
408, 409 Retried with backoff Timeout or transient conflict
400, 401, 404 Raises APIStatusError immediately Your payload, your key, or a bad model name — retrying repeats the same mistake

So the loop in the previous topic collapses to one line of configuration. Override the budget per client, or for a single call when one path can afford to wait longer:

patient = client.with_options(max_retries=5)
reply = patient.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "Summarize the incident report"}],
)

When to turn retries down. Retries are not free: each attempt re-sends the full prompt, so a retried call bills the prompt tokens twice, and three attempts on a 10-second generation can blow a 2-second interactive budget. Set max_retries=0 when you are writing your own recovery loop, when the call streams (a stream that dies mid-answer cannot be replayed — see Streaming Responses), or when the SDK's total worst-case wait exceeds the latency target you promised the user. The full policy — circuit breakers, jitter formulas, budget caps — lives in Rate Limiting and Retries.

JSON You Can Validate on Arrival

Free-text output forces you to strip markdown fences, hunt for the first brace, and hope the model did not add a commentary sentence. Parsing failures like that are the most common source of bugs in LLM features, and the SDK can eliminate most of them.

Three output modes, increasing in strictness:

Mode You send What you get back
Default Nothing extra Free text; you find and parse the JSON yourself
JSON mode response_format={"type": "json_object"} Parses as JSON, but any shape the model likes
Structured output response_format={"type": "json_schema", "json_schema": {..., "strict": true}} Keys, types, and required fields match your schema
schema = {
    "name": "ticket_classification",
    "strict": True,
    "schema": {
        "type": "object",
        "properties": {
            "category": {"type": "string", "enum": ["billing", "bug", "question"]},
            "urgency": {"type": "string", "enum": ["low", "medium", "high"]},
        },
        "required": ["category", "urgency"],
        "additionalProperties": False,
    },
}

reply = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "Ticket: charges are duplicated on invoice 88"}],
    response_format={"type": "json_schema", "json_schema": schema},
)
print(reply.choices[0].message.content)

Input: Ticket: charges are duplicated on invoice 88

Output: {"category": "billing", "urgency": "medium"} — a string you can pass straight to json.loads, then to a validator.

A strict schema turns "did the model feel like producing JSON today" into a contract: missing keys and wrong types are rejected rather than discovered downstream. The additionalProperties: False line and the required list are what make that contract airtight; omit them and the model may slip in extra fields you never see. The SDK's client.chat.completions.parse helper wraps this pattern with a Pydantic model so the schema is derived from your type instead of hand-written — JSON and Pydantic Basics walks through it.

Two limits worth knowing. Structured output constrains the shape, not the truth: a schema with an enum of urgency levels cannot stop the model from picking the wrong one, so grounding and evaluation still do that job — Schema-Constrained Generation covers the tradeoffs, and JSON mode in JSON Mode Outputs is the lighter option when you only need parseable text.

Turning Usage Into an Invoice

A summarization feature that costs nothing visible until the monthly bill arrives is a feature with an expiration date. Every non-streaming response carries usage, and reading it at the call site is the whole of cost attribution.

reply = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "Summarize ticket 88"}],
)
u = reply.usage
record_event(
    tenant="acme",
    feature="ticket-summary",
    input_tokens=u.prompt_tokens,
    output_tokens=u.completion_tokens,
)

Two numbers, two prices: prompt_tokens cover the instructions and history you resupply on every turn, while completion_tokens cover what the model wrote — and output tokens usually cost several times more per token than input. Re-sending a 4,000-token conversation to generate a 40-token reply means the prompt dominates the bill, which is why trimming history pays for itself. Token Usage Metering builds the aggregation layer on top of this event, and Token Cost Estimation turns counts into currency before you ship.

Streaming changes where usage lives. With stream=True the usage block only appears if you ask for it with stream_options={"include_usage": True}, and it arrives on the final chunk — which has an empty choices array. Code written against the non-streaming shape will raise IndexError on that chunk, so guard it:

last = None
for chunk in stream:
    if chunk.choices:          # final usage chunk has choices == []
        last = chunk
usage = chunk.usage           # read from the terminal chunk, not from choices

Log the counts alongside a trace id rather than the raw completion text, and you keep PII out of the billing pipeline while still being able to answer "why did this tenant's spend triple" — Trace LLM Calls shows the layout.

Quiz

1. You construct `OpenAI()` with no arguments in a fresh environment. Where does the API key come from?

Question 1 options

2. A call returns 429 twice and then succeeds on the third attempt. What did the SDK do, and what happens if the first response had been a 400?

Question 2 options

3. Your service parses the model's reply with `json.loads` and crashes when the model adds a sentence before the object. Which request change addresses the root cause?

Question 3 options

4. You need to bill each tenant for the tokens they consumed. Where does that data come from?

Question 4 options

Flashcards

Question

How does the OpenAI client resolve its API key?

Answer

An explicit `api_key` argument wins; otherwise it reads `OPENAI_API_KEY` from the environment and raises at construction if neither is set.

Question

Which HTTP statuses does the SDK retry automatically, and which does it raise on?

Answer

Retries with backoff: 429, 5xx, 408, 409. Raises immediately: 400, 401, 404 — request or credential errors that retrying cannot fix.

Question

What does `response_format` with a strict `json_schema` guarantee?

Answer

The reply parses as JSON and its keys, types, enums, and required fields match the schema; shape is enforced, semantic correctness is not.

Question

Which response fields feed cost attribution?

Answer

`usage.prompt_tokens` and `usage.completion_tokens`, stored per request with tenant and feature ids; output tokens are priced higher than input tokens.

Question

Why should one OpenAI client be built per process rather than per request?

Answer

The client owns a connection pool; rebuilding it discards keep-alive state and adds a TLS handshake per call, which shows up as latency.

Revision Notes

Key Takeaways

  • 1. Credential resolution is explicit api_key, then OPENAI_API_KEY — nothing else, and failure happens at construction
  • 2. The SDK's retry helper covers 429 and transient 5xx with backoff; 400 and 401 are raised immediately
  • 3. A strict json_schema response_format converts parsing from regex-guessing into a contract, but still cannot guarantee the content is true
  • 4. prompt_tokens and completion_tokens are the billing primitive, and streaming moves them to the final chunk
  • 5. One pooled client per process; retries and timeouts are latency budget you spend deliberately

Interview Tips

  • • Expect to be asked which errors are retryable and why — answer 429/5xx with backoff, 400/401 never, and mention honoring retry-after
  • • Be ready to compare JSON mode with structured output and say what `strict: true` adds (shape enforcement, not truth)
  • • A common prompt: 'how would you know what this call cost?' — cite `usage`, the input/output price difference, and per-request attribution
  • • Have an opinion on timeouts: give the SDK a budget, cap retries when the UI has a latency target, and explain that retries double prompt spend

Cheat Sheet

OpenAI SDK Essentials — Cheat Sheet

Client construction

client = OpenAI(timeout=30.0, max_retries=2)   # key from $OPENAI_API_KEY
patient = client.with_options(max_retries=5)   # per-call override
  • Build once per process, reuse everywhere
  • Override base_url for gateways, OPENAI_ORG_ID / OPENAI_PROJECT_ID for billing scope

Retry decision rule

Status Action
429 / 5xx / 408 / 409 SDK retries, exponential backoff + jitter
400 / 401 / 404 Raises APIStatusError — fix payload or key
any, on a streaming call Set max_retries=0 and handle recovery yourself

Structured output ladder

  1. Default → free text, you parse
  2. {"type": "json_object"} → parseable, shape unconstrained
  3. {"type": "json_schema", strict: true} → schema-enforced; requires required list + additionalProperties: false
  4. client.chat.completions.parse(response_format=Model) → Pydantic-derived schema

Usage capture

u = reply.usage            # .prompt_tokens / .completion_tokens
# streaming: stream_options={"include_usage": True}, read chunk.usage on the last chunk
  • Record per request with tenant + feature id
  • Guard streaming loops with if chunk.choices — the usage chunk has none

Continue Learning

Before You Start

  • No prerequisites listed.

Related Reading