2026-06-03 · flo2 blog

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:

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)

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.

One key, every model — zero markup.
Bring your own provider keys. flo2 routes to the cheapest, fastest model with fallback, racing and true cost accounting — free during Beta.
Get your flo2 key →
© 2026 flo2.com — the zero-markup LLM gateway & router. flow → to