elbruno/ElBruno.LocalLLMs

C# local LLM chat completions library using ONNX Runtime, compatible with Microsoft.Extensions.AI

21

stars

153

commits

C#

primary language

Aug 26, 2026

updated

ai
chat-completion
csharp
dotnet
embeddings
local-llm
machine-learning
microsoft-extensions-ai
net10
net8
nuget
onnx
onnx-runtime
rag

README

ElBruno.LocalLLMs

NuGet NuGet Downloads Build Status License: MIT HuggingFace .NET GitHub stars Twitter Follow

Run local LLMs in .NET through IChatClient 🧠

Run local LLMs in .NET through IChatClient β€” the same interface you'd use for Azure OpenAI, Ollama, or any other provider. Powered by ONNX Runtime GenAI and BitNet.

What's New

The last 5 notable additions to the library. Updated with each NuGet release.

  • 🧩 ElBruno.LocalLLMs.BlazorComponents β€” new Razor Class Library with 7 ready-to-use Blazor components: ModelStatusCard (download progress bar + actions), ModelGallery (filterable grid), ModelSelector (two-way-bindable dropdown), ChatBox (streaming token display), EnvironmentDashboard (CPU/CUDA/DirectML badges), LocalLLMHealthBadge (nav-bar status dot), and RagPlayground. Call services.AddLocalLLMsBlazorComponents() to register. See the Blazor Components Guide and the BlazorDemo sample.
  • 🧠 GPT-OSS 20B support β€” OpenAI's open-weight MoE model (Apache-2.0) now runs locally via the official onnxruntime/gpt-oss-20b-onnx artifacts. Adds the Harmony prompt format, channel-aware output filtering (chain-of-thought is stripped, never shown to users), Harmony tool calling, and a ReasoningEffort option. Two model IDs: gpt-oss-20b (CPU INT4) and gpt-oss-20b-cuda. See the GptOssChat sample. Also fixes a token-duplication bug that repeated the final token of every generation.
  • πŸ” v0.21.0 β€” Clean re-publish after v0.20.12 failed to propagate on NuGet.org; carries forward the issue #49 assembly-version fix and issue #51 vision-probe hardening.
  • πŸš€ v0.20.12 β€” Corrects sibling-package assembly versions, hardens vision token probing against model context limits, and verifies Fara smart image resizing for screenshot workflows.
  • ⬆️ v0.20.9 β€” Upgraded onnxruntime-genai to 0.15.1 and Microsoft.Extensions.AI.Abstractions to 10.8.3 across all projects. No API changes.

Features

  • 🧩 Blazor components β€” ModelStatusCard, ChatBox, ModelGallery, ModelSelector, EnvironmentDashboard, LocalLLMHealthBadge, RagPlayground via ElBruno.LocalLLMs.BlazorComponents (guide)
  • πŸ”Œ IChatClient implementation β€” seamless integration with Microsoft.Extensions.AI
  • πŸ“¦ Automatic model download β€” models are fetched from HuggingFace on first use
  • πŸš€ Zero friction β€” works out of the box with sensible defaults (Phi-3.5 mini)
  • πŸ–₯️ Multi-hardware β€” CPU, CUDA, and DirectML execution providers
  • πŸ’‰ DI-friendly β€” register with AddLocalLLMs() or AddBitNetChatClient() in ASP.NET Core
  • πŸ”„ Streaming β€” token-by-token streaming via GetStreamingResponseAsync
  • πŸ“Š Multi-model β€” switch between Phi-3.5, Phi-4, Qwen2.5, Qwen3, Llama 3.2, MagenticBrain, and more
  • πŸ‘οΈ Vision-language models β€” run Fara 1.5-9B image+text models via LocalVisionChatClient
  • πŸ€– Agentic models β€” Qwen3 / MagenticBrain support for multi-agent orchestration loops
  • 🎯 Fine-tuned models β€” pre-trained Qwen2.5 variants for tool calling and RAG (guide)
  • ⚑ BitNet support β€” run 1.58-bit ternary models via bitnet.cpp with extreme efficiency (guide)
  • πŸ“ˆ OpenTelemetry diagnostics β€” lifecycle activities and metrics for queued, first-token, completion, cancellation, and failure (guide)

Packages

PackageNuGetDownloadsDescription
ElBruno.LocalLLMsNuGetDownloadsCore library β€” ONNX Runtime GenAI models via IChatClient
ElBruno.LocalLLMs.RagNuGetDownloadsRAG pipeline β€” document chunking, indexing, retrieval
ElBruno.LocalLLMs.BitNetNuGetDownloadsBitNet 1.58-bit models via bitnet.cpp + IChatClient
ElBruno.LocalLLMs.BlazorComponentsNuGetDownloadsBlazor components β€” ModelStatusCard, ChatBox, ModelGallery, and more

Installation

dotnet add package ElBruno.LocalLLMs

For CPU scenarios, no extra package is required β€” the transitive buildTransitive shim copies onnxruntime-genai.dll automatically on Windows.

Add a runtime package only when you want a specific GPU provider:

# 🟒 NVIDIA GPU (CUDA):
dotnet add package Microsoft.ML.OnnxRuntimeGenAI.Cuda

# πŸ”΅ Any Windows GPU β€” AMD, Intel, NVIDIA (DirectML):
dotnet add package Microsoft.ML.OnnxRuntimeGenAI.DirectML

⚠️ Add at most one GPU runtime package. Do not reference both Microsoft.ML.OnnxRuntimeGenAI.Cuda and Microsoft.ML.OnnxRuntimeGenAI.DirectML simultaneously.

