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 architectural pattern.
Model Providers and BYO Credentials
Every AI provider and model — system default and user-supplied — is a MeshNode. The platform catalog is a DB-synced NodeType catalog under the top-level Provider partition, treated exactly like the Agent / Skill / Harness catalogs; a user's own providers live in their dotfile namespace {user}/_Memex/…. The chat-client factory resolves credentials through the same live synced query the model picker already uses.
No central registry. No IConfiguration sidecar. No per-user MemoryCache to invalidate. The built-in/config provider (BuiltInLanguageModelProvider) is only a sync source: on boot ModelStaticRepoSource imports the catalog into the Provider partition, and from then on it is served from the database.
Three credential scopes (system, user, org) merge via a unioned synced query; the credential resolver follows a four-step priority chain to produce a ready IChatClient.
Path layout
The platform catalog is a top-level partition named Provider — a DB-synced NodeType catalog. Org/context-shared providers live under {org}/Provider/…; a user's own providers live in their dotfile namespace {user}/_Memex/… (not a _Provider satellite). Each provider node holds the credential its child LanguageModel nodes share, nested beneath it.
Provider/ ← system catalog (DB-synced, top-level partition)
_Policy PartitionAccessPolicy (PublicRead · lifted write caps)
Anthropic ModelProvider (ApiKey from "Anthropic:" config)
claude-opus-4-8 LanguageModel (ProviderRef → Provider/Anthropic)
claude-sonnet-4-6 LanguageModel
claude-haiku-4-5-… LanguageModel
OpenAI ModelProvider
gpt-4o LanguageModel
…
rbuergi/_Memex/ ← user-owned (Permission.Api RLS)
Anthropic ModelProvider (ApiKey = personal sk-ant-…)
claude-opus-4-8 LanguageModel
…
Selection ModelProviderSelection (the user's picked providers)
acme/Provider/ ← org / context-shared
AzureFoundry ModelProvider
…
The chat picker unions the system Provider catalog, the current context's and NodeType's {path}/Provider subtrees, and the user's {user}/_Memex subtree, so models defined at any level are naturally available.
⚠️ Do not confuse
{user}/_Memex/…(the model/provider catalog) with the unrelated GitSync user-credential namespace{user}/_Provider/GitHub(GitHub OAuth) — a different satellite owned byMeshWeaver.GitSync.
Provider self-registration
Each AI provider package ships a single builder extension that wires everything for that provider: the catalog source, the IOptions binding, and the IChatClientFactory.
builder
.AddAI()
.AddAnthropic() // direct api.anthropic.com (Anthropic Messages API)
.AddAzureFoundry() // Azure-hosted multi-model gateway
.AddAzureOpenAI(); // Azure OpenAI
LanguageModelCatalogOptions.Sources is populated incrementally by each AddXxx call — there is no central registry to edit. The Settings UI and ModelProviderService both read from this live list. Adding a new provider means shipping one builder extension in a new package.
Picker query shape
AgentPickerProjection.BuildModelQueries follows the single-nodeType-filter pattern. Every query varies only on namespace and scope; the type filter stays identical throughout.
"namespace:Provider nodeType:LanguageModel|ModelProvider scope:descendants" // system catalog
"namespace:{currentPath}/Provider nodeType:LanguageModel|ModelProvider scope:descendants" // context-shared
"namespace:{nodeTypePath}/Provider nodeType:LanguageModel|ModelProvider scope:descendants" // per-NodeType
"namespace:{user}/_Memex nodeType:LanguageModel|ModelProvider scope:descendants" // the user's own
"namespace:{selectedProviderPath} nodeType:LanguageModel|ModelProvider scope:selfAndDescendants" // a picked provider
🚨 CRITICAL — identical type filters across queries. The synced collection's all-Initial gating breaks when a multi-query mixes different
nodeTypefilters. Keep the filter identical; vary onlynamespaceandscope. See SyncedMeshNodeQueries.md for the full gating contract.
Credential resolution
ChatClientCredentialResolver is a top-level singleton on the mesh hub. Each Resolve reads live from the same workspace.GetQuery cache the picker uses (no materialised dictionary of node content), follows ModelDefinition.ProviderRef to the parent ModelProvider, and returns the credential.
Resolution proceeds in this order:
| Priority | Source | When it applies |
|---|---|---|
| 1 | Explicit ProviderRef → ModelProvider content at that path |
Normal path for catalog entries and user-supplied providers (e.g. Provider/Anthropic) |
| 2 | Conventional fallback at Provider/{Provider} |
Catalog entries without a stamped ProviderRef |
| 3 | ModelDefinition.ApiKeySecretRef / Endpoint |
Legacy layouts that put the key directly on the LanguageModel node |
| 4 | CredentialResolution.Missing |
Factory falls back to its own IOptions<XxxConfiguration> binding |
🔑 Every rung is a NODE — deployment configuration is a SEED into one, not a rung. A provider's credential has ONE administered home, and
{Section}:ApiKeyreaches it throughProviderCredentialSeed, which runs at boot on a DB-synced deployment and fills the node's key only when it is empty (encrypted at rest; refused outright, loudly, when noAi:KeyProtection:MasterKeyis configured). An administered key is never overwritten, and a key configured after the provider node was created converges instead of never arriving — the create-if-absent seam that madeProvider/Anthropicread as keyless for a week while every round it served worked (MeshWeaver#1965 → #1982). Nothing reads that configuration at resolve time; a driver factory's ownIOptionsfallback is the driver's business, not a second source of truth the platform arbitrates.
ChatClientCredentialResolver.WatchPartition(userPath) widens the live read to include a specific user or org partition's provider subtree ({user}/_Memex / {org}/Provider). The fixture and chat pipeline call this once per active partition (idempotent; records the path only — no node content is cached).
Hierarchical discovery
ModelDiscoveryService exposes three layers, all anchored on the top-level mesh hub — never on a per-thread or _Exec hub that could block.
// (a) one node's Provider subtree
service.GetModelsAtNode("rbuergi/Underwriting");
// (b) walks UP the path: node + parent + grandparent + … + root
service.GetModelsForNodeHierarchy("rbuergi/Underwriting/Case-1234");
// (c) (b) on the node-path + (b) on the NodeType-path, unioned
service.GetEffectiveModels(
nodePath: "rbuergi/Underwriting/Case-1234",
nodeTypePath: "acme/Underwriting/Project");
Each layer is rebuilt on demand over the workspace's per-id GetQuery cache (Replay(1).RefCount upstream), so every call reflects live state — no materialised observable cache, hence nothing to invalidate. RLS is per-subscription — denied callers see an empty result without affecting other callers.
Key protection
| Where the key lives | Read access |
|---|---|
Provider/Anthropic.ApiKey (system) |
Permission.Api on the Provider partition |
{user}/_Memex/Anthropic.ApiKey (user) |
Permission.Api on user partition — owner only |
Provider/Anthropic/claude-opus-4-8.ApiKeySecretRef (LanguageModel) |
null — LanguageModel nodes never carry the key (WithPublicRead); the Provider/_Policy opens public Read of the key-less children |
CreateNodePermissionAttribute.GetPermissionForNodeType maps "ModelProvider" → Permission.Api, the same gate as "ApiToken". Cross-partition reads are blocked by the standard RLS path. An owner can view their own key through authenticated MCP or the Settings UI — the same access model as API tokens.
Settings UI
A user manages their own keys in the Models tab of the user-settings page; platform admins manage the shared Provider catalog through the standard mesh catalog (mesh search UI) — which has a permission-gated delete (trash) affordance plus keyboard shortcuts — not a bespoke settings form.
- The provider dropdown is populated from the live
LanguageModelCatalogOptions.Sourceslist. - On save,
ModelProviderServicewrites aModelProvidernode plus oneLanguageModelper default model ID. A user's own keys land under{ownerPath}/_Memex/{provider}; a platform provider (whentargetNamespaceis theProviderpartition) lands underProvider/{provider}and is sync-excluded so admin edits survive redeploys.
ModelProviderService.GetProvidersForOwner(path) powers the listed view — backed by a per-owner Replay(1).RefCount stream with a one-hour TTL. Writes invalidate the cache entry immediately.
See also
- NodeType Catalogs — how the
Providerpartition is rooted + theIsDefinitionOnlytype-def dissociation - Static-Repo Import — how the built-in catalog is materialized into the
Providerpartition on boot - SyncedMeshNodeQueries.md — the synced collection's all-Initial gating contract
- ExtensibleDefaults.md — system defaults + mesh extensions pattern
- CqrsAndContentAccess.md — when to use synced queries vs
GetMeshNodeStreamvsQueryAsync - AccessControl.md —
Permission.Apiand per-partition RLS