How aisuite's Provider Factory Pattern Works for Adding New LLM Providers

aisuite implements a centralized factory pattern in the Client class that instantiates and stores provider instances based on a configuration object, automatically routing requests to the correct LLM backend by parsing provider/model strings.

The andrewyng/aisuite library abstracts vendor-specific AI implementations behind a unified interface using a lightweight provider factory pattern. This architecture allows developers to add support for new language models by extending a base class and registering the provider in the central factory, without modifying existing client code or request handling logic.

Configuration-Driven Factory Initialization

The factory logic resides entirely in src/client.ts, where the Client class constructor accepts a ProviderConfigs object containing optional configuration sections for each supported provider.

Inside the initializeProviders method, the code checks which configuration sections are present (such as openai, anthropic, mistral, or groq), instantiates the corresponding provider class, and stores the instance in internal Maps. This design decouples provider lifecycle management from the request execution logic.

Provider Registration Maps

The Client class maintains two separate Maps to handle different provider capabilities:

  • chatProviders: Map<string, Provider> for chat-completion providers
  • asrProviders: Map<string, ASRProvider> for speech-to-text providers

When initializeProviders detects a configuration section, it creates the provider instance and registers it using the provider name as the key. For example, as implemented in aisuite-js/src/client.ts:

if (config.openai) {
  const openaiProvider = new OpenAIProvider(config.openai);
  this.chatProviders.set("openai", openaiProvider);
  this.asrProviders.set("openai", openaiProvider);
}
if (config.groq) {
  this.chatProviders.set("groq", new GroqProvider(config.groq));
}

Providers that support both functionalities (like OpenAI) are registered in both Maps, while specialized providers may only populate one.

Model Parsing and Request Routing

When a user calls client.chat.completions.create, the model string must follow the format provider/model-name (for example, openai/gpt-4). The parseModel utility in src/utils/model-parser.ts splits this string into its components.

The Client then retrieves the appropriate provider instance from the registration Map and forwards the request:

const { provider, model } = parseModel(request.model);
const providerInstance = this.chatProviders.get(provider);
return providerInstance.chatCompletion(requestWithParsedModel, options);

This routing mechanism ensures that any component calling the unified API automatically gains support for new providers as soon as they are registered in the factory.

Adding a New LLM Provider

Extending aisuite to support a new LLM provider requires four specific steps:

  1. Implement the provider class by extending BaseProvider from src/core/base-provider.ts and implementing the required methods like chatCompletion and streamChatCompletion.

  2. Export the provider by creating src/providers/<new>/index.ts and adding the export to src/providers/index.ts.

  3. Update the configuration types in src/types/providers.ts by adding a new optional field to the ProviderConfigs interface.

  4. Register in the factory by adding instantiation logic to Client.initializeProviders in src/client.ts.

Complete Implementation Example

The following example demonstrates adding a hypothetical MyLLM provider to the factory.

First, implement the provider class:

// src/providers/myllm/provider.ts
import { BaseProvider } from "../../core/base-provider";

export class MyLLMProvider extends BaseProvider {
  constructor(private readonly cfg: MyLLMConfig) {
    super();
    // initialise SDK/client here
  }

  async chatCompletion(req: MyLLMChatRequest) {
    // call the MyLLM SDK and return a ChatCompletionResponse
  }

  async streamChatCompletion(req: MyLLMChatRequest) {
    // return an async iterable of ChatCompletionChunk
  }
}

Export the provider:

// src/providers/myllm/index.ts
export { MyLLMProvider } from "./provider";
export type { MyLLMConfig } from "./types";

Update the aggregate exports:

// src/providers/index.ts
export { OpenAIProvider } from "./openai";
export { AnthropicProvider } from "./anthropic";
export { GroqProvider } from "./groq";
export { MyLLMProvider } from "./myllm";   // ← new export

Extend the configuration interface:

// src/types/providers.ts
export interface ProviderConfigs {
  openai?: OpenAIConfig;
  anthropic?: AnthropicConfig;
  myllm?: MyLLMConfig;                     // ← new config entry
}

Finally, register the provider in the factory:

// src/client.ts
if (config.myllm) {
  this.chatProviders.set("myllm", new MyLLMProvider(config.myllm));
}

Using the new provider follows the standard pattern:

import { Client } from "aisuite-js";

const client = new Client({
  myllm: { apiKey: "YOUR_KEY", baseUrl: "https://api.my-llm.com" },
});

const resp = await client.chat.completions.create({
  model: "myllm/awesome-model",
  messages: [{ role: "user", content: "Hello!" }],
});

Summary

  • Centralized factory: The Client class in src/client.ts acts as the sole factory responsible for provider instantiation and lifecycle management.
  • Map-based storage: Providers are stored in chatProviders and asrProviders Maps using the provider name as the key for O(1) lookup during request routing.
  • String-based routing: The parseModel utility splits provider/model strings to determine which registered provider instance should handle the request.
  • Minimal integration overhead: Adding a new provider requires only implementing BaseProvider, updating type definitions in src/types/providers.ts, and adding a single registration block in Client.initializeProviders.

Frequently Asked Questions

What base class must new providers extend?

New providers must extend BaseProvider from src/core/base-provider.ts and implement the required interface methods, including chatCompletion for standard requests and optionally streamChatCompletion for streaming responses.

How does the factory handle providers that support both chat and ASR?

Providers that support both chat completions and audio transcriptions (such as OpenAI) are instantiated once and registered in both the chatProviders and asrProviders Maps using the same provider name key, as shown in the initializeProviders implementation.

Can I use environment variables instead of the configuration object?

While the factory accepts an explicit ProviderConfigs object in the constructor, individual provider implementations typically read API keys from environment variables (like OPENAI_API_KEY) if the configuration object does not explicitly provide them.

What is the required format for model strings?

The model string must use the format provider/model-name (for example, anthropic/claude-3-opus or groq/llama2-70b). The parseModel function in src/utils/model-parser.ts splits this string on the first forward slash to identify the provider and the specific model identifier.

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 →