If you use a GPU runtime package and want to disable the transitive CPU copy shim, set: <ElBrunoLocalLLMsDisableCpuNativeCopy>true</ElBrunoLocalLLMsDisableCpuNativeCopy> in your application .csproj.

πŸš€ The library defaults to ExecutionProvider.Auto β€” on Windows it tries DirectML β†’ CUDA β†’ CPU, and on Linux it tries CUDA β†’ CPU. No code changes needed.

Quick Start

using ElBruno.LocalLLMs;
using Microsoft.Extensions.AI;

// Create a local chat client (downloads Phi-3.5 mini on first run)
using var client = await LocalChatClient.CreateAsync();

var response = await client.GetResponseAsync([
    new(ChatRole.User, "What is the capital of France?")
]);

Console.WriteLine(response.Text);

First Run

The first time you create a LocalChatClient, the model is downloaded from HuggingFace to your local cache directory (~2-4 GB). This typically takes 30-60 seconds depending on your internet connection.

Track download progress:

using var client = await LocalChatClient.CreateAsync(
    new LocalLLMsOptions { Model = KnownModels.Phi35MiniInstruct },
    progress: new Progress<ModelDownloadProgress>(p =>
    {
        var percent = (p.BytesDownloaded * 100) / p.TotalBytes;
        Console.WriteLine($"{p.FileName}: {percent:F1}%");
    })
);

Subsequent runs load instantly from cache (%LOCALAPPDATA%/ElBruno/LocalLLMs/models).

Skip auto-download if using a pre-downloaded model:

var options = new LocalLLMsOptions
{
    Model = KnownModels.Phi35MiniInstruct,
    ModelPath = "/path/to/local/model",
    EnsureModelDownloaded = false
};
using var client = await LocalChatClient.CreateAsync(options);

Streaming

using ElBruno.LocalLLMs;
using Microsoft.Extensions.AI;

using var client = await LocalChatClient.CreateAsync(new LocalLLMsOptions
{
    Model = KnownModels.Phi35MiniInstruct
});

await foreach (var update in client.GetStreamingResponseAsync([
    new(ChatRole.System, "You are a helpful assistant."),
    new(ChatRole.User, "Explain quantum computing in simple terms.")
]))
{
    Console.Write(update.Text);
}

GPU Acceleration

By default, ExecutionProvider.Auto tries GPU first and falls back to CPU automatically:

// Use explicit GPU provider (fails if CUDA not installed; use Auto to fallback to CPU)
var options = new LocalLLMsOptions
{
    ExecutionProvider = ExecutionProvider.Cuda
};

// Multi-GPU systems: select device ID
var options2 = new LocalLLMsOptions
{
    ExecutionProvider = ExecutionProvider.Cuda,
    GpuDeviceId = 1  // Use second GPU
};

Auto fallback behavior:

  • Windows + DirectML available β†’ uses a Windows GPU through DirectML
  • Windows + DirectML unavailable, CUDA available β†’ uses NVIDIA GPU
  • Linux + CUDA available β†’ uses NVIDIA GPU
  • GPU unavailable β†’ falls back to CPU (no errors, just slower)

⚠️ CUDA note: ONNX Runtime GenAI 0.15.x expects CUDA 13.*, cuDNN 9.*, and the latest Microsoft Visual C++ 2015-2022 runtime. When those native libraries are missing, provider diagnostics now surface the exact DLL mismatch or missing dependency instead of entering the failing native path.

See Troubleshooting: GPU Setup for debugging GPU issues.

Model Metadata

Inspect model capabilities at runtime β€” context window size, model name, and vocabulary:

using var client = await LocalChatClient.CreateAsync();

var metadata = client.ModelInfo;
Console.WriteLine($"Model:          {metadata?.ModelName}");
Console.WriteLine($"Context window: {metadata?.MaxSequenceLength}");
Console.WriteLine($"Vocab size:     {metadata?.VocabSize}");

This is useful for prompt-length validation, adaptive chunking, and model selection logic.

Dependency Injection

builder.Services.AddLocalLLMs(options =>
{
    options.Model = KnownModels.Phi35MiniInstruct;
    options.ExecutionProvider = ExecutionProvider.DirectML;
});

// Inject IChatClient anywhere
public class MyService(IChatClient chatClient) { ... }

Error Handling

The library provides structured exception types for graceful error handling:

using ElBruno.LocalLLMs;
using Microsoft.Extensions.AI;

try
{
    using var client = await LocalChatClient.CreateAsync();
    var response = await client.GetResponseAsync([
        new(ChatRole.User, "Your question here")
    ]);
}
catch (ExecutionProviderException ex)
{
    // GPU/provider-specific error (no CUDA, DirectML not available, etc.)
    Console.WriteLine($"Provider error: {ex.Message}");
}
catch (ModelCapacityExceededException ex)
{
    // Prompt/response too long for model's context window
    Console.WriteLine($"Capacity error: {ex.Message}");
    // Solution: use a larger model or truncate the prompt
}
catch (InvalidOperationException ex)
{
    // General operation error (model not found, download failed, etc.)
    Console.WriteLine($"Operation error: {ex.Message}");
}

Observability

LocalChatClient emits generation lifecycle diagnostics through ActivitySource and Meter, both named ElBruno.LocalLLMs.

using ElBruno.LocalLLMs.Diagnostics;

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing.AddSource(LocalLLMsInstrumentation.ActivitySourceName))
    .WithMetrics(metrics => metrics.AddMeter(LocalLLMsInstrumentation.MeterName));

By default, telemetry excludes prompt and completion text. Opt in only when you want content attached:

var options = new LocalLLMsOptions
{
    CaptureTelemetryContent = true
};

