You built your app on Claude. Now you need it on OpenAI too — maybe for cost, maybe for resilience, maybe a customer requires it. The good news: if your prompts are well-built, migration is
mostly mechanical renames, not a rewrite. Here's the exact field-by-field map, plus the two things that actually change.
Ref : https://ai.studybydoing.in/oap9-prompting-migration
The one-call translation
Everything starts with the single API call. Here's the same request on both SDKs:
Claude:
from anthropic import Anthropic
client = Anthropic()
resp = client.messages.create(
model="claude-opus-4-8", max_tokens=512,
system="You are terse.",
messages=[{"role": "user", "content": "Define idempotency."}],
)
text = next(b.text for b in resp.content if b.type == "text")
OpenAI (Responses API):
from openai import OpenAI
client = OpenAI()
resp = client.responses.create(
model="gpt-5.5", max_output_tokens=512,
instructions="You are terse.",
input="Define idempotency.",
)
text = resp.output_text
Both do the identical thing. Every change is a rename — and reading the reply collapses from a content-block loop to a single resp.output_text.
The full migration map
- **Client:** `Anthropic()` → `OpenAI()`
-
The call:
client.messages.create(...)→client.responses.create(...) -
System prompt:
system=→instructions= -
User content:
messages=[{role, content}]→input=(string or list) -
Output cap:
max_tokens→max_output_tokens -
Read the text: loop
resp.contenttext blocks →resp.output_text -
Structured output:
messages.parse(output_format=…)→responses.parse(text_format=…) -
Tool definition:
{name, input_schema}→{"type": "function", name, parameters} -
Tool request:
tool_useblock +tool_use_id→function_callitem +call_id -
Tool result back:
tool_resultin a user message →function_call_outputitem -
Loop control:
stop_reason == "tool_use"→ anyfunction_callinresp.output -
Reasoning:
thinking+output_config.effort→reasoning={"effort": …} -
Exceptions:
anthropic.APIError→openai.APIErrorText calls are a 5-minute change. The one place with real structural difference is tool-calling: Claude branches on stop_reason and matches tool_use blocks by tool_use_id; OpenAI has no stop_reason, so you loop while resp.output still contains function_call items and match by call_id.
The one prompting change that trips people up
On older models, "let's think step by step" measurably helped hard problems. On modern reasoning models, that's often redundant — the model reasons internally, and you control how much with
a dial, not a magic phrase:
# OpenAI
resp = client.responses.create(model="gpt-5.5", reasoning={"effort": "high"}, input=prompt)
So after migrating, don't just port your chain-of-thought incantations — raise effort for hard tasks and keep prompts clean.
A migration isn't done when it compiles — it's done when the evals pass
This is the part people skip. A different model behaves differently, so a prompt tuned on Claude may need adjustment on GPT. The right process:
- Swap the client + call surface using the table above.
- Move system → instructions, restructure tool loops.
- Put the model id in one config value.
- Re-run your eval suite against the OpenAI version — ideally behind a flag so you can run both and compare before cutting over.
Keep model choice in config and behind an interface, and "Claude vs OpenAI vs both" becomes a configuration decision, not a rewrite.
This is one lesson from a free, no-sign-up AI Engineering course where every code lesson shows Claude and OpenAI side by side on a toggle — so you can learn in either SDK, or migrate
between them. The full version of this chapter (with runnable examples you can execute in your browser) is here:
👉 Prompting GPT & Migrating from Claude — ai.studybydoing.in
The course covers the full path from Python foundations to production LLM systems — RAG, agents, evals, and now a complete OpenAI side alongside the Claude one.
Top comments (0)