Migrating from the OpenAI API to the Claude API
The request shape is close enough to migrate in an afternoon — and different enough that a blind cutover breaks in three predictable places: the system message, the required token limit, and tool calling.
https://aicomp.ai/v1).
Create one free →
Side-by-side request
OpenAI style:
resp = client.chat.completions.create(
model="gpt-5.6-luna",
messages=[
{"role": "system", "content": "You are a careful editor."},
{"role": "user", "content": "Proofread this."},
],
)
Claude style:
resp = client.messages.create(
model="claude-sonnet-5",
system="You are a careful editor.", # top-level, not a message
max_tokens=1024, # required
messages=[{"role": "user", "content": "Proofread this."}],
)
print(resp.content[0].text)
The mapping table
| Concept | OpenAI | Claude |
|---|---|---|
| System prompt | {"role":"system"} inside messages | Top-level system parameter |
| Token limit | max_tokens, optional | max_tokens, required |
| Text output | choices[0].message.content | content[0].text |
| Input usage | prompt_tokens | input_tokens |
| Output usage | completion_tokens | output_tokens |
| Stop sequences | stop | stop_sequences |
| Tool calling | functions / tools | tools with input_schema |
| JSON mode | response_format | Prefill or tool-based extraction |
| Auth header | Authorization: Bearer | x-api-key + anthropic-version |
Tool calling: the part that needs real rewriting
OpenAI puts arguments in function.arguments as a JSON string. Claude returns a tool_use content block with an already-parsed input object, and you return results as a tool_result block rather than a message with a role. Budget most of your migration time here.
tools = [{
"name": "get_weather",
"description": "Current weather for a city",
"input_schema": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
}]
What it costs after you move
| Model | Gateway rate in / out per 1M tokens | Official list in / out per 1M tokens | Diff |
|---|---|---|---|
| gpt-5.6-luna | $0.1 / $0.6 | $0.2 / $1.2 | 50% |
| claude-sonnet-5 | $1 / $5 | $2 / $10 | 50% |
| claude-opus-5 | $2.5 / $12.5 | $5 / $25 | 50% |
Rates checked 2026-11-01. Gateway rates move with upstream promotions (verify the current number in your dashboard before committing to a budget).
Do not assume the migration saves money. Compare tokens per task on real traffic, not list price per million. Models differ in how verbose they are, and output is the expensive side.
A cutover plan that does not hurt
- Shadow mode. Send the same prompts to both, store both outputs, ship the OpenAI one.
- Diff quality. Review where they disagree; those are your risk areas.
- Route 10%. Real traffic, real latency, real cost.
- Compare cost per task. Not per token (per completed task).
- Ramp. Increase only after a week of clean metrics.
Common errors during migration
| Error | Cause |
|---|---|
| max_tokens is required | Claude requires it; OpenAI made it optional |
| Invalid system message position | System prompt left inside messages |
| 401 / authentication_error | Using Authorization: Bearer instead of x-api-key |
| Tools ignored | Schema passed as parameters rather than input_schema |
Migration failures that look like model problems but are not
| Symptom | What is actually happening | How to confirm | Fix |
|---|---|---|---|
| The system prompt is silently ignored | OpenAI puts system instructions in a message with role=system; Anthropic expects a separate top-level field. | Ask the model to repeat the first line of its instructions, if it cannot, they never arrived. | Move the system message out of the messages array into the dedicated parameter. |
| max_tokens rejected or responses truncated | Anthropic requires an explicit output cap; OpenAI treats it as optional. | Look for a validation error naming the parameter, or outputs that stop mid-sentence. | Set the cap explicitly on every call rather than relying on a default. |
| Tool calls come back as text instead of structured blocks | The two APIs describe tools differently, and a schema copied across often fails to bind. | Inspect the response for a tool-use block versus plain content. | Re-declare tools in the target format; do not assume the schema transfers. |
| Streaming consumer breaks on the first chunk | The event names and the shape of each chunk differ between the two APIs. | Log the raw first three chunks and compare them to what your parser expects. | Port the streaming handler, not just the request builder. |
| Image or document input returns an error | Multimodal content blocks are structured differently and are not interchangeable. | Test with a single small attachment before converting the whole path. | Rebuild the content blocks in the target format. |
| Token counts differ from the old provider for the same text | Tokenisers differ; counts are close but not identical. | Compare usage numbers on one fixed input across both providers. | Budget with a margin rather than assuming parity. |
Rolling it out without finding out the hard way
The failure mode you want to avoid is discovering a problem through a production bill or a customer-visible error. Four steps, in order:
- Prove it on one read-only endpoint. One call, from a script, with an explicit timeout and the model ID echoed back. You are testing reachability, authentication and model availability (three things that can fail independently).
- Measure before you switch. Record tokens per task and cost per task on the current path first. Without that baseline, "it got cheaper" is an impression, not a result.
- Move one workload, not everything. Pick the workload with the most predictable shape, batch jobs over interactive traffic. Leave the interactive path on the old configuration until the batch numbers are in.
- Decide the rollback condition in advance. Write down what makes you revert (error rate above X, cost per task above Y, latency above Z) before you start, so the decision is not made under pressure.
Keep the base URL in configuration, never inline. That single choice is what makes step four take a minute instead of an afternoon.
FAQ
Can I reuse my OpenAI code with Claude?
Partly. If you went through an OpenAI-compatible layer, changing the base URL and key may be enough. If you called the OpenAI API directly, you need the changes in the mapping table above (mostly the system field, max_tokens, and tool calling).
What is the biggest gotcha in the migration?
max_tokens is required on Claude and optional on OpenAI. Requests that worked for months will fail with a validation error until you set it.
Does the response format change?
Yes. Instead of choices[0].message.content you read content[0].text, and usage is reported as input_tokens and output_tokens rather than prompt_tokens and completion_tokens.
How do I keep costs under control during migration?
Run both in parallel on a sample first, measure tokens per task on each, then cut over. Migrating blind is how teams double their bill on day one.
Can I migrate gradually?
Yes, and you should. Route a percentage of traffic, compare quality and cost for a week, then increase. The per-task cost difference is usually visible within a day of real traffic.