OpenAI API vs Anthropic API: the format differences and what to pick for your code

Two request formats solve the same task but differ in details that break code when porting. A comparison of structure, tools, streaming, cache and headers.

6 min · · API keys · сравнение

Both formats send messages to a model and get an answer back, so they look interchangeable. But when porting code from one to the other, details surface: where system lives, whether max_tokens is required, what the response looks like and how to read tool calls. Below is an OpenAI API vs Anthropic API breakdown: a comparison of /v1/chat/completions and /v1/messages, code examples for each and an explanation of which format is more economical through a gateway. On Zukko any model can be called in either format, so the choice is determined by your code and spend, not by whose model it is.

Quick comparison

WhatOpenAI: /v1/chat/completionsAnthropic: /v1/messages
Key headerAuthorization: Bearerx-api-key (the gateway accepts Bearer too)
API versionnot neededanthropic-version: 2023-06-01 (the SDK sets it)
System instructiona message with role system in messagesa separate top-level system field
max_tokensoptionalrequired
Responsechoices[0].message.contenta list of content blocks, text in content[0].text
Token usageusage.prompt_tokens, completion_tokensusage.input_tokens, output_tokens and cache fields
Toolstools with type: "function", reply in tool_callstools with input_schema, reply as a tool_use block
Streamingdelta chunks, ending with [DONE]typed events (message_start, content_block_delta, message_stop)
Cache for Claudeno breakdown: all input at the input priceyes: cache read and write are accounted separately
Base URL in the SDKhttps://api.zukko.pro/v1https://api.zukko.pro (the SDK adds /v1)

Now point by point.

Request and response structure

OpenAI format

bash
import os
from openai import OpenAI

client = OpenAI(base_url="https://api.zukko.pro/v1", api_key=os.environ["ZUKKO_API_KEY"])

resp = client.chat.completions.create(
    model="claude-sonnet-5-5",
    messages=[
        {"role": "system", "content": "Answer briefly."},
        {"role": "user", "content": "What is a token?"},
    ],
    max_tokens=300,
)
print(resp.choices[0].message.content)
print(resp.usage.prompt_tokens, resp.usage.completion_tokens)

Anthropic format

bash
import os
import anthropic

client = anthropic.Anthropic(
    base_url="https://api.zukko.pro",  # no /v1: the SDK adds it
    api_key=os.environ["ZUKKO_API_KEY"],
)

resp = client.messages.create(
    model="claude-sonnet-5-5",
    system="Answer briefly.",
    messages=[{"role": "user", "content": "What is a token?"}],
    max_tokens=300,
)
print(resp.content[0].text)
print(resp.usage.input_tokens, resp.usage.output_tokens)

Notable differences: system is moved out of the message list, max_tokens is mandatory, and the response arrives as an array of blocks. There may be several blocks (for example text and then a tool call), so in real code select blocks with type == "text" rather than taking the first one.

For curl requests and field descriptions see the documentation: chat/completions and messages.

Tools (function calling)

In the OpenAI format a tool is described with a function wrapper, and the model returns the call in message.tool_calls; arguments arrive as a JSON string in function.arguments that you must parse, and the result goes back as a message with role tool and the tool_call_id:

bash
tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "Weather in a city",
        "parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},
    },
}]

In the Anthropic format the schema lives in input_schema, the call arrives as a tool_use block with an already parsed input object, and the result goes back as a user message with a tool_result block (carrying tool_use_id):

bash
tools = [{
    "name": "get_weather",
    "description": "Weather in a city",
    "input_schema": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},
}]

The full loop (call, run the function, return the result) takes roughly the same amount of code in both formats; only the field names differ.

Streaming

In the OpenAI format the server sends chunks with choices[0].delta, and at the end of the stream comes a chunk with usage and empty choices. So always check if chunk.choices: before accessing chunk.choices[0]:

bash
stream = client.chat.completions.create(
    model="claude-sonnet-5-5",
    messages=[{"role": "user", "content": "Tell me about prompt caching"}],
    stream=True,
    max_tokens=500,
)
for chunk in stream:
    if chunk.choices:
        print(chunk.choices[0].delta.content or "", end="")

In the Anthropic SDK a stream manager is more convenient:

bash
with client.messages.stream(
    model="claude-sonnet-5-5",
    messages=[{"role": "user", "content": "Tell me about prompt caching"}],
    max_tokens=500,
) as stream:
    for text in stream.text_stream:
        print(text, end="")

Under the hood these are typed events, and you do not have to parse them yourself. The event format is described in the streaming section of the documentation.

Cache and cost: the main practical difference