See docs/observability.md for the lifecycle event contract, metric names, and Aspire wiring notes, and docs/cancellation.md for voice barge-in cancellation behavior.

Cache Management

Inspect and manage the local model cache programmatically:

// Remove a model from the cache (no-op if not cached)
await LocalChatClient.DeleteModelFromCacheAsync(KnownModels.Phi35MiniInstruct);

// Or use a custom cache directory
await LocalChatClient.DeleteModelFromCacheAsync(
    KnownModels.Phi35MiniInstruct,
    cacheDirectory: @"D:\my-models");

// Get cached size in bytes for one model (0 if not downloaded)
long bytes = LocalChatClient.GetModelCacheSize(KnownModels.Phi35MiniInstruct);
Console.WriteLine($"Cached: {bytes / 1024 / 1024:N0} MB");

// List all cached models with size and last-modified date
var cached = LocalChatClient.ListCachedModels();
foreach (var repo in cached)
    Console.WriteLine($"{repo.LocalDirectory}  {repo.TotalSizeBytes / 1024 / 1024:N0} MB  {repo.LastModified:yyyy-MM-dd}");

// Same APIs available on LocalVisionChatClient for vision models
await LocalVisionChatClient.DeleteModelFromCacheAsync(KnownModels.Fara15_9B);
long visionBytes = LocalVisionChatClient.GetModelCacheSize(KnownModels.Fara15_9B);

The default cache directory is %LOCALAPPDATA%/ElBruno/LocalLLMs/models (Windows) or ~/.local/share/ElBruno/LocalLLMs/models (Linux/macOS).

These operations delegate to ElBruno.HuggingFace.Downloader which provides the underlying DeleteCachedFilesAsync, GetCachedSize, and ListCachedRepos implementation.

Troubleshooting

GPU not working? Use ExecutionProvider.Cpu explicitly. See GPU Setup Validation.

Out of memory? Try a smaller model:

var options = new LocalLLMsOptions
{
    Model = KnownModels.Qwen25_05BInstruct  // 0.5B instead of 3.8B
};

Model download fails?

  • Check your internet connection
  • For private HuggingFace models, set the HF_TOKEN environment variable

For detailed troubleshooting, see docs/troubleshooting-guide.md.

Supported Models

TierModelParametersONNXID
βšͺ TinyTinyLlama-1.1B-Chat1.1Bβœ… Nativetinyllama-1.1b-chat
βšͺ TinySmolLM2-1.7B-Instruct1.7Bβœ… Nativesmollm2-1.7b-instruct
βšͺ TinyQwen2.5-0.5B-Instruct0.5Bβœ… Nativeqwen2.5-0.5b-instruct
βšͺ TinyQwen2.5-1.5B-Instruct1.5Bβœ… Nativeqwen2.5-1.5b-instruct
βšͺ TinyGemma-2B-IT2Bβœ… Nativegemma-2b-it
βšͺ TinyGemma-4-E2B-IT5.1B (2B active)πŸ”„ Convertgemma-4-e2b-it
βšͺ TinyStableLM-2-1.6B-Chat1.6Bβœ… Nativestablelm-2-1.6b-chat
🟒 SmallPhi-3.5 mini instruct3.8Bβœ… Nativephi-3.5-mini-instruct
🟒 SmallQwen2.5-3B-Instruct3Bβœ… Nativeqwen2.5-3b-instruct
🟒 SmallLlama-3.2-3B-Instruct3Bβœ… Nativellama-3.2-3b-instruct
🟒 SmallGemma-2-2B-IT2Bβœ… Nativegemma-2-2b-it
🟒 SmallGemma-4-E4B-IT8B (4B active)πŸ”„ Convertgemma-4-e4b-it
🟑 MediumQwen2.5-7B-Instruct7Bβœ… Nativeqwen2.5-7b-instruct
🟑 MediumQwen2.5-Coder-7B-Instruct7Bβœ… Nativeqwen2.5-coder-7b-instruct
🟑 MediumLlama-3.1-8B-Instruct8Bβœ… Nativellama-3.1-8b-instruct
🟑 MediumMistral-7B-Instruct-v0.37Bβœ… Nativemistral-7b-instruct-v0.3
🟑 MediumGemma-2-9B-IT9Bβœ… Nativegemma-2-9b-it
🟑 MediumGemma-4-12B-IT12BπŸ”„ Convertgemma-4-12b-it
🟑 MediumPhi-414Bβœ… Nativephi-4
🟑 MediumDeepSeek-R1-Distill-Qwen-14B14Bβœ… Nativedeepseek-r1-distill-qwen-14b
🟑 MediumMistral-Small-24B-Instruct24Bβœ… Nativemistral-small-24b-instruct
πŸ”΄ LargeQwen2.5-14B-Instruct14Bβœ… Nativeqwen2.5-14b-instruct
πŸ”΄ LargeQwen2.5-32B-Instruct32Bβœ… Nativeqwen2.5-32b-instruct
πŸ”΄ LargeLlama-3.3-70B-Instruct70Bβœ… ONNXllama-3.3-70b-instruct
πŸ”΄ LargeMixtral-8x7B-Instruct-v0.18x7Bβœ… Nativemixtral-8x7b-instruct-v0.1
πŸ”΄ LargeDeepSeek-R1-Distill-Llama-70B70Bβœ… Nativedeepseek-r1-distill-llama-70b
πŸ”΄ LargeCommand-R (35B)35Bβœ… Nativecommand-r-35b
πŸ”΄ LargeGemma-4-26B-A4B-IT25.2B (3.8B active)πŸ”„ Convertgemma-4-26b-a4b-it
πŸ”΄ LargeGemma-4-31B-IT30.7BπŸ”„ Convertgemma-4-31b-it
🟣 Next-GenQwen3-14B-Instruct14.77Bβœ… Nativeqwen3-14b-instruct
🧠 GPT-OSSGPT-OSS 20B (CPU INT4)21B (3.6B active, MoE)βœ… Nativegpt-oss-20b
🧠 GPT-OSSGPT-OSS 20B (CUDA INT4)21B (3.6B active, MoE)βœ… Nativegpt-oss-20b-cuda
πŸ€– AgenticMagenticBrain~14.77Bβœ… Nativemagentic-brain
πŸ‘οΈ VLMFara 1.5-9B~9.4Bβœ… Nativefara-1.5-9b

