OpenAI & Claude APIs
Theme
Palette
Download
Caption (tap to copy)
📖 Deep dive (full written explanation)
This is the build post, so the detail entries focus on why each piece of code is shaped the way it is, not just what it does. The throughline is that everything here is meant to survive contact with production: env-based secrets, a thin per-provider wrapper, structured output, tool use on both APIs, and retries. It is a paste-and-adapt skeleton, not a throwaway demo.
This step establishes the only acceptable way to handle keys: in environment variables, never in source. The shell exports show the convention, and the two asserts fail fast if a key is missing, which turns a confusing downstream 401 into an obvious startup error.
Failing fast on missing configuration is a small habit with outsized value. It means a misconfigured deployment breaks immediately and legibly at boot, rather than mysteriously on the first user request hours later. The same pattern generalizes to any required secret or setting your service depends on.
Here both clients are constructed with no arguments, letting each SDK read its respective environment variable. The two model names are pulled into named constants so they appear exactly once and can be changed in one place — the configuration-not-literals principle from the second post, applied.
Keeping both clients side by side in one module is deliberate for this teaching script. In a real service you might split them, but seeing them together makes the symmetry and the differences obvious, which is the point of a day that compares the two providers directly.
This wrapper is the most important snippet of the post: it hides the dialect differences behind one signature, chat(provider, system, user). For OpenAI it puts the system prompt as a message in the array and reads choices[0].message.content. For Claude it passes system as the top-level field, sets the required max_tokens, and reads content[0].text.
The inline comment on the system field flags the single most common cross-provider mistake. By isolating both branches in one function, the rest of your application calls chat() and stays blissfully unaware of which provider is behind it — which is exactly the abstraction the 'why' post argued for. Swapping providers later touches this function and nothing else.
This compare diagram makes explicit what the wrapper conceals so the reader does not mistake the abstraction for the two APIs being identical. The OpenAI path treats system as a message, returns text under choices[0].message, and makes max_tokens optional. The Claude path treats system as a top-level field, returns text under content[0].text, and requires max_tokens.
Understanding the differences you are abstracting over is essential: when something breaks, you debug at the provider layer, and you cannot do that if the wrapper has let you forget the shapes ever differed. A good abstraction hides complexity for the caller while remaining transparent to its maintainer.
Structured output is where LLM apps connect to real software, and this slide shows OpenAI's native JSON mode via response_format. Setting it to json_object instructs the model to emit syntactically valid JSON, which you then json.loads into a Python dict and use like any data structure.
The important caveat — expanded in the mistakes post — is that JSON mode guarantees valid JSON syntax, not that the content matches the schema you wanted. It is a strong tool for getting machine-readable output, but it is not a substitute for validating the parsed object before you trust its fields.
Tool calling, shown here for OpenAI, is how the model reaches beyond text to invoke your functions. You declare tools with a name, description, and a JSON-schema parameter spec. When the model decides a tool is needed, it does not call anything itself — it returns a structured tool_calls entry naming the function and a JSON arguments string.
The critical mental model is that the model only requests the call; your code is responsible for executing get_weather, then sending the result back in a follow-up message so the model can incorporate it. This request-then-execute-then-continue loop is the foundation of every agentic pattern built on these APIs.
This is the Claude counterpart to the previous slide, included so the day treats both providers as first-class. The schema differs in shape: tools use a top-level name and an input_schema field (versus OpenAI's nested function object), and the model's request arrives as a content block with type 'tool_use' carrying name and a parsed input object.
Notice that Claude returns the tool arguments already parsed as a dict, whereas OpenAI returns them as a JSON string you must parse. Small differences like this are exactly why the per-provider adapter from earlier matters: the calling code wants a clean dict, and the adapter is where you normalize each provider's quirks into that shape.
Retries are non-optional for any real LLM integration because both APIs return 429 rate limits and transient 5xx errors under load. The safe() helper wraps a callable and retries on rate-limit and timeout errors, using exponential backoff (2 ** i) plus random jitter to avoid synchronized retry storms when many requests fail at once.
The jitter detail matters more than it looks: without it, every client that failed at the same moment retries at the same moment, hammering the API in waves. Adding a random fraction of a second spreads them out. The final-attempt re-raise ensures a genuinely failing call still surfaces as an error rather than being swallowed forever.
This flow diagram shows how the pieces compose at runtime: a call to chat() builds the provider-appropriate arguments, safe() wraps it with retry logic, the provider SDK performs the actual HTTPS call, and finally you parse the result — text, a tool request, or JSON — depending on what you asked for.
Seeing the composition clarifies the separation of concerns. chat() handles provider differences, safe() handles transient failures, and parsing handles the response shape. Each layer does one job, which is what makes the skeleton extensible: you can add logging, caching, or a third provider by slotting it into the right layer rather than rewriting everything.
The checklist condenses the post into the things that separate a demo from a deployable integration. Keys come from the environment and never the repo. The client gets an explicit timeout so a slow upstream call cannot hang your request indefinitely. Every call logs model, usage, and latency for cost and performance visibility.
The last two items are subtle but important: validate JSON before trusting it, because valid-looking output can still be wrong, and ensure retries send a fresh, idempotent request so a retry cannot cause duplicate side effects. Together these turn the working snippets above into code you would be comfortable putting in front of users.
With a runnable, production-shaped client in hand, the natural next question is what still goes wrong even with good code — the misuse patterns that cause real incidents. That is exactly where the final post goes.
The transition is intentional: you have now seen the right way to call both APIs, so the mistakes post can be read as a checklist of the specific ways teams deviate from these patterns and pay for it.