Catalypt LogoCatalypt.ai

Industry Focus

Developer Options

Resources

Back to Blog

The Great Model Switching Disaster of 2024

March 28, 2024 Josh Butler Technical

"Claude is 5x cheaper and just as good!" The CFO was excited. We were burning $8k/month on GPT-4. Claude would drop that to $1.6k. Easy decision, right?

Two weeks later, half our AI features were broken, customer complaints were flooding in, and I was explaining to that same CFO why we needed an emergency budget to switch back.

The "Drop-In Replacement" Myth

Everyone said it would be simple:

// Just change this:
const openai = new OpenAI({ apiKey: process.env.OPENAI_KEY });

// To this:
const anthropic = new Anthropic({ apiKey: process.env.CLAUDE_KEY });

If only. Here's what actually broke:

Breaking Change #1: The Token Counting Disaster

Our code carefully managed context windows:

// GPT-4 token counting
const tokens = encode(prompt).length;
if (tokens > 8000) {
  prompt = truncateToTokens(prompt, 8000);
}

Claude counts tokens differently. Our 8000 GPT-4 tokens became 11000 Claude tokens. Every prompt got truncated. Important context got cut off. The AI became progressively dumber.

Breaking Change #2: The System Message Confusion

GPT-4 approach:

messages: [
  { role: "system", content: "You are a helpful assistant..." },
  { role: "user", content: userPrompt }
]

Claude approach:

// System goes in a different parameter entirely
await anthropic.messages.create({
  system: "You are a helpful assistant...",
  messages: [{ role: "user", content: userPrompt }]
});

Our system messages were being sent as user messages. Claude was very confused why users kept telling it what it was.

Breaking Change #3: The JSON Mode Nightmare

With GPT-4:

response_format: { type: "json_object" }
// Guaranteed valid JSON output

With Claude:

// LOL what's JSON mode?
// "Please respond with valid JSON"
// Claude: "Sure! Here's some JSON: ```json
{maybe valid json}```'

We had to add JSON parsing, validation, retry logic, and prayer. Half the time Claude would add markdown formatting around the JSON "to be helpful."

Breaking Change #4: Function Calling Doesn't Exist

Our GPT-4 code used function calling extensively:

functions: [{
  name: "get_weather",
  parameters: {
    type: "object",
    properties: {
      location: { type: "string" }
    }
  }
}]

Claude's response: 'Function calling? Best I can do is XML tags."

<function_call>
<name>get_weather</name>
<parameters>
<location>San Francisco</location>
</parameters>
</function_call>

Except sometimes it would forget the tags. Or nest them wrong. Or use different tag names.

The Subtle Behavior Differences

Even when the API worked, the models behaved differently:

Code Generation:
GPT-4: Writes complete, runnable code
Claude: Writes code with comments like "// implement error handling here"

Instruction Following:
GPT-4: "Return only the SQL query"
Returns: `SELECT * FROM users`

Claude: "Return only the SQL query"
Returns: "Here's the SQL query you requested: SELECT * FROM users'

Context Handling:
GPT-4: Remembers everything in the conversation
Claude: Forgets the middle parts but pretends it remembers

The Emergency Migration Back

After two weeks, we had:

  • 47 customer complaints about "AI being dumber"
  • 12 production bugs from malformed responses
  • 3 developers threatening to quit
  • 1 very unhappy CFO

The migration back was painful because we'd already 'fixed" code to work with Claude:

// Our codebase was now full of this
if (AI_PROVIDER === 'claude') {
  // Claude-specific logic
  response = parseClaudeXMLResponse(response);
} else {
  // GPT logic
  response = response.choices[0].message.function_call;
}

The Abstraction Layer That Saved Us

After the disaster, we built a proper abstraction:

interface AIProvider {
  complete(prompt: string): Promise<string>;
  completeJSON(prompt: string): Promise<any>;
  functionCall(prompt: string, functions: Function[]): Promise<FunctionCall>;
  countTokens(text: string): number;
}

class OpenAIProvider implements AIProvider {
  // OpenAI-specific implementation
}

class ClaudeProvider implements AIProvider {
  // Claude-specific implementation with all the workarounds
}

// Now switching is actually simple
const ai: AIProvider = getProvider(process.env.AI_PROVIDER);

What I Learned About Model Differences

GPT-4 Strengths:

  • Better at following exact output formats
  • Superior function calling
  • More consistent JSON output
  • Better at code generation

Claude Strengths:

  • Better at nuanced writing
  • More cautious (sometimes too cautious)
  • Better at admitting uncertainty
  • Cheaper (if you can make it work)

The Real Cost of Switching

Planned savings: $6,400/month
Actual costs:
- Developer time fixing issues: $12,000
- Lost productivity: $8,000
- Customer churn from degraded service: $15,000
- Therapy for involved developers: Priceless

Total cost of "saving money": $35,000

How to Actually Switch Models

  1. Build an abstraction layer FIRST
  2. Run both models in parallel - Compare outputs
  3. Have feature flags - Switch per feature, not globally
  4. Monitor quality metrics - Not just costs
  5. Plan for differences - They're not drop-in replacements
  6. Test with real workloads - Not just sample prompts

The Plot Twist

Six months later, we successfully use both models. But now:

  • GPT-4 for code generation and structured data
  • Claude for content writing and analysis
  • Proper abstraction layer handling differences
  • 28% cost reduction with better results

The lesson? You can save money switching models. Just budget 10x more time than you think, build proper abstractions, and prepare for everything to break at least once.

Pro tip: If someone says "just switch the API endpoint," they've never actually done it. Model switching is like changing databases - technically possible, practically painful, and full of surprises you won't enjoy discovering in production.

Get Started