← All posts

How to use Claude with the OpenAI SDK, and what breaks

NemanjaFounder @ ApexApi7 min read
How to use Claude with the OpenAI SDK, and what breaks

You already have working code against the OpenAI SDK and you want to run it on Claude. You do not need Anthropic's SDK, and you do not need to rewrite the call sites. Three values change and everything else stays as written.

from openai import OpenAI

client = OpenAI(
    api_key=os.environ["APEXAPI_KEY"],
    base_url="https://api.apexapi.dev/v1",
)

response = client.chat.completions.create(
    model="anthropic/claude-opus-5",
    messages=[
        {"role": "system", "content": "You are a precise technical editor."},
        {"role": "user", "content": "Tighten this paragraph without losing detail."},
    ],
)
print(response.choices[0].message.content)

That runs against Claude Opus 5. Your streaming loop, your retry logic, your message builders and your error handling are untouched, because the SDK only ever cared about the URL it posts to and the JSON it gets back.

The interesting part is what happens in between, and where the translation runs out.

Why this needs a translation layer at all

Anthropic's native API is not OpenAI-shaped. The formats express the same ideas with different structure, and three differences do real work.

The system prompt lives somewhere else. OpenAI puts it in the messages array as a message with role: "system". Anthropic puts it in a top-level system field outside the array. A converter has to lift it out or push it in depending on direction.

Tools are declared differently. An OpenAI tool is a function with a parameters schema. An Anthropic tool declares an input_schema. Same JSON Schema underneath, different key, so a literal pass-through fails validation.

Tool results are structured differently. OpenAI models a tool result as its own message with role: "tool" and a tool_call_id. Anthropic carries tool_use and tool_result as content blocks inside messages, and a single user message can hold several tool results at once.

That last one is the least obvious and the most work. Converting an Anthropic-shaped conversation into chat completions means taking one user message containing three tool_result blocks and splitting it into three separate tool role messages, emitted in order, before whatever user text remained in that message. Get the order wrong and the model sees results attached to the wrong calls.

Our compatibility layer does this in both useful directions. The chat completions endpoint detects an Anthropic Messages-shaped body, by looking for a top-level system, tools carrying input_schema, or tool_use and tool_result content blocks, and converts it before the request reaches the router. So a client written against Anthropic's format also works on the OpenAI path, without you writing an adapter.

One precision worth stating, because it is the kind of thing that wastes an afternoon: that detection happens on the chat completions path. There is no /v1/messages route to post to. If your client targets Anthropic's URL by path rather than by body shape, it will not find it.

What survives the translation

Nearly everything you actually use.

Multi-turn conversations, system prompts, temperature, max_tokens, top_p, stop sequences and streaming all behave as expected. Streaming arrives as server-sent events with delta fragments, terminating normally, which means an existing streaming parser needs no changes.

Vision works. Every Anthropic model in the catalog supports image input, and OpenAI-style image_url content parts carry through, including base64 data URIs.

Tool calling works, with the caveat that tool calling is the least standardized part of any provider switch. If your workload depends on it, why tool calling breaks when you switch AI providers covers the failure modes that only appear on the second turn.

Context windows are large enough that this is rarely the constraint. The current Anthropic models in the catalog:

ModelContextPrice per 1M in / out
Claude Opus 51,000,000$6.00 / $30.00
Claude Opus 4.81,000,000$6.00 / $30.00
Claude Sonnet 4.61,000,000$3.60 / $18.00
Claude Haiku 4.5200,000$1.20 / $6.00

All four support tools and vision. Note the Haiku context window, which is five times smaller than the others and is the one that catches people who assume the family is uniform.

What does not survive

Four things, in rough order of how likely they are to matter to you.

Anthropic-specific controls have nowhere to live. If a parameter exists only in Anthropic's schema, an OpenAI-shaped request has no field for it. Unknown fields are forwarded upstream rather than rejected or dropped, so a parameter that happens to be valid downstream can still reach the provider, but nothing guarantees the OpenAI SDK will let you set it in the first place. If you depend on a control that only exists natively, that is a reason to use the native SDK for that call path.

Token counts differ, so your estimates transfer badly. Different tokenizers produce different counts for identical text. A prompt you sized against an OpenAI model does not have the same cost or the same fit against Claude. Treat the usage object in the response as authoritative rather than trusting a local count, and re-derive your cost model rather than scaling the old one.

Embeddings are not part of this. Anthropic does not offer an OpenAI-style embeddings endpoint and neither do we. Any client.embeddings.create() call in your codebase needs to keep pointing wherever it points today. This is the single most common reason a migration that looked like three lines turns into two days, so grep for it before you start.

