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: framework config & chat-client factories.

MeshWeaver speaks to multiple LLM providers — Claude via Anthropic, GPT-class and open-weight models via the Azure AI Services multi-model gateway, embedding models, and more — but the configuration surface is intentionally small: one shared key, a handful of endpoints, and a short per-provider model list. This page explains the credential/endpoint wiring and the factory routing. For getting models to actually appear in the picker — provider/model mesh nodes, the space/user layers, and the install-time gotchas — read the operational guide first: Setting Up Model Providers.

Why read this? This page is about credentials, endpoints, and which factory handles a model. If your question is "why is the model picker empty / how do I add models," start with Setting Up Model Providers — the picker is fed by ModelProvider / LanguageModel mesh nodes, which this deployment seeds from the config sections below.

Azure Foundry azure-foundry-key + endpoint params Anthropic__ApiKey Claude provider AzureAIS__ApiKey Multi-model gateway Embedding__* endpoint + model Agent Definition ModelTier AgentChatClient GetFactoryForModel AzureClaude ChatClientFactory claude-* → Anthropic AzureFoundry ChatClientFactory gpt-*, o*, Mistral-* Custom Factory Supports() predicate Credentials & Endpoints Agent Config Factory Routing

One shared Azure Foundry key backs multiple providers; the agent definition selects the model; AgentChatClient routes to the matching factory via Supports() predicates.


One Azure Foundry Key, Two Providers

A single Aspire parameter — azure-foundry-key, declared in ../MeshWeaver.Plugins/src/Memex.AppHost/Program.cs:51 — backs both the Anthropic and AzureAIS credentials:

Env var Provider Endpoint path
Anthropic__ApiKey Claude (Anthropic) /anthropic/...
AzureFoundry__ApiKey Multi-model gateway (open-weight: DeepSeek, Llama, Mistral, Phi…) /models/...

Both routes share one credential under the Azure Foundry resource. You do not need a separate Anthropic key in this deployment. A dedicated Anthropic key only makes sense if you route directly to api.anthropic.com rather than through Foundry.


Endpoints Are Always Parametrised

Endpoints are never literal strings in source code. In development they come from dotnet user-secrets on the AppHost; in production they are injected as GitHub Actions secrets and Azure Container Apps environment variables.

Aspire parameter Environment variable
anthropic-endpoint Anthropic__Endpoint
azure-foundry-endpoint AzureFoundry__Endpoint
embedding-endpoint Embedding__Endpoint
embedding-model Embedding__Model

Section-name caveat (latent bug): the code binds the AzureFoundry: section (AzureFoundryConfiguration, AddAzureFoundry, and the catalog source). The Aspire AppHost currently emits AzureAIS__Endpoint / AzureAIS__ApiKey, which nothing in src/ binds — so that config is dead. Use AzureFoundry__*. The Helm chart (deploy/helm) already uses the correct names; the AppHost (../MeshWeaver.Plugins/src/Memex.AppHost/Program.cs) should be renamed AzureAIS__* → AzureFoundry__*.

The embedding pair establishes the canonical pattern — a sibling endpoint + model parameter per provider. Chat providers follow the same shape.


Model Selection — Composer First, Tier as Optional Fallback

Two things select a model, at two different layers. A deployment advertises which models exist by listing them in each provider's {Section}:Models config, which BuiltInLanguageModelProvider turns into LanguageModel mesh nodes for the picker (see Setting Up Model Providers). What the AppHost does not do is hardcode model ids in framework C#.

Which model a conversation actually runs on resolves in this order (see ChatClientAgentFactory.ResolveTierModel and the concrete factories):

  1. The chat composer selection (ThreadComposer.ModelNameCurrentModelName) — the user's explicit pick always wins. The one exception is Auto, the default selection for a new thread: Auto is a router, so it is dispatched rather than served (see below).
  2. The agent's AgentConfiguration.ModelTier — a USAGE tier (utility / chat / reasoning / coding), resolved against the tier label on the model NODES. This is also what Auto dispatches on. Optional: with no tier declared, or a tier no model carries, it falls through. See Model Tiers.
  3. The deprecated ModelTier:* config (ModelTier__Heavy/Standard/Light/Utility) — still read so an existing deployment keeps its mapping, and only ever consulted for a tier no model node carries.
  4. The deployment default — the lowest-order model whose credentials resolve.

