Skip to content
Relaykit
Blog

OpenAI-Compatible vs Anthropic-Compatible APIs: What Differs and When It Matters

Chat Completions and the Messages API look alike but differ in system prompts, max_tokens, tool calls, streaming events and usage fields. When it matters.

Relaykit TeamPublished 8 min read
On this page

In short:

  • Both formats send a list of chat messages and get text back, so simple prompts port easily. The differences show up in system prompts, max_tokens, tool calling, streaming events and usage fields.
  • Your tool usually decides the format for you: Claude Code speaks the Anthropic format; Codex CLI, Cursor, Cline and most other agents speak the OpenAI format.
  • For your own code, choose the format whose SDK you already use. Choose the native Anthropic format when you need Claude-specific features such as explicit prompt-cache breakpoints.
  • On Relaykit, the same key works with both: https://api.oneaiapikey.com/v1 for OpenAI format, https://api.oneaiapikey.com for Anthropic format.

“OpenAI-compatible” has become the default promise of almost every LLM provider and gateway. Anthropic’s Messages API is the other format you will meet often, mainly because Claude Code and the Claude Agent SDK use it. This post explains, field by field, where the two actually differ, so you can choose with confidence and debug faster when a request that worked in one format fails in the other.

The same request, two ways

Here is the same one-question request in each format, using the official Python SDKs pointed at our gateway. Both use the same key and the same model.

OpenAI format (Chat Completions)
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.oneaiapikey.com/v1",
    api_key=os.environ["LLM_API_KEY"],
)

resp = client.chat.completions.create(
    model="claude-sonnet-5",
    max_tokens=200,
    messages=[
        {"role": "system", "content": "You are a concise assistant."},
        {"role": "user", "content": "Explain idempotency in one sentence."},
    ],
)

print(resp.choices[0].message.content)
print(resp.choices[0].finish_reason)          # "stop", "length", "tool_calls"
print(resp.usage.prompt_tokens, resp.usage.completion_tokens)
Anthropic format (Messages)
import os
import anthropic

client = anthropic.Anthropic(
    base_url="https://api.oneaiapikey.com",               # no /v1: the SDK adds /v1/messages
    api_key=os.environ["LLM_API_KEY"],
)

msg = client.messages.create(
    model="claude-sonnet-5",
    max_tokens=200,                           # required in this format
    system="You are a concise assistant.",    # top-level, not a message
    messages=[
        {"role": "user", "content": "Explain idempotency in one sentence."},
    ],
)

print(msg.content[0].text)
print(msg.stop_reason)                        # "end_turn", "max_tokens", "tool_use"
print(msg.usage.input_tokens, msg.usage.output_tokens)

They look nearly identical, and for plain chat they are. Now the details.

Differences at a glance

Topic OpenAI format Anthropic format
Endpoint POST /v1/chat/completions (also /v1/responses) POST /v1/messages
Auth header Authorization: Bearer sk-... x-api-key: sk-... (gateways often also accept Bearer)
Version header none anthropic-version: 2023-06-01
System prompt a message with role system (or developer) top-level system field
max_tokens optional required
Response text choices[0].message.content content array of blocks, e.g. content[0].text
Stop signal finish_reason: stop, length, tool_calls stop_reason: end_turn, max_tokens, tool_use, stop_sequence
Usage prompt_tokens, completion_tokens input_tokens, output_tokens, plus cache read and write counts
Tool call arguments JSON string in arguments JSON object in input
Streaming data: chunks, ends with data: [DONE] named events: message_start, content_block_delta, message_stop and others
Prompt caching automatic on supported models explicit cache_control breakpoints

System prompts

In the OpenAI format, the system prompt is just the first message, with role system (newer OpenAI models also accept developer). In the Anthropic format, it is a separate top-level system field, which can be a string or a list of text blocks. Putting a system role inside messages in an Anthropic request is one of the most common porting errors; the native API only accepts user and assistant there.

This matters more than it seems. Because the Anthropic system field can be a list of blocks, you can attach a cache breakpoint to a long system prompt and pay the cached-input rate when it is reused. More on that in our cost estimation guide.

max_tokens and why “required” is a good thing

The Anthropic format requires max_tokens on every request. The OpenAI format lets you omit it, and the model then writes until it stops naturally or reaches the model’s own output limit.

Setting it explicitly in both formats is a good habit. Output tokens are the expensive side of every model’s price: on our current catalogue, output costs at least 2× as much as input for every model (for example, Claude Sonnet 5 is $2.20 per million input tokens and $11 per million output). A sensible cap protects you from a runaway response. When the cap is hit you get finish_reason: "length" or stop_reason: "max_tokens", which your code should treat as “the answer may be truncated”, not as success.

Content: strings vs blocks

