Anthropic Messages vs OpenAI Chat Completions: Mapping the APIs
The Anthropic vs OpenAI API divide is one of the most common friction points developers hit when they want to use Claude alongside GPT models. The two APIs solve the same problem — send a prompt, get a completion — but their wire formats are meaningfully different. System prompts live in different places, content is represented differently, streaming events carry different shapes, and token-usage fields have different names. This guide maps the differences precisely, shows equivalent requests side by side, and explains how a gateway that speaks both formats lets you call Claude with OpenAI-style code without rewriting a line.
The structural differences: Messages API vs Chat Completions
OpenAI's Chat Completions API and Anthropic's Messages API share a surface-level resemblance — both accept a list of conversational turns and return a completion — but the schemas diverge at almost every field.
System prompt placement
In the OpenAI Chat Completions API, the system prompt is just another message in the messages array, distinguished by "role": "system":
// OpenAI Chat Completions
{
"model": "gpt-4o",
"messages": [
{ "role": "system", "content": "You are a concise assistant." },
{ "role": "user", "content": "What is a transformer?" }
]
}
In the Anthropic Messages API, the system prompt is a top-level field on the request body, completely separate from the messages array. The messages array must contain only user and assistant turns:
// Anthropic Messages API
{
"model": "claude-opus-4-5",
"system": "You are a concise assistant.",
"messages": [
{ "role": "user", "content": "What is a transformer?" }
],
"max_tokens": 1024
}
This is the most common breaking change when converting between the two formats. Naively passing a system-role message into Anthropic's messages array will return a validation error.
Content: strings vs content blocks
OpenAI accepts a plain string as the value of content in each message. Anthropic's API accepts either a string or an array of typed content blocks. A simple text message can use a string, but multimodal messages (image + text) require the block form:
// Anthropic content block form (required for images)
{
"role": "user",
"content": [
{ "type": "text", "text": "Describe this image." },
{ "type": "image", "source": { "type": "url", "url": "https://..." } }
]
}
Responses from Anthropic also return a content array, not a string. You read the text from content[0].text, not from choices[0].message.content.
Token usage field names
Both APIs include a usage object, but the field names differ:
| Metric | OpenAI field | Anthropic field |
|---|---|---|
| Prompt / input tokens | usage.prompt_tokens |
usage.input_tokens |
| Completion / output tokens | usage.completion_tokens |
usage.output_tokens |
| Total tokens | usage.total_tokens |
(sum manually) |
| Cache read tokens | usage.prompt_tokens_details.cached_tokens |
usage.cache_read_input_tokens |
If your cost-tracking or observability code reads usage.prompt_tokens, it will silently get undefined when hitting the Anthropic API — a bug that shows up as zero-cost completions in your dashboards.
Tool use: tool_calls vs tool_use blocks
Function / tool calling is where the formats diverge most sharply at the response level.
OpenAI signals a tool call by setting choices[0].finish_reason to "tool_calls" and populating choices[0].message.tool_calls, an array of objects each with an id, a function.name, and a JSON-string function.arguments.
Anthropic signals a tool call by setting stop_reason to "tool_use" and including a content block of type: "tool_use" inside the content array. The arguments are already parsed JSON in the input field, not a string that needs JSON.parse().
// OpenAI tool call response (simplified)
{
"choices": [{
"finish_reason": "tool_calls",
"message": {
"tool_calls": [{
"id": "call_abc",
"function": { "name": "get_weather", "arguments": "{\"city\":\"Paris\"}" }
}]
}
}]
}
// Anthropic tool use response (simplified)
{
"stop_reason": "tool_use",
"content": [{
"type": "tool_use",
"id": "toolu_abc",
"name": "get_weather",
"input": { "city": "Paris" }
}]
}
Sending the tool result back also uses different shapes. OpenAI expects a message with "role": "tool" and a matching tool_call_id. Anthropic expects a user message containing a tool_result content block referencing the tool_use_id.
Streaming event shapes
Both APIs use server-sent events (SSE), but the event names and delta payloads differ:
- OpenAI emits
data: {...}lines where each chunk carrieschoices[0].delta.contentfor text and terminates withdata: [DONE]. - Anthropic emits named events:
event: content_block_start,event: content_block_delta(with adelta.textfield),event: message_delta(carryingstop_reasonand final usage), andevent: message_stop. There is no[DONE]sentinel.
A streaming handler written for OpenAI will fail silently or throw on Anthropic's stream because the event: lines it ignores are the only way to know when the stream is complete and to read final token counts.
Side-by-side equivalent request mapping
| Concept | OpenAI Chat Completions | Anthropic Messages |
|---|---|---|
| Endpoint | /v1/chat/completions |
/v1/messages |
| Auth header | Authorization: Bearer sk-… |
x-api-key: sk-ant-… + anthropic-version: 2023-06-01 |
| System prompt | messages[0] = { role: "system", … } |
Top-level system: "…" field |
| Max output tokens | max_tokens (optional) |
max_tokens (required) |
| Text response | choices[0].message.content |
content[0].text |
| Stop reason | choices[0].finish_reason |
stop_reason |
| Input tokens | usage.prompt_tokens |
usage.input_tokens |
| Output tokens | usage.completion_tokens |
usage.output_tokens |
Common gotchas when converting Anthropic to OpenAI format (or vice versa)
- Missing
max_tokens: Anthropic requires it; OpenAI makes it optional. Omitting it from an Anthropic request returns a 400 error. - System message in the messages array: Sending
{ "role": "system" }inside Anthropic'smessagesis invalid. Extract it to the top-levelsystemfield. - Consecutive same-role turns: Anthropic requires messages to strictly alternate between
userandassistant. OpenAI allows multiple consecutiveuserturns. Conversations with back-to-back user messages need to be merged before sending to Anthropic. - Tool arguments as string vs object: OpenAI gives you
function.argumentsas a JSON string. Anthropic gives youinputas a parsed object. CallingJSON.parse()on an Anthropic input will throw; skipping it on an OpenAI argument leaves you with a raw string. - Streaming termination: Checking for
[DONE]to end a stream works for OpenAI but not Anthropic. Waiting formessage_stopworks for Anthropic but not OpenAI. A shared streaming parser must handle both conventions. - Response ID field: OpenAI uses
idas the response identifier; Anthropic usesidtoo but the format differs (msg_…vschatcmpl-…). Not a breaking issue but worth knowing for logging and deduplication.
How flo2 bridges both APIs without rewriting your code
Manually handling these differences — lifting the system prompt, renaming usage fields, re-shaping tool calls, adapting streaming parsers — is exactly the kind of low-value work a gateway should absorb. flo2 exposes a single endpoint that speaks both the OpenAI Chat Completions format and the Anthropic Messages format. One API key unlocks both, and the gateway translates between them at the wire level.
In practice this means you can call Claude models using your existing OpenAI SDK integration — no new SDK, no payload conversion code:
// Call Claude claude-opus-4-5 with OpenAI SDK via flo2
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://api.flo2.com/v1",
apiKey: process.env.FLO2_KEY,
});
const resp = await client.chat.completions.create({
model: "claude-opus-4-5", // Claude model, OpenAI format
messages: [
{ role: "system", content: "You are a concise assistant." },
{ role: "user", content: "What is a transformer?" },
],
});
console.log(resp.choices[0].message.content); // works as expected
console.log(resp.usage.prompt_tokens); // normalized field name
flo2 receives the OpenAI-format request, translates the system message to Anthropic's top-level system field, adds the required max_tokens, rewrites the auth headers, forwards to Anthropic, then maps the response back to the choices[0].message.content shape and normalizes usage field names — all transparently. The same token normalisation works in reverse: send a native Anthropic-format request to flo2 and target an OpenAI model.
Because flo2 uses your own provider keys with zero markup on tokens, this translation layer costs nothing beyond the tokens themselves. The gateway also routes to the cheapest or fastest available model when you want it to, using a unified LLM API that covers dozens of providers behind the same endpoint. Format translation is just one capability of the broader OpenAI-compatible API surface flo2 exposes.
If you're building with both Claude and GPT models — or evaluating one against the other — flo2 removes the integration tax and lets you treat model selection as a runtime decision rather than an architectural one. It's free during Beta: bring your own keys and start routing.