πŸ”„ Convert = Use the conversion scripts in scripts/ to export ONNX locally before running the model.

ΒΉ MagenticBrain ONNX: Native ONNX hosted at elbruno/MagenticBrain-onnx (INT4 quantized). Auto-downloads when EnsureModelDownloaded=true.

Β² Fara 1.5-9B ONNX: elbruno/Fara1.5-9B-onnx now includes the validated multimodal package (qwen3vl-vision.onnx, qwen3vl-embedding.onnx, patched genai_config.json, and ORT-compatible processor_config.json). See ONNX Conversion β€” Fara.

Β³ GPT-OSS 20B: Apache-2.0, from the official onnxruntime/gpt-oss-20b-onnx repository. The CPU INT4 variant is a ~12 GB download, and because GPT-OSS is a mixture-of-experts model, CPU inference is slow β€” prefer gpt-oss-20b-cuda with Microsoft.ML.OnnxRuntimeGenAI.Cuda when a GPU is available. GPT-OSS reasons before answering; that chain-of-thought is filtered out and never surfaced, per the model card. Reasoning depth is controlled by LocalLLMsOptions.ReasoningEffort.

Fine-Tuned Models

Pre-trained variants optimized for specific tasks. A fine-tuned 0.5B model often matches or exceeds a base 1.5B on its specialized task.

ModelSizeTaskHuggingFace ID
Qwen2.5-0.5B-ToolCalling~1 GBTool/function callingelbruno/Qwen2.5-0.5B-LocalLLMs-ToolCalling
Qwen2.5-0.5B-RAG~1 GBRAG with citationselbruno/Qwen2.5-0.5B-LocalLLMs-RAG
Qwen2.5-0.5B-Instruct~1 GBGeneral-purposeelbruno/Qwen2.5-0.5B-LocalLLMs-Instruct

See the Supported Models Guide for detailed model cards, performance benchmarks, and selection guidance.

Samples

SampleDescription
HelloChatMinimal console chat
StreamingChatToken-by-token streaming
MultiModelChatSwitch models at runtime
DependencyInjectionASP.NET Core DI registration
ToolCallingAgentFunction calling and tool use
FineTunedToolCallingFine-tuned model for improved tool calling
RagChatbotRAG pipeline with document retrieval
ZeroCloudRagZero-cloud RAG pipeline with real local embeddings and LLM inference
BitNetChatBitNet 1.58-bit model chat completion
BitNetPerformancePerformance benchmark: BitNet vs ONNX models
MagenticBrainAgentMulti-agent orchestration loop using Qwen3/MagenticBrain
FaraVisionAgentVision-language model (Fara 1.5-9B) image+text inference
GptOssChatGPT-OSS 20B chat, streaming, reasoning effort, and tool calling
MagenticUIServerASP.NET Core + SignalR multi-agent server (FileSurfer, WebFetcher, Coder)
ConsoleAppDemoInteractive console application

🌐 Reference App: ElBruno.MagenticUI β€” full Blazor Server port of microsoft/magentic-ui running locally with this library.

Requirements

  • .NET 8.0 or .NET 10.0
  • CPU (default), NVIDIA GPU (CUDA), or Windows GPU (DirectML)
  • ~2-8 GB disk space per model (depending on size and quantization)

Building from Source

git clone https://github.com/elbruno/ElBruno.LocalLLMs.git
cd ElBruno.LocalLLMs
dotnet restore ElBruno.LocalLLMs.slnx
dotnet build ElBruno.LocalLLMs.slnx
dotnet test ElBruno.LocalLLMs.slnx --framework net8.0

Run integration tests (downloads real models β€” requires internet):

RUN_INTEGRATION_TESTS=true dotnet test ElBruno.LocalLLMs.slnx --framework net8.0

Integration tests validate the full lifecycle (download β†’ infer β†’ cache hit β†’ delete) for all 35 supported models. See docs/tests/README.md for details.

Documentation

🀝 Contributing

Contributions are welcome! Please:

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

πŸ“„ License

This project is licensed under the MIT License β€” see the LICENSE file for details.

πŸ‘‹ About the Author

Hi! I'm ElBruno 🧑, a passionate developer and content creator exploring AI, .NET, and modern development practices.

Made with ❀️ by ElBruno

If you like this project, consider following my work across platforms:

  • πŸ“» Podcast: No Tienen Nombre β€” Spanish-language episodes on AI, development, and tech culture
  • πŸ’» Blog: ElBruno.com β€” Deep dives on embeddings, RAG, .NET, and local AI
  • πŸ“Ί YouTube: youtube.com/elbruno β€” Demos, tutorials, and live coding
  • πŸ”— LinkedIn: @elbruno β€” Professional updates and insights
  • 𝕏 Twitter: @elbruno β€” Quick tips, releases, and tech news

πŸ™ Acknowledgments

Contributors

elbruno

153 commits

elbruno/ElBruno.LocalLLMs