Stored conversation history may not replay. New conversations are fine. If you persist history and let users resume, and that history contains tool calls, the structural difference in how results are represented will surface exactly there. Worth testing on real stored data rather than on a fresh session.

Cost, and the thing worth checking first

Coding-shaped and agent-shaped workloads are output heavy, and output is where the price sits. Opus at $30 per million output tokens against Haiku at $6 is a five times difference on the number that dominates most bills. Sonnet at $18 sits in between and is frequently the right default.

Whatever you choose, the useful habit is measuring rather than estimating. Every response carries its exact cost in USD in the X-ApexApi-Cost header, to eight decimal places, including on streaming responses. That turns "what would this cost at scale" from a spreadsheet exercise into a number you read off a hundred real calls.

If you are running an evaluation across several Claude models, the fact that they share one key and one balance is the practical benefit here. Swapping anthropic/claude-opus-5 for anthropic/claude-haiku-4.5 in a loop needs no second credential, and the comparison ends up in one spend figure rather than two invoices.

When to use Anthropic's SDK instead

This article argues for a path, not against the alternative. The native SDK is the better choice in three situations.

Claude is the only model you call, now and later. Then the translation layer buys you nothing and adds a hop. Go direct.

You need new capabilities the day they ship. Anything intermediated lags the source. If being first on a new Anthropic feature matters to your product, the native SDK is where it appears first.

You depend on something with no OpenAI equivalent. If a native-only control is load-bearing for you, forcing it through a shape that has no field for it is the wrong shape of solution.

The case for the OpenAI SDK route is the mirror image. Your code already reaches several providers, or will. You want to compare Claude against other models on the same prompt without maintaining two clients and two billing relationships. Or the integration already exists and you are adding Claude to it rather than starting from nothing.

Verify it in five minutes

Before you commit, run the same five checks you would run against any provider switch, on your own workload rather than on a hello world.

# 1. Confirm the model ID exists and see its limits
curl -s https://api.apexapi.dev/v1/models \
  -H "Authorization: Bearer $APEXAPI_KEY" \
  | jq '.data[] | select(.id | startswith("anthropic/"))
        | {id, context_length, tools: .capabilities.tools}'

# 2. One non-streaming call, check choices, finish_reason and usage
curl -s https://api.apexapi.dev/v1/chat/completions \
  -H "Authorization: Bearer $APEXAPI_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "anthropic/claude-opus-5",
    "messages": [{"role": "user", "content": "Reply with the word ok."}]
  }'

Then repeat with "stream": true and watch for the terminating frame, exercise whichever advanced feature you actually depend on, and send one deliberately broken request so you can see the error shape your retry logic will branch on.

Five minutes there beats discovering the gap after the cutover. The quickstart has the rest of the setup, and the model catalog lists every Anthropic model with its context window and price.

Frequently asked questions

How do I use Claude with the OpenAI SDK?
Keep the OpenAI client and change three values. Point base_url at an OpenAI-compatible gateway that carries Anthropic models, swap the API key, and set the model string to an Anthropic slug such as anthropic/claude-opus-5. Your message arrays, streaming loop and error handling stay as written, because the SDK only cares about the URL it posts to and the JSON shape that comes back.
Does Anthropic's own API accept OpenAI-shaped requests?
Anthropic's native API uses its own Messages format, where a system prompt is a top-level field and tool calls are content blocks rather than a separate role. Getting the OpenAI SDK to talk to Claude therefore requires something in between that converts the shapes. A gateway does that conversion, which is why the change on your side is three values rather than a rewrite.
What is the difference between the OpenAI and Anthropic message formats?
Three things matter in practice. OpenAI puts the system prompt in the messages array as a system role, Anthropic puts it in a top-level system field. OpenAI declares tools with a parameters schema, Anthropic uses input_schema. OpenAI returns tool results as messages with role tool, Anthropic carries tool_use and tool_result as content blocks inside messages.
What does not work when calling Claude through the OpenAI SDK?
Anything with no counterpart in the OpenAI schema. Anthropic-specific controls have nowhere to live in an OpenAI request, token counts differ because the tokenizers differ, and any code path that calls embeddings needs its own home since neither the Anthropic API nor our gateway exposes an OpenAI-style embeddings endpoint.
Should I use the OpenAI SDK or Anthropic's native SDK?
Use the native SDK when Claude is the only model you call and you want features the moment they ship. Use the OpenAI SDK through a gateway when your code has to reach several providers, when you want to compare models on the same prompt without maintaining two clients, or when the integration already exists and you are adding Claude to it rather than starting fresh.
guidesanthropicapi

Recommended

More posts

One API key for every AI model

Start free