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

The Settings → Models page is the user's single destination for wiring AI into Memex — adding API keys, enabling specific models, and connecting CLI-based providers like Claude Code and GitHub Copilot. This document is the actionable implementation spec: it identifies the exact files to touch, the behavioral seams to introduce, and the testing approach.

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

The fundamental insight driving this design: API providers and CLI providers need completely different layouts. Rendering both as a key/endpoint form is wrong.

Settings → Models ProviderKind dispatch API Provider Azure AI Foundry · Azure OpenAI · Anthropic · OpenAI CLI Provider Claude Code · GitHub Copilot Endpoint / Key form saved & validated Fetch model list ProviderModelLister Enable selected models Check IsLoggedIn IConnectStrategy CLI auth flow ConnectSessionManager Store encrypted token

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

This design has shipped — the sections below describe the code as it stands, not pending work.


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. ModelsSettingsTab switches the rendered card on ProviderKind — this single branch drives the entire different-layout requirement.

2 API providers — key/endpoint form + model list

3 CLI providers — login status + delegate to the CLI

No key form. No model list. The card shows exactly two states:

The backend lives in src/MeshWeaver.AI/Connect/:

IConnectStrategy — one implementation per CLI.

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 captureModelProviderService.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]──────────────────┘

Where the code lives

File Role
memex/Memex.Portal.Shared/Settings/ModelsSettingsTab.cs Switches on ProviderKind; renders the API card (form + model list) or BuildCliCard (login status + connect button)
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:

  1. RenderingModelsSettingsTab renders a model list for an API provider and a connect button (no list) for a CLI provider. Assert on the control tree.

  2. Connect flow — a committed fake CLI (prints an auth URL, reads stdin, prints a token) drives IConnectStrategy: IsLoggedIn returns false → connect → strategy captures the token → a ModelProvider node is written with an enc:-tagged key that round-trips through ChatClientCredentialResolver.

  3. 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

What shipped is Phase 1: per-user CLI Connect plus the Models-tab rework — the UI and the CLI login backend. 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

Provider-first, lazy model load

The picker does not eager-load every model from every space. It operates in two steps:

  1. List providers — fan-out nodeType:ModelProvider scope:descendants over the Provider catalog 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.

  2. Select a provider → load its models — the provider's path is appended to {user}/_Memex/Selection.SelectedProviderPaths; the selected-path query namespace:{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).

Reconnecting…
The server was updated. Reloading the page to pick up the latest version.