C# local LLM chat completions library using ONNX Runtime, compatible with Microsoft.Extensions.AI

21

stars

153

commits

C#

primary language

Aug 26, 2026

updated

ai
chat-completion
csharp
dotnet
embeddings
local-llm
machine-learning
microsoft-extensions-ai
net10
net8
nuget
onnx
onnx-runtime
rag

README

ElBruno.LocalLLMs

NuGet NuGet Downloads Build Status License: MIT HuggingFace .NET GitHub stars Twitter Follow

Run local LLMs in .NET through IChatClient 🧠

Run local LLMs in .NET through IChatClient β€” the same interface you'd use for Azure OpenAI, Ollama, or any other provider. Powered by ONNX Runtime GenAI and BitNet.

What's New

The last 5 notable additions to the library. Updated with each NuGet release.

  • 🧩 ElBruno.LocalLLMs.BlazorComponents β€” new Razor Class Library with 7 ready-to-use Blazor components: ModelStatusCard (download progress bar + actions), ModelGallery (filterable grid), ModelSelector (two-way-bindable dropdown), ChatBox (streaming token display), EnvironmentDashboard (CPU/CUDA/DirectML badges), LocalLLMHealthBadge (nav-bar status dot), and RagPlayground. Call services.AddLocalLLMsBlazorComponents() to register. See the Blazor Components Guide and the BlazorDemo sample.
  • 🧠 GPT-OSS 20B support β€” OpenAI's open-weight MoE model (Apache-2.0) now runs locally via the official onnxruntime/gpt-oss-20b-onnx artifacts. Adds the Harmony prompt format, channel-aware output filtering (chain-of-thought is stripped, never shown to users), Harmony tool calling, and a ReasoningEffort option. Two model IDs: gpt-oss-20b (CPU INT4) and gpt-oss-20b-cuda. See the GptOssChat sample. Also fixes a token-duplication bug that repeated the final token of every generation.
  • πŸ” v0.21.0 β€” Clean re-publish after v0.20.12 failed to propagate on NuGet.org; carries forward the issue #49 assembly-version fix and issue #51 vision-probe hardening.
  • πŸš€ v0.20.12 β€” Corrects sibling-package assembly versions, hardens vision token probing against model context limits, and verifies Fara smart image resizing for screenshot workflows.
  • ⬆️ v0.20.9 β€” Upgraded onnxruntime-genai to 0.15.1 and Microsoft.Extensions.AI.Abstractions to 10.8.3 across all projects. No API changes.

Features

  • 🧩 Blazor components β€” ModelStatusCard, ChatBox, ModelGallery, ModelSelector, EnvironmentDashboard, LocalLLMHealthBadge, RagPlayground via ElBruno.LocalLLMs.BlazorComponents (guide)
  • πŸ”Œ IChatClient implementation β€” seamless integration with Microsoft.Extensions.AI
  • πŸ“¦ Automatic model download β€” models are fetched from HuggingFace on first use
  • πŸš€ Zero friction β€” works out of the box with sensible defaults (Phi-3.5 mini)
  • πŸ–₯️ Multi-hardware β€” CPU, CUDA, and DirectML execution providers
  • πŸ’‰ DI-friendly β€” register with AddLocalLLMs() or AddBitNetChatClient() in ASP.NET Core
  • πŸ”„ Streaming β€” token-by-token streaming via GetStreamingResponseAsync
  • πŸ“Š Multi-model β€” switch between Phi-3.5, Phi-4, Qwen2.5, Qwen3, Llama 3.2, MagenticBrain, and more
  • πŸ‘οΈ Vision-language models β€” run Fara 1.5-9B image+text models via LocalVisionChatClient
  • πŸ€– Agentic models β€” Qwen3 / MagenticBrain support for multi-agent orchestration loops
  • 🎯 Fine-tuned models β€” pre-trained Qwen2.5 variants for tool calling and RAG (guide)
  • ⚑ BitNet support β€” run 1.58-bit ternary models via bitnet.cpp with extreme efficiency (guide)
  • πŸ“ˆ OpenTelemetry diagnostics β€” lifecycle activities and metrics for queued, first-token, completion, cancellation, and failure (guide)

Packages

PackageNuGetDownloadsDescription
ElBruno.LocalLLMsNuGetDownloadsCore library β€” ONNX Runtime GenAI models via IChatClient
ElBruno.LocalLLMs.RagNuGetDownloadsRAG pipeline β€” document chunking, indexing, retrieval
ElBruno.LocalLLMs.BitNetNuGetDownloadsBitNet 1.58-bit models via bitnet.cpp + IChatClient
ElBruno.LocalLLMs.BlazorComponentsNuGetDownloadsBlazor components β€” ModelStatusCard, ChatBox, ModelGallery, and more

Installation

dotnet add package ElBruno.LocalLLMs

For CPU scenarios, no extra package is required β€” the transitive buildTransitive shim copies onnxruntime-genai.dll automatically on Windows.

Add a runtime package only when you want a specific GPU provider:

# 🟒 NVIDIA GPU (CUDA):
dotnet add package Microsoft.ML.OnnxRuntimeGenAI.Cuda

# πŸ”΅ Any Windows GPU β€” AMD, Intel, NVIDIA (DirectML):
dotnet add package Microsoft.ML.OnnxRuntimeGenAI.DirectML

⚠️ Add at most one GPU runtime package. Do not reference both Microsoft.ML.OnnxRuntimeGenAI.Cuda and Microsoft.ML.OnnxRuntimeGenAI.DirectML simultaneously.

If you use a GPU runtime package and want to disable the transitive CPU copy shim, set: <ElBrunoLocalLLMsDisableCpuNativeCopy>true</ElBrunoLocalLLMsDisableCpuNativeCopy> in your application .csproj.