Here the format affects money. By Zukko's rules:

  • for Claude in the Anthropic format the gateway sees cache reads and writes (the cache_read_input_tokens and cache_creation_input_tokens fields) and bills them at their own prices;
  • for Claude in the OpenAI format there is no cache breakdown, and all input is billed at the input price;
  • for GPT the cache read is visible in both formats (prompt_tokens_details.cached_tokens).

An example. An agent step on claude-sonnet-5-5 (input $0.4, cache read $0.05 per 1M tokens, prices as of October 18, 2026) with a 50,000-token prefix that is fully read from the cache. We count only this prefix, without output and new tokens:

  • /v1/messages: 50,000 × 0.05 / 1,000,000 = $0.0025 per request;
  • /v1/chat/completions: 50,000 × 0.4 / 1,000,000 = $0.02 per request.

Over 100 requests that is $0.25 versus $2.00. This is an upper bound that assumes the cache hit on every request (the first cache write is billed separately, $0.5 per 1M for Sonnet 5.5). But the direction is clear: for long repeated prefixes (agents, chat with a big system prompt) it is more economical to call Claude through the Anthropic format. More in Claude via /v1/messages or /v1/chat/completions: what happens to the cache.

For the cache to work in the Anthropic format, the client must mark the cacheable part. Claude Code and many agents do it themselves. In your own code it looks like this:

bash
resp = client.messages.create(
    model="claude-sonnet-5-5",
    max_tokens=300,
    system=[{
        "type": "text",
        "text": LONG_INSTRUCTION,                   # a long stable prefix
        "cache_control": {"type": "ephemeral"},     # cache marker
    }],
    messages=[{"role": "user", "content": "A question about the instruction"}],
)
print(resp.usage.cache_creation_input_tokens, resp.usage.cache_read_input_tokens)

The minimum length of a cacheable prefix depends on the model; see Anthropic's documentation. The general formula: fresh input × input price + cache reads × read price + cache writes × write price + output × output price.

Other models through either format

Through the gateway models are not tied to "their own" format. GPT, Gemini, DeepSeek, Qwen and GLM can be called through both /v1/chat/completions and /v1/messages: the gateway translates the request. For example, glm-5.3-flash works in the Anthropic format, and the gateway converts the request and response itself. But models have different nuances: not all of them support the same set of parameters, so check behavior in your own code. The current model list and prices: GET https://api.zukko.pro/v1/models or the Models page in the dashboard.

What to choose

SituationFormat
You already have OpenAI SDK code, many models from different vendors/v1/chat/completions
The main model is Claude, long repeated prefixes, agents/v1/messages
Claude Code, the Anthropic SDK, any client in "Anthropic mode"/v1/messages (the client does it)
Cursor, Cline in OpenAI mode, LangChain/v1/chat/completions
A new project with no preferenceOpenAI format as the most widespread; Anthropic for Claude with cache

If you pick a provider in an IDE, use the Choose provider page in the Zukko docs: Anthropic with the address https://api.zukko.pro or OpenAI-compatible with /v1. The key is the same in both cases. A third endpoint, /v1/responses, is also available, but that is a separate topic.

Switching from one to the other: a checklist

  1. Change the base URL: with /v1 for the OpenAI SDK, without /v1 for the Anthropic SDK.
  2. Move system out of messages, or back.
  3. Add max_tokens for the Anthropic format.
  4. Rework response parsing and the usage fields.
  5. Rewrite tool descriptions and result handling.
  6. Check streaming, including the last chunk with usage.
  7. Run tests on your own prompts and compare spend in the dashboard.

One key works for both formats; you create it in the Zukko dashboard.

FAQ

Can I call Claude through the OpenAI SDK?

Yes, through /v1/chat/completions with the address https://api.zukko.pro/v1. It works, but the Claude cache is not broken down in this format and all input is billed at the input price.

Can I call GPT through the Anthropic SDK?

The gateway lets you call any model in any format, but check the behavior of the specific model in your code. For GPT the OpenAI format is simpler and more familiar.

How do the authorization headers differ?

OpenAI uses Authorization: Bearer, Anthropic uses x-api-key and anthropic-version. The Zukko gateway accepts the key in either header.

Why is max_tokens required in the Anthropic format?

That is how the Anthropic format is designed: the field is mandatory. In the OpenAI format you can omit it, but to control spend we recommend setting a limit in both.

zukko

Pay 25×
less

for Claude Code & ChatGPT

One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.

Get API keyDocumentation

api.zukko.pro · fetch_*

OpenAI API vs Anthropic API: format differences · Zukko