The Evolution of LLM APIs: OpenAI vs Anthropic vs Google
The first LLM APIs took one string in and returned one string out — no roles, no tools, no memory of the previous call. Three years later, a production integration is a typed, multi-turn contract: structured message arrays, function-calling schemas, streaming deltas, and provider-specific reasoning controls. Teams that built against the old shape now maintain adapter code just to keep up, and teams that assume any two providers' "chat completion" endpoints are interchangeable get surprised the first time a tool call round-trips differently.
This guide traces that evolution through one lens: what actually changed in the request/response contract, not just what each vendor announced. It follows one concrete task — a customer-support assistant that looks up an order via a function call — through OpenAI's, Anthropic's, and Google's current APIs, so the differences show up as code, not marketing copy. For the architectural pattern this feeds into, see tool-calling reliability patterns and structured LLM outputs with JSON schema.
Three generations of the same API
Every major provider's API went through a recognizable arc. Generation one was a single prompt string with a single completion string back — useful for text generation, unworkable for anything needing memory or structure. Generation two added a message array with roles (system/user/assistant) and, shortly after, function/tool declarations the model could ask the caller to invoke. Generation three, which is where the three current APIs sit today, adds typed structured outputs (a JSON schema the response must conform to, not just a hopeful prompt instruction) and native multi-step tool loops with explicit reasoning controls.
The useful takeaway is not "the newest generation is best" — it's that the generations are not interchangeable, and code written against generation one (a single prompt string, no roles) silently breaks trust boundaries once you add tools, because there is no role separation to keep a tool result from being mistaken for a user instruction. Prompt injection through an unstructured tool result is a direct consequence of skipping the message-role generation; see prompt injection in RAG and tool-using agents.
Quick reference
- Generation one APIs (and any code still shaped like them) have no place to put a tool result without it looking like user input to the model.
- Generation two's role separation (system/user/assistant/tool) is the mechanism that lets a model distinguish an instruction from data returned by a function.
- Generation three's structured outputs return a value that validates against a schema, not text you hope parses — a real reliability gain, not a convenience feature.
- Reasoning controls (effort/thinking budget) are a generation-three addition across providers, but the parameter names and semantics are not shared.
Remember this
Treat "which generation of the contract does this SDK example use" as a real compatibility question — a generation-one-shaped integration has no safe place to put a tool result, which is a security gap, not just a stylistic one.
OpenAI: Chat Completions to the Responses API
OpenAI's Chat Completions API set the pattern most of the industry copied: a messages array with roles, a tools array of function schemas, and a tool_calls field the model populates when it wants to invoke one. The newer Responses API keeps that shape but adds a persistent previous_response_id chain for multi-turn state and built-in tool types (web search, code execution) the caller doesn't have to implement.
For the order-lookup assistant, Chat Completions requires you to manage the message history yourself and re-send it every call. The Responses API can carry state server-side via the response ID chain, trading a stateless request for one less thing your backend has to store — a real operational trade-off, not just an API preference.
Quick reference
- Chat Completions remains the most widely mirrored shape — many third-party and self-hosted APIs copy its
messages/toolsfields for compatibility. - The Responses API's server-side chain reduces payload size on long conversations but adds a dependency on OpenAI retaining that state.
tool_callscan contain more than one call in a single turn — code that only reads index[0]silently drops parallel tool requests.- Confirm current model and field names against OpenAI's API reference before shipping — both endpoints continue to evolve.
Remember this
Chat Completions vs. Responses is a stateless-vs-server-tracked trade-off, not an upgrade you get automatically — moving to the newer API changes who owns conversation history.
Anthropic's Messages API and Google's generateContent
Anthropic's Messages API uses the same role-based shape but keeps the system prompt as a separate top-level field rather than a message with role: "system", and returns tool calls as typed content blocks (type: "tool_use") inside the assistant message rather than a separate tool_calls array. Functionally equivalent to OpenAI's approach, but the field paths differ enough that naive code ports break silently — reading message.tool_calls on an Anthropic response returns undefined, not an error, which is the worse failure mode.
Google's Gemini API took a different structural choice: contents is a list of turns, each with parts that can mix text, inline data (images), and function calls in the same array — reflecting Gemini's multimodal-first design. A tool call arrives as a functionCall part alongside any text parts in the same turn, rather than a dedicated field.
Quick reference
- Read each provider's current API reference before porting code — field names for tool calls (
tool_calls,tool_use,functionCall) are not interchangeable and fail silently, not loudly. - System prompts are a top-level field in Anthropic's API, a
systemInstructionconfig field in Gemini's, and a message with a role in OpenAI's Chat Completions — three different places to look for the same concept. - Streaming event shapes differ per provider (SSE event types, chunk field names) — a streaming adapter written for one provider needs its own parser for each other provider, not a shared one.
- Multimodal input (image + text in one turn) is native to Gemini's
partsarray; OpenAI and Anthropic require a specific content-block type per modality within the message.
Remember this
The concept of a tool call is now shared across providers, but the field path to find it is not — a portability layer needs an explicit per-provider adapter, not a hope that the shapes line up.
What to build once, and what to keep provider-specific
Given three real, incompatible wire formats, the decision that matters is what to abstract behind your own interface versus what to leave provider-specific. Abstract the concepts that are stable across generation three: message turns, tool declarations, and a normalized "tool call requested" event your application logic reacts to. Do not abstract away provider-specific reasoning controls, context-caching behavior, or rate-limit semantics — those differ enough in practice that a leaky abstraction there produces bugs that only show up under load.
The realistic failure to design against: a team builds one internal LlmClient interface, ships it, and later discovers a parallel-tool-call response from one provider silently drops all but the first call because the adapter was written against a single-call assumption from an earlier integration. The fix is treating "can this turn contain more than one tool call" as a contract question to verify per provider, not an assumption to copy from the first one you integrated.
Quick reference
- Normalize on your own internal
ToolCallRequest/ToolResulttypes; translate to and from each provider's wire format at the edge, not throughout your business logic. - Test the parallel-tool-call case explicitly for every provider you integrate — do not assume single-call behavior generalizes.
- Keep provider selection for a request behind AI model routing strategies so swapping a provider is a routing config change, not a rewrite.
- Version-pin the SDK and re-run your adapter's test suite before any SDK upgrade — provider SDKs change field shapes between major versions without always calling it a breaking change.
Remember this
A cross-provider abstraction earns its keep only if it is tested against each provider's actual edge cases (parallel tool calls, streaming errors) — an abstraction built from one provider's happy path is a bug waiting for the second provider.
Key takeaway
Implement the order-lookup assistant against two providers using your own ToolCallRequest abstraction: OpenAI's Chat Completions or Responses API, and either Anthropic's Messages API or Gemini's generateContent. Verify success by confirming both integrations return a normalized tool-call event for the same prompt ("Where is order A1092?") despite different wire formats.
Then break it deliberately: prompt the model in a way that triggers two tool calls in one turn (for example, asking for the status of two different order IDs at once) and confirm your adapter surfaces both calls, not just the first. Pass criterion: both providers produce a normalized event your application code handles identically, and neither integration silently drops a second parallel tool call.
Related Articles
Explore this topic