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 1564 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
    1359
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1251
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    957
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1188
  • image_logo-advance_0.webp
    B2B Advance company logo design
    646
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    929

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.

LLM Development: Fine-Tuning, RAG, Agents, and Production Deployment

Using GPT‑4 or Claude 3.5 Sonnet through a public API is not a solution — it's just a tool. When the requirement is to "make it like ChatGPT, but on our data," there is a real engineering challenge behind it: from prompt engineering to training a 70B model on your own infrastructure. End-to-end LLM solution development is a complex stack, and we have been doing it for over 5 years. During this time, we have completed over 20 projects in generative AI: from RAG systems for legal departments to custom support agents. Where exactly your task falls depends on data, latency requirements, budget, and how critical confidentiality is.

A typical situation: the client has already tried ChatGPT, but results are unstable — sometimes accurate, sometimes hallucinating. Or they need integration into a corporate portal while complying with security policies. Let's break down each layer of the stack in detail — from RAG to production deployment.

Why Do RAG Systems Break and How to Fix It?

RAG (Retrieval-Augmented Generation) looks simple: find relevant documents, put them in context, get an answer. In practice, it fails in several places.

Chunking without overlap. Classic mistake: chunk_size=512, overlap=0. If the answer lies across two chunks, retrieval won't find either with sufficient confidence. Solution: overlap 15–25% of chunk_size, or better yet, sentence-aware splitting with spaCy or NLTK instead of naive character splitting.

Poor embedder. text-embedding-ada-002 is good for general use, but on legal or medical texts, specialized models like E5-large-v2, BGE-M3, or fine-tuned sentence-transformers on domain data outperform it. Recall@5 differences can be 15–25%.

No re-ranking. Vector search optimizes for speed, not relevance. A cross-encoder re-ranker (ms-marco-MiniLM-L-6-v2, bge-reranker-large) after initial retrieval improves top-3 accuracy with acceptable latency (+50–150ms). This is often more impactful than improving the embedding model.

Hybrid search. Dense vectors alone work poorly on exact queries: names, SKUs, codes. BM25 (sparse) finds exact matches but misses semantics. Hybrid via RRF (Reciprocal Rank Fusion) is the optimal compromise. Qdrant, Weaviate, and pgvector 0.7+ support hybrid search natively.

Typical production architecture for a corporate knowledge base
  1. Documents → preprocessing (PyMuPDF, Unstructured)
  2. Chunking → embedding (BGE-M3)
  3. Qdrant (hybrid dense+sparse)
  4. Cross-encoder re-ranking
  5. Context → LLM (vLLM or OpenAI API)
  6. Answer with sources (RAGAS for quality evaluation)

When to Fine-Tune Instead of Prompt Engineering?

Prompt engineering solves ~70% of LLM adaptation tasks for a domain. The remaining 30% require fine-tuning. Three indicators: the model ignores a specific output format even with detailed prompting; the task requires deep knowledge of specialized vocabulary (medicine, law); you need to significantly reduce token costs by replacing a large model with a smaller specialized one.

LoRA and QLoRA are the standard for SFT. LoRA adds trainable low-rank matrices to attention layers. A typical configuration for Llama-3 8B: r=64, lora_alpha=128, target_modules=["q_proj","v_proj","k_proj","o_proj"] yields ~0.8% trainable parameters, training on one A100 40GB. QLoRA adds 4-bit quantization (NF4) and allows fine-tuning 70B models on two A100 40GB, though speed drops by half compared to bf16.

DPO instead of RLHF. Direct Preference Optimization requires only (chosen, rejected) pairs, not scalar reward signals. DPOTrainer from the trl library (Hugging Face) implements it in a few dozen lines.

Common mistake. A dataset of 500 examples, 5 epochs, validation loss 0.8 — seems fine. But on test, the model degrades on general instructions. Cause: catastrophic forgetting. Solution: add 10–20% general instruction-following examples (Alpaca, FLAN) to the training set to preserve original capabilities.

How to Choose a Base Model: 8B or 70B?

Model Parameters Strengths Context
Llama-3.1 8B 8B Quality/speed balance 128k
Llama-3.1 70B 70B Complex reasoning 128k
Mistral 7B / Mixtral 8x7B 7B / 47B Efficiency for size 32k
Qwen2.5 72B 72B Code, multilingual 128k
Gemma 2 27B 27B Open license 8k

For most tasks, fine-tuning an 8B model is sufficient. 70B is needed when deep reasoning is required or the 8B baseline does not reach the required quality even after fine-tuning. Inference cost for Llama-3 8B via vLLM on A100 is efficient; the exact cost depends on volume.

What Does PagedAttention Bring to Production?

vLLM is the first choice for serving open-source models. PagedAttention is the key technical innovation: KV-cache is managed like virtual memory in an OS, without fragmentation. This yields 2–4x higher throughput compared to naive HuggingFace Transformers inference. The vLLM documentation confirms that continuous batching and PagedAttention are the standard for high-load LLM services.

Typical numbers on A100 80GB for Llama-3 8B (bf16): 400–600 req/s, P50 latency 200–400ms, P99 latency 600–900ms at concurrency 64. For 70B on two A100 with tensor parallelism: 80–120 req/s, P99 latency 1.5–2.5s. AWQ or GPTQ quantization reduces memory consumption by 2x with quality loss within 1–3%.

Multi-Agent Systems

Agents are LLMs with access to tools: search, code execution, API calls, database interaction. Common patterns:

  • ReAct (Reason + Act): the model reasons → chooses a tool → observes the result → reasons again. LangChain and LlamaIndex implement it out of the box.
  • Multi-agent orchestration: multiple specialized agents with a coordinator on top. Example: coordinator → researcher (search + summarization) → coder (code generation and execution) → critic (verification). Tools: AutoGen (Microsoft), CrewAI, custom implementation on LangGraph.

In production, agent systems are non-deterministic. Essential: guardrails, step limits, logging of each step, human-in-the-loop for critical actions.

How We Work: Stages, Timeline, Deliverables

Stage Duration What You Get
Audit and data collection 1–2 weeks Eval dataset of 100+ examples, task formalization
Baseline (prompt + RAG) 1–2 weeks Working prototype, quality metrics
Fine-tuning (if needed) 2–4 weeks Trained model, LoRA weights, model card
Deployment and monitoring 1–2 weeks vLLM server, Grafana + Prometheus
Documentation and training 1 week API documentation, team training

What Is Included

We deliver:

  • Technical documentation (model card, configs, deployment instructions)
  • Access to infrastructure (code repository, trained weights)
  • 1 month of post-deployment support (consultations, bug fixes)
  • Customer team training (2–3 sessions on system operation)

Timeline: basic RAG prototype — 1–2 weeks. Fine-tuning with customer data — 3–6 weeks (including data preparation). Production system with monitoring and retraining — 2–4 months. Cost is calculated individually based on data volume, model complexity, and infrastructure requirements.

We guarantee the quality of the final model with performance benchmarks and ongoing monitoring. Our engineers have hands‑on experience with dozens of production LLM systems.

Want to evaluate your project? Leave a request — we will prepare a preliminary summary within 1–2 business days. Or get a consultation on choosing the approach: RAG, fine-tuning, or hybrid — we will tell you what works best for you. Contact us to discuss your LLM development needs. Schedule a free consultation today.