●FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtask●LIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its own●MCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS●PLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callback●SONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October●FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtask●LIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its own●MCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS●PLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callback●SONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
Migrating from OpenAI to the Claude API: Code Conversion to Zero-Downtime Production Rollout (2026)
How to migrate from OpenAI GPT-4 to the Claude API: authentication, message-format conversion, streaming, tool use, error handling, and a zero-downtime phased rollout, all with full implementation code.
Why Claude API Migrations Are Accelerating in 2026
In 2026, production teams worldwide are moving from OpenAI GPT-4 to the Claude API. The drivers are clear: Claude Sonnet 4.6 and Opus 4.6 deliver top-tier performance, the 200,000-token context window unlocks new use cases, and the pricing model is increasingly competitive.
Yet every developer tackling this migration hits the same wall: where do you even start? The two APIs share a similar philosophy but differ in enough specifics—message structure, streaming events, tool definitions, required parameters—that a naive find-and-replace breaks things fast.
Before diving in, it's worth familiarizing yourself with Claude API cost management. Claude API Cost Optimization Guide covers prompt caching and model selection strategies that will complement your migration work.
1. Understanding the Core Architectural Differences
Before writing a single line of code, internalize these structural differences. Misunderstanding them is the root cause of most migration bugs.
Message Structure
OpenAI places the system prompt inside the messages array as role: "system". Claude uses a dedicated top-level system parameter instead.
# OpenAI styleopenai_request = { "model": "gpt-4o", "messages": [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Explain Python decorators."} ]}# Claude style — system is a separate top-level parameterclaude_request = { "model": "claude-sonnet-4-6", "system": "You are a helpful assistant.", # ← top-level, not in messages "messages": [ {"role": "user", "content": "Explain Python decorators."} ], "max_tokens": 1024 # ← required in Claude, optional in OpenAI}
The Required max_tokens Parameter
Claude API treats max_tokens as required. Omitting it throws a validation error. In OpenAI, it's optional and defaults to model-specific limits.
Response Structure
# OpenAI response accesstext = response.choices[0].message.content# Claude response accesstext = response.content[0].text# response.content is a list — it can contain text blocks, tool use blocks, etc.
The list-based content structure reflects Claude's ability to interleave text, tool calls, and image outputs in a single response.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Convert OpenAI Function Calling to Claude Tool Use with working code, including tool_result block structure and stop_reason handling.
✦Prevent truncated output and instruction drift from missing max_tokens and merged system prompts, using per-task limits and system isolation.
✦Reproduce a zero-downtime phased rollout via an AIGateway, measuring cost and latency while keeping a clean rollback path.
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
# Before: OpenAIfrom openai import OpenAIclient = OpenAI(api_key="YOUR_OPENAI_API_KEY")# After: Claude (Anthropic SDK)import anthropicclient = anthropic.Anthropic(api_key="YOUR_ANTHROPIC_API_KEY")# Or rely on the ANTHROPIC_API_KEY environment variable:client = anthropic.Anthropic()
Node.js / TypeScript (SDK)
// Before: OpenAIimport OpenAI from 'openai';const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });// After: Claudeimport Anthropic from '@anthropic-ai/sdk';const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });// Or: reads ANTHROPIC_API_KEY automaticallyconst anthropic = new Anthropic();
Environment Variable Setup
# .env# OldOPENAI_API_KEY=sk-...# New (keep the old key during phased rollout)ANTHROPIC_API_KEY=sk-ant-...# OPENAI_API_KEY=sk-... # Keep as fallback during migration
3. Basic Text Generation Conversion
Python (Synchronous)
import anthropicfrom typing import Optionaldef chat_with_claude( user_message: str, system_prompt: Optional[str] = None, model: str = "claude-sonnet-4-6", max_tokens: int = 2048) -> str: """Drop-in replacement wrapper for OpenAI chat calls.""" client = anthropic.Anthropic() params = { "model": model, "max_tokens": max_tokens, "messages": [{"role": "user", "content": user_message}] } # Claude rejects an empty string for system — only set it when non-empty if system_prompt: params["system"] = system_prompt response = client.messages.create(**params) return response.content[0].text# Usageresult = chat_with_claude( user_message="What is asyncio in Python?", system_prompt="You are an experienced Python developer. Be concise.")print(result)
Multi-turn conversations follow the same messages array pattern as OpenAI, with one important rule: conversations must start and end with a user message.
def multi_turn_chat(): client = anthropic.Anthropic() history = [] def chat(user_input: str) -> str: history.append({"role": "user", "content": user_input}) response = client.messages.create( model="claude-sonnet-4-6", max_tokens=1024, system="You are a helpful coding assistant.", messages=history ) assistant_reply = response.content[0].text history.append({"role": "assistant", "content": assistant_reply}) return assistant_reply print(chat("What is FastAPI?")) print(chat("What are its three main advantages?")) print(chat("Show me a minimal Hello World example."))multi_turn_chat()
4. Streaming Response Migration
Streaming is one of the biggest implementation differences. Claude uses different event types and a context-manager pattern that feels cleaner than OpenAI's iterator-based approach.
Python Streaming
import anthropicdef stream_chat(user_message: str, system_prompt: str = "") -> str: client = anthropic.Anthropic() full_response = "" params = { "model": "claude-sonnet-4-6", "max_tokens": 2048, "messages": [{"role": "user", "content": user_message}] } if system_prompt: params["system"] = system_prompt # Recommended: use the context-manager streaming interface with client.messages.stream(**params) as stream: for text in stream.text_stream: print(text, end="", flush=True) full_response += text print() # newline after streaming finishes return full_responsestream_chat( "Explain Python decorators in detail.", "Provide clear technical explanations.")
Node.js Streaming
import Anthropic from '@anthropic-ai/sdk';const anthropic = new Anthropic();async function streamChat( userMessage: string, systemPrompt?: string): Promise<string> { let fullResponse = ''; const params: Anthropic.MessageStreamParams = { model: 'claude-sonnet-4-6', max_tokens: 2048, messages: [{ role: 'user', content: userMessage }], }; if (systemPrompt) params.system = systemPrompt; const stream = anthropic.messages.stream(params); stream.on('text', (text) => { process.stdout.write(text); fullResponse += text; }); await stream.finalMessage(); console.log(); return fullResponse;}streamChat( 'Explain the TypeScript type system.', 'Target an experienced developer audience.').then((result) => { console.log(`\nDone: ${result.length} characters`);});
5. Tool Use: Converting Function Calling
This is the most involved part of the migration. The concepts are identical, but the schema format changes in one key place.
Schema Conversion
# OpenAI Function Callingopenai_tools = [ { "type": "function", "function": { "name": "get_weather", "description": "Get current weather for a city.", "parameters": { # ← "parameters" in OpenAI "type": "object", "properties": { "location": {"type": "string"}, "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]} }, "required": ["location"] } } }]# Claude Tool Useclaude_tools = [ { "name": "get_weather", "description": "Get current weather for a city.", "input_schema": { # ← "input_schema" in Claude "type": "object", "properties": { "location": {"type": "string"}, "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]} }, "required": ["location"] } }]
Tool Use Agent Loop
import anthropicimport jsonfrom typing import Anydef process_tool_call(name: str, inputs: dict) -> Any: if name == "get_weather": return {"temperature": 22, "condition": "Sunny", "humidity": 60} return {"error": f"Unknown tool: {name}"}def tool_use_agent(user_message: str) -> str: client = anthropic.Anthropic() tools = [ { "name": "get_weather", "description": "Get current weather for a city.", "input_schema": { "type": "object", "properties": { "location": {"type": "string"}, "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]} }, "required": ["location"] } } ] messages = [{"role": "user", "content": user_message}] while True: response = client.messages.create( model="claude-sonnet-4-6", max_tokens=2048, tools=tools, messages=messages ) if response.stop_reason == "end_turn": return " ".join( block.text for block in response.content if block.type == "text" ) if response.stop_reason == "tool_use": # Add assistant response to history messages.append({"role": "assistant", "content": response.content}) # Execute each tool call and collect results tool_results = [] for block in response.content: if block.type == "tool_use": result = process_tool_call(block.name, block.input) tool_results.append({ "type": "tool_result", "tool_use_id": block.id, "content": json.dumps(result) }) messages.append({"role": "user", "content": tool_results}) else: break return "Done"print(tool_use_agent("What's the weather in Tokyo right now?"))
For advanced prompt design patterns that apply equally to both APIs, see Prompt Engineering Patterns for Production.
What the migration taught me that the docs don't mention
The conversion patterns above cover the mechanics. Let me close with a few things I only learned after moving my own services from OpenAI to the Claude API in production.
I run Dolice Labs, a set of AI technical blogs with an automated content-formatting pipeline, as an indie developer. For a long time its backend leaned on OpenAI-based summarization and formatting logic, and the description generator for a small AdMob-monetized app shared the same lineage. When I moved them to the Claude API, the hard part wasn't the code conversion — it was the behavioral differences that surfaced quietly afterward.
max_tokens directly shapes output quality
OpenAI tolerates omitting max_tokens; Claude requires it. The body already notes this, but in practice the surprise was that a placeholder limit chosen during porting resurfaced later as truncated summaries. Right after the cutover, formatting tasks intermittently lost their tails, and the cause was a too-small limit I had left in during the port.
Here are the values that proved stable in my environment, by task:
Task
Feel before (OpenAI)
Recommended max_tokens (Claude)
Truncation
Classification / labeling
Fine unset
256
None
Article summary (~400 chars)
Fine unset
1,024
Sporadic below 512
Long-form formatting
~2,048
4,096
Tail loss at 2,048
Raising the ceiling does not inflate the bill when the actual output is short, so erring on the generous side is safe. That is my takeaway after the migration.
Isolating system alone improved instruction-following
On OpenAI I used to fold role instructions into the first message. Claude expects them in the system parameter. That looks like a formatting nicety, but moving them over changed how reliably instructions were followed. In my formatting pipeline, format drift — ignoring the heading hierarchy I had specified, for instance — visibly dropped. Once I consolidated tone and constraints into system and routed only the variable input through user, things stabilized without lowering the temperature.
Cost intuition is not captured by the per-token price table
It is tempting to judge the migration from the unit-price table, but the bill is driven by average tokens per request and how well caching applies. In my automation, pinning the shared instructions in system and combining that with prompt caching cut the billed input tokens by roughly 40-60% in practice. How you handle the repeated, fixed portion matters more to the monthly cost than the headline unit price. See the Claude API Cost Optimization Guide and Semantic Cache Production Design for the details.
A migration looks like a one-time excavation, but in hindsight it was a process of getting used to small behavioral differences while running in production. I hope it spares fellow developers a few of the stumbles.
Summary: Your Migration Checklist
Migrating from OpenAI to Claude is well worth the effort for most teams. Here's your final checklist:
Before writing code: Set ANTHROPIC_API_KEY, add max_tokens to every call, and move system prompts to the top-level system parameter.
During migration: Use the AIGateway pattern for staged rollout. Run the MigrationAnalyzer to measure latency and cost in parallel. Convert Function Calling to Tool Use carefully using the patterns in Section 5.
After going live: Monitor error logs for rate limit (429) and overload (529) events. Verify that the retry logic in Section 7 handles them gracefully. Keep the OpenAI key available as a rollback option for at least 30 days.
The 200K-token context window and strong long-document reasoning are available the moment you flip the switch — take advantage of them from day one.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.