AI Chat normalizes commercial, open and locally-hosted providers behind one model selector. Users move between fast inexpensive models, frontier reasoning models, private local models and specialized generation models without learning a different application for each.
Built-in providers​
| Provider | llms.json key |
API Key |
|---|---|---|
| OpenAI | openai |
OPENAI_API_KEY |
| Anthropic | anthropic |
ANTHROPIC_API_KEY |
| Google Gemini | google |
GEMINI_API_KEY |
| Groq | groq |
GROQ_API_KEY |
| xAI | xai |
XAI_API_KEY |
| Cerebras | cerebras |
CEREBRAS_API_KEY |
| Mistral | mistral |
MISTRAL_API_KEY |
| Codestral | codestral |
CODESTRAL_API_KEY |
| OpenRouter | openrouter |
OPENROUTER_API_KEY |
| Fireworks | fireworks-ai |
FIREWORKS_API_KEY |
| DeepSeek | deepseek |
DEEPSEEK_API_KEY |
| Moonshot | moonshotai |
MOONSHOT_API_KEY |
| Z.ai | zai / zai-coding-plan |
ZAI_API_KEY / ZHIPU_API_KEY |
| MiniMax | minimax |
MINIMAX_API_KEY |
| Nvidia | nvidia |
NVIDIA_API_KEY |
| Chutes | chutes |
CHUTES_API_KEY |
| Alibaba | alibaba |
- |
| Hugging Face | huggingface |
- |
| GitHub Copilot / Models | github-copilot, github-models |
- |
| Ollama | ollama |
none (local) |
| Ollama Cloud | ollama-cloud |
OLLAMA_API_KEY |
| LM Studio | lmstudio |
none (local) |
| OpenAI-compatible (local) | openai-local |
- |
| llms.py | llmspy |
- |
The ChatApiKey static class names every environment variable AI Chat looks for:
ChatApiKey.OpenAI // "OPENAI_API_KEY"
ChatApiKey.Anthropic // "ANTHROPIC_API_KEY"
ChatApiKey.Gemini // "GEMINI_API_KEY"
How a provider becomes live​
A provider is created and registered when all of these hold:
- It has an entry in
llms.json'sprovidersobject. - It is enabled - either
"enabled": truein its definition, or named inChatFeature.EnableProviders. - Its
npmsdk id resolves to a factory inChatFeature.ProviderTypes. provider.Test()passes, which normally means an API key was resolved.
API keys are resolved in this order:
services.AddPlugin(new ChatFeature {
Variables = {
["OPENAI_API_KEY"] = context.Configuration["OpenAi:ApiKey"]!,
["ANTHROPIC_API_KEY"] = context.Configuration["Anthropic:ApiKey"]!,
},
});
TIP
Providers that can't resolve a key are simply skipped with an informational log entry - a missing key never fails startup.
Restricting the providers users can reach​
EnableProviders overrides every enabled flag in llms.json, which makes it the simplest way to pin a deployment to an approved set:
services.AddPlugin(new ChatFeature {
// only these providers, whatever llms.json says
EnableProviders = ["anthropic", "ollama"],
});
For a fully self-hosted deployment, enable only ollama or lmstudio - nothing then leaves your network.
llms.json​
App_Data/chat/llms.json is seeded from an embedded default on first run and is yours to edit:
{
"version": 3,
"disable_extensions": [],
"defaults": {
"headers": { "Content-Type": "application/json" },
"text": { "model": "DeepSeek V4 Flash", "messages": [] },
"image": { "model": "MiMo-V2.5", "messages": [] }
},
"limits": {
"client_timeout": 240,
"client_max_size": 20971520,
"retries": 3
},
"loading": ["Computing", "Cooking", "Crafting"],
"providers": {
"groq": { "enabled": true },
"anthropic": { "enabled": true },
"ollama": { "enabled": true }
}
}
| Key | Purpose |
|---|---|
defaults |
Default model + message shape per modality, and headers sent to every provider |
limits |
Maps onto ChatFeature.Limits |
loading |
Words shown while a response streams |
disable_extensions |
Merged with ChatFeature.DisableExtensions |
providers |
Which providers are enabled, plus any per-provider overrides |
Supplying config in code​
Set Config (or ConfigJson) to bypass the App_Data file entirely - useful when configuration should come from your appsettings or a secret store:
services.AddPlugin(new ChatFeature {
ConfigJson = File.ReadAllText("llms.json"),
});
Top-level $VAR values in Config are substituted on load, so "api_key": "$MY_KEY" works anywhere a key is expected.
providers.json and the model catalog​
App_Data/chat/providers.json holds the model catalog - ids, display names, context sizes, pricing and capability flags - sourced from models.dev. A provider definition in llms.json is merged over its providers.json entry, with the definition winning.
Refresh the catalog at runtime, keeping only your configured providers and layering in your own overrides:
var count = await feature.UpdateProviderModelsAsync();
providers-extra.json​
Models that aren't in models.dev - a private deployment, a preview model, a self-hosted checkpoint - go in App_Data/chat/providers-extra.json and survive every catalog refresh:
{
"openai-local": {
"models": {
"my-finetune-v3": {
"name": "My Finetune v3",
"limit": { "context": 128000, "output": 8192 }
}
}
}
}
Enabling and disabling at runtime​
Providers can be toggled without a restart. Both persist the change to llms.json and rebuild the live provider set:
// returns null on success, or the reason it couldn't be enabled
var error = await feature.EnableProviderAsync("anthropic");
await feature.DisableProviderAsync("openai");
var (enabled, disabled) = feature.ProviderStatus();
The Chat UI exposes the same operations, so an administrator can enable a provider from the browser once its key is present.
Local models​
Ollama and LM Studio need no API key - AI Chat just needs to reach their endpoint:
{
"providers": {
"ollama": {
"enabled": true,
"base_url": "http://localhost:11434/v1"
}
}
}
Any other OpenAI-compatible endpoint can be added under openai-local, or registered as a new sdk id - see Custom Extensions.
Modalities​
A provider definition can declare sub-providers for non-text modalities. Each is created as its own provider and attached to the parent:
{
"providers": {
"openai": {
"enabled": true,
"modalities": {
"image": { "npm": "openai/image" }
}
}
}
}
Built-in modality sdk ids include openai/image, openrouter/image, openrouter/audio, openrouter/text-to-speech, fireworks/image, zai/image, chutes/image, nvidia/image and mistral/transcriptions.
An unsupported modality sdk drops just that modality rather than disabling the whole provider. See Voice & Media.
Server tools​
Some providers host their own tools - web search, web fetch, code execution. AI Chat surfaces whatever the selected provider advertises, generating the configuration UI from the tool's JSON Schema:
Adding a provider type​
Register a new npm sdk id from an extension's Install:
public override void Install(ExtensionContext ctx)
{
ctx.AddProvider("my-company/llm", () => new MyCompanyProvider());
}
Then reference it from llms.json:
{
"providers": {
"my-company": { "enabled": true, "npm": "my-company/llm", "api_key": "$MY_COMPANY_KEY" }
}
}
Most services only need OpenAiCompatibleProvider with a different base_url.