---
title: A local LLM in Blazor with Microsoft.Extensions.AI
date: 2026-08-17
tags: ai, llm, blazor, dotnet, lm-studio
summary: Build a small Blazor chat that talks to a model in LM Studio through IChatClient. Nothing leaves your machine.
---

I wanted to connect a local AI model to a Blazor app without sending anything to the cloud. With a few NuGet packages it is quite simple. The source is on GitHub: [pschnidrig/localAiChat](https://github.com/pschnidrig/localAiChat).

## What you need

- LM Studio with a model loaded. I used `qwen2.5-coder-1.5b-instruct`. See [the LM Studio post](/p/2026-06-13-local-llm-lm-studio/) if you need to set it up.
- The LM Studio local server running (the Developer tab, then **Start Server**). The default port is 1234.
- A Blazor Server app on .NET 10.

## 1. NuGet packages

```sh title="terminal"
dotnet add package Microsoft.Extensions.AI
dotnet add package Microsoft.Extensions.AI.OpenAI
dotnet add package OpenAI
```

What each one does:

- `OpenAI` is the actual client that talks to the server.
- `Microsoft.Extensions.AI.OpenAI` connects that client to the generic `IChatClient` interface.
- `Microsoft.Extensions.AI` is the abstraction itself: `IChatClient`, `ChatMessage` and friends.

## 2. Configuration

LM Studio's server is OpenAI-compatible, so we just point the client at localhost. The SDK requires an API key, but LM Studio ignores it unless you set one up. I didn't, so it is a placeholder.

```json title="appsettings.json"
"LmStudio": {
  "Endpoint": "http://localhost:1234/v1",
  "ModelName": "qwen2.5-coder-1.5b-instruct",
  "ApiKey": "lm-studio"
}
```

`ModelName` must match the model ID shown in LM Studio. The same settings work for cloud models. You can develop against a local model and use a hosted one, or your own container, in production by changing only the config.

## 3. Program.cs

```csharp title="Program.cs"
// Read config (I use an options class out of habit, hence the section name)
var lmStudio = builder.Configuration
    .GetRequiredSection(LmStudioOptions.SectionName)
    .Get<LmStudioOptions>()!;

// An OpenAI client pointed at LM Studio
var lmStudioClient = new OpenAIClient(
    new ApiKeyCredential(lmStudio.ApiKey),
    new OpenAIClientOptions { Endpoint = new Uri(lmStudio.Endpoint) });

// Register it as IChatClient so any component can inject it
builder.Services.AddChatClient(
    lmStudioClient.GetChatClient(lmStudio.ModelName).AsIChatClient());
```

## 4. The chat page

Inject `IChatClient`, keep the chat history in a list and call `GetResponseAsync` with it. Sending the whole history each time is what gives the model its memory.

```razor title="Home.razor"
@page "/"
@inject IChatClient ChatClient
@rendermode InteractiveServer

<PageTitle>Local LLM Chat</PageTitle>

<h1>Local LLM Chat</h1>

<div class="chat">
    @foreach (var msg in history)
    {
        <p><strong>@(msg.Role == ChatRole.User ? "You" : "AI"):</strong> @msg.Text</p>
    }
</div>

<input @bind="input" @bind:event="oninput" @onkeyup="HandleKeyUp" disabled="@busy" placeholder="Ask something..." />
<button @onclick="SendAsync" disabled="@(busy || string.IsNullOrWhiteSpace(input))">Send</button>

@code {
    private string input = string.Empty;
    private bool busy;
    private List<ChatMessage> history = new();

    private async Task SendAsync()
    {
        if (string.IsNullOrWhiteSpace(input) || busy) return;

        history.Add(new ChatMessage(ChatRole.User, input));
        input = string.Empty;
        busy = true;

        try
        {
            var response = await ChatClient.GetResponseAsync(history);
            history.AddRange(response.Messages);
        }
        catch (Exception ex)
        {
            history.Add(new ChatMessage(ChatRole.Assistant, $"Error: {ex.Message}"));
        }
        finally
        {
            busy = false;
        }
    }

    private async Task HandleKeyUp(KeyboardEventArgs e)
    {
        if (e.Key == "Enter") await SendAsync();
    }
}
```

The role decides the label: `ChatRole.User` is you, `ChatRole.Assistant` is the AI. The actual call is this one line:

```csharp title="the call"
var response = await ChatClient.GetResponseAsync(history);
```

## Result

Start LM Studio, load a model, start the server, run the Blazor app. You have a chat with conversation history, and nothing leaves your machine.

Note that this version waits for the full answer. For a typing effect, `IChatClient` also has `GetStreamingResponseAsync`.
