# ChatMCP LLM Client Factory Architecture: How It Supports Multiple Providers

> Explore the ChatMCP LLM client factory architecture. Discover how it uses a factory pattern and a uniform API to support multiple LLM providers, enabling runtime selection with ease.

- Repository: [刀刀/chatmcp](https://github.com/daodao97/chatmcp)
- Tags: architecture
- Published: 2026-02-28

---

**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.

```dart
enum LLMProvider {
  openai,
  claude,
  ollama,
  deepseek,
  gemini,
  foundry,
  claudeCode,
  copilot,
}

```

*Source:* [`lib/llm/llm_factory.dart`](https://github.com/daodao97/chatmcp/blob/main/lib/llm/llm_factory.dart#L15-L16)

### 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.

```dart
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`](https://github.com/daodao97/chatmcp/blob/main/lib/llm/base_llm_client.dart#L10-L70)

### 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.

```dart
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`](https://github.com/daodao97/chatmcp/blob/main/lib/llm/llm_factory.dart#L18-L36)

### 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`.

```dart
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`](https://github.com/daodao97/chatmcp/blob/main/lib/llm/llm_factory.dart#L69-L90)

### 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.

```dart
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`](https://github.com/daodao97/chatmcp/blob/main/lib/llm/llm_factory.dart#L47-L56)

## 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`](https://github.com/daodao97/chatmcp/blob/main/lib/provider/settings_provider.dart#L8-L38)

### 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:

1. **Add the enum value** to `LLMProvider` in `lib/llm/llm_factory.dart`.
2. **Implement the client class** extending `BaseLLMClient` in a new file under `lib/llm/`.
3. **Register the mapping** in `LLMFactoryHelper.providerMap` to 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:

```dart
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**: `LLMFactory` centralizes instantiation logic using an exhaustive switch on the `LLMProvider` enum to return concrete client instances.
- **Abstract Contract**: `BaseLLMClient` enforces a uniform API across all providers, isolating HTTP-specific implementations from chat interface code.
- **Runtime Resolution**: `LLMFactoryHelper.createFromModel` dynamically 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.