n4nAI

How to migrate from OpenAI to Claude without rewrites

Step-by-step guide to migrating OpenAI to Claude API without rewriting your code, using an OpenAI-compatible gateway and minimal config changes.

n4n Team4 min read820 words

Audio narration

Coming soon — every post will get a voice note here.

Migrating OpenAI to Claude API often sounds like a full SDK swap: different client, different message schema, different tool-calling format. If your stack already uses the OpenAI Python or TypeScript client, you can switch to Claude by changing a base URL and a model string—no business logic changes required. This guide gives the exact steps to cut over with zero rewrites and how to prove the migration worked.

Step 1: Audit your current OpenAI integration

Open your codebase and find every place that constructs an OpenAI client or calls chat.completions.create. You care about five things:

  • The base_url (if overridden)
  • The model string
  • Presence of tools or functions
  • Use of stream=True or stream: true
  • Custom max_tokens, stop, or temperature settings

A typical Python call looks like this:

from openai import OpenAI

client = OpenAI(api_key="sk-openai-...")
resp = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "Summarize this log"}],
    max_tokens=512,
)

If you use the default api.openai.com/v1 endpoint, you are already speaking the OpenAI wire protocol. That protocol is what we will keep. Grep for import openai, OpenAI(, and .chat.completions to map your surface area. Note any middleware that inspects response headers or error shapes—those will be tested later.

Step 2: Choose an OpenAI-compatible gateway that fronts Claude

Anthropic’s native API uses a different request shape (top-level system field, anthropic-version header, distinct tool schema). You do not want to touch that from app code. Instead, route through a gateway that translates the OpenAI protocol to Claude’s backend.

A gateway like n4n.ai exposes one OpenAI-compatible endpoint that addresses 240+ models, including Claude, and handles the translation, automatic fallback when a provider is degraded, and per-token usage metering. Your app still thinks it is talking to OpenAI. The translation layer absorbs the schema differences so your openai SDK calls work unchanged.

Step 3: Swap the base URL and model identifier

Change two strings. Nothing else.

from openai import OpenAI

client = OpenAI(
    base_url="https://api.n4n.ai/v1",   # was: default OpenAI
    api_key="your-gateway-key",
)
resp = client.chat.completions.create(
    model="claude-3-5-sonnet-20241022",  # was: gpt-4o
    messages=[{"role": "user", "content": "Summarize this log"}],
    max_tokens=512,
)

Model ID mapping is straightforward:

OpenAI model Claude equivalent
gpt-4o claude-3-5-sonnet-20241022
gpt-4o-mini claude-3-haiku-20240307
gpt-4-turbo claude-3-opus-20240229

The gateway accepts the OpenAI chat completions path (/v1/chat/completions) and returns the same JSON shape. For a quick smoke test without code changes, use curl:

curl https://api.n4n.ai/v1/chat/completions \
  -H "Authorization: Bearer $GW_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-3-5-sonnet-20241022",
    "messages": [{"role": "user", "content": "ping"}]
  }'

You should get a choices[0].message.content string back, identical in structure to OpenAI’s response.

Step 4: Handle subtle protocol differences

The gateway hides most differences, but three areas deserve a look.

System prompts

OpenAI puts system instructions in a message with "role": "system". Claude expects a top-level system field. The gateway extracts the system message and maps it correctly. Keep your existing messages array; do not refactor.

Tools and function calls

OpenAI defines tools as:

{
  "tools": [
    {"type": "function", "function": {"name": "get_weather", "parameters": {...}}}
  ]
}

Claude’s native format differs, but the gateway translates the OpenAI schema into Anthropic’s tools block and back. Your tool_calls parsing code stays the same. Verify one complex tool call end to end before declaring victory.

Stop sequences and token limits

OpenAI accepts stop as an array of strings; Claude supports stop sequences similarly and the gateway passes them through. Max output tokens vary by Claude model—stay within the model’s published ceiling by reusing your existing max_tokens value only if it is safely below that limit.

Cache control and routing

Claude supports prompt caching via a cache_control breakpoint. The gateway honors client routing directives and forwards provider cache-control hints. If you want to cache a long system prompt, pass it through OpenAI-style extra_headers:

resp = client.chat.completions.create(
    model="claude-3-5-sonnet-20241022",
    messages=[{"role": "system", "content": LONG_PROMPT},
              {"role": "user", "content": "Go"}],
    extra_headers={"anthropic-cache-control": "ephemeral"},
)

This header is forwarded to Claude; your app code does not need Anthropic SDK.

Step 5: Validate streaming and token accounting

If you stream, flip stream=True and iterate as before:

stream = client.chat.completions.create(
    model="claude-3-5-sonnet-20241022",
    messages=[{"role": "user", "content": "Count to 5"}],
    stream=True,
)
for chunk in stream:
    if chunk.choices[0].delta.content:
        print(chunk.choices[0].delta.content, end="")

The chunk shape (choices[0].delta.content) is byte-for-byte compatible with OpenAI’s SSE format. Check the usage object on the final chunk (or response when not streaming). The gateway returns prompt_tokens and completion_tokens using Claude’s tokenizer counts. Wire those into your existing metering dashboards.

Step 6: Cut over in production with shadow traffic

Do not flip the switch globally on day one. Use an environment variable for base_url and model:

import os
from openai import OpenAI

client = OpenAI(
    base_url=os.getenv("LLM_BASE_URL", "https://api.openai.com/v1"),
    api_key=os.getenv("LLM_KEY"),
)
MODEL = os.getenv("LLM_MODEL", "gpt-4o")

Run a shadow test: send 5% of traffic to Claude via the gateway, compare outputs and latency. Because the gateway provides automatic fallback when a provider is rate-limited, a Claude outage will not take down your app if you configure fallback to a second model. Your existing retry logic against OpenAI-style 429s keeps working.

Step 7: Monitor, verify, and rollback

Watch gateway access logs for Claude model IDs. Confirm error rates match or beat the OpenAI baseline. If output quality regresses on a specific prompt, flip LLM_MODEL back to gpt-4o without a deploy—the code is identical. The per-token usage metering lets you compare cost per request directly.

Verification checklist

You have successfully finished migrating OpenAI to Claude API when:

  • All OpenAI() client constructions point at the gateway base URL.
  • No anthropic SDK imports exist in your app code.
  • A representative suite of non-streaming and streaming calls returns valid choices with expected content.
  • At least one tool-calling path executes and the parsed tool_calls matches your pre-migration schema.
  • usage.prompt_tokens and usage.completion_tokens are populated and logged.
  • Production traffic shows Claude model IDs in gateway access logs, confirming routing.
  • Shadow traffic comparison shows acceptable latency and output parity.

Migrating OpenAI to Claude API this way took our team an afternoon, not a sprint. Keep the OpenAI client, change two strings, and let the translation layer absorb the provider differences.

Tagsopenaiclaudemigrationapi-compatibility

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All migrating between llm providers posts →