πŸš€ The library defaults to ExecutionProvider.Auto β€” on Windows it tries DirectML β†’ CUDA β†’ CPU, and on Linux it tries CUDA β†’ CPU. No code changes needed.

Quick Start

using ElBruno.LocalLLMs;
using Microsoft.Extensions.AI;

// Create a local chat client (downloads Phi-3.5 mini on first run)
using var client = await LocalChatClient.CreateAsync();

var response = await client.GetResponseAsync([
    new(ChatRole.User, "What is the capital of France?")
]);

Console.WriteLine(response.Text);

First Run

The first time you create a LocalChatClient, the model is downloaded from HuggingFace to your local cache directory (~2-4 GB). This typically takes 30-60 seconds depending on your internet connection.

Track download progress:

using var client = await LocalChatClient.CreateAsync(
    new LocalLLMsOptions { Model = KnownModels.Phi35MiniInstruct },
    progress: new Progress<ModelDownloadProgress>(p =>
    {
        var percent = (p.BytesDownloaded * 100) / p.TotalBytes;
        Console.WriteLine($"{p.FileName}: {percent:F1}%");
    })
);

Subsequent runs load instantly from cache (%LOCALAPPDATA%/ElBruno/LocalLLMs/models).

Skip auto-download if using a pre-downloaded model:

var options = new LocalLLMsOptions
{
    Model = KnownModels.Phi35MiniInstruct,
    ModelPath = "/path/to/local/model",
    EnsureModelDownloaded = false
};
using var client = await LocalChatClient.CreateAsync(options);

Streaming

using ElBruno.LocalLLMs;
using Microsoft.Extensions.AI;

using var client = await LocalChatClient.CreateAsync(new LocalLLMsOptions
{
    Model = KnownModels.Phi35MiniInstruct
});

await foreach (var update in client.GetStreamingResponseAsync([
    new(ChatRole.System, "You are a helpful assistant."),
    new(ChatRole.User, "Explain quantum computing in simple terms.")
]))
{
    Console.Write(update.Text);
}

GPU Acceleration

By default, ExecutionProvider.Auto tries GPU first and falls back to CPU automatically:

// Use explicit GPU provider (fails if CUDA not installed; use Auto to fallback to CPU)
var options = new LocalLLMsOptions
{
    ExecutionProvider = ExecutionProvider.Cuda
};

// Multi-GPU systems: select device ID
var options2 = new LocalLLMsOptions
{
    ExecutionProvider = ExecutionProvider.Cuda,
    GpuDeviceId = 1  // Use second GPU
};

Auto fallback behavior:

  • Windows + DirectML available β†’ uses a Windows GPU through DirectML
  • Windows + DirectML unavailable, CUDA available β†’ uses NVIDIA GPU
  • Linux + CUDA available β†’ uses NVIDIA GPU
  • GPU unavailable β†’ falls back to CPU (no errors, just slower)

⚠️ CUDA note: ONNX Runtime GenAI 0.15.x expects CUDA 13.*, cuDNN 9.*, and the latest Microsoft Visual C++ 2015-2022 runtime. When those native libraries are missing, provider diagnostics now surface the exact DLL mismatch or missing dependency instead of entering the failing native path.

See Troubleshooting: GPU Setup for debugging GPU issues.

Model Metadata

Inspect model capabilities at runtime β€” context window size, model name, and vocabulary:

using var client = await LocalChatClient.CreateAsync();

var metadata = client.ModelInfo;
Console.WriteLine($"Model:          {metadata?.ModelName}");
Console.WriteLine($"Context window: {metadata?.MaxSequenceLength}");
Console.WriteLine($"Vocab size:     {metadata?.VocabSize}");

This is useful for prompt-length validation, adaptive chunking, and model selection logic.

Dependency Injection

builder.Services.AddLocalLLMs(options =>
{
    options.Model = KnownModels.Phi35MiniInstruct;
    options.ExecutionProvider = ExecutionProvider.DirectML;
});

// Inject IChatClient anywhere
public class MyService(IChatClient chatClient) { ... }

Error Handling

The library provides structured exception types for graceful error handling:

using ElBruno.LocalLLMs;
using Microsoft.Extensions.AI;

try
{
    using var client = await LocalChatClient.CreateAsync();
    var response = await client.GetResponseAsync([
        new(ChatRole.User, "Your question here")
    ]);
}
catch (ExecutionProviderException ex)
{
    // GPU/provider-specific error (no CUDA, DirectML not available, etc.)
    Console.WriteLine($"Provider error: {ex.Message}");
}
catch (ModelCapacityExceededException ex)
{
    // Prompt/response too long for model's context window
    Console.WriteLine($"Capacity error: {ex.Message}");
    // Solution: use a larger model or truncate the prompt
}
catch (InvalidOperationException ex)
{
    // General operation error (model not found, download failed, etc.)
    Console.WriteLine($"Operation error: {ex.Message}");
}

Observability

LocalChatClient emits generation lifecycle diagnostics through ActivitySource and Meter, both named ElBruno.LocalLLMs.

using ElBruno.LocalLLMs.Diagnostics;

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing.AddSource(LocalLLMsInstrumentation.ActivitySourceName))
    .WithMetrics(metrics => metrics.AddMeter(LocalLLMsInstrumentation.MeterName));

By default, telemetry excludes prompt and completion text. Opt in only when you want content attached:

var options = new LocalLLMsOptions
{
    CaptureTelemetryContent = true
};

See docs/observability.md for the lifecycle event contract, metric names, and Aspire wiring notes, and docs/cancellation.md for voice barge-in cancellation behavior.

