Skip to main content

Migrating from OpenAI to Anthropic API: Fixing 400 Errors and System Prompts

 You have a production-ready application running on GPT-4. You decide to integrate Claude 3.5 Sonnet for its reasoning capabilities or cost profile. You swap the SDK, change the API key, and update the model name.

You run the code, expecting a seamless transition. Instead, your logs explode with anthropic.BadRequestError.

The error messages are specific but frustrating: "messages: roles must alternate between 'user' and 'assistant'" or "max_tokens is required". This is not a simple drop-in replacement. While OpenAI and Anthropic share similar underlying concepts, their API architectures differ strictly regarding conversation history and parameter enforcement.

This guide provides a root-cause analysis of these failures and a robust, reusable Python solution to normalize your message history for the Anthropic SDK.

The Root Cause: Loose vs. Strict State Machines

To fix the code, you must understand the architectural divergence between the two providers.

OpenAI's "Bag of Messages"

OpenAI’s Chat Completion API is relatively permissive. It treats the messages array as a loose context window.

  • System Roles: You can include multiple system messages, and they can appear anywhere (though usually at the top).
  • Consecutive Roles: having user followed immediately by another user message is generally tolerated; the model effectively concatenates them internally.
  • Defaults: Parameters like max_tokens are optional. If omitted, the model simply generates until it hits its context limit.

Anthropic's Strict Dialogue Enforcement

Anthropic’s API enforces a strict conversational state machine designed to mimic actual human-AI dialogue.

  1. Top-Level System Param: The system role is not allowed inside the messages list. It is a separate top-level parameter.
  2. Strict Alternation: The messages list must follow a specific pattern: User -> Assistant -> User. You cannot have two consecutive user messages.
  3. Mandatory Constraints: max_tokens is not optional. You must explicitly define a limit to prevent runaway costs and infinite loops.

If your existing database stores chat history in the standard OpenAI format (a list of dictionaries), sending that raw list to Claude will result in a 400 HTTP error.

The Fix: A Robust Message Normalizer

We need a translation layer. We cannot simply pass the list; we must transform it.

The following Python solution implements a normalize_for_anthropic function. This utility handles:

  1. Extraction of the system message.
  2. Merging consecutive user messages to satisfy alternation rules.
  3. Ensuring the conversation starts with a user message.

Prerequisites

Ensure you have the latest Anthropic SDK installed.

pip install anthropic

The Solution Code

Here is the complete, production-ready implementation.

import os
from typing import List, Dict, Any, Tuple
from anthropic import Anthropic

# Initialize the client
client = Anthropic(
    api_key=os.environ.get("ANTHROPIC_API_KEY"),
)

def normalize_to_anthropic_format(
    messages: List[Dict[str, str]]
) -> Tuple[str | None, List[Dict[str, str]]]:
    """
    Converts OpenAI-style messages to Anthropic format.
    
    Returns:
        Tuple containing (system_prompt, valid_messages_list)
    """
    system_prompt = None
    anthropic_messages = []
    
    for msg in messages:
        role = msg.get("role")
        content = msg.get("content", "")
        
        # 1. Handle System Prompts
        # Anthropic uses a top-level parameter for system, not a message in the list.
        if role == "system":
            if system_prompt:
                # If multiple system prompts exist, concatenate them
                system_prompt += f"\n\n{content}"
            else:
                system_prompt = content
            continue
            
        # 2. Handle User/Assistant formatting
        # Anthropic strictly requires: User -> Assistant -> User
        if role in ["user", "assistant"]:
            # If this is the very first message and it's 'assistant', 
            # we skip it or convert it, because Anthropic must start with 'user'.
            # (Here we skip for simplicity, but you could prepend a dummy user msg)
            if not anthropic_messages and role == "assistant":
                continue
            
            # Check for consecutive messages of the same role
            if anthropic_messages and anthropic_messages[-1]["role"] == role:
                # Merge content with the previous message
                anthropic_messages[-1]["content"] += f"\n\n{content}"
            else:
                anthropic_messages.append({"role": role, "content": content})

    return system_prompt, anthropic_messages

# --- Example Usage ---

# Your existing OpenAI-style history (causing 400 errors currently)
openai_history = [
    {"role": "system", "content": "You are a helpful coding assistant."},
    {"role": "user", "content": "Here is my first question."},
    {"role": "user", "content": "Actually, let me clarify that question."}, # <--- CAUSES ERROR (Consecutive User)
    {"role": "assistant", "content": "Sure, go ahead."},
    {"role": "user", "content": "How do I fix a 400 error?"}
]

# 1. Normalize the data
sys_prompt, cleaned_msgs = normalize_to_anthropic_format(openai_history)

# 2. Make the API Call
try:
    response = client.messages.create(
        model="claude-3-5-sonnet-20240620",
        max_tokens=1024,  # MANDATORY PARAMETER
        system=sys_prompt, # Passed explicitly here
        messages=cleaned_msgs
    )
    
    print("--- Success ---")
    print(response.content[0].text)

except Exception as e:
    print(f"Error: {e}")

Deep Dive: Why This Logic Works

Merging vs. Dropping

In the code above, when we detect anthropic_messages[-1]["role"] == role, we append the content using \n\n. This is crucial.

If you have a chat UI where a user sends two texts in a row, dropping one loses context. Merging them creates a single "turn" in the conversation, which satisfies the Anthropic API requirement that roles must alternate, while preserving the full intent of the user's input.

The Missing max_tokens

Notice the max_tokens=1024 argument in the client.messages.create call.

In the OpenAI SDK, this is optional. If you forget it in the Anthropic SDK, the request will fail immediately. Anthropic forces developers to think about token limits upfront. This is often better for production safety, as it prevents a loop where a model generates text until it hits the hard limit of the context window (which could cost significantly more).

Common Pitfalls and Edge Cases

Even with the normalizer above, you might encounter edge cases.

1. Empty Content Blocks

OpenAI sometimes accepts a message with empty content (e.g., if a user submits whitespace). Anthropic validation is stricter.

  • The Fix: Add a filter in your loop: if not content.strip(): continue. Sending an empty string or a string of only spaces as content can trigger a 400 error.

2. Pre-filling Assistant Responses

Anthropic has a unique feature called "Prefill" where you can start the assistant's response for them.

  • Scenario: You want the JSON output to start immediately.
  • Implementation: You add an assistant message as the last message in the list.
  • Warning: If you use the normalizer above, ensure your logic doesn't accidentally merge a pre-fill into a previous assistant message if your history logic is complex.

3. Image Handling (Multimodal)

The code above handles text. If you are migrating GPT-4 Vision code, the content field will be a list of dictionaries (text and image blocks) rather than a simple string. You will need to extend the content merging logic to extend the list of blocks rather than concatenating strings.

Conclusion

Migrating from OpenAI to Anthropic is not just a configuration change; it is a structural change in how conversation history is managed. By extracting system prompts and enforcing strict User-Assistant alternation, you can eliminate BadRequestError and leverage the power of Claude 3.5 reliably.

Use the normalize_to_anthropic_format utility as a middleware layer in your application to ensure that no matter how your database stores chat history, your API calls remain compliant.