LLM Migration: GPT to Claude to Open Source

We design and deploy artificial intelligence systems: from prototype to production-ready solutions. Our team combines expertise in machine learning, data engineering and MLOps to make AI work not in the lab, but in real business.
Showing 1 of 1All 1566 services
LLM Migration: GPT to Claude to Open Source
Medium
~1-2 weeks
Frequently Asked Questions

AI Development Areas

AI Solution Development Stages

Latest works

  • image_website-b2b-advance_0.webp
    B2B ADVANCE company website development
    1319
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1226
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    927
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1161
  • image_logo-advance_0.webp
    B2B Advance company logo design
    622
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    897

LLM Migration: GPT to Claude to Open Source

Imagine: your service running on GPT-4 starts delivering irrelevant answers after switching to Claude. Or you want to move to open source models to cut costs but fear losing quality. We've faced this dozens of times. Our experience shows: a properly planned migration not only preserves but can improve quality by choosing a better model for your task.

Consider a specific case: migrating from GPT-4 to Claude 3 Sonnet for a support chatbot. We adapted 150 prompts, rewrote tool call logic, and ran A/B testing. Result: 60% cost reduction (from $10,000/month to $4,000/month) with a 10% CSAT increase. This is not an isolated example: we have completed over 20 successful migrations in the past 5 years. Typical annual savings for our clients range from $5,000 to $30,000. One client cut their API costs from $12,000 to $3,500 after moving to an open-source model.

Problems Encountered During Migration

  • Different prompt formats. OpenAI uses a messages array with a system role; Claude uses a separate system parameter with XML tags. Directly transferring the system prompt causes loss of some instructions.
  • Different tool call structures. OpenAI uses functions with parameter descriptions; Claude uses tool_use with input_schema. Conversion requires type mapping and strict format adherence.
  • Different tokenization models and context windows. GPT-4 has an 8k/32k window; Claude 3 has 200k; open source models (LLaMA 3, Mistral) have 8k-32k. This affects strategies for splitting long texts.
  • Quality loss. Even with identical prompts, models behave differently. Without testing, accuracy can drop by 20-30%.

How We Solve These Problems

We use LLM-as-judge for automatic response comparison. This allows quickly assessing how well the new model matches the old one. For prompt adaptation, we wrote a PromptAdapter class that converts system prompts between providers and adds XML tags for Claude. The unified UnifiedLLMClient hides API differences and allows switching providers with a single line of code.

Our automated approach is 3 times faster than manual migration, and LLM-as-judge reduces testing time by 90%.

from anthropic import Anthropic
from openai import OpenAI
import json
import time
from typing import Callable

anthropic_client = Anthropic()
openai_client = OpenAI()

class LLMMigrationAnalyzer:
    """Analyzes compatibility and quality during migration"""

    def compare_responses(
        self,
        test_cases: list[dict],
        source_fn: Callable,
        target_fn: Callable,
    ) -> dict:
        """Compares responses of two models on test cases"""

        results = []
        for case in test_cases:
            source_response = source_fn(case["messages"], case.get("system"))
            target_response = target_fn(case["messages"], case.get("system"))

            # LLM-as-judge for quality assessment
            quality_score = self.judge_quality(
                case["messages"][-1]["content"],
                source_response,
                target_response,
            )

            results.append({
                "input": case["messages"][-1]["content"],
                "source": source_response[:200],
                "target": target_response[:200],
                "quality_score": quality_score,
                "recommendation": "migrate" if quality_score >= 0.8 else "review",
            })

        return {
            "total_cases": len(results),
            "safe_to_migrate": len([r for r in results if r["recommendation"] == "migrate"]),
            "needs_review": len([r for r in results if r["recommendation"] == "review"]),
            "avg_quality": sum(r["quality_score"] for r in results) / len(results),
            "cases": results,
        }

    def judge_quality(self, question: str, source: str, target: str) -> float:
        """Evaluates quality of target response relative to source"""
        response = openai_client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{
                "role": "user",
                "content": f"""Compare two AI responses to the same question.
Question: {question}
Response A: {source[:500]}
Response B: {target[:500]}

Rate Response B compared to A on a scale 0-1 where:
1.0 = B is better or equal to A
0.7 = B is slightly worse but acceptable
0.5 = B has notable quality degradation
0.0 = B is significantly worse

Return only a number."""
            }],
            temperature=0,
        )
        try:
            return float(response.choices[0].message.content.strip())
        except ValueError:
            return 0.5

Prompt Adaptation During GPT to Claude Migration

class PromptAdapter:
    """Adapts prompts between providers"""

    # Differences between models
    GPT_TO_CLAUDE_RULES = {
        # OpenAI uses messages array for system
        # Claude uses separate system parameter
        "system_prompt": "separate_parameter",

        # Claude prefers XML tags for structuring
        # GPT does not require special formatting
        "prefer_xml_tags": True,

        # Claude follows instructions with explicit constraints better
        "explicit_constraints": True,
    }

    def adapt_system_prompt(self, gpt_system: str) -> str:
        """Adapts system prompt for Claude"""
        response = anthropic_client.messages.create(
            model="claude-haiku-4-5",
            max_tokens=2048,
            messages=[{
                "role": "user",
                "content": f"""Adapt this system prompt from OpenAI GPT to Anthropic Claude.

Adaptation rules:
- Preserve the main meaning and instructions
- Use XML tags for structuring (<instructions>, <constraints>, <format>)
- Claude follows concrete examples better, add them if needed
- Remove mentions of "GPT", "ChatGPT" if present

Original prompt:
{gpt_system}

Return only the adapted prompt."""
            }]
        )
        return response.content[0].text

    def adapt_function_tools(self, openai_tools: list) -> list:
        """Converts OpenAI tools to Claude tool_use format"""
        claude_tools = []
        for tool in openai_tools:
            if tool.get("type") == "function":
                func = tool["function"]
                claude_tools.append({
                    "name": func["name"],
                    "description": func["description"],
                    "input_schema": func.get("parameters", {
                        "type": "object",
                        "properties": {}
                    })
                })
        return claude_tools