Cache Management

Inspect and manage the local model cache programmatically:

// Remove a model from the cache (no-op if not cached)
await LocalChatClient.DeleteModelFromCacheAsync(KnownModels.Phi35MiniInstruct);

// Or use a custom cache directory
await LocalChatClient.DeleteModelFromCacheAsync(
    KnownModels.Phi35MiniInstruct,
    cacheDirectory: @"D:\my-models");

// Get cached size in bytes for one model (0 if not downloaded)
long bytes = LocalChatClient.GetModelCacheSize(KnownModels.Phi35MiniInstruct);
Console.WriteLine($"Cached: {bytes / 1024 / 1024:N0} MB");

// List all cached models with size and last-modified date
var cached = LocalChatClient.ListCachedModels();
foreach (var repo in cached)
    Console.WriteLine($"{repo.LocalDirectory}  {repo.TotalSizeBytes / 1024 / 1024:N0} MB  {repo.LastModified:yyyy-MM-dd}");

// Same APIs available on LocalVisionChatClient for vision models
await LocalVisionChatClient.DeleteModelFromCacheAsync(KnownModels.Fara15_9B);
long visionBytes = LocalVisionChatClient.GetModelCacheSize(KnownModels.Fara15_9B);

The default cache directory is %LOCALAPPDATA%/ElBruno/LocalLLMs/models (Windows) or ~/.local/share/ElBruno/LocalLLMs/models (Linux/macOS).

These operations delegate to ElBruno.HuggingFace.Downloader which provides the underlying DeleteCachedFilesAsync, GetCachedSize, and ListCachedRepos implementation.

Troubleshooting

GPU not working? Use ExecutionProvider.Cpu explicitly. See GPU Setup Validation.

Out of memory? Try a smaller model:

var options = new LocalLLMsOptions
{
    Model = KnownModels.Qwen25_05BInstruct  // 0.5B instead of 3.8B
};

Model download fails?

  • Check your internet connection
  • For private HuggingFace models, set the HF_TOKEN environment variable

For detailed troubleshooting, see docs/troubleshooting-guide.md.

Supported Models

TierModelParametersONNXID
βšͺ TinyTinyLlama-1.1B-Chat1.1Bβœ… Nativetinyllama-1.1b-chat
βšͺ TinySmolLM2-1.7B-Instruct1.7Bβœ… Nativesmollm2-1.7b-instruct
βšͺ TinyQwen2.5-0.5B-Instruct0.5Bβœ… Nativeqwen2.5-0.5b-instruct
βšͺ TinyQwen2.5-1.5B-Instruct1.5Bβœ… Nativeqwen2.5-1.5b-instruct
βšͺ TinyGemma-2B-IT2Bβœ… Nativegemma-2b-it
βšͺ TinyGemma-4-E2B-IT5.1B (2B active)πŸ”„ Convertgemma-4-e2b-it
βšͺ TinyStableLM-2-1.6B-Chat1.6Bβœ… Nativestablelm-2-1.6b-chat
🟒 SmallPhi-3.5 mini instruct3.8Bβœ… Nativephi-3.5-mini-instruct
🟒 SmallQwen2.5-3B-Instruct3Bβœ… Nativeqwen2.5-3b-instruct
🟒 SmallLlama-3.2-3B-Instruct3Bβœ… Nativellama-3.2-3b-instruct
🟒 SmallGemma-2-2B-IT2Bβœ… Nativegemma-2-2b-it
🟒 SmallGemma-4-E4B-IT8B (4B active)πŸ”„ Convertgemma-4-e4b-it
🟑 MediumQwen2.5-7B-Instruct7Bβœ… Nativeqwen2.5-7b-instruct
🟑 MediumQwen2.5-Coder-7B-Instruct7Bβœ… Nativeqwen2.5-coder-7b-instruct
🟑 MediumLlama-3.1-8B-Instruct8Bβœ… Nativellama-3.1-8b-instruct
🟑 MediumMistral-7B-Instruct-v0.37Bβœ… Nativemistral-7b-instruct-v0.3
🟑 MediumGemma-2-9B-IT9Bβœ… Nativegemma-2-9b-it
🟑 MediumGemma-4-12B-IT12BπŸ”„ Convertgemma-4-12b-it
🟑 MediumPhi-414Bβœ… Nativephi-4
🟑 MediumDeepSeek-R1-Distill-Qwen-14B14Bβœ… Nativedeepseek-r1-distill-qwen-14b
🟑 MediumMistral-Small-24B-Instruct24Bβœ… Nativemistral-small-24b-instruct
πŸ”΄ LargeQwen2.5-14B-Instruct14Bβœ… Nativeqwen2.5-14b-instruct
πŸ”΄ LargeQwen2.5-32B-Instruct32Bβœ… Nativeqwen2.5-32b-instruct
πŸ”΄ LargeLlama-3.3-70B-Instruct70Bβœ… ONNXllama-3.3-70b-instruct
πŸ”΄ LargeMixtral-8x7B-Instruct-v0.18x7Bβœ… Nativemixtral-8x7b-instruct-v0.1
πŸ”΄ LargeDeepSeek-R1-Distill-Llama-70B70Bβœ… Nativedeepseek-r1-distill-llama-70b
πŸ”΄ LargeCommand-R (35B)35Bβœ… Nativecommand-r-35b
πŸ”΄ LargeGemma-4-26B-A4B-IT25.2B (3.8B active)πŸ”„ Convertgemma-4-26b-a4b-it
πŸ”΄ LargeGemma-4-31B-IT30.7BπŸ”„ Convertgemma-4-31b-it
🟣 Next-GenQwen3-14B-Instruct14.77Bβœ… Nativeqwen3-14b-instruct
🧠 GPT-OSSGPT-OSS 20B (CPU INT4)21B (3.6B active, MoE)βœ… Nativegpt-oss-20b
🧠 GPT-OSSGPT-OSS 20B (CUDA INT4)21B (3.6B active, MoE)βœ… Nativegpt-oss-20b-cuda
πŸ€– AgenticMagenticBrain~14.77Bβœ… Nativemagentic-brain
πŸ‘οΈ VLMFara 1.5-9B~9.4Bβœ… Nativefara-1.5-9b

