# How aisuite Handles Provider-Specific Message and Response Normalization

> Learn how aisuite normalizes provider-specific messages and responses using a unified OpenAI-compatible contract and central message converter. Streamline your LLM integrations.

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

---

**aisuite abstracts every LLM provider behind a common OpenAI-compatible contract using a centralized message converter that transforms inbound requests to OpenAPI schema and outbound responses to a unified `ChatCompletionResponse` model.**

The aisuite library from Andrew Ng's AI Fund unifies dozens of AI providers under one interface. This article explains how the framework achieves **provider-specific message and response normalization** through a three-layer conversion pipeline: request transformation, response adaptation, and usage normalization for tracing.

---

## The Core Normalization Engine: `OpenAICompliantMessageConverter`

All normalization logic centers on [`aisuite/providers/message_converter.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/providers/message_converter.py). The `OpenAICompliantMessageConverter` class provides the base implementation that every provider either uses directly or extends.

When a client initiates a chat completion, the provider delegates message handling to this converter. The design ensures that regardless of whether you're calling OpenAI, Anthropic, HuggingFace, or Ollama, the data flows through the same shape.

---

## Request Normalization: Converting Messages to OpenAI Schema

Every provider chat method begins with request transformation. In [`message_converter.py`](https://github.com/andrewyng/aisuite/blob/main/message_converter.py), the `convert_request` method walks through each message and produces a plain dictionary matching the OpenAI schema.

```python
transformed_messages = self.transformer.convert_request(messages)

```

The method handles:

- **Role preservation** — Maps `user`, `assistant`, `system`, and `tool` roles
- **Content extraction** — Preserves string or structured content
- **Tool call formatting** — Normalizes `tool_calls` arrays with `id`, `type`, and `function` fields
- **Tool result coercion** — When `tool_results_as_strings` is `True`, converts tool outputs to strings for providers that don't accept structured results

This transformation happens in [`aisuite/providers/message_converter.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/providers/message_converter.py), specifically within the `convert_request` implementation that providers invoke before sending data upstream.

---

## Response Normalization: Building `ChatCompletionResponse`

Providers that don't return native OpenAI-shaped objects implement `convert_response` or a private `_normalize_response` method. The base `OpenAICompliantMessageConverter` constructs a `ChatCompletionResponse` from raw provider payloads:

```python
completion_response = ChatCompletionResponse()
choice = response_data["choices"][0]
message = choice["message"]
completion_response.choices[0].message.content = message["content"]
completion_response.choices[0].message.role = message.get("role", "assistant")

# optional usage

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

# optional tool calls

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

```

Each concrete provider either:

1. **Inherits directly** — The OpenAI provider passes responses through unchanged since they're already compliant
2. **Adapts then converts** — Non-OpenAI providers reshape their native payloads into OpenAI-like dictionaries before calling `convert_response`

---

## Provider Implementation Patterns

### OpenAI Provider: Direct Pass-Through

In [`aisuite/providers/openai_provider.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/providers/openai_provider.py), responses require no transformation:

```python
provider = OpenaiProvider(api_key="sk-…")
response = provider.chat_completions_create(
    model="gpt-4o",
    messages=[
        Message(role="user", content="What's the weather?"),
        Message(role="assistant", content="Sure, which city?", tool_calls=[]),
    ],
)

# response is a native OpenAI ChatCompletion object (already normalized)

```

### HuggingFace Provider: Manual Adaptation

Providers with divergent schemas implement custom adaptation. Here's the pattern from [`aisuite/providers/huggingface_provider.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/providers/huggingface_provider.py):

```python
class HuggingFaceProvider(Provider):
    ...
    def _normalize_response(self, raw):
        # adapt HF-specific keys → OpenAI shape

        openai_like = {
            "choices": [{"message": {"role": "assistant",
                                    "content": raw["generated_text"]}}],
            "usage": {"prompt_tokens": raw["prompt_tokens"],
                      "completion_tokens": raw["generated_tokens"],
                      "total_tokens": raw["total_tokens"]},
        }
        return OpenAICompliantMessageConverter().convert_response(openai_like)

```

This pattern—adapt native fields, then delegate to the shared converter—appears across [`aisuite/providers/anthropic_provider.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/providers/anthropic_provider.py), [`aisuite/providers/ollama_provider.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/providers/ollama_provider.py), [`aisuite/providers/watsonx_provider.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/providers/watsonx_provider.py), and others.

---

## Tracing and Usage Normalization

For observability, aisuite applies a third normalization layer in [`aisuite/tracing/normalize.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/tracing/normalize.py). This ensures consistent logging and UI display regardless of provider.

```python
from aisuite.tracing.normalize import normalize_model_input, normalize_model_response

model_input = normalize_model_input(messages, preview_chars=100)
model_output = normalize_model_response(response, preview_chars=200)

# `model_input` and `model_output` now contain plain dicts ready for logging/UI

```

Three functions handle this:

- **`normalize_model_input`** — Builds message previews with configurable truncation
- **`normalize_model_response`** — Standardizes response structure with usage data
- **`normalize_usage`** — Normalizes token counts across providers with inconsistent field naming

This tracing layer operates independently of the request/response path, giving operators uniform visibility into cross-provider usage.

---

## Data Models and Type Safety

The normalization pipeline relies on shared data models defined in [`aisuite/framework/message.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/framework/message.py):

- **`Message`** — Universal message container with `role`, `content`, and optional `tool_calls`
- **`ChatCompletionMessageToolCall`** — Structured tool call with `id`, `type`, and `function` fields
- **`ChatCompletionResponse`** — Standardized response envelope with `choices` and `usage`

These models decouple provider implementations from the client interface, enabling the framework to add new providers without changing consumer code.

---

## Summary

- **Request normalization** happens via `OpenAICompliantMessageConverter.convert_request` in [`aisuite/providers/message_converter.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/providers/message_converter.py), transforming messages to OpenAI-compatible dictionaries
- **Response normalization** uses `convert_response` or provider-specific `_normalize_response` methods to build `ChatCompletionResponse` objects
- **Provider implementations** either use the base converter directly (OpenAI) or adapt their native payloads before conversion (HuggingFace, Anthropic, Ollama, Watson-x)
- **Tracing normalization** in [`aisuite/tracing/normalize.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/tracing/normalize.py) provides uniform logging and UI display through `normalize_model_input`, `normalize_model_response`, and `normalize_usage`

This three-layer approach lets aisuite treat every LLM provider as if it were an OpenAI API, eliminating provider-specific code from application logic.

---

## Frequently Asked Questions

### What file contains the main message conversion logic?

[`aisuite/providers/message_converter.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/providers/message_converter.py) houses the `OpenAICompliantMessageConverter` class, which provides `convert_request` and `convert_response` methods used across all providers. This is the central engine for provider-specific message and response normalization.

### How do non-OpenAI providers handle responses that don't match OpenAI's schema?

Providers like HuggingFace implement a `_normalize_response` method that maps their native response fields to OpenAI-shaped dictionaries, then pass those dictionaries to `OpenAICompliantMessageConverter().convert_response()`. This two-step process keeps provider-specific logic isolated while reusing the shared conversion machinery.

### What happens to tool call results in the normalization process?

When `tool_results_as_strings` is `True` on the converter class, tool results are coerced to strings via `convert_request`. This ensures compatibility with providers that only accept string content in tool messages, even when the source data is structured.

### Where does usage data get normalized for logging and monitoring?

[`aisuite/tracing/normalize.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/tracing/normalize.py) provides `normalize_usage` and related functions that standardize token counts and response metadata. This operates independently of the core request/response flow to provide consistent observability across all provider integrations.