Every step after the first skips models with no usable credential, and skips the router. Resolution never fails: the only outcome with no model is an entirely-unusable catalog, which fails the round audibly.


When a Selected Model Is Unusable — the Fallback Is Honest, Never Silent

A pinned model can stop resolving (its provider node lost its key, the catalog was refactored, the model was deleted). AgentChatClient.ApplyStaleModelFallback then swaps it for a working model so the thread keeps running. Three rules make that swap honest:

The credential check cannot see quota — so the refusal has to read well

A usable credential means the deployment will answer, not that it will serve. A model with a perfectly good key can still refuse every round because it is out of quota (HTTP 429) or because the deployment itself is faulting (HTTP 5xx). No local check can predict that — only the provider's answer reveals it — so the requirement is not "never fall back onto a throttled model", it is fail legibly when the provider refuses.

ProviderFailureClassifier names the condition from the exception chain (typed HttpRequestException.StatusCode, else the conventional Status: NNN banner that Azure.Core and System.ClientModel both render), and ThreadExecution builds the prose at write time off the round's own AccessContext.Locale:

The raw transport text is never discarded, only relocated: it stays on the LogError(ex, …) that precedes the terminal write, alongside a PROVIDER_REFUSED warning carrying the status, the serving model and the requested one. What changed is what the user reads — previously ex.Message went straight into the cell's Text and Summary, which for these failures is the status line plus the response body plus the complete HTTP header block.


How the Model Picker Is Populated

The picker is node-based, not factory-based. AgentPickerProjection runs nodeType:LanguageModel|ModelProvider queries over the platform Provider catalog, the context's {path}/Provider subtrees, and the user's own {user}/_Memex namespace, and shows the resulting LanguageModel nodes, grouped by provider. Those nodes come from two places: the system catalog BuiltInLanguageModelProvider materialises from each {Section}:Models config list (imported into the Provider partition on boot and served from the DB), and space/user ModelProvider nodes authored in the mesh.

So an empty picker means no provider/model nodes are visible to the user — almost always because the deployment carries no {Section}:Models config signal (the classic Helm/AKS gap) or the user's {user}/_Memex/Selection points at a provider that doesn't exist. The full diagnosis + fix is in Setting Up Model Providers → Troubleshooting.

Don't try to mirror "everything the provider sells." List a short, curated set in {Section}:Models (the deployment's catalog) — the user picks from it in the composer.


Model-to-Factory Routing

When an agent needs a chat client for a given model name, AgentChatClient.GetFactoryForModel iterates the registered IChatClientFactory implementations in Order (lower first) and calls Supports(string) on each. Routing works without any populated Models[] array because the concrete factories implement shape-aware predicates:

Factory Supports predicate
AzureClaudeChatClientAgentFactory name.StartsWith("claude", IgnoreCase)
AzureFoundryChatClientAgentFactory catch-all for non-claude names (gpt-*, o*, Mistral-*, DeepSeek-*, …)

The default IChatClientFactory.Supports falls back to the legacy Models[] lookup, so factories that don't override still work through explicit Models config — useful for tests or for serving a curated subset.


Adding a New Provider

To wire in a new provider (a second Azure OpenAI deployment, a hosted local model, etc.):

  1. Implement IChatClientFactory and register it via DI (services.AddAzureOpenAI(...) or similar).
  2. Bind its options from a new section in MemexConfiguration.cs — endpoint and auth fields only, not model names.
  3. Add Aspire parameters in ../MeshWeaver.Plugins/src/Memex.AppHost/Program.cs for the endpoint (and a key, if it doesn't share azure-foundry-key).
  4. Label the new model's node with a tier (ModelDefinition.Tier), so agents reach it by declaring modelTier in their front matter. An agent names a tier, never a model id — there is no per-agent "preferred model" field.

Do not hardcode model identifiers in framework code. If you find yourself writing "gpt-4o" or "claude-sonnet-4-5" in a .cs file outside an agent definition, that is precisely the pattern this page exists to prevent.


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