How aisuite Handles Provider-Specific Message and Response Normalization

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. 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, the convert_request method walks through each message and produces a plain dictionary matching the OpenAI schema.

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, 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:

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, responses require no transformation:

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:

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, aisuite/providers/ollama_provider.py, aisuite/providers/watsonx_provider.py, and others.


Tracing and Usage Normalization

For observability, aisuite applies a third normalization layer in aisuite/tracing/normalize.py. This ensures consistent logging and UI display regardless of provider.

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:

  • 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, 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 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 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 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.

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 →