# How to Add a Custom LLM Provider to aisuite Using the Adapter Pattern

> Easily add a custom LLM provider to aisuite by creating a Python module and subclassing the abstract Provider. Learn to implement chat_completions_create and use MessageConverter for seamless integration.

- Repository: [Andrew Ng/aisuite](https://github.com/andrewyng/aisuite)
- Tags: how-to-guide
- Published: 2026-07-27

---

**To add a custom LLM provider to aisuite, create a Python module named `<provider>_provider.py` in `aisuite/providers/`, subclass the abstract `Provider` base class from [`aisuite/provider.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/provider.py), and implement the `chat_completions_create` method while optionally using a `MessageConverter` to translate between aisuite's generic message format and your vendor-specific API schema.**

aisuite is a unified Python interface for multiple large language model services developed by Andrew Ng. It isolates each backend service behind a common abstraction using the **adapter pattern**, allowing you to plug in custom LLMs without modifying the core library.

## How the Adapter Pattern Works in aisuite

The architecture separates concerns into three layers: the abstract interface, the dynamic factory, and the adapter implementation.

**The Provider Abstract Base**  
In [`aisuite/provider.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/provider.py) (lines 24-59), the `Provider` class defines the contract for chat completions, async operations, streaming, and audio transcription. Any custom provider must inherit from this base and implement the required abstract methods.

**Dynamic Discovery via ProviderFactory**  
The `ProviderFactory.create_provider` method in [`aisuite/provider.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/provider.py) (lines 92-118) handles instantiation. It converts a configuration key like `"myllm"` into a class name `MyllmProvider` and dynamically imports the corresponding module [`aisuite/providers/myllm_provider.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/providers/myllm_provider.py). This follows the open-closed principle: you add new files without touching existing factory code.

**Message Conversion Adapters**  
To keep providers thin, aisuite includes a `MessageConverter` base class in [`aisuite/providers/message_converter.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/providers/message_converter.py). Your adapter implements `convert_request()` to transform aisuite's `Message` objects into the vendor's JSON payload, and `convert_response()` to normalize the API response back into aisuite's framework objects.

## Step-by-Step Implementation Guide

### Step 1: Create the Provider Module

Create a new file in `aisuite/providers/` following the strict naming convention: `<key>_provider.py`. The filename determines how users reference your provider in configuration.

- **File**: [`aisuite/providers/myllm_provider.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/providers/myllm_provider.py)
- **Config key**: `myllm`
- **Class name**: `MyllmProvider` (PascalCase conversion)

### Step 2: Subclass the Provider Abstract Base

Import `Provider` from `aisuite/provider` and implement the required methods. At minimum, you must implement `chat_completions_create`. You may also override `achat_completions_create` for async support and `chat_completions_create_stream` for streaming responses.

```python
from aisuite.provider import Provider, LLMError
from typing import List, Dict, Any

class MyllmProvider(Provider):
    def __init__(self, endpoint: str, api_key: str, **_: dict):
        super().__init__()
        self.endpoint = endpoint
        self.api_key = api_key
    
    def chat_completions_create(self, model: str, messages: List[Dict[str, Any]], **kwargs):
        # Implementation details in Step 3

        pass

```

### Step 3: Implement the Message Converter Adapter

Create an adapter class that inherits from `MessageConverter` to handle payload translation. This isolates format-specific logic from the provider's HTTP handling.

```python
from aisuite.providers.message_converter import MessageConverter

class MyllmMessageConverter(MessageConverter):
    def convert_request(self, messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
        """Transform aisuite messages to MyLLM's expected format."""
        return [{"role": m["role"], "content": m["content"]} for m in messages]
    
    def convert_response(self, raw: Dict[str, Any]) -> Dict[str, Any]:
        """Extract the message from MyLLM's response structure."""
        return raw["choices"][0]["message"]

```

Instantiate this converter in your provider's `__init__` and use it within `chat_completions_create`:

```python
import requests

class MyllmProvider(Provider):
    def __init__(self, endpoint: str, api_key: str, **_: dict):
        super().__init__()
        self.endpoint = endpoint.rstrip("/")
        self.headers = {
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json"
        }
        self.transformer = MyllmMessageConverter()
    
    def chat_completions_create(self, model: str, messages: List[Dict[str, Any]], **kwargs):
        payload = {
            "model": model,
            "messages": self.transformer.convert_request(messages),
            **kwargs
        }
        try:
            resp = requests.post(
                f"{self.endpoint}/v1/chat/completions",
                json=payload,
                headers=self.headers
            )
            resp.raise_for_status()
            return self.transformer.convert_response(resp.json())
        except Exception as exc:
            raise LLMError(f"MyLLM request failed: {exc}")

```

### Step 4: Handle Configuration

The `Client` passes a configuration dictionary directly to your provider's constructor via `**config`. Accept these parameters in `__init__` to store API keys, base URLs, or timeout settings.

Common pattern from [`openai_provider.py`](https://github.com/andrewyng/aisuite/blob/main/openai_provider.py):
- Extract required parameters like `api_key` and `base_url`
- Store headers or client instances as instance attributes
- Pass optional parameters through to the underlying SDK or HTTP client

### Step 5: Add Optional Audio Support

If your custom LLM offers speech-to-text capabilities, subclass `Audio` and define a `Transcriptions` inner class. Reference the implementation in [`aisuite/providers/openai_provider.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/providers/openai_provider.py) (lines 98-106):

```python
from aisuite.provider import Audio

class MyllmProvider(Provider):
    def __init__(self, **config):
        super().__init__()
        self.audio = self.MyllmAudio(self)
    
    class MyllmAudio(Audio):
        def __init__(self, provider):
            self.provider = provider
        
        class Transcriptions:
            def create(self, file, model, **kwargs):
                # Implementation for audio transcription

                pass

```

## Complete Working Example

Here is a full implementation demonstrating the adapter pattern with error handling and message conversion:

```python

# File: aisuite/providers/myllm_provider.py

from typing import List, Dict, Any
import requests
from aisuite.provider import Provider, LLMError, Audio
from aisuite.providers.message_converter import MessageConverter

class MyllmMessageConverter(MessageConverter):
    """Adapter translating between aisuite and MyLLM API formats."""
    
    def convert_request(self, messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
        return [{"role": m["role"], "content": m["content"]} for m in messages]
    
    def convert_response(self, raw: Dict[str, Any]) -> Dict[str, Any]:
        return {
            "role": raw["choices"][0]["message"]["role"],
            "content": raw["choices"][0]["message"]["content"]
        }

class MyllmProvider(Provider):
    """Custom provider for MyLLM API using the adapter pattern."""
    
    def __init__(self, endpoint: str, api_key: str, **_: dict):
        super().__init__()
        self.endpoint = endpoint.rstrip("/")
        self.headers = {
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json"
        }
        self.transformer = MyllmMessageConverter()
    
    def chat_completions_create(self, model: str, messages: List[Dict[str, Any]], **kwargs):
        payload = {
            "model": model,
            "messages": self.transformer.convert_request(messages),
            **kwargs
        }
        
        try:
            response = requests.post(
                f"{self.endpoint}/v1/chat/completions",
                json=payload,
                headers=self.headers,
                timeout=30
            )
            response.raise_for_status()
            return self.transformer.convert_response(response.json())
        except requests.RequestException as exc:
            raise LLMError(f"MyLLM API error: {exc}")

```

## Using Your Custom Provider

No registration code is required. Simply reference your provider by key in the configuration dictionary:

```python
from aisuite import Client

config = {
    "myllm": {
        "endpoint": "https://api.my-llm.com",
        "api_key": "sk-your-key-here"
    }
}

client = Client(config=config)

response = client.chat.completions.create(
    model="myllm:gpt-4",
    messages=[{"role": "user", "content": "Explain the adapter pattern"}]
)
print(response.choices[0].message.content)

```

## Summary

- **Create a module** named `<key>_provider.py` in `aisuite/providers/` to enable dynamic discovery by `ProviderFactory`.
- **Subclass `Provider`** from [`aisuite/provider.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/provider.py) and implement `chat_completions_create` to satisfy the abstract contract.
- **Use a `MessageConverter`** adapter to isolate translation logic between aisuite's generic format and your LLM's specific API schema.
- **Store configuration** in `__init__(self, **config)` to receive API keys and endpoints passed by the `Client`.
- **Leverage auto-discovery**: The factory converts keys like `"myllm"` into class names `MyllmProvider` automatically, requiring no manual registration.

## Frequently Asked Questions

### What file naming convention must I follow for aisuite to recognize my provider?

You must name your file `<key>_provider.py` where `<key>` is the lowercase identifier users will reference in configuration. For example, [`myllm_provider.py`](https://github.com/andrewyng/aisuite/blob/main/myllm_provider.py) becomes accessible via the key `"myllm"` and instantiates the class `MyllmProvider`.

### Do I need to register my custom provider anywhere in the codebase?

No. The `ProviderFactory.create_provider` method in [`aisuite/provider.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/provider.py) (lines 92-118) dynamically constructs the class name from your configuration key and imports the corresponding module. Simply adding the file to `aisuite/providers/` completes registration.

### Which methods are mandatory when subclassing the Provider abstract base?

You must implement `chat_completions_create(self, model, messages, **kwargs)`. The `Provider` base class in [`aisuite/provider.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/provider.py) defines other methods like `achat_completions_create` and `chat_completions_create_stream` with default implementations that raise `NotImplementedError`, making them optional unless you need async or streaming support.

### Can I reuse my MessageConverter for other providers?

Yes. Since `MessageConverter` is a separate class inheriting from [`aisuite/providers/message_converter.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/providers/message_converter.py), you can import and instantiate it in multiple provider files. This promotes code reuse when integrating APIs that share similar request/response schemas, such as OpenAI-compatible endpoints.