multilingual-e5-small
384-dimension embeddings in 100 languages. Small, fast, and a strong default for semantic search.
Starlings.Models.E5
0.5.0 on NuGet · 13 Stable profiles, more in Experimental
Embeddings, entity extraction, image and video search, and chat as ordinary NuGet packages. They run in-process on ONNX Runtime, on CPU or CUDA, with no Python sidecar and no cloud API. A model is only called Stable once it has passed every release gate against the upstream reference.
dotnet add package Starlings.Models.E5.Extensions.AI
Quickstart
PackageReference.Requires the .NET 10 SDK. Add a Starling package plus exactly one native ONNX Runtime package. DownloadAsync() fetches the pinned files once. After that, inference never touches the network.
dotnet add package Starlings.Models.E5.Extensions.AI
dotnet add package Microsoft.ML.OnnxRuntime --version 1.30.0using System.Numerics.Tensors;
using Microsoft.Extensions.AI;
using Starlings.Models.E5;
using Starlings.Models.E5.Extensions.AI;
// One-time download, pinned by commit and checked by SHA-256. Inference never touches the network.
E5ModelFiles files = await E5ModelAssets.DownloadAsync();
using var embedder = E5EmbeddingGenerator.Create(files);
// e5 expects "passage: " on documents and "query: " on queries.
string[] docs = ["passage: The Eiffel Tower is in Paris.",
"passage: Der Rhein fließt durch Köln.",
"passage: 富士山は日本一高い山です。"];
var docVectors = await embedder.GenerateAsync(docs);
var query = await embedder.GenerateVectorAsync("query: Which river runs through Cologne?");
foreach (var (doc, v) in docs.Zip(docVectors))
Console.WriteLine($"{TensorPrimitives.CosineSimilarity(query.Span, v.Vector.Span):F3} {doc}");0.680 passage: The Eiffel Tower is in Paris.
0.807 passage: Der Rhein fließt durch Köln.
0.688 passage: 富士山は日本一高い山です。dotnet add package Starlings.Models.Qwen35.Extensions.AI
dotnet add package Microsoft.ML.OnnxRuntimeGenAI --version 0.17.1using Microsoft.Extensions.AI;
using Starlings.Models.Qwen35;
using Starlings.Models.Qwen35.Extensions.AI;
Qwen35ModelFiles files = await Qwen35ModelAssets.DownloadAsync(Qwen35Variant.Fp32x); // about 3 GB, once
using IChatClient chat = Qwen35ChatClient.Create(files);
await foreach (var update in chat.GetStreamingResponseAsync("In one sentence, why is the sky blue?"))
Console.Write(update.Text);It's a standard IChatClient, so code you wrote against a hosted provider moves to a local model with a one-line change.
var getWeather = AIFunctionFactory.Create((string city) => $"18 C and sunny in {city}.",
"get_weather", "Get the current weather for a city.");
using IChatClient agent = new ChatClientBuilder(chat).UseFunctionInvocation().Build();
ChatResponse answer = await agent.GetResponseAsync("What is the weather in Paris today?", new ChatOptions { Tools = [getWeather] });
Console.WriteLine(answer.Text);Tools are ordinary AIFunctions, and UseFunctionInvocation() runs the loop. A 0.8B model doesn't always call a tool when it should, so check calls before running them. ChatClientBuilder is in the Microsoft.Extensions.AI package, so add that too.
The JSON response format and required tool calls run through guided decoding, which is experimental. It needs the Qwen3.5 files hosted since 0.5.0, so a cache from an earlier release must download again.
dotnet add package Starlings.Models.Gliner2
dotnet add package Microsoft.ML.OnnxRuntime --version 1.30.0using Starlings.Models.Gliner2;
Gliner2ModelFiles files = await Gliner2ModelAssets.DownloadAsync();
using var extractor = Gliner2EntityExtractor.Create(files);
var result = extractor.Extract("Tim Cook announced the iPhone 15 in Cupertino.", ["person", "product", "city"]);
foreach (var e in result.Entities)
Console.WriteLine($"{e.Label}: {e.Text} [{e.Start}, {e.End}) {e.Confidence:F2}");person: Tim Cook [0, 8) 1.00
product: iPhone 15 [23, 32) 1.00
city: Cupertino [36, 45) 1.00Labels are free text, chosen per call. Offsets index the original string. For long-running processes on Linux, set MALLOC_MMAP_THRESHOLD_=65536 in the environment to keep memory bounded.
dotnet add package Starlings.Models.EmbeddingGemma2.Extensions.AI
dotnet add package Starlings.Models.EmbeddingGemma2.Imaging.SkiaSharp
dotnet add package Microsoft.ML.OnnxRuntime --version 1.30.0using System.Numerics.Tensors;
using Starlings.Models.EmbeddingGemma2;
using Starlings.Models.EmbeddingGemma2.Imaging.SkiaSharp;
EmbeddingGemma2ImageModelFiles files = await EmbeddingGemma2ModelAssets.DownloadImageAssetsAsync();
using var embedder = EmbeddingGemma2MultimodalEmbedder.Create(files);
float[] image = embedder.Embed(EmbeddingGemma2ImageDecoder.DecodeFile("photo.jpg")); // images take no prompt
float[] query = embedder.Embed(EmbeddingGemma2MultimodalInput.FromText("a photo of a cat"), EmbeddingGemma2Prompt.Query);
float score = TensorPrimitives.CosineSimilarity(image, query);Text and images share one embedding space, and so does video. Add Starlings.Models.EmbeddingGemma2.Video.FFmpeg (FFmpeg 8) or .Video.FFmpeg9 and supply your own FFmpeg shared libraries. See the EmbeddingGemma2Search sample.
dotnet add package Starlings.Tokenizersusing Starlings.Tokenizers;
Tokenizer tokenizer = Tokenizer.FromFile("tokenizer.json");
TokenEncoding encoding = tokenizer.Encode("Hello, world!");
string text = tokenizer.Decode(encoding.Ids.AsSpan(), skipSpecialTokens: true);Any Hugging Face tokenizer.json, in pure .NET. No native code and no dependencies. More below ↓
Want a whole program? The samples cover semantic search, reranking, local RAG, ticket triage in ML.NET, and e5 in the browser.
Recipes
Three complete programs, built on the published packages. Each downloads its models once. After that, it runs with the network off.
LocalRag answers questions over your Markdown and text files, using EmbeddingGemma 2 and Qwen3.5 through Microsoft.Extensions.AI. About 4 GB, once. It's a small-folder demonstration: it re-embeds on every question, and a 0.8B model can be wrong, so it prints its sources.
TicketTriage trains a router on English tickets in an ordinary ML.NET pipeline, then routes German, Spanish, French and Japanese tickets too, with GLiNER2 pulling out the entities. About 1.3 GB, once.
EmbeddingGemma2Search ranks a folder of images, or videos, against a text query. About 2.9 GB, once, and about 4.5 s per photo on a 12-thread CPU, so start small or use CUDA.
What's new
Everything below is on NuGet. New models start as Experimental, and each release record lists what changed package by package.
0.5.0Qwen3.5 gets an int4 variant for CPU, and guided decoding works on the hosted files. GLiNER2 adds text classification on CPU, and new helpers put a reranker after a vector search. All of these are Experimental. Every model package now has a generated capability record.
0.4.0The BERT family arrives with seven embedding and reranking profiles, including bge-m3. e5 runs in the browser on WebAssembly and WebGPU, and LFM3-1.7B joins as a generator. Generation packages split into a model definition and an ONNX Runtime package, which is a breaking change for direct users.
0.3.0Kev decision models reach NuGet. They return a calibrated probability per option, with no generated text. Qwen3-0.6B and Qwen2.5-0.5B-Instruct add small text chat on CPU, and batching becomes an opt-in for the embedding and extraction packages.
Upgrading? Move every Starlings.* model and runtime reference to the same version together. Starlings.Tokenizers stays at the version they pin, which is 0.3.4 for 0.5.0. All releases →
No lock-in
Starling implements the vendor-neutral Microsoft.Extensions.AI abstractions: IEmbeddingGenerator and IChatClient. Whatever sits behind them today, a local model is one line away. The libraries depend only on Microsoft.Extensions.AI.Abstractions, and CI rejects any Azure or OpenAI SDK dependency.
- IChatClient chat = /* your hosted provider */;
+ using IChatClient chat = Qwen35ChatClient.Create(files);
var reply = await chat.GetResponseAsync(prompt);Models
Each model is pinned to an exact upstream commit and its files are verified by SHA-256. The four below are Stable, so their outputs have passed every gate against the original PyTorch/Python implementation.
384-dimension embeddings in 100 languages. Small, fast, and a strong default for semantic search.
Starlings.Models.E5
768-dimension Matryoshka embeddings, with text, images and video in one shared space.
Starlings.Models.EmbeddingGemma2
Zero-shot named entity recognition. Pass any labels you like per call, with no fine-tuning.
Starlings.Models.Gliner2
Local streaming chat and tool calling through IChatClient, on ONNX Runtime GenAI, CPU or CUDA.
Starlings.Models.Qwen35.Extensions.AI
These four are Stable on Linux x64, on CPU and on CUDA with an NVIDIA GeForce RTX 3090. Other GPUs aren't covered by the Stable contracts. Windows CPU is experimental for e5, GLiNER2 and EmbeddingGemma 2 text, and untested otherwise. macOS and ARM64 are untested. ML.NET transformers are available for e5 and EmbeddingGemma 2. Full status per platform and provider is in the compatibility matrix. Want another model? Request one.
You can install these today. Release gates G0 to G7 haven't finished for them, so expect their contracts to change.
| Task | Package | Models and where they run |
|---|---|---|
| Embeddings & reranking | Starlings.Models.Bert | all-MiniLM-L6-v2, bge-small-en-v1.5, bge-base-en-v1.5, bge-m3, nomic-embed-text-v1.5, and the rerankers ms-marco-MiniLM-L6-v2 and bge-reranker-v2-m3 · CPU |
| Decisions | Starlings.Models.Kev.Onnx | Kev-0.8B and Kev-4B: a calibrated probability per option · CPU and CUDA |
| Browser embeddings | Starlings.Models.E5.Browser | multilingual-e5-small on onnxruntime-web · WebAssembly and WebGPU, checked in Chromium on Linux |
| Small chat | Starlings.Models.QwenChat.OnnxGenAI | Qwen3-0.6B and Qwen2.5-0.5B-Instruct · CPU |
| Smaller Qwen3.5 | Starlings.Models.Qwen35.OnnxGenAI | Qwen3.5-0.8B int4 · CPU |
| Text classification | Starlings.Models.Gliner2 | GLiNER2's classification head · CPU |
| Generation | Starlings.Models.Lfm3 | LFM3-1.7B · CPU. Not MIT or Apache-2.0: under the LFM Open License v1.0, commercial use is licensed only below USD 10 million annual revenue |
Whisper large-v3-turbo, PaddleOCR and Qwen3-Embedding-0.6B are in the repository but not on NuGet, so build them from source. Status for every profile is in the model catalog.
Pipeline
Twelve stages take a model from an upstream release to a package that runs offline in your app. The original Python implementation runs once to cut a golden master: protected fixtures and tolerances only the maintainer can change. The tokenizer, the exported graph and the C# path are each measured against it, and the first stage that diverges is named.
The stages are defined in the delivery plan. Open the full-size view (opens in a new tab) →
Evidence
A profile is Stable only when every release gate has passed, and each gate links to the run that passed it in the model catalog. As of 0.5.0, 13 profiles are Stable, with the same contracts as in 0.2.0: no gate, tolerance or protected fixture has changed. Profiles added since then are Experimental until their gates run. Qualification runs weekly on CPU, and on every release tag it also tries an RTX 3090.
Not every run is green. Since late October, including on the 0.4.0 and 0.5.0 tags, the CPU video gates couldn't download their pinned FFmpeg builds (#277), which is a fetch failure and not a model regression. For 0.5.0, no RTX 3090 was available, so the GPU regression run didn't happen. Both are still owed. Two jobs for Experimental models that report but don't gate are also failing (#278). Qualification runs →
Measured with multilingual-e5-small, C# library vs the Python reference on the same ONNX Runtime backend. The chart shows the ratio of medians with 95% bootstrap confidence intervals. 1.0 means parity, and lower means C# is faster.
Once warm, latency is within noise of Python on CPU and slightly faster on CUDA. Cold start, meaning the first call including model load, is currently slower in C#. We show that too.
Text path, concurrency 1. CUDA runs on an NVIDIA GeForce RTX 3090. Full results, 2026-10-09 →
| Provider | Workload | C# ms | Python ms | Ratio | 95% CI |
|---|
Why Starling
PackageReference. The Python reference is what we test against.Built for coding agents
Starling ships files written for Claude Code, Copilot, Cursor and Codex:
llms.txt is a short map of what matters when writing code.starling-dotnet Agent Skill covers which package to use, verified code for every model, CUDA, and the pitfalls.starling-add-model skill walks an agent through contributing a new model as a pull request.Read https://raw.githubusercontent.com/starlinghq/starling/main/llms.txt and
https://raw.githubusercontent.com/starlinghq/starling/main/skills/starling-dotnet/SKILL.md,
then use the Starlings.* NuGet packages to <what you want to build>.mkdir -p .claude/skills/starling-dotnet
curl -fsSL https://raw.githubusercontent.com/starlinghq/starling/main/skills/starling-dotnet/SKILL.md \
-o .claude/skills/starling-dotnet/SKILL.mdStarlings.Tokenizers
The tokenizer inside every Starling model is also its own library. It reads Hugging Face tokenizer.json files in pure .NET and produces the same token ids, offsets and decoded text as huggingface/tokenizers, checked case by case.
Every supported tokenizer has a profile: a pinned file with reference outputs for multilingual text, emoji, combining marks, pairs, truncation and padding. CI fails on any difference.
A component it doesn't implement fails at load with UnsupportedTokenizerComponentException, naming the JSON path. Nothing is approximated.
No native code, no package dependencies, net10.0. Instances are immutable and thread-safe. A 17 MB vocabulary loads in about 0.27 s.
| Model | Algorithm | Evidence |
|---|---|---|
intfloat/multilingual-e5-small | SentencePiece Unigram | 49/49 cases exact |
sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2 | SentencePiece Unigram, WhitespaceSplit | 49/49 cases exact |
fastino/gliner2-base-v1 | SentencePiece Unigram (DeBERTa-v3) | per-word and 10 whole-text cases · offsets, pairs and decode untested |
jaredpalmer/kev-0.8b | byte-level BPE (Qwen2/Qwen3 regex) | 137/137 cases exact · 0 mismatches in 1.15 M randomized encodes |
Qwen/Qwen3.5-0.8B | byte-level BPE | 137/137 cases exact · 0 mismatches in 1.15 M randomized encodes |
google/embeddinggemma-2 | SentencePiece-style BPE | 49/49 cases exact · 0 mismatches in 1.7 M randomized encodes |
Qwen/Qwen3-Embedding-0.6B | byte-level BPE | 137/137 cases exact |
Qwen/Qwen3-Reranker-0.6B | byte-level BPE | 137/137 cases exact |
LiquidAI/LFM3-1.7B | byte-level BPE (Llama 3 regex) | 137/137 cases exact |
Diffuspace/Clover-35B-A3B-Preview | byte-level BPE | 137/137 cases exact |
microsoft/Phi-4-mini-instruct | byte-level BPE (o200k regex) | 137/137 cases exact · 1,500 randomized strings with the Phi-4 tags exact |
Alibaba-NLP/gte-reranker-modernbert-base | byte-level BPE (GPT-2 regex) | 137/137 cases exact |
Starlings.Tokenizers is at 0.3.5. Every tokenizer profile is still experimental. Starling 0.5.0 pins 0.3.4 exactly, and the next release moves to 0.3.5. Other files may load and work, but without a profile, matching the reference is untested. Full list and details →
Built on Starling
Two projects from StarlingHq run on Starling's published packages.
A local-first .NET agent. Kev, GLiNER2 and the embedding models make the many small decisions in-process, and a swappable local or cloud model does the writing. Secrets stay in Corvid Vault, a separate process that runs no model.
v0.1.0 pre-release · Linux x86_64
A C# serving engine for NVIDIA GPUs, built to replace vLLM and serve the Starling model library behind one host. Each GPU has its own worker process, so a GPU fault never takes down the host, and there is no Python or libtorch at runtime.
In development · nothing released yet
Starling is open source and in active development. Issues, model requests and pull requests are welcome.