πŸ”„ Convert = Use the conversion scripts in scripts/ to export ONNX locally before running the model.

ΒΉ MagenticBrain ONNX: Native ONNX hosted at elbruno/MagenticBrain-onnx (INT4 quantized). Auto-downloads when EnsureModelDownloaded=true.

Β² Fara 1.5-9B ONNX: elbruno/Fara1.5-9B-onnx now includes the validated multimodal package (qwen3vl-vision.onnx, qwen3vl-embedding.onnx, patched genai_config.json, and ORT-compatible processor_config.json). See ONNX Conversion β€” Fara.

Β³ GPT-OSS 20B: Apache-2.0, from the official onnxruntime/gpt-oss-20b-onnx repository. The CPU INT4 variant is a ~12 GB download, and because GPT-OSS is a mixture-of-experts model, CPU inference is slow β€” prefer gpt-oss-20b-cuda with Microsoft.ML.OnnxRuntimeGenAI.Cuda when a GPU is available. GPT-OSS reasons before answering; that chain-of-thought is filtered out and never surfaced, per the model card. Reasoning depth is controlled by LocalLLMsOptions.ReasoningEffort.

Fine-Tuned Models

Pre-trained variants optimized for specific tasks. A fine-tuned 0.5B model often matches or exceeds a base 1.5B on its specialized task.

ModelSizeTaskHuggingFace ID
Qwen2.5-0.5B-ToolCalling~1 GBTool/function callingelbruno/Qwen2.5-0.5B-LocalLLMs-ToolCalling
Qwen2.5-0.5B-RAG~1 GBRAG with citationselbruno/Qwen2.5-0.5B-LocalLLMs-RAG
Qwen2.5-0.5B-Instruct~1 GBGeneral-purposeelbruno/Qwen2.5-0.5B-LocalLLMs-Instruct

See the Supported Models Guide for detailed model cards, performance benchmarks, and selection guidance.

Samples

SampleDescription
HelloChatMinimal console chat
StreamingChatToken-by-token streaming
MultiModelChatSwitch models at runtime
DependencyInjectionASP.NET Core DI registration
ToolCallingAgentFunction calling and tool use
FineTunedToolCallingFine-tuned model for improved tool calling
RagChatbotRAG pipeline with document retrieval
ZeroCloudRagZero-cloud RAG pipeline with real local embeddings and LLM inference
BitNetChatBitNet 1.58-bit model chat completion
BitNetPerformancePerformance benchmark: BitNet vs ONNX models
MagenticBrainAgentMulti-agent orchestration loop using Qwen3/MagenticBrain
FaraVisionAgentVision-language model (Fara 1.5-9B) image+text inference
GptOssChatGPT-OSS 20B chat, streaming, reasoning effort, and tool calling
MagenticUIServerASP.NET Core + SignalR multi-agent server (FileSurfer, WebFetcher, Coder)
ConsoleAppDemoInteractive console application

🌐 Reference App: ElBruno.MagenticUI β€” full Blazor Server port of microsoft/magentic-ui running locally with this library.

Requirements

  • .NET 8.0 or .NET 10.0
  • CPU (default), NVIDIA GPU (CUDA), or Windows GPU (DirectML)
  • ~2-8 GB disk space per model (depending on size and quantization)

Building from Source

git clone https://github.com/elbruno/ElBruno.LocalLLMs.git
cd ElBruno.LocalLLMs
dotnet restore ElBruno.LocalLLMs.slnx
dotnet build ElBruno.LocalLLMs.slnx
dotnet test ElBruno.LocalLLMs.slnx --framework net8.0

Run integration tests (downloads real models β€” requires internet):

RUN_INTEGRATION_TESTS=true dotnet test ElBruno.LocalLLMs.slnx --framework net8.0

Integration tests validate the full lifecycle (download β†’ infer β†’ cache hit β†’ delete) for all 35 supported models. See docs/tests/README.md for details.

Documentation

🀝 Contributing

Contributions are welcome! Please:

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

πŸ“„ License

This project is licensed under the MIT License β€” see the LICENSE file for details.

πŸ‘‹ About the Author

Hi! I'm ElBruno 🧑, a passionate developer and content creator exploring AI, .NET, and modern development practices.

Made with ❀️ by ElBruno

If you like this project, consider following my work across platforms:

  • πŸ“» Podcast: No Tienen Nombre β€” Spanish-language episodes on AI, development, and tech culture
  • πŸ’» Blog: ElBruno.com β€” Deep dives on embeddings, RAG, .NET, and local AI
  • πŸ“Ί YouTube: youtube.com/elbruno β€” Demos, tutorials, and live coding
  • πŸ”— LinkedIn: @elbruno β€” Professional updates and insights
  • 𝕏 Twitter: @elbruno β€” Quick tips, releases, and tech news

πŸ™ Acknowledgments

Contributors

elbruno

153 commits

Languages

C#

72.0%

Python

12.9%

Jupyter Notebook

5.4%

PowerShell

3.8%

HTML

2.9%