This multi-model chat interface allows users to toggle between cloud LLMs and private (local) models within a single environment. Our sample project supports persistent chat threads with history management and automated title generation based on a user's initial prompt.
The sample app leverages the following DevExpress Blazor components:
To run this sample, configure project dependencies and set up secure authentication for the desired AI service.
We use the following versions of Microsoft AI packages in this project:
| NuGet Package | Version |
|---|---|
| Microsoft.Extensions.AI | 9.7.1 |
| Microsoft.Extensions.AI.OpenAI | 9.7.1-preview.1.25365.4 |
| Microsoft.Extensions.AI.Ollama | 9.7.0-preview.1.25356.2 |
| Azure.AI.OpenAI | 2.2.0-beta.5 |
Note
We cannot guarantee compatibility or correct execution with newer versions. Refer to the following announcement for additional information in this regard: DevExpress.AIIntegration references stable versions of Microsoft AI packages.
Note
DevExpress AI-powered extensions follow the "bring your own key" principle. DevExpress does not offer a REST API and does not ship any built-in LLMs/SLMs. You need an active Azure/Open AI subscription to obtain the REST API endpoint, key, and model deployment name. These variables must be specified at application startup to register AI clients and enable DevExpress AI-powered Extensions in your application.
This example uses the following AI services:
| AI Provider | Model |
|---|---|
| Azure OpenAI | gpt-4.1 |
| Local Ollama deployment | phi4:latest |
For security reasons, secrets are stored in the appsettings.json file. Update the following sections with your own credentials:
OpenAISettingsEndpoint: Your Azure OpenAI endpointKey: Your Azure OpenAI keyDeploymentName: Azure OpenAI model ID
OllamaSettingsUri: Local Ollama API endpointModelName: Local Ollama model
Note Update
appsettings.Development.jsonto test this example in your local development environment.
The following code in Program.cs retrieves the provider API configuration. Modify this code if you prefer to keep keys in environment variables or User Secrets.
var openAiServiceSettings = builder.Configuration.GetSection("OpenAISettings").Get<OpenAIServiceSettings>();
var ollamaSettings = builder.Configuration.GetSection("OllamaSettings").Get<OllamaSettings>();
// Register individual IChatClient instances as keyed scoped services
builder.Services.AddKeyedScoped<IChatClient>("azure-openai", (_, _) =>
new AzureOpenAIClient(
new Uri(openAiServiceSettings.Endpoint),
new AzureKeyCredential(openAiServiceSettings.Key))
.GetChatClient(openAiServiceSettings.DeploymentName)
.AsIChatClient());
builder.Services.AddKeyedScoped<IChatClient>("ollama-phi4", (_, _) =>
new OllamaChatClient(
new Uri(ollamaSettings.Uri),
ollamaSettings.ModelName,
new HttpClient { Timeout = TimeSpan.FromMinutes(10) }));
// Assemble the composite client from keyed IChatClient services
builder.Services.AddScoped<CompositeChatClient>(provider => {
var threadStore = provider.GetRequiredService<IChatThreadStore>();
var titleGenerator = provider.GetRequiredService<IChatThreadTitleGenerator>();
return new CompositeChatClient(
threadStore,
titleGenerator,
new ChatClientSession(
provider.GetRequiredKeyedService<IChatClient>("azure-openai"),
"azure-openai",
$"Azure Open AI - {openAiServiceSettings.DeploymentName}"),
new ChatClientSession(
provider.GetRequiredKeyedService<IChatClient>("ollama-phi4"),
"ollama-phi4",
$"Ollama - {ollamaSettings.ModelName}"));
});This section introduces key code blocks used in the example and how they work together to deliver a complete AI chat experience.
This application uses a two-pane layout with a DxSplitter that separates the sidebar and chat pane.
CSS styles that define size and spacing for the splitter, sidebar, and chat component reside in Index.razor.css.
The application allows users to switch between cloud and local AI providers on-the-fly.
- Program.cs integrates two named ChatClientSession instances (Azure OpenAI and Ollama) and registers them in the CompositeChatClient object. CompositeChatClient implements the IChatClient interface and forwards chat requests to the current session.
- Index.razor uses the DxComboBox component bound to
CompositeChatClient.AvailableChatClientsand updates the selected session. Changing the model preserves existing chat history and continues the conversation with the newly selected model. - Each thread stores model selection in the
ModelSessionIdproperty. Switching chat threads restores the model selection.
To support streaming responses, our chat implementation uses the default message pipeline without the MessageSent event override.
CompositeChatClient intercepts user prompts using GetResponseAsync / GetStreamingResponseAsync. The selected AI model generates an automatic thread title (3–6 words) from the first user prompt in the background.
In case of failure, the first six words of the user message serve as the title.
Each conversation thread is a ChatThread object. It contains a list of messages and metadata that is used in the UI for titles and ordering. Index.razor calls the SaveMessages method before switching threads and LoadMessages when a thread becomes active.
InMemoryChatThreadStore keeps chat history in a dictionary guarded by a lock for thread-safety:
- Creates new threads with a title, model session ID, and timestamps.
- Returns ordered threads.
- Saves messages, updates titles, and updates model session IDs.
Since thread state is stored in memory, all chat history is lost when the application restarts.
To persist chat history when the application restarts, implement IChatThreadStore with a database-backed store (for example, EF Core). Then replace InMemoryChatThreadStore with your implementation in Program.cs:
// Replace with your database-backed implementation
builder.Services.AddSingleton<IChatThreadStore, InMemoryChatThreadStore>();
- Program.cs
- appsettings.json (use
appsettings.Development.jsonfor your local development environment) - Index.razor
- Index.razor.css
- CompositeChatClient.cs
- ChatClientSession.cs
- ChatThread.cs
- IChatThreadStore.cs
- InMemoryChatThreadStore.cs
- IChatThreadTitleGenerator.cs
- ChatThreadTitleGenerator.cs
- DevExpress AI-powered Extensions for Blazor
- DevExpress Blazor AI Chat Control
- DevExpress Blazor Splitter
- DevExpress Blazor List Box
- Blazor AI Chat - How to add the DevExpress Blazor AI Chat component to your next Blazor, MAUI, WPF, and WinForms application
- Blazor AI Chat — Implement Function/Tool Calling
- Rich Text Editor and HTML Editor for Blazor - How to integrate AI-powered extensions
(you will be redirected to DevExpress.com to submit your response)