OpenAI-format message content is usually a string, or a list of parts when you send images. Anthropic-format content is always conceptually a list of blocks: text, image, tool_use, tool_result, thinking and so on. The response is also a list of blocks, so msg.content[0].text works for simple answers but will break when the first block is a thinking or tool-use block. Robust code loops over blocks and joins the ones with type == "text".

Tool calling

Both formats support function calling, but the shapes differ enough that a hand-written adapter needs care.

Defining a tool. OpenAI wraps the schema in a function object and calls it parameters. Anthropic puts the name at the top level and calls the schema input_schema:

// OpenAI format
{ "type": "function",
  "function": { "name": "get_weather",
                "parameters": { "type": "object", "properties": { "city": { "type": "string" } } } } }

// Anthropic format
{ "name": "get_weather",
  "input_schema": { "type": "object", "properties": { "city": { "type": "string" } } } }

Receiving a call. OpenAI returns message.tool_calls, each with function.arguments as a JSON-encoded string that you must parse. Anthropic returns a tool_use content block whose input is already an object.

Returning the result. OpenAI expects a new message with role tool and the matching tool_call_id. Anthropic expects a user message containing a tool_result block with the matching tool_use_id.

If you only ever use one SDK, none of this matters. It matters when you switch formats, or when a tool’s “OpenAI-compatible” provider setting is used with a Claude model: the gateway translates between the two, which works well for standard tool calls but may not carry newer, format-specific features.

Streaming

Both formats stream over Server-Sent Events, but the events differ:

  • OpenAI format: every event is an unnamed data: line holding a chunk object. Text arrives in choices[0].delta.content. The stream ends with a literal data: [DONE]. Token usage is only included in the final chunk if you ask for it with stream_options: {"include_usage": true}.
  • Anthropic format: events are named. You get message_start, then for each block a content_block_start, several content_block_delta events (text arrives as text_delta), and a content_block_stop. Then message_delta (carrying stop_reason and output usage) and message_stop. There may be ping events in between and an error event if something goes wrong mid-stream.

The SDKs hide these differences, so prefer them over hand-written parsers. If you need to write one, our streaming guide has working parsers for both.

Usage and prompt caching

Usage fields are named differently (prompt_tokens vs input_tokens), and the Anthropic format reports cache activity separately as cache_creation_input_tokens and cache_read_input_tokens. In the OpenAI format, cached tokens appear under usage.prompt_tokens_details.cached_tokens on models that support it.

The bigger difference is control. OpenAI-format caching is automatic for long, repeated prompt prefixes on supported models. Anthropic-format caching is opt-in: you mark the end of a reusable prefix with "cache_control": {"type": "ephemeral"}. Explicit control is more work, but it is predictable, which is why agent tools that resend large contexts on every turn use it.

So when does the format actually matter?

1. When your tool only speaks one. This is the most common case and it decides itself:

  • Claude Code uses the Anthropic format via ANTHROPIC_BASE_URL.
  • Codex CLI, Cursor, Cline and most other editors and agents use the OpenAI format via a custom base URL.

2. When you need a model only one format exposes. On Relaykit, Claude models are available in both formats; the other families are OpenAI format only:

  • OpenAI format (11 models): Claude Opus 5.5, Claude Sonnet 5, Claude Haiku 4.5, GPT-6.1 Sol, GPT-5.6 Sol, GPT-5.6 Terra, Grok 4.6, Claude Sonnet 5.5, Grok 4.7, DeepSeek V4 Flash, DeepSeek V4 Pro.
  • Anthropic format (4 models): Claude Opus 5.5, Claude Sonnet 5, Claude Haiku 4.5, Claude Sonnet 5.5.

If you want to compare Claude with GPT or Grok in your own app using one code path, the OpenAI format is the practical choice.

3. When you rely on Claude-specific features. Explicit cache breakpoints, extended thinking blocks and other Claude features are first-class in the Messages API. Through a translation layer they may be simplified or missing. If you are building on Claude specifically, use the Anthropic SDK.

4. When you parse responses yourself. Any code that reads raw JSON, logs usage or bills customers per token must handle the field names in the table above. This is where most “it worked yesterday” bugs come from after switching formats.

Porting checklist

When moving code from one format to the other:

  1. Change the base URL: OpenAI SDKs need /v1 on the end; Anthropic SDKs must not have it.
  2. Move the system prompt between messages and the top-level system field.
  3. Add max_tokens if you are moving to the Anthropic format.
  4. Rewrite tool definitions, tool-call parsing (string vs object) and tool-result messages.
  5. Update the response reading code: choices[0].message.content vs looping over content blocks.
  6. Update usage and stop-reason handling, including truncation checks.
  7. Re-test streaming; event names and the end-of-stream signal differ.

Next steps

More in Blog

Try it

One API key for Claude, GPT, Grok and DeepSeek.

Relaykit speaks the OpenAI and Anthropic formats, so you change the base URL and the key, not your code. Top-ups start at $3, credit packs at $10, and every model has a published per-token rate.