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

> Understand how the free-claude-code message converter translates Anthropic to OpenAI formats. Learn about block normalization, tool result splitting, and XML tagging for seamless integration.

- Repository: [Ali Khokhar/free-claude-code](https://github.com/Alishahryar1/free-claude-code)
- Tags: internals
- Published: 2026-04-24

---

**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`](https://github.com/Alishahryar1/free-claude-code/blob/main/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.

```python
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:**

```json
[
  {"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.

```python

# 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:**

```json
[
  {
    "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`](https://github.com/Alishahryar1/free-claude-code/blob/main/providers/common/utils.py).

```python
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`](https://github.com/Alishahryar1/free-claude-code/blob/main/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`.