Abstraction Layer for Smooth Migration

from enum import Enum

class LLMProvider(str, Enum):
    OPENAI = "openai"
    ANTHROPIC = "anthropic"
    OLLAMA = "ollama"

class UnifiedLLMClient:
    """Unified interface for all providers"""

    def __init__(self, provider: LLMProvider, model: str):
        self.provider = provider
        self.model = model

    def complete(self, messages: list[dict], system: str = "", **kwargs) -> str:
        """Unified method for all providers"""

        if self.provider == LLMProvider.ANTHROPIC:
            response = anthropic_client.messages.create(
                model=self.model,
                max_tokens=kwargs.get("max_tokens", 2048),
                system=system,
                messages=messages,
                temperature=kwargs.get("temperature", 0.1),
            )
            return response.content[0].text

        elif self.provider == LLMProvider.OPENAI:
            all_messages = []
            if system:
                all_messages.append({"role": "system", "content": system})
            all_messages.extend(messages)
            response = openai_client.chat.completions.create(
                model=self.model,
                messages=all_messages,
                max_tokens=kwargs.get("max_tokens", 2048),
                temperature=kwargs.get("temperature", 0.1),
            )
            return response.choices[0].message.content

        elif self.provider == LLMProvider.OLLAMA:
            import requests
            all_messages = []
            if system:
                all_messages.append({"role": "system", "content": system})
            all_messages.extend(messages)
            response = requests.post(
                "http://localhost:11434/v1/chat/completions",
                json={"model": self.model, "messages": all_messages}
            )
            return response.json()["choices"][0]["message"]["content"]

# Change provider — single line
client = UnifiedLLMClient(LLMProvider.ANTHROPIC, "claude-haiku-4-5")
# -> client = UnifiedLLMClient(LLMProvider.OPENAI, "gpt-4o-mini")

Why Direct Prompt Transfer Doesn't Work

OpenAI uses system message as part of the context; Claude uses a separate parameter with higher weight. If you simply copy the text, Claude may ignore some instructions due to lack of XML markup. Additionally, GPT and Claude interpret roles differently: in GPT you can specify "role": "system", while in Claude the system is set outside the message array. Without adaptation, you risk getting formal, templated responses or context loss.

How to Accelerate Migration Without Losing Quality

Our approach is automation through LLM-as-judge and PromptAdapter. We collect 50-100 real production requests, run them through both models, and evaluate quality. If the average score is below 0.8, we adapt prompts. This surfaces problematic cases in a day instead of a week of manual testing. For typical tasks, we use adaptation templates, speeding up the process 2-3 times.

Typical Migration Mistakes
  • Copying system prompt without XML tags for Claude.
  • Ignoring context window differences (truncating text without adaptation).
  • Skipping testing of edge cases (long dialogues, tool calls).
  • Omitting a fallback strategy for failures.

Step-by-Step Migration Process

  1. Collect test cases – Gather 50-100 real user queries from production logs.
  2. Run A/B comparison – Use LLM-as-judge to compare responses from old and new models.
  3. Adapt prompts and tools – Convert system prompts and function calls to target provider format.
  4. Deploy unified client – Implement UnifiedLLMClient with fallback support.
  5. Gradual rollout – Route 5% of traffic, monitor metrics, then increase to full deployment.

What's Included in the Service

Stage What We Do Result
Analysis Examine current architecture, collect 50-100 test requests Compatibility report
Prompt Adaptation Convert system prompts and tool calls Adapted prompts tested on test cases
Client Development Implement UnifiedLLMClient with fallback support Unified interface for all providers
A/B Test Route 5% of traffic to new model Comparison of quality and cost metrics
Rollout Gradually increase new model share to 100% Stable operation on new LLM
Documentation and Training Document provider switching process, train team Documentation and workshop
Support 2 weeks of post-migration monitoring Quick resolution of any issues

Timeline

  • Compatibility analysis + testing: 1 week
  • Prompt and tool adaptation: 1 week
  • A/B test in production + rollout: 1-2 weeks

Migration Checklist

Step Action Criticality
1 Collect 50-100 test requests from production Required
2 Conduct A/B comparison via LLM-as-judge Required
3 Adapt system prompts Required
4 Convert format of tool calls Required
5 Update error handling (different error codes) Required
6 Set up retry/fallback Recommended
7 Update cost monitoring Recommended
8 A/B test in production (5% traffic) Recommended

We offer a free assessment of your project. We will analyze your current architecture and propose a migration plan tailored to your budget. With over 5 years of experience and 20+ completed projects, we guarantee quality preservation and support at all stages.