How aisuite Converts Messages Between LLM Provider Formats

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. 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. 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. This base class handles the majority of modern providers (OpenAI, Groq, Mistral, etc.) with minimal customization.


# 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.

Anthropic-Specific Conversion

Anthropic's API requires distinct handling for system messages and tool results. The AnthropicMessageConverter in aisuite/providers/anthropic_provider.py overrides the base behavior to handle these differences:


# 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. This normalization ensures that tool calls, usage statistics, and finish reasons appear in a consistent structure regardless of the underlying provider.

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

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

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
  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, decoupling application logic from vendor-specific APIs.
  • The OpenAICompliantMessageConverter in 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 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 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.

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 →