The model-provider docs at a glance: Model Providers — the architectural pattern · Provider Configuration — framework config & chat-client factories · Model Provider Setup — operational setup & troubleshooting · Model Provider Settings — the settings UI. This page: the settings UI.
AI Model Provider Settings
Current state first. The Settings → Models tab this page was written to specify (
ModelsSettingsTab) no longer exists. Today a user wires AI in three places: provider keys in the Providers app's ProviderSetup view, models in the/Provider/AiModelscatalog, and a CLI harness login through/loginin the chat composer. What is wired below names the code. Everything from Two provider kinds through the Scope note is the original design, kept as history: its card layouts, diagrams and state machine describe a UI that is not served. The AI engine code named on this page lives in MeshWeaver.Plugins, not in this repository.
This page began as the implementation spec for a single Settings → Models destination: adding API keys, enabling specific models, and connecting CLI-based providers like Claude Code and GitHub Copilot.
Setting up models (admin or user)? This page is the UI design spec. For the operational how-to — provider/model mesh nodes, the system/space/user layers, which query goes where in a user's namespace, the open-weight tier choices, and the install-time config gaps — read Setting Up Model Providers.
Two provider kinds, two different UIs (original design — historical)
The fundamental insight driving this design: API providers and CLI providers need completely different layouts. Rendering both as a key/endpoint form is wrong.
Two provider kinds — API providers add a key and pick models; CLI providers delegate to their own auth flow.
| Provider kind | Examples | What the card shows |
|---|---|---|
| API (bring-your-own-key) | Azure AI Foundry, Azure OpenAI, Anthropic, OpenAI | Endpoint / key form + a fetched list of models to enable |
| CLI (co-hosted, subscription) | Claude Code, GitHub Copilot | Login status — no key form, no model list; a button that delegates to the CLI's own auth flow |
What is wired
What is served today. The original design shipped and was later replaced; these bullets describe the code as it stands on MeshWeaver.Plugins main.
- The catalog chain registers all providers in
MemexConfigurationvia.AddAnthropic().AddAzureFoundry().AddAzureOpenAI().AddOpenAI().AddClaudeCode().AddCopilot(), each gated by itsFeatures:Ai:Providers:*/Features:Ai:Clis:*flag. - Where a user enters provider keys today is the Providers app's ProviderSetup view (MeshWeaver.Plugins
Providers/ProvidersApp/Source/ProviderSetupAreas.cs; its rules live in the pureProviderSetup.cs, tested byProviders/ProvidersApp/Test/ProviderSetupTests.cs). The AI menu's Models entry links the scope-tabbed model catalog at/Provider/AiModels(AiCatalogLayoutAreas.ModelsArea). The settings tab this page originally specified,ModelsSettingsTabwith itsBuildCliCard, no longer exists in either repository. - A CLI harness login is no longer a settings card: it is the
/loginharness command in the chat composer (ThreadChatView.StartHarnessConnectin MeshWeaver.Pluginssrc/MeshWeaver.Blazor.Chat/), which drives the sameConnectSessionManager.StartConnectbackend described below. - Per-user credentials live in
ModelProvidermesh nodes (ModelProviderService,ModelProviderNodeType), with keys encrypted viaAi:KeyProtection:MasterKey(sourced from Key Vault). - The CLI connect flow is implemented:
ConnectSessionManagerdrivesClaudeConnectStrategy/CopilotConnectStrategy(src/MeshWeaver.AI/Connect/).
Design
1 The ProviderKind seam
An explicit ProviderKind enum (Api | Cli, declared in BuiltInLanguageModelProvider.cs) sits on the provider catalog entry, replacing the implicit "has a key form" test. CLI providers (AddClaudeCode, AddCopilot) report Cli; everything else reports Api. The settings card that originally switched on ProviderKind (ModelsSettingsTab) has been removed; the enum remains on the catalog entry (MeshWeaver.Plugins src/MeshWeaver.AI/BuiltInLanguageModelProvider.cs).
2 API providers — key/endpoint form + model list
- Render the endpoint/key form. Azure providers also take an endpoint; Anthropic and OpenAI take only a key.
- With a key entered, Fetch models calls
ProviderModelLister.ListModels(endpoint, apiKey, providerName)— anIObservable<IReadOnlyList<string>>— and shows the returned ids as checkable rows so the user enables the ones they want. The selection is persisted on theModelProvidernode by Save provider. - API providers are the only kind that produce a model list — CLI providers deliberately do not.
3 CLI providers — login status + delegate to the CLI
No key form. No model list. The card shows exactly two states:
- Logged in → green "Connected as …" indicator + a "Re-connect / Log out" affordance.
- Not logged in → a "Connect / Log in" button that drives the CLI's native auth flow.
The backend lives in src/MeshWeaver.AI/Connect/:
IConnectStrategy — one implementation per CLI.
ClaudeConnectStrategy: spawnsclaudeunder the user'sCLAUDE_CONFIG_DIR, probes the CLI (or its.credentials.json) for login status, and if absent runsclaude setup-token(or/login), scrapes the auth URL, surfaces it, and captures the pasted code/token.CopilotConnectStrategy: runs the Copilot SDK device-flow — surfaces the device URL + code, pollsGetAuthStatusAsync.- Both reuse the subprocess shape from
MeshPlugin/KernelExecutor(RedirectStandardInput). The process boundary runs on theProcessIIoPool—InvokeBlockingfor the spawn,Invokefor the stdin write — carrying only the async leaves, with the auth-URL/token scrape composed as an ordinary observable. NeverObservable.FromAsync, which is forbidden outsideIoPool.
ConnectSessionManager — a mesh-scoped singleton that holds the live Process between "show URL" and "paste code", keyed per user (instance ConcurrentDictionary, never static), with a 5-minute timeout that calls Kill(entireProcessTree:true).
Login-status check is the cheap, always-on part: each card calls strategy.IsLoggedIn(userConfigDir) on render. Only the not-logged-in branch shows the login button.
On token capture → ModelProviderService.CreateProvider(ownerPath, "ClaudeCode"|"Copilot", token) (already calls Protect() on the key); re-connect uses RotateKey. The CLI agent factory already injects this.
4 Fix the missing icon
The AI settings tab is registered without an Icon. Add one where the Settings tabs are declared (the AI/Models tab registration in the portal settings) — a FluentUI Sparkle or Bot icon, consistent with other tabs.
UI — inline login
The CLI login expands inside the provider card — no modal, no side panel. This is the lightest-weight option and keeps the user in context. The card is a small state machine.
Models tab layout
Settings ▸ ✦ AI / Models
API providers — add a key, choose models
┌─ Azure AI Foundry ──────────────────────────────── [API] ┐
│ Endpoint https://….services.ai.azure.com [Save] ✓ │
│ API key •••••••••••••••• │
│ Models ☑ gpt-4o ☑ o3-mini ☐ embed-v-4-0 │
└────────────────────────────────────────────────────────────┘
┌─ Anthropic ─────────────────────────────────────── [API] ┐
│ API key •••••••• [Save] Models ☑ claude-opus-4 │
└────────────────────────────────────────────────────────────┘
CLI providers — log in with your subscription (no key, no model list)
┌─ Claude Code ───────────────────────────────────── [CLI] ┐
│ ● Not connected — uses your Claude subscription │
│ [ Connect Claude Code ] │
└────────────────────────────────────────────────────────────┘
┌─ GitHub Copilot ────────────────────────────────── [CLI] ┐
│ ✓ Connected as @rbuergi [ Disconnect ] │
└────────────────────────────────────────────────────────────┘
CLI card — connecting states
Claude Code (paste-a-code flow):
┌─ Claude Code ──────────────── [CLI] ┐
│ ● Connecting… │
│ 1 Authorize in your browser: │
│ claude.ai/oauth/auth?… [Copy][Open]
│ 2 Paste the code Claude shows: │
│ [ __________________ ] [Submit] │
│ ⏳ waiting for code… (4:58) │
└──────────────────────────────────────┘
GitHub Copilot (device code, auto-poll — nothing to paste):
┌─ GitHub Copilot ───────────── [CLI] ┐
│ ● Connecting… enter code at │
│ github.com/login/device │
│ ┌───────────────┐ │
│ │ AB12-CD34 │ [Copy] │
│ └───────────────┘ │
│ ⏳ auto-checking… │
└──────────────────────────────────────┘
Connected: ✓ Connected as <name> [ Disconnect ] · Error/Expired: red status line + [ Retry ]
Inline state machine
NotConnected ──[Connect]──▶ Connecting ──(code submitted / device poll OK)──▶ Connected
▲ │ ▲ │
└──────[Disconnect]────────┘ └──(5-min timeout · Cancel · auth error)──▶ Error/Expired
└──────────────[Retry]──────────────────┘
- Connecting is driven by
ConnectSessionManager+ the provider'sIConnectStrategy. It holds the live CLIProcess, exposes the auth URL (and, for Copilot, the device code), and either accepts a pasted code (RequiresPastedCode = true) or polls auth status. A 5-minute timeout disposes the process viaKill(entireProcessTree:true). - On success the strategy returns the token →
ModelProviderService.CreateProvider/RotateKey(encrypted with the Key-Vault master key) → the card flips to Connected reactively. - The card body is chosen off
strategy.RequiresPastedCode(paste field vs. device code) andIsLoggedIn(userConfigDir)(Connected vs. NotConnected on first render).
Where the code lives
| File | Role |
|---|---|
Providers/ProvidersApp/Source/ProviderSetupAreas.cs (MeshWeaver.Plugins) |
The per-user provider key form (one card per visible provider, its fields and its models); rules in ProviderSetup.cs |
src/MeshWeaver.Blazor.Chat/ThreadChatView.razor.cs (MeshWeaver.Plugins) |
/login harness command → StartHarnessConnect → ConnectSessionManager.StartConnect, rendered inline in the chat |
memex/Memex.Portal.Shared/Models/ProviderModelLister.cs |
ListModels(endpoint, apiKey, providerName) behind the Fetch models button |
src/MeshWeaver.AI.ClaudeCode/ClaudeCodeExtensions.cs |
Exposes ProviderKind = Cli + its IConnectStrategy |
src/MeshWeaver.AI.Copilot/* |
Same — ProviderKind = Cli + CopilotConnectStrategy |
src/MeshWeaver.AI/Connect/ |
ConnectSessionManager, IConnectStrategy, ClaudeConnectStrategy, IConnectTokenSink |
src/MeshWeaver.AI/BuiltInLanguageModelProvider.cs |
The ProviderKind enum on the catalog entry |
memex/Memex.Portal.Shared/MemexConfiguration.cs |
Registers ConnectSessionManager + the strategies |
Testing
No mocks. Use
MonolithMeshTestBase/AITestBase.
Three test scenarios:
Rendering — the ProviderSetup rules (
ProviderSetupTestsin MeshWeaver.PluginsProviders/ProvidersApp/Test/) decide which fields and models a provider card shows. Assert on what the pure helper returns.Connect flow — a committed fake CLI (prints an auth URL, reads stdin, prints a token) drives
IConnectStrategy:IsLoggedInreturns false → connect → strategy captures the token → aModelProvidernode is written with anenc:-tagged key that round-trips throughChatClientCredentialResolver.Login status — with the fake CLI reporting "logged in", the card renders the connected state and shows no login button. Real-CLI end-to-end is gated by
CLAUDE_CONNECT_E2E=1(developer-run only).
Scope note
(Historical.) What originally shipped was Phase 1: per-user CLI Connect plus the Models-tab rework, meaning the UI and the CLI login backend. The Models tab has since been removed; the CLI login backend remains and is driven from /login. The ProviderKind layout split was the quick visible win; the CLI login backend (ConnectSessionManager + strategies) was the substantive part.
Model picker: provider-first selection and empty state
Providers and models are mesh nodes discovered via a nodeType: fan-out query — not a flat config list. The picker lists providers first; selecting one loads only that provider's models. When nothing is configured it routes the user directly to Settings.
Providers and models are nodes
- A provider is a
ModelProvidernode; its models are childLanguageModelnodes nested beneath it. - Canonical path: the platform catalog at
Provider/{Provider}/{modelId}(e.g.Provider/AzureFoundry/gpt-5), a space provider at{space}/Provider/{Provider}/{modelId}(e.g.Systemorph/Provider/AzureFoundry/gpt-5), and a user's own at{user}/_Memex/{Provider}/{modelId}(e.g.rbuergi/_Memex/ClaudeCode). - The built-in system catalog at
Provider/{provider}/{model}is a DB-synced NodeType catalog:BuiltInLanguageModelProvideris the sync source,ModelStaticRepoSourceimports it into theProviderpartition on boot, and the DB serves it thereafter, so configuration values (AzureFoundry:Models) materialise as real, queryableMeshNodes — there is no parallel, non-mesh path.
Provider-first, lazy model load
The picker does not eager-load every model from every space. It operates in two steps:
List providers — fan-out
nodeType:ModelProvider scope:descendantsover theProvidercatalog and every space the user can read. Listing providers (not models) is cheap, making it safe to broaden across spaces without loading the full model universe.Select a provider → load its models — the provider's path is appended to
{user}/_Memex/Selection.SelectedProviderPaths; the selected-path querynamespace:{providerPath} nodeType:LanguageModel scope:selfAndDescendants(AgentPickerProjection) loads just that provider's models.
Selection is the per-user selection store at {user}/_Memex/Selection. It is seeded empty at onboarding, so the RoutingGrain NotFound: {user}/_Memex/Selection read no longer occurs against a missing node.
Empty state → Settings
When the provider fan-out returns nothing (no provider configured), the model picker does not render an empty dropdown. Instead it shows an actionable empty state: "No model provider configured" with a link that navigates to Settings → Models (action://settings/models / the OnActionLink hook) where the user can add an API key, connect a CLI, or select an org provider.
Org-default provider
An admin may pre-create an org provider node — <org>/Provider/AzureFoundry with model children sourced from your Azure AI Foundry resource (endpoint https://<foundry-account>.services.ai.azure.com/models, key in Key Vault) — that every user with read access can select. This complements per-user BYO-key and Connect flows; it does not replace the empty-state link. ModelProvider is a creatable node type (search-hidden), so it can be authored in the UI by anyone with Permission.Api, not only through configuration.
Managing a provider's models
Selecting a provider in Settings → Models lists its child LanguageModel nodes, where the user can add, remove, or enable individual models (CRUD on {provider}/{modelId} nodes).
- Fetch from provider — where the provider exposes a list API (Azure Foundry / Azure OpenAI deployment list, OpenAI / Anthropic list-models), a "Fetch models" action queries it and offers the returned IDs for import as child
LanguageModelnodes, so the catalog does not need to be hand-typed. - Refresh — re-runs the fetch and reconciles against the current children (adds newly-deployed models, flags dropped ones). Manual button now; can become periodic later.