How the Free-Claude-Code Message Converter Translates Anthropic to OpenAI Formats

The message converter in free-claude-code translates Anthropic's native message format into OpenAI-compatible chat completions by normalizing block structures, splitting tool results into separate messages, and wrapping thinking blocks in XML tags while preserving conversation order.

The free-claude-code repository provides a bridge between Anthropic's Claude API and OpenAI's Chat Completion interface. At the heart of this interoperability lies a single utility in providers/common/message_converter.py that handles the bidirectional translation of message payloads, tool definitions, and system prompts.

Core Architecture and Entry Points

The conversion logic is centralized in the AnthropicToOpenAIConverter class. Its public interface consists of static methods that transform Anthropic-style message lists into OpenAI-compliant dictionaries.

The primary entry point is convert_messages(messages, ...), which iterates over each message and dispatches to role-specific handlers. Helper functions get_block_attr and get_block_type (lines 7-13) normalize access to message content, allowing the converter to handle both plain dictionaries and objects with attributes uniformly.

Converting User Messages

User messages in the Anthropic format contain either a plain string or a list of content blocks (text, image, tool_result). The _convert_user_message method (lines 27-61) handles this conversion through a specific sequencing strategy:

  • Plain text blocks are accumulated into a single user message.
  • Tool result blocks trigger a flush of any pending text, then emit a separate role: "tool" message containing the tool_use_id and result content.

This split preserves the exact ordering of the conversation while satisfying OpenAI's requirement that tool results exist as distinct messages with a tool_call_id reference.

from providers.common.message_converter import AnthropicToOpenAIConverter

# Example: User message with text and tool result

msg = type(
    "Msg",
    (),
    {
        "role": "user",
        "content": [
            {"type": "text", "text": "Here is the result you asked for:"},
            {"type": "tool_result", "tool_use_id": "tc2", "content": "Sunny, 21°C"},
        ],
    },
)

converted = AnthropicToOpenAIConverter.convert_messages([msg])
print(converted)

Output:

[
  {"role": "user", "content": "Here is the result you asked for:"},
  {"role": "tool", "tool_call_id": "tc2", "content": "Sunny, 21°C"}
]

Converting Assistant Messages

Assistant messages require the most complex transformation. The _convert_assistant_message method (lines 65-122) processes blocks of type text, thinking, and tool_use according to specific rules:

  1. Text blocks are concatenated to form the message content.
  2. Thinking blocks are wrapped in <thinking> XML tags (unless include_thinking=False) and appended to content.
  3. Tool use blocks are extracted into the OpenAI tool_calls array format with id, type, and function fields.

If the assistant message contains only tool calls with no textual content, the converter forces the content to a single space to satisfy NIM provider requirements that reject empty content strings.


# Example: Assistant with thinking and tool use

assistant_msg = type(
    "Msg",
    (),
    {
        "role": "assistant",
        "content": [
            {"type": "thinking", "thinking": "Analyzing weather request..."},
            {"type": "text", "text": "I'll check the weather for you."},
            {"type": "tool_use", "id": "tc1", "name": "get_weather", "input": {"city": "Paris"}}
        ],
    },
)

print(AnthropicToOpenAIConverter.convert_messages([assistant_msg]))

Output:

[
  {
    "role": "assistant",
    "content": "<thinking>Analyzing weather request...</thinking>\nI'll check the weather for you.",
    "tool_calls": [
      {
        "id": "tc1",
        "type": "function",
        "function": {"name": "get_weather", "arguments": "{\"city\": \"Paris\"}"}
      }
    ]
  }
]

Handling System Prompts and Tools

The converter handles auxiliary request components through dedicated static methods.

System prompt conversion via convert_system_prompt (lines 194-211) accepts either a raw string or a list of "text" blocks, emitting a single OpenAI system message.

Tool schema translation maps Anthropic's name, description, and input_schema fields to OpenAI's function object format. The convert_tools method (lines 166-174) restructures each tool definition, while convert_tool_choice (lines 176-194) translates Anthropic's auto/any/tool enums into OpenAI's auto/required/function representations.

Building the Complete Request Body

The build_base_request_body function (lines 214-263) assembles the final OpenAI-compatible payload. It orchestrates the conversion of messages, system prompts, and tools, then injects optional parameters like max_tokens, temperature, and top_p using the helper set_if_not_none from providers/common/utils.py.

from providers.common.message_converter import build_base_request_body

class FakeRequest:
    model = "gpt-4o"
    max_tokens = 512
    temperature = 0.7
    messages = []  # Anthropic format messages

    tools = [
        type("Tool", (), {
            "name": "get_weather",
            "description": "Fetch weather",
            "input_schema": {"type": "object", "properties": {"city": {"type": "string"}}}
        })
    ]
    tool_choice = {"type": "any"}

body = build_base_request_body(FakeRequest())

This produces a complete request body with the model identifier, converted message history, and properly formatted tool definitions ready for the OpenAI Chat Completions endpoint.

Summary

  • The free-claude-code message converter lives in providers/common/message_converter.py and provides a centralized translation layer between Anthropic and OpenAI APIs.
  • convert_messages delegates to role-specific handlers: _convert_user_message for user content (splitting tool results into separate messages) and _convert_assistant_message for assistant content (handling thinking blocks and tool calls).
  • Thinking content is wrapped in XML tags, while tool definitions are remapped from Anthropic's input_schema to OpenAI's parameters format.
  • The build_base_request_body function assembles the complete payload, ensuring compatibility with strict NIM providers by avoiding empty content strings.

Frequently Asked Questions

How does the converter handle Anthropic's thinking blocks?

The converter wraps thinking content in <thinking> XML tags and appends it to the message's text content. This behavior can be disabled by setting include_thinking=False in the conversion call, which strips thinking blocks entirely from the output.

Why are tool results split into separate messages?

OpenAI's Chat Completion API requires tool results to exist as distinct messages with role: "tool" and a tool_call_id field referencing the original tool call. The converter automatically extracts tool_result blocks from Anthropic's user messages and emits them as separate entries while preserving the conversational order established in the source list.

What happens if an assistant message contains only tool calls?

When an assistant message has no text content but contains tool calls, the converter forces the content field to a single space character. This prevents API errors with certain NIM (NVIDIA Inference Microservices) providers that reject empty content strings, ensuring compatibility across different OpenAI-compatible endpoints.

Where does the tool schema translation occur?

Tool schema conversion happens in the convert_tools method (lines 166-174), which maps Anthropic's input_schema field to OpenAI's parameters field within a function object. The convert_tool_choice method (lines 176-194) handles the mapping of Anthropic's tool_choice enums to OpenAI's equivalent values, translating any to required and auto to auto.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →