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 providersasrProviders: 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:
-
Implement the provider class by extending
BaseProviderfromsrc/core/base-provider.tsand implementing the required methods likechatCompletionandstreamChatCompletion. -
Export the provider by creating
src/providers/<new>/index.tsand adding the export tosrc/providers/index.ts. -
Update the configuration types in
src/types/providers.tsby adding a new optional field to theProviderConfigsinterface. -
Register in the factory by adding instantiation logic to
Client.initializeProvidersinsrc/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
Clientclass insrc/client.tsacts as the sole factory responsible for provider instantiation and lifecycle management. - Map-based storage: Providers are stored in
chatProvidersandasrProvidersMaps using the provider name as the key for O(1) lookup during request routing. - String-based routing: The
parseModelutility splitsprovider/modelstrings to determine which registered provider instance should handle the request. - Minimal integration overhead: Adding a new provider requires only implementing
BaseProvider, updating type definitions insrc/types/providers.ts, and adding a single registration block inClient.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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →