Gemini to Claude Migration: What Breaks and How to Fix It
Moving a production app from Gemini to Claude is almost never a model swap. The request shape changes, tool calls are matched differently, structured output uses a different mechanism, safety behavior moves from thresholds to refusals, and some inputs you relied on may not exist. This post walks through every place the migration breaks, gives you the adapter code to fix it, and ends with a rollout plan that keeps users unaffected.
Decide what you are actually migrating
Before touching code, write down which Gemini surface you use and which features you depend on. Teams on the AI Studio API with plain text generation have a small migration. Teams on Vertex AI with function calling, context caching, Google Search grounding, and video input have a large one. The difference is not the model. It is the number of Gemini-specific features baked into your request layer.
List these five things first: the models you call, whether you use tools, whether you use a response schema, which input types you send (text, images, PDF, audio, video), and whether you rely on caching or grounding. Every section below maps to one of them.
Model mapping and the cost reality
Pick the target tier by job, not by name. The closest equivalents are below, using the 2026 pricing on our provider hubs.
| Gemini model | Closest Claude model | Input / Output per M tokens | Context |
|---|---|---|---|
| Gemini 3.5 Pro | Claude Sonnet 4.6 (claude-sonnet-4-6) | $3.50 / $10.50 vs $3.00 / $15.00 | 2M vs 1M |
| Gemini 3 Ultra | Claude Opus 5 (claude-opus-5) | $12 / $36 vs $15 / $75 | 2M vs 500K |
| Gemini 3.5 Flash | Claude Haiku 4.5 (claude-haiku-4-5) | $0.30 / $2.50 vs $0.80 / $4.00 | 1M vs 200K |
| Gemini 3.5 Nano | No direct equivalent | n/a | n/a |
Two things stand out. Sonnet 4.6 is slightly cheaper on input than Gemini 3.5 Pro but noticeably more expensive on output, so output-heavy workloads can cost more after the move. And Haiku 4.5 costs more than Flash on both sides, so a blanket Flash to Haiku swap raises your bill. Plan for model routing from day one, and see the AI Chatbot Cost Guide for the optimization stack that closes the gap.
The context column matters more than it looks. If you ever send more than 1M tokens to Gemini, that workload cannot move as-is. It needs retrieval or chunking, which we cover below.
Break 1: The request shape
This is the break everyone hits first, and it is the easiest to fix with an adapter. Gemini sends a contents array of turns, each with a role and a list of parts. Claude sends a messages array of turns, each with a role and a list of content blocks. The differences that bite:
- Roles are
userandmodelin Gemini,userandassistantin Claude. - Gemini takes system text as
system_instruction. Claude takes it as a top-levelsystemparameter, never as a message. max_tokensis required on every Claude request. Gemini'smaxOutputTokensis optional, so code that omitted it will fail with a 400.- Claude temperature runs from 0 to 1. Gemini allows up to 2. Clamp it, and if you ran Gemini hot, retune.
- Claude expects alternating user and assistant turns, starting with a user turn. Merge consecutive turns from the same role.
Here is the adapter that handles all of it, including the tool parts we cover in the next section:
import uuid
ROLE_MAP = {"user": "user", "model": "assistant"}
def gemini_to_claude(contents, system_instruction=None):
messages = []
pending_ids = {} # function name -> tool_use id awaiting a result
for item in contents:
role = ROLE_MAP[item["role"]]
blocks = []
for part in item["parts"]:
if "text" in part:
blocks.append({"type": "text", "text": part["text"]})
elif "function_call" in part:
call = part["function_call"]
tool_id = "toolu_" + uuid.uuid4().hex[:16]
pending_ids[call["name"]] = tool_id
blocks.append({
"type": "tool_use", "id": tool_id,
"name": call["name"], "input": call.get("args", {}),
})
elif "function_response" in part:
resp = part["function_response"]
blocks.append({
"type": "tool_result",
"tool_use_id": pending_ids.pop(resp["name"]),
"content": str(resp["response"]),
})
if messages and messages[-1]["role"] == role:
messages[-1]["content"].extend(blocks)
else:
messages.append({"role": role, "content": blocks})
system = None
if system_instruction:
system = " ".join(p["text"] for p in system_instruction["parts"])
return system, messages
Note that Gemini function responses arrive in a user turn, and so do Claude tool results, which is why the role mapping lines up. Where it does not line up is the matching rule, covered next.
Break 2: Tool calling
Function calling is where most migrations lose a week. The concept is identical. The wiring is not.
Tools are matched by ID, not by name
Gemini pairs a functionResponse with a functionCall by function name. Claude pairs a tool_result with a tool_use by tool_use_id. If you only stored names in your agent loop, you now need to carry IDs through your whole tool pipeline. Every tool_use block in an assistant turn must be answered by a matching tool_result in the very next user turn, or the API rejects the request. Parallel calls mean several results in one user message.
Schema dialects differ
Gemini declarations use an OpenAPI 3.0 subset, and in some SDK paths the type names are uppercase (STRING, OBJECT). Claude's input_schema is standard JSON Schema with lowercase types. A small converter fixes it:
def to_json_schema(node):
if isinstance(node, dict):
out = {}
for key, value in node.items():
if key == "type" and isinstance(value, str):
out[key] = value.lower()
else:
out[key] = to_json_schema(value)
return out
if isinstance(node, list):
return [to_json_schema(item) for item in node]
return node
def declarations_to_tools(declarations):
tools = []
for decl in declarations:
schema = to_json_schema(
decl.get("parameters", {"type": "object", "properties": {}})
)
schema.setdefault("type", "object")
tools.append({
"name": decl["name"],
"description": decl.get("description", ""),
"input_schema": schema,
})
return tools
Because Claude accepts real JSON Schema, schemas you contorted to avoid Gemini's missing oneOf still work, and you can simplify them later. Keep tool counts reasonable: 8 to 12 per turn performs better than 30. The full pattern, including argument validation and retry, is in the Function Calling Reliability guide.
Break 3: Structured output
On Gemini you set responseMimeType to JSON and pass a responseSchema. Claude's production pattern is different: define your output shape as a tool and force the model to call it. You get schema-conformant JSON back as a parsed object, with no string to parse.
schema = to_json_schema(gemini_response_schema)
schema["additionalProperties"] = False
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1500,
temperature=0,
tools=[{
"name": "emit_result",
"description": "Return the extracted data.",
"input_schema": schema,
}],
tool_choice={"type": "tool", "name": "emit_result"},
messages=messages,
)
data = next(b.input for b in response.content if b.type == "tool_use")
Three details to carry over. Add additionalProperties: false at every object level, or the model may add fields. Keep client-side validation with Pydantic or Zod, because schema conformance does not stop invented values. And set max_tokens generously, since a truncated tool call is an incomplete object. The Structured Output Reliability guide covers the failure taxonomy end to end.
Break 4: Safety settings become refusals
Gemini lets you set per-category thresholds with safetySettings and signals blocks through finishReason: SAFETY or a promptFeedback.blockReason. Claude has no per-request threshold dial. Instead it declines in-model, and you detect it from the response.
- Delete your
safetySettingsplumbing. There is nothing to map it to. - Check
stop_reasonon every response and treat a refusal as a distinct outcome, not an empty answer. - Do not retry the identical prompt on a refusal. It will refuse again. Reword, add legitimate context, or route to a fallback.
- Rerun your sensitive-content test cases. Content Gemini passed at a loose threshold may be declined, and the reverse also happens.
Tip: Add a refusal-rate metric before you migrate. A jump from 0.2 percent to 2 percent is invisible in error dashboards because the HTTP status is still 200.
Break 5: Multimodal inputs and files
Gemini's Files API, inline base64 data, and native video and audio understanding have no one-to-one replacement. On Claude, images go in as image content blocks (base64 or URL) and PDFs go in as document blocks. Check the current docs for exact limits, since they change, but plan around the documented ones: an image size cap in the single-digit MB range and a page cap on PDFs.
- Images: resize and recompress before sending. An oversized image is a hard 400, not a graceful downscale.
- PDFs: split long documents by page range and summarize per chunk.
- Audio: transcribe first, then send text.
- Video: extract key frames and a transcript, then send both.
- Uploaded file URIs: Gemini file URIs expire after 48 hours and do not work with Claude. Re-host the source files yourself.
If video or audio understanding is core to your product, this is the one break that may justify keeping Gemini for that route permanently.
Break 6: Long context and token counting
The context ceiling drops from Gemini's 2M to 1M on Sonnet 4.6, 500K on Opus 5, and 200K on Haiku 4.5. Tokenizers also differ, so the same text produces a different count. Do not reuse your Gemini token estimates.
- Use Anthropic's token counting endpoint for real numbers instead of estimating.
- Budget a 10 to 20 percent margin on any request near the limit.
- For workloads that exceeded 1M tokens, move to retrieval or hierarchical summarization.
- Cap conversation history with a sliding window or rolling summary. The Context Window Overflow guide has the patterns.
Break 7: Caching
Gemini uses explicit CachedContent objects with a TTL and a minimum size, plus implicit caching on some surfaces. Claude uses cache_control markers on content blocks. You mark the stable prefix (system prompt, tool definitions, reference documents) and later requests that share that prefix read it at a fraction of the normal input price.
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=800,
system=[{
"type": "text",
"text": LONG_STABLE_SYSTEM_PROMPT,
"cache_control": {"type": "ephemeral"},
}],
tools=tools,
messages=messages,
)
Order matters: stable content first, volatile content last, or the prefix never matches. The cache is short-lived and refreshes on use, so it rewards steady traffic. There is a limit on how many breakpoints one request can carry, so mark the boundaries that matter. Our prompt caching setup guide covers the cross-provider details.
Break 8: Streaming
Gemini streams chunks from streamGenerateContent, each carrying candidate parts. Claude streams typed server-sent events: message_start, content_block_start, content_block_delta, content_block_stop, message_delta, and message_stop. Text arrives in text deltas. Tool arguments arrive as input_json_delta fragments of partial JSON that you must concatenate and parse only after the block stops.
with client.messages.stream(
model="claude-sonnet-4-6",
max_tokens=1000,
messages=messages,
) as stream:
for text in stream.text_stream:
send_to_client(text)
final = stream.get_final_message()
if final.stop_reason == "refusal":
handle_refusal(final)
Use the SDK stream helper instead of hand-parsing events. If you must parse raw events, never call JSON parse on a partial tool input. The most common streaming bug after migration is parsing a half-built argument string.
Break 9: Errors and retries
Your retry classifier almost certainly keys on Gemini status strings. Remap it:
| Gemini | Claude | Retry? |
|---|---|---|
| 400 INVALID_ARGUMENT | 400 invalid_request_error | No |
| 401 UNAUTHENTICATED | 401 authentication_error | No |
| 403 PERMISSION_DENIED | 403 permission_error | No |
| 429 RESOURCE_EXHAUSTED | 429 rate_limit_error | Yes, honor Retry-After |
| 500 INTERNAL | 500 api_error | Yes, with backoff |
| 503 UNAVAILABLE | 529 overloaded_error | Yes, with backoff or fail over |
The one that catches teams out is 529. If your code retries on 503 only, Claude overload events will pass straight through as failures. Also decide who owns retries. The SDK retries by default, so either accept that or set max_retries=0 and keep a single retry layer. Our Bulletproof Claude API Wrapper shows the full pattern.
Break 10: Prompt behavior
Even with perfect plumbing, behavior shifts. These are the changes that matter most:
- Structure with XML tags. Claude follows prompts that separate instructions, documents, and examples with tags like
<document>and<instructions>. - Put long documents first and the question last. This consistently improves long-context answers.
- Use the system parameter. Move persona and rules out of the first user turn.
- Be explicit about format. State length and structure instead of assuming house style carries over.
- Translate thinking controls. Gemini's
thinking_budgetbecomes Claude's extended thinking with abudget_tokensvalue. The budget has a minimum, and it must be lower thanmax_tokens.
Features without a one-to-one equivalent deserve their own list: Google Search grounding (use a web search tool or your own retrieval), URL context, native audio and video input, and the Nano on-device tier. Decide a replacement or an explicit cut for each before launch.
The rollout plan
Never flip the switch. Put both providers behind one interface, then move traffic in stages.
- Build the adapter. One internal interface, two implementations, selected by a flag.
- Assemble a golden dataset. 200 to 500 real production prompts with expected outputs or scoring rubrics. Include your ugliest edge cases.
- Run shadow mode. Send each request to both providers, return only Gemini's answer, and log the differences.
- Canary. Route 1 percent of live traffic to Claude, then 5, 25, 50, and 100, holding at each step long enough to see a full day of traffic.
- Automate rollback. Trip the flag back to Gemini if error rate, p95 latency, schema-failure rate, or refusal rate crosses a threshold.
- Keep Gemini warm. Leave it as a fallback behind a health-checked router. The Multi-Provider Load Balancer is the same adapter doing double duty.
Track these metrics per provider through the whole migration: schema-valid rate, tool-call success rate, refusal rate, p50 and p95 latency, cost per successful task, and a quality score from your golden set. If you only watch error rate, you will miss the failures that return HTTP 200.
Migration checklist
- Models mapped, with routing for simple traffic
contentsconverted tomessages, system moved tosystem,max_tokensset everywhere- Temperature clamped to 1 or lower and retuned
- Tool IDs carried through the agent loop, schemas converted to lowercase JSON Schema
- Structured output moved to forced tool calls with
additionalProperties: false safetySettingsremoved, refusal handling and metrics added- Images, PDFs, audio, and video re-handled, file URIs re-hosted
- Token counts re-measured with a safety margin
- Caching moved to
cache_controlwith stable content first - Streaming rewritten around the SDK helper
- Retry classifier remapped, including 529
- Shadow run, canary stages, and automatic rollback in place
Frequently asked questions
Can I reuse my Gemini prompts on Claude unchanged?
Mostly, but not blindly. Plain instructions transfer well. Prompts that lean on Gemini-specific habits (very high temperature, long unstructured context, system text mixed into the first user turn) usually need a pass. Run your golden dataset before and after, and expect to tune about one prompt in five.
Is there a shortcut for testing Claude with my existing Gemini code?
Gemini exposes an OpenAI-compatible endpoint and Anthropic offers an OpenAI SDK compatibility layer, so you can point a thin OpenAI-style client at either for quick evaluation. Treat it as a test harness only. Use the native Claude API in production for tools, caching, and extended thinking.
Which Claude model replaces Gemini 3.5 Flash?
Claude Haiku 4.5 is the closest tier, but it costs more per token than Flash. Many teams keep Flash for the highest-volume simple calls and route only quality-sensitive traffic to Claude. Measure cost per successful task, not cost per token.
Will my bill go up after migrating?
Often yes on output-heavy workloads and when moving from Flash to Haiku. Sonnet 4.6 input pricing is slightly below Gemini 3.5 Pro, but output is higher. Prompt caching, model routing, and shorter outputs usually close the gap. Model the numbers with your real token mix before you commit.
How do I replace Gemini Google Search grounding?
Use Claude's web search tool where available, or run your own retrieval step and pass the results as documents in the prompt. Keep source URLs in the context and ask Claude to cite them so you retain the grounding behavior your users expect.
What do I do about video and audio inputs?
Check the current Claude docs for supported input types, since this changes. If video or audio is not accepted directly, preprocess upstream: transcribe audio, extract key frames from video, and send text plus images.
Do I need to rewrite all my JSON schemas?
Usually only small changes. Lowercase the type names, add additionalProperties: false on every object, and move the schema into a tool input_schema. Schemas that avoided oneOf already travel well.
How do I migrate without users noticing?
Put both providers behind one interface, run Claude in shadow mode first, then canary 1 percent, 5 percent, 25 percent, and 100 percent with automatic rollback on error rate, latency, or schema-failure thresholds.
Where does the system prompt go in Claude?
In the top-level system parameter, not as a message. Gemini calls this system_instruction. Do not paste it into the first user turn, because that changes how the model weighs it.
Should I remove Gemini completely after migrating?
Not necessarily. Keeping Gemini as a warm fallback behind a load balancer protects you from Claude capacity events and gives you a cost lever for simple traffic. The adapter you build for migration is the same code that makes fallback possible.