ChatMCP LLM Client Factory Architecture: How It Supports Multiple Providers
ChatMCP implements a factory pattern where LLMFactory instantiates provider-specific clients that inherit from BaseLLMClient, enabling runtime selection through an enum-based switch and string-to-enum mapping while maintaining a uniform API across all LLM services.
ChatMCP is an open-source Flutter application that integrates with diverse Large Language Model services including OpenAI, Claude, Ollama, and Gemini. The daodao97/chatmcp repository decouples provider-specific HTTP logic from the core chat interface through a robust abstraction layer located in lib/llm/. This architecture allows the application to treat every LLM service identically while handling unique authentication schemas, endpoints, and response formats internally.
Core Components of the Factory Architecture
The ChatMCP LLM client factory separates three distinct concerns: provider identification, implementation instantiation, and common interface definition.
Provider Enumeration
All supported services are declared in the LLMProvider enum within lib/llm/llm_factory.dart. This serves as the authoritative registry of built-in providers and acts as the switch key for the factory.
enum LLMProvider {
openai,
claude,
ollama,
deepseek,
gemini,
foundry,
claudeCode,
copilot,
}
Source: lib/llm/llm_factory.dart
The Abstract Base Class
BaseLLMClient in lib/llm/base_llm_client.dart defines the contract that every concrete provider must implement. It standardizes both synchronous and streaming chat completions while providing shared utilities for proxy handling, error management, and endpoint construction.
abstract class BaseLLMClient {
Future<LLMResponse> chatCompletion(CompletionRequest request);
Stream<LLMResponse> chatStreamCompletion(CompletionRequest request);
// Helper methods (endpoint building, proxy handling, error handling, etc.)
}
Source: lib/llm/base_llm_client.dart
Concrete Provider Implementations
Each LLM service extends BaseLLMClient with provider-specific logic. OpenAIClient, ClaudeClient, OllamaClient, DeepSeekClient, GeminiClient, FoundryClient, CopilotClient, and ClaudeCodeClient handle unique request schemas, authentication headers, and response parsing while exposing the same public methods.
How the Factory Creates LLM Clients
The instantiation logic operates through two primary pathways: direct enum-based creation and dynamic resolution from user settings.
Direct Instantiation with LLMFactory.create
The static create method receives the provider enum value and required credentials, then returns the appropriate concrete instance through an exhaustive switch statement.
static BaseLLMClient create(
LLMProvider provider,
{required String apiKey, required String baseUrl, String? apiVersion}) {
switch (provider) {
case LLMProvider.openai: return OpenAIClient(apiKey: apiKey, baseUrl: baseUrl);
case LLMProvider.claude: return ClaudeClient(apiKey: apiKey, baseUrl: baseUrl);
case LLMProvider.claudeCode: return ClaudeCodeClient();
case LLMProvider.deepseek: return DeepSeekClient(apiKey: apiKey, baseUrl: baseUrl);
case LLMProvider.ollama: return OllamaClient(baseUrl: baseUrl);
case LLMProvider.gemini: return GeminiClient(apiKey: apiKey, baseUrl: baseUrl);
case LLMProvider.foundry: return FoundryClient(apiKey: apiKey, baseUrl: baseUrl, apiVersion: apiVersion);
case LLMProvider.copilot: return CopilotClient(apiKey: apiKey);
}
}
Source: lib/llm/llm_factory.dart
Runtime Resolution via LLMFactoryHelper.createFromModel
When a user selects a model from the UI, createFromModel bridges the gap between stored configuration and the factory. It retrieves the LLMProviderSetting from ProviderManager, validates that the provider is enabled, logs the masked API key, resolves the provider enum via string mapping or API style fallback, and invokes LLMFactory.create.
static BaseLLMClient createFromModel(llm_model.Model currentModel) {
final setting = ProviderManager.settingsProvider.apiSettings
.firstWhere((e) => e.providerId == currentModel.providerId);
if (setting.enable == false) throw Exception('Provider disabled');
_logApiKeyUsage(currentModel.providerId, currentModel.name, setting.apiKey);
var provider = providerMap[currentModel.providerId] ??
LLMProvider.values.byName(currentModel.apiStyle);
return LLMFactory.create(provider,
apiKey: setting.apiKey,
baseUrl: setting.apiEndpoint,
apiVersion: setting.apiVersion);
}
Source: lib/llm/llm_factory.dart
String-to-Enum Mapping
The UI and persistent storage use string identifiers (e.g., "openai", "claude-code"). LLMFactoryHelper maintains providerMap to translate these IDs into enum values, with a fallback to LLMProvider.values.byName using the model's apiStyle when a direct mapping is absent.
static final Map<String, LLMProvider> providerMap = {
"openai": LLMProvider.openai,
"claude": LLMProvider.claude,
"claude-code": LLMProvider.claudeCode,
"deepseek": LLMProvider.deepseek,
"ollama": LLMProvider.ollama,
"gemini": LLMProvider.gemini,
"foundry": LLMProvider.foundry,
"copilot": LLMProvider.copilot,
};
Source: lib/llm/llm_factory.dart
Supporting Multiple Providers Seamlessly
The architecture enables multi-provider support through uniform configuration storage, defensive fallback mechanisms, and minimal-friction extensibility.
Unified Configuration Storage
LLMProviderSetting in lib/provider/settings_provider.dart stores apiKey, apiEndpoint, apiStyle, and providerId for every service in a standardized schema. This allows the UI to present a single "Add API" form regardless of the target LLM service, while the factory handles provider-specific constructor arguments.
Source: lib/provider/settings_provider.dart
Graceful Fallback Mechanism
If createFromModel encounters an unrecognized providerId or fails to resolve the enum, the factory defaults to instantiating an OpenAIClient using the "openai" settings. This ensures the application remains stable even when configurations reference deprecated or experimental providers.
Extending the Architecture
Adding a new LLM service requires only three steps:
- Add the enum value to
LLMProviderinlib/llm/llm_factory.dart. - Implement the client class extending
BaseLLMClientin a new file underlib/llm/. - Register the mapping in
LLMFactoryHelper.providerMapto link the string ID to the enum.
No modifications to chat logic, UI components, or request handling are necessary.
Implementation Example
The following pattern demonstrates how higher-level code consumes the factory without knowledge of the underlying provider:
import 'package:chatmcp/llm/llm_factory.dart';
import 'package:chatmcp/provider/provider_manager.dart';
// Suppose the user selects a model from the UI
void sendMessage(llm_model.Model selectedModel, List<ChatMessage> history) async {
try {
// 1️⃣ Build the appropriate client
final client = LLMFactoryHelper.createFromModel(selectedModel);
// 2️⃣ Prepare a request (model name, messages, etc.)
final request = CompletionRequest(
model: selectedModel.name,
messages: history,
);
// 3️⃣ Call the unified API (works for any provider)
final response = await client.chatCompletion(request);
// 4️⃣ Handle the response...
print('Assistant: ${response.content}');
} catch (e) {
// Uniform error handling across providers
print('LLM request failed: $e');
}
}
This approach ensures that calls to chatCompletion or chatStreamCompletion execute the correct provider-specific HTTP logic while returning data in a consistent format.
Summary
- Factory Pattern:
LLMFactorycentralizes instantiation logic using an exhaustive switch on theLLMProviderenum to return concrete client instances. - Abstract Contract:
BaseLLMClientenforces a uniform API across all providers, isolating HTTP-specific implementations from chat interface code. - Runtime Resolution:
LLMFactoryHelper.createFromModeldynamically selects providers based on user settings, validates enabled status, and maps string IDs to enum values. - Extensibility: New providers require only enum registration, class implementation, and map entry—no changes to consuming code.
- Defensive Design: The architecture includes graceful fallbacks to OpenAI-compatible clients when provider resolution fails.
Frequently Asked Questions
What is the role of BaseLLMClient in ChatMCP's architecture?
BaseLLMClient is an abstract class defined in lib/llm/base_llm_client.dart that mandates the implementation of chatCompletion and chatStreamCompletion methods. It guarantees that every provider-specific client (OpenAI, Claude, Ollama, etc.) exposes an identical interface to the rest of the application, while internally handling unique authentication, endpoint construction, and response parsing.
How does ChatMCP handle unknown or disabled providers?
The LLMFactoryHelper.createFromModel method explicitly checks the enable flag on the provider's settings and throws an exception if disabled. If the provider string ID is not found in providerMap, the factory falls back to resolving the enum via LLMProvider.values.byName(currentModel.apiStyle). If all resolution fails, the system defaults to creating an OpenAIClient, ensuring the application does not crash due to unrecognized configurations.
What files must be modified to add a new LLM provider to ChatMCP?
Adding support for a new service requires editing lib/llm/llm_factory.dart to add the provider name to the LLMProvider enum and the corresponding string mapping in providerMap. You must also create a new implementation file (e.g., lib/llm/newprovider_client.dart) that extends BaseLLMClient. The LLMFactory.create switch statement must include a case returning the new client class with appropriate constructor arguments.
How are API credentials and endpoints managed across different providers?
All provider configurations are stored as LLMProviderSetting objects in lib/provider/settings_provider.dart, which maintains a list of apiSettings containing apiKey, apiEndpoint, apiVersion, and providerId. When LLMFactoryHelper.createFromModel is invoked, it extracts these values from the settings store and passes them to LLMFactory.create, ensuring each concrete client receives the correct credentials without hard-coding sensitive data in the factory logic.
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 →