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, andtoolroles - Content extraction — Preserves string or structured content
- Tool call formatting — Normalizes
tool_callsarrays withid,type, andfunctionfields - Tool result coercion — When
tool_results_as_stringsisTrue, 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:
- Inherits directly — The OpenAI provider passes responses through unchanged since they're already compliant
- 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 truncationnormalize_model_response— Standardizes response structure with usage datanormalize_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 withrole,content, and optionaltool_callsChatCompletionMessageToolCall— Structured tool call withid,type, andfunctionfieldsChatCompletionResponse— Standardized response envelope withchoicesandusage
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_requestinaisuite/providers/message_converter.py, transforming messages to OpenAI-compatible dictionaries - Response normalization uses
convert_responseor provider-specific_normalize_responsemethods to buildChatCompletionResponseobjects - 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.pyprovides uniform logging and UI display throughnormalize_model_input,normalize_model_response, andnormalize_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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →