# How aisuite Converts Messages Between LLM Provider Formats

> Learn how aisuite converts messages between LLM provider formats. Discover the Message class and converter objects that normalize chat messages for any API.

- Repository: [Andrew Ng/aisuite](https://github.com/andrewyng/aisuite)
- Tags: internals
- Published: 2026-07-28

---

**aisuite normalizes chat messages through a provider-agnostic `Message` class and dedicated converter objects that translate between the framework's internal representation and each LLM provider's specific API schema.**

The aisuite library by Andrew Ng provides a unified interface for multiple large language model (LLM) providers, eliminating the need to juggle different client libraries. To achieve this interoperability, the framework implements a sophisticated message conversion system that transparently handles format translation between its internal objects and provider-specific request/response schemas.

## The Provider-Agnostic Message Architecture

At the core of aisuite's conversion system lies a clean separation between the framework's internal representation and external provider formats.

### The Internal Message Model

All chat interactions within aisuite use the **Message** class defined in [`aisuite/framework/message.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/framework/message.py). This provider-agnostic model standardizes fields like `role`, `content`, `tool_calls`, and `tool_call_id`, ensuring that application code remains decoupled from any specific LLM vendor's API structure.

### The Converter Pattern

Each LLM provider ships with a dedicated **message converter** class that inherits from conversion utilities in [`aisuite/providers/message_converter.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/providers/message_converter.py). These converters implement two critical transformation methods:

- `convert_request()` – Transforms framework `Message` objects into provider-specific payload shapes
- `convert_response()` – Normalizes provider-specific responses into the framework's `ChatCompletionResponse` format

## Converting Requests from Framework to Provider Format

When you call `chat_completions_create()`, aisuite delegates message transformation to the appropriate converter before sending HTTP requests.

### OpenAI-Compatible Conversion

Providers using OpenAI-style schemas rely on `OpenAICompliantMessageConverter` in [`aisuite/providers/message_converter.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/providers/message_converter.py). This base class handles the majority of modern providers (OpenAI, Groq, Mistral, etc.) with minimal customization.

```python

# aisuite/providers/message_converter.py

class OpenAICompliantMessageConverter:
    @staticmethod
    def convert_request(messages):
        """Convert messages to OpenAI-compatible format."""
        transformed_messages = []
        for message in messages:
            # Accept both framework Message objects and raw dicts

            if isinstance(message, Message):
                message_dict = message.model_dump(mode="json")
                message_dict.pop("refusal", None)  # OpenAI does not support refusal

                tmsg = message_dict
            else:
                tmsg = message
            
            # Handle tool results as strings when required

            if tmsg.get("role") == "tool" and OpenAICompliantMessageConverter.tool_results_as_strings:
                tmsg["content"] = str(tmsg["content"])
            
            transformed_messages.append(tmsg)
        return transformed_messages

```

The provider's `chat_completions_create` method invokes this transformer before calling the underlying client, as implemented in [`aisuite/providers/openai_provider.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/providers/openai_provider.py).

### Anthropic-Specific Conversion

Anthropic's API requires distinct handling for system messages and tool results. The `AnthropicMessageConverter` in [`aisuite/providers/anthropic_provider.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/providers/anthropic_provider.py) overrides the base behavior to handle these differences:

```python

# aisuite/providers/anthropic_provider.py

class AnthropicMessageConverter:
    def convert_request(self, messages):
        """Convert framework messages to Anthropic format."""
        system_message = self._extract_system_message(messages)
        converted_messages = [self._convert_single_message(msg) for msg in messages]
        return system_message, converted_messages
    
    def _extract_system_message(self, messages):
        # Removes system role entries and returns them for Anthropic's system= parameter

        pass

```

Key transformations include:

- **System message extraction** – The first message with `role="system"` is removed from the message list and passed separately to Anthropic's `system=` argument, as seen in lines 66-74 of the anthropic provider.
- **Content conversion** – OpenAI-style image URLs (`{"type": "image_url", ...}`) are transformed into Anthropic's expected image blocks, handling both data URLs and remote URLs.
- **Tool result formatting** – Framework messages with `role="tool"` are converted to Anthropic's `tool_result` blocks.

## Normalizing Provider Responses

After receiving API responses, converters normalize the data back into framework-standard objects.

### Response Conversion Flow

Each converter implements `convert_response()` to build a `ChatCompletionResponse` object from [`aisuite/framework/chat_completion_response.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/framework/chat_completion_response.py). This normalization ensures that tool calls, usage statistics, and finish reasons appear in a consistent structure regardless of the underlying provider.

```python
def convert_response(self, response_data) -> ChatCompletionResponse:
    """Normalize the response to match OpenAI's response format."""
    completion_response = ChatCompletionResponse()
    message = response_data["choices"][0]["message"]
    completion_response.choices[0].message.content = message["content"]
    completion_response.choices[0].message.role = message.get("role", "assistant")
    
    # Normalize tool calls into ChatCompletionMessageToolCall objects

    if "tool_calls" in message and message["tool_calls"] is not None:
        tool_calls = [
            ChatCompletionMessageToolCall(
                id=tc.get("id"),
                type="function",
                function=tc.get("function"),
            )
            for tc in message["tool_calls"]
        ]
        completion_response.choices[0].message.tool_calls = tool_calls
    
    # Extract usage metadata

    if usage_data := response_data.get("usage"):
        completion_response.usage = self.get_completion_usage(usage_data)
    
    return completion_response

```

The Anthropic converter performs the inverse operation, extracting content from Anthropic's proprietary response objects and reconstructing standard `ChatCompletionMessageToolCall` instances for function calling workflows.

## Working with Message Converters in Practice

Provider classes instantiate their converters during initialization and delegate transformation tasks to them.

### Example: OpenAI Tool Call Conversion

```python
from aisuite.providers.openai_provider import OpenaiProvider
from aisuite.framework.message import Message

provider = OpenaiProvider(api_key="sk-...")
framework_messages = [
    Message(role="user", content="What is the weather in Paris?"),
    Message(role="assistant", content=None, tool_calls=[
        {"id": "tc_1", "type": "function",
         "function": {"name": "get_weather", "arguments": '{"city":"Paris"}'}}
    ]),
]

# The provider internally calls the converter

payload = provider.transformer.convert_request(framework_messages)

# payload is now formatted for OpenAI's /chat/completions endpoint

```

### Example: Anthropic Response Normalization

```python
from aisuite.providers.anthropic_provider import AnthropicProvider

provider = AnthropicProvider()
anthropic_response = provider.client.messages.create(
    model="claude-3-opus-20240229",
    system="You are a helpful assistant.",
    messages=[{"role": "user", "content": "Hello"}]
)

# Convert proprietary response to framework standard

framework_response = provider.converter.convert_response(anthropic_response)

# Access normalized fields: framework_response.choices[0].message.content

```

### Extending to New Providers

To add support for a new LLM provider (e.g., "FooAI"):

1. Create [`aisuite/providers/fooai_provider.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/providers/fooai_provider.py)
2. Implement `FooAIMessageConverter` with `convert_request()` and `convert_response()` methods mapping to FooAI's JSON schema
3. Instantiate the converter in the provider's `__init__` method and invoke it during `chat_completions_create()` calls

## Summary

- **aisuite** maintains a **provider-agnostic** internal representation using the `Message` class in [`aisuite/framework/message.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/framework/message.py), decoupling application logic from vendor-specific APIs.
- The **`OpenAICompliantMessageConverter`** in [`aisuite/providers/message_converter.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/providers/message_converter.py) provides a base class for OpenAI-compatible providers, handling request transformation and response normalization.
- **Provider-specific converters** like `AnthropicMessageConverter` override base behavior to handle unique schema requirements such as system message separation and distinct tool-call formats.
- All converters implement **bidirectional translation**: `convert_request()` prepares provider payloads from framework objects, while `convert_response()` normalizes API responses into standard `ChatCompletionResponse` objects.
- This architecture allows developers to switch between LLM providers (OpenAI, Anthropic, Gemini, etc.) without modifying application message handling code.

## Frequently Asked Questions

### What is the Message class in aisuite?

The `Message` class in [`aisuite/framework/message.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/framework/message.py) is a standardized data model representing chat messages within the framework. It encapsulates fields like `role`, `content`, `tool_calls`, and `refusal` in a provider-agnostic structure, ensuring that application logic remains independent of any specific LLM vendor's API format before conversion occurs.

### How does aisuite handle Anthropic's unique system message format?

Anthropic requires system instructions as a separate parameter rather than as a message with `role="system"`. The `AnthropicMessageConverter` in [`aisuite/providers/anthropic_provider.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/providers/anthropic_provider.py) extracts system messages from the message list using `_extract_system_message()`, removing them from the conversation array and passing them to the `system=` argument in the API call, while converting remaining messages to Anthropic's expected format.

### Can I extend aisuite to support a new LLM provider?

Yes. Create a new provider file in `aisuite/providers/` and implement a message converter class with `convert_request()` and `convert_response()` methods. Instantiate this converter in your provider's `__init__` method and use it to transform messages before API calls and normalize responses afterward, following the pattern established by `OpenaiProvider` and `AnthropicProvider`.

### How are tool calls converted between different provider formats?

Tool calls are normalized into `ChatCompletionMessageToolCall` objects within the framework's response handling. During requests, converters transform these objects into provider-specific schemas—for example, Anthropic's `tool_use` blocks or OpenAI's function definitions. During response processing, converters extract tool call data from provider-specific response structures (like Anthropic's content blocks) and reconstruct standard `ChatCompletionMessageToolCall` instances with `id`, `type`, and `function` attributes.