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.


System (Provider/) Provider/Anthropic (ModelProvider) claude-opus-4-8 (LanguageModel) claude-sonnet-4-6 (LanguageModel) DB-synced catalog · config key User (rbuergi/_Memex/) rbuergi/_Memex/Anthropic claude-opus-4-8 ApiKey = personal sk-ant-… Permission.Api · owner only Org (acme/Provider/) acme/Provider/AzureFoundry gpt-4o (LanguageModel) Endpoint = azure-gateway Permission.Api · org partition Picker Query Union (AgentPickerProjection) namespace:Provider · namespace:{currentPath}/Provider · namespace:{user}/_Memex nodeType:LanguageModel|ModelProvider scope:descendants (identical filter, vary namespace) ChatClientCredentialResolver Priority: explicit ProviderRef → conventional Provider/{P} → ApiKeySecretRef → IOptions fallback IChatClient (resolved, ready to call)

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 by MeshWeaver.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 nodeType filters. Keep the filter identical; vary only namespace and scope. 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 ProviderRefModelProvider 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}:ApiKey reaches it through ProviderCredentialSeed, 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 no Ai:KeyProtection:MasterKey is 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 made Provider/Anthropic read 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 own IOptions fallback 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.

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

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