AI Deployment Capabilities
A registry of deployment features and deployment parameters that lets deployments declare what their model supports, so editors render only the relevant options, the runtime only sends supported values, and the chat UI adapts its input to the selected model.
Why this exists
Different models expose different knobs. A reasoning model accepts a reasoning effort level, a small chat model does not, and a speech-to-speech model has no text box at all. Without metadata, every new knob turns into hardcoded, provider-specific UI and provider-specific request-building code.
The capability system replaces that with two extensible concepts:
| Concept | Shape | Example |
|---|---|---|
| Feature | A binary capability. The deployment either supports it or it does not. | toolCalling, reasoning, realtime |
| Parameter | A configurable option carrying metadata: kind, allowed values, range, and a default. | reasoningEffort |
Definitions are registered once at startup. An AI Deployment then declares which of those definitions its model actually exposes, optionally narrowing the allowed values. AI Profiles, AI Profile Templates, and Chat Interactions store the selected values. At request time the framework binds the selected values into the outgoing request.
Capabilities and slots are two different questions. A capability says what the model can do, and
lives on the deployment. A slot says what this installation uses a deployment for — chat,
utility, embedding, image, vision, speechToText, textToSpeech, realtime — and pairs the
capability a deployment must declare with the site-wide default that fills it.
You declare capabilities; the framework derives routing from them. Nothing separate has to be tagged for
a deployment to appear in the right picker: declaring textEmbedding is what puts a model in the
embedding slot. See Deployment slots.
Quick Start
builder.Services.AddCoreAIDeploymentCapabilities();
You rarely need to call this directly — AddCoreAIServices() chains it automatically.
Built-in definitions
Features
| Name | Constant | Default on new deployments | Description |
|---|---|---|---|
textGeneration | AIDeploymentFeatureNames.TextGeneration | ✅ | The model can hold a text conversation. Clear this only for a speech-to-speech-only model that cannot handle text. |
toolCalling | AIDeploymentFeatureNames.ToolCalling | ✅ | The model can call tools and functions supplied with the request. |
structuredOutputs | AIDeploymentFeatureNames.StructuredOutputs | The model can return responses that follow a supplied JSON schema. | |
streaming | AIDeploymentFeatureNames.Streaming | ✅ | The model can stream response updates as they are produced. |
reasoning | AIDeploymentFeatureNames.Reasoning | The model performs internal reasoning before producing an answer. | |
imageInput | AIDeploymentFeatureNames.ImageInput | The model can understand image inputs (vision). | |
imageOutput | AIDeploymentFeatureNames.ImageOutput | The model can generate images. | |
audioInput | AIDeploymentFeatureNames.AudioInput | The model accepts audio input. | |
audioOutput | AIDeploymentFeatureNames.AudioOutput | The model produces audio output. | |
videoInput | AIDeploymentFeatureNames.VideoInput | The model can understand video inputs. | |
videoOutput | AIDeploymentFeatureNames.VideoOutput | The model can generate video. | |
realtime | AIDeploymentFeatureNames.Realtime | The model supports real-time, bidirectional speech-to-speech sessions. | |
textEmbedding | AIDeploymentFeatureNames.TextEmbedding | The model generates text embedding vectors. Declare this only for a dedicated embedding model, not for a chat model. | |
speechToText | AIDeploymentFeatureNames.SpeechToText | The model transcribes audio into text. Declare this for a dedicated transcription model such as Whisper — not for a chat model that merely accepts audio input. | |
textToSpeech | AIDeploymentFeatureNames.TextToSpeech | The model synthesizes speech from text. Declare this for a dedicated synthesis model — not for a chat model that merely emits audio output. |
speechToText and textToSpeech are deliberately distinct from audioInput and audioOutput. The
audio pair means "this chat model accepts or emits audio inline"; the other pair means "this is a
transcription or synthesis endpoint". Conflating them would offer gpt-4o-audio in the Whisper slot and
Whisper in the chat picker.
These represent trained capabilities the underlying model was built with. Provider-hosted tools
(such as a web-search tool the provider runs on your behalf, or a computer-use tool) are not modeled
as features because almost any tool-calling model can be handed such a tool — they are ordinary tools,
not a trained trait. Set AIDeploymentFeatureDescriptor.EnabledByDefault when registering a feature to
pre-select it on newly created deployments; existing deployments are never changed by this flag.
:::tip Opt-out vs opt-in features
textGeneration is opt-out: a deployment is assumed to support it unless it declares metadata that
omits it. Most models are text models, so this keeps every existing deployment working without change.
realtime is opt-in: only a deployment that explicitly lists it is treated as a realtime model. The
two together let a single flag flip a chat surface between text and voice — see
Building a capability-aware chat UI.
:::
Parameters
| Name | Constant | Kind | Allowed values | Default |
|---|---|---|---|---|
reasoningEffort | AIDeploymentParameterNames.ReasoningEffort | Choice | None (shown as Minimal), Low, Medium, High, ExtraHigh | Medium |
reasoningEffort maps onto Microsoft.Extensions.AI.ChatOptions.Reasoning.Effort, so it is
provider-agnostic and ships in the core AI package rather than in a provider module. It also declares
RequiredFeature = AIDeploymentFeatureNames.Reasoning, which links the parameter to the reasoning
trained feature (see Linking a parameter to a feature).
Declaring what a deployment supports
Capability metadata is stored on the deployment through the
extensible entity Properties bag using AIDeploymentMetadata:
deployment.Put(new AIDeploymentMetadata
{
Features =
[
AIDeploymentFeatureNames.ToolCalling,
AIDeploymentFeatureNames.Reasoning,
],
Parameters = new Dictionary<string, AIDeploymentParameter>(StringComparer.OrdinalIgnoreCase)
{
[AIDeploymentParameterNames.ReasoningEffort] = new AIDeploymentParameter
{
AllowedValues = ["Low", "Medium", "High"],
DefaultValue = "Medium",
},
},
});
A speech-to-speech-only model declares realtime and omits textGeneration, which is what tells
every chat surface to run this deployment as audio-only:
deployment.Put(new AIDeploymentMetadata
{
// No textGeneration: this model cannot handle a text turn.
Features = [ AIDeploymentFeatureNames.Realtime ],
});
Because AIDeploymentCatalogHandler deep-merges Properties, the same metadata can be supplied from
configuration or a recipe with no extra code. Note the property key is the type name,
AIDeploymentMetadata:
{
"CrestApps": {
"AI": {
"Deployments": [
{
"Name": "gpt-5-chat",
"ModelName": "gpt-5",
"ConnectionName": "openai",
"Properties": {
"AIDeploymentMetadata": {
"Features": [ "textGeneration", "toolCalling", "reasoning", "streaming" ],
"Parameters": {
"reasoningEffort": {
"AllowedValues": [ "Low", "Medium", "High" ],
"DefaultValue": "Medium"
}
}
}
}
},
{
"Name": "gpt-realtime",
"ModelName": "gpt-realtime",
"ConnectionName": "openai",
"Properties": {
"AIDeploymentMetadata": {
"Features": [ "realtime" ]
}
}
}
]
}
}
}
A deployment-level parameter entry may narrow or override the registered definition:
| Property | Effect |
|---|---|
AllowedValues | Restricts a Choice parameter to a subset of the registered options. |
DefaultValue | Overrides the registered default. Ignored when the value is not valid for the parameter. |
Minimum, Maximum, Step | Overrides the numeric bounds for Number and Integer parameters. |
Resolving capabilities
IAIDeploymentCapabilityService merges the registered definitions with the deployment metadata and
returns only what the deployment exposes:
public sealed class MyService
{
private readonly IAIDeploymentCapabilityService _capabilityService;
public MyService(IAIDeploymentCapabilityService capabilityService)
{
_capabilityService = capabilityService;
}
public async Task<bool> SupportsReasoningAsync(string deploymentName)
{
var capabilities = await _capabilityService.GetCapabilitiesAsync(deploymentName);
return capabilities.SupportsFeature(AIDeploymentFeatureNames.Reasoning);
}
}
| Member | Description |
|---|---|
GetRegisteredFeatures() | Every registered feature descriptor, ordered. |
GetRegisteredParameters() | Every registered parameter descriptor, ordered. |
GetCapabilities(AIDeployment) | Resolves capabilities from an already loaded deployment. |
GetCapabilitiesAsync(string, CancellationToken) | Loads the deployment by name and resolves its capabilities. |
SupportsFeatureOrUnconstrained(AIDeployment, string) | Opt-out check: a deployment with no capability metadata counts as supporting the feature. Use for textGeneration. |
GetDeploymentsWithFeatureAsync(string, CancellationToken) | Every deployment whose model declares the feature. |
ResolveDeploymentWithFeatureAsync(string, string?, CancellationToken) | Returns the named deployment only if it declares the feature; when no name is given, the first deployment that declares it. Returns null when none qualifies. |
The returned AIDeploymentCapabilities exposes Features, Parameters, SupportsFeature,
SupportsParameter, and GetParameter. Descriptors are cloned, so the registered definitions are
never mutated by a deployment override.
:::note Opt-in vs opt-out at the call site
Use GetCapabilities(deployment).SupportsFeature("realtime") for opt-in features — a deployment must
explicitly declare realtime. Use SupportsFeatureOrUnconstrained(deployment, "textGeneration") for
opt-out features — a deployment with no metadata is treated as text-capable, so legacy deployments keep
working.
:::
Storing selected values
Consumers store their selections with AIDeploymentParametersMetadata, again through the extensible
entity Properties bag:
profile.Put(new AIDeploymentParametersMetadata
{
Values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
[AIDeploymentParameterNames.ReasoningEffort] = "High",
},
});
This works on AIProfile, AIProfileTemplate, and ChatInteraction. Profile and chat interaction
context builder handlers copy the stored values onto AICompletionContext.ModelParameters before the
request is built.
Utility deployment values
A profile, profile template, or chat interaction selects two deployments: a chat deployment and a utility deployment. The utility deployment backs the background completions the framework runs on its own — title generation, orchestration planning, data extraction, and post-session processing — and is often a different, cheaper model with different capabilities.
AIDeploymentParametersMetadata.UtilityValues holds the values selected for the utility deployment,
alongside Values, which holds the values selected for the chat deployment:
profile.Put(new AIDeploymentParametersMetadata
{
Values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
[AIDeploymentParameterNames.ReasoningEffort] = "High",
},
UtilityValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
[AIDeploymentParameterNames.ReasoningEffort] = "Low",
},
});
The context builder handlers copy these onto AICompletionContext.UtilityModelParameters. A caller
marks a request as a background utility completion by setting
AICompletionContext.IsUtilityCompletion, which makes the completion client resolve the utility
deployment (falling back to the chat deployment) and apply UtilityModelParameters instead of
ModelParameters.
The editors for AI profiles, AI profile templates, and chat interactions render a second parameter editor bound to the selected utility deployment, so an operator can pick a reasoning effort for the utility model independently of the chat model. Each editor shows only the parameters its own deployment declares.
Markdown profile templates
Markdown-authored profile templates can set values through the ModelParameters and
UtilityModelParameters front-matter keys. Pairs are name=value, separated by ; or by a new line:
---
Name: Deep research
ModelParameters: reasoningEffort=High
UtilityModelParameters: reasoningEffort=Low
---
You are a meticulous research assistant.
Runtime binding
ModelParametersAICompletionServiceHandler runs as an IAICompletionServiceHandler and delegates to
IAIDeploymentParameterApplier, which is the single enforcement point:
- It iterates only the parameters the deployment exposes. A value stored for an unsupported parameter is ignored and never leaves the process.
- When the stored value is missing or invalid for the resolved descriptor, the deployment default is used and a warning is logged.
- If an
IAIDeploymentParameterBinderis registered for the parameter, the binder shapes the request. - Otherwise the value is written to
ChatOptions.AdditionalPropertiesso providers that read raw properties still receive it.
ReasoningEffortModelParameterBinder implements step 3 for reasoningEffort by setting
ChatOptions.Reasoning.Effort.
The applier takes an AIDeploymentParameterScope that selects which stored values to read: Chat
reads AICompletionContext.ModelParameters, Utility reads
AICompletionContext.UtilityModelParameters. The handler picks the scope from
AICompletionContext.IsUtilityCompletion.
Background completions that resolve an IChatClient from IAIClientFactory and call it directly
bypass the completion pipeline. Those call sites add the applier to the client pipeline with
ChatClientBuilder.UseModelParameters (or UseUtilityModelParameters, which derives the context from
an AIProfile), so a directly resolved client honors the same operator selections:
var client = await clientFactory.CreateChatClientAsync(
utilityDeployment,
builder => builder
.UseDefaultResilience()
.UseUtilityModelParameters(parameterApplier, utilityDeployment, profile));
Feature enforcement
ModelFeaturesAICompletionServiceHandler enforces the features a deployment declares. It runs
after the tool-adding handlers so it can strip options that depend on an unsupported trained
capability:
- When the deployment does not declare
toolCalling, anyChatOptions.ToolsandChatOptions.ToolModeare cleared before the request leaves the process. - When the deployment does not declare
structuredOutputs, a JSONChatOptions.ResponseFormatis removed. - When the deployment does not declare
reasoning,ChatOptions.Reasoningis removed. When it does declarereasoning, the requested effort is validated against the effectivereasoningEffortparameter and coerced to the deployment default (or removed when the parameter is not exposed).
The same feature logic also runs inside a CapabilityEnforcingChatClient that IAIClientFactory wraps
around every chat client it creates, as the terminal layer immediately above the provider-facing
client. This guarantees enforcement even when a caller resolves an IChatClient from the factory and
calls it directly, outside the completion pipeline.
Because streaming is a method choice rather than a ChatOptions field, it is enforced at the call
site: when a deployment does not declare streaming, a streaming request is transparently completed as
a single non-streaming response and replayed as one streaming update. This applies both to the
client-factory path (CapabilityEnforcingChatClient) and to AzureOpenAICompletionClient, which
streams through the Azure SDK directly.
Enforcement is opt-in: it only applies to deployments that declare capability metadata. A
deployment with no AIDeploymentMetadata is treated as unconstrained, so existing configurations keep
working unchanged. Combined with the parameter handler above, this guarantees that neither unsupported
parameters (for example reasoningEffort) nor unsupported options (tools, structured output,
reasoning) are sent to a model that was not trained for them.
Azure OpenAI builds OpenAI.Chat.ChatCompletionOptions directly instead of going through
Microsoft.Extensions.AI.ChatOptions. AzureOpenAICompletionClient therefore translates the resolved
reasoning effort onto ChatCompletionOptions.ReasoningEffortLevel as well, so both request paths
behave the same.
Building a capability-aware chat UI
This is the pattern the built-in hosts follow, and the one your own chat surface should follow. The guiding rule is simple:
The chat input is chosen from the effective deployment's capabilities — not from a global setting. A deployment that declares
realtimerenders as audio-only; every other chat deployment renders a text box.
The two features that drive input modality
| Feature | Meaning for the UI |
|---|---|
textGeneration (opt-out, default on) | The model can take a typed turn. Render the text box, send button, and — when the site has the matching default deployments — the speech-to-text and STT/TTS conversation controls. |
realtime (opt-in) | The model is speech-to-speech. Render only a Start speaking control that opens a realtime voice session, and hide the text box — such a model cannot process a text turn, so sending text would fail. |
A model that declares realtime and omits textGeneration is voice-only. A model that declares both is
unusual but valid — treat it as realtime-capable for the voice control while still allowing text.
Effective chat mode
ChatMode has four values: TextInput, AudioInput, Conversation, and Realtime. Resolve the
effective mode on the server from the deployment the surface will actually use, letting a realtime model
win:
// The selected chat deployment answers this. A realtime model is a voice conversation; anything else is
// a text profile.
var isRealtime = await _capabilityService.IsRealtimeDeploymentAsync(deploymentName);
// The chat mode layers speech-to-text and text-to-speech over a text model, so it does not apply to a
// realtime deployment, which speaks natively.
var effectiveChatMode = isRealtime
? ChatMode.TextInput
: /* your STT / conversation / text logic */ ChatMode.TextInput;
Every surface asks the same question of the same field:
| Surface | Deployment it converses with |
|---|---|
| AI Profile / AI Chat widget | AIProfile.ChatDeploymentName |
| Chat Interaction | ChatInteraction.ChatDeploymentName |
There is no separate realtime deployment field and no realtime chat mode. Selecting a realtime-capable
model in the chat deployment picker is how a profile or interaction becomes a voice conversation — the
picker offers text-capable and realtime-capable deployments together, and the capability decides the rest.
DefaultAIDeploymentSettings.DefaultRealtimeDeploymentName still backs the realtime slot for callers
that resolve it directly, such as the realtime orchestrator.
ChatMode still exists for the speech-to-text and text-to-speech features layered over a text model
(TextInput, AudioInput, Conversation). It no longer has a Realtime member.
Switching the input live
When your surface lets the operator change the deployment without a reload (the Chat Interaction picker does), pass the set of realtime-capable deployment names to the client and flip the input on change:
// Server: expose which deployments are realtime-capable.
var realtimeDeployments = await _capabilityService.GetDeploymentsWithFeatureAsync(
AIDeploymentFeatureNames.Realtime);
model.RealtimeCapableDeploymentNames = realtimeDeployments
.Select(d => d.Name)
.Where(name => !string.IsNullOrWhiteSpace(name))
.ToArray();
// Client: audio-only when the selected deployment is realtime, text otherwise.
function applyRealtimeMode(enable) {
chatInput.hidden = enable;
sendButton.hidden = enable;
realtimeButton.hidden = !enable; // the "Start speaking" control
}
deploymentSelect.addEventListener('change', function () {
var name = deploymentSelect.value.toLowerCase();
applyRealtimeMode(realtimeCapableDeployments.indexOf(name) !== -1);
});
Realtime transport
Realtime runs over the chat SignalR hub, not the request/response completion pipeline. The client and server exchange raw PCM16, 24 kHz, mono audio as base64 frames:
- Send — capture the microphone, downmix to 16-bit PCM, and stream base64 frames through a
signalR.Subjectto the hub'sStartRealtimeConversationmethod (its first argument is the session/interaction identifier, followed by the audio stream, the voice, and the language). - Receive — assistant audio arrives as
ReceiveAudioChunk(id, base64, "audio/pcm"); schedule those frames for immediate playback through the Web Audio API. (STT/TTS conversation audio arrives on the same callback but taggedaudio/mp3/audio/wavand is collected and played on completion — branch on the content type.) User and assistant transcripts arrive through the same conversation callbacks that STT/TTS conversation mode already uses, so realtime renders in the same message list.
For complete client implementations of this contract, see the CrestApps.Core.Mvc.Web
Areas/AIChat and Areas/ChatInteractions chat views (and the shared ai-chat.js), which capture the
microphone, stream PCM16 frames, and play back the assistant audio.
Guardrails you get for free
Even if a UI lets a bad combination through, the framework refuses to fail silently:
- Save-time validation — the AI Profile, AI Profile Template, and Chat Interaction editors reject a
realtime-only model (one that does not support
textGeneration) in a text chat slot, and reject a non-realtime model in a realtime slot, usingSupportsFeatureOrUnconstrainedandResolveDeploymentWithFeatureAsyncrespectively. - Runtime message — if a text turn still reaches a realtime-only model, the chat surface returns a clear explanation ("the selected chat deployment may not support text conversation…") as the assistant response instead of an empty or failed turn.
- Slot filtering — resolution itself refuses to hand back a deployment that cannot do the job. A
realtime deployment never fills the
chatorutilityslot, even when an operator also tickstextGenerationfor it, because those slots declarerealtimeas an excluded capability. This is the rule that used to be re-checked by hand at every background-completion call site.
Deployment slots
A slot is a named role this installation uses a deployment for. It pairs the capability a deployment must
declare with the site-wide default that fills it, and — for utility — a fallback slot.
| Slot | Required capability | Excluded | Falls back to |
|---|---|---|---|
chat | textGeneration | realtime | |
utility | textGeneration | realtime | chat |
embedding | textEmbedding | ||
image | imageOutput | ||
vision | imageInput | ||
speechToText | speechToText | ||
textToSpeech | textToSpeech | ||
realtime | realtime |
textGeneration is opt-out, so a deployment that declares no capability metadata at all still fills
the chat and utility slots. Every other capability is opt-in and must be declared.
Resolution walks one ordered chain, and evaluates "first capable" exactly once, at the very end:
explicit name for the slot
-> the slot's site-wide default
-> explicit name for the fallback slot
-> the fallback slot's site-wide default
-> the first capable deployment
That ordering is why background work runs on the caller's own chat deployment when no utility deployment is configured, rather than on an arbitrary text model.
// Resolve the deployment that fills a slot.
var deployment = await deploymentManager.ResolveSlotAsync(AIDeploymentSlotNames.Embedding);
// List every deployment eligible for a slot — this is what the settings pickers use.
var candidates = await deploymentManager.GetAllBySlotAsync(AIDeploymentSlotNames.Chat);
Modules can register their own slots:
services.AddAIDeploymentSlot("moderation", new LocalizedString("moderation", "Moderation"), slot =>
{
slot.RequiredFeature = AIDeploymentFeatureNames.TextGeneration;
slot.GetDefaultDeploymentName = static settings => settings.DefaultUtilityDeploymentName;
slot.FallbackSlotName = AIDeploymentSlotNames.Utility;
slot.AllowUnconstrained = true;
});
Registering your own definitions
Any module can contribute definitions during startup.
services.AddAIDeploymentFeature(
"webSearch",
new LocalizedString("webSearch", "Web search"),
feature =>
{
feature.Description = new LocalizedString("webSearch", "The provider runs a hosted web-search tool for this model.");
feature.Order = 200;
});
services.AddAIDeploymentParameter(
"verbosity",
new LocalizedString("verbosity", "Verbosity"),
parameter =>
{
parameter.Kind = AIDeploymentParameterKind.Choice;
parameter.DefaultValue = "medium";
parameter.AllowedValues =
[
new AIDeploymentParameterOption { Value = "low", DisplayName = new LocalizedString("low", "Low") },
new AIDeploymentParameterOption { Value = "medium", DisplayName = new LocalizedString("medium", "Medium") },
new AIDeploymentParameterOption { Value = "high", DisplayName = new LocalizedString("high", "High") },
];
});
Registering the same name again updates the existing descriptor instead of adding a duplicate, so a provider module can refine a definition contributed by another module.
Linking a parameter to a feature
A parameter can declare that it only applies when the model exposes a specific trained feature by
setting RequiredFeature to the feature name. The built-in reasoningEffort parameter uses this to
depend on the reasoning feature:
services.AddAIDeploymentParameter(
AIDeploymentParameterNames.ReasoningEffort,
new LocalizedString(AIDeploymentParameterNames.ReasoningEffort, "Reasoning effort"),
parameter =>
{
parameter.Kind = AIDeploymentParameterKind.Choice;
parameter.RequiredFeature = AIDeploymentFeatureNames.Reasoning;
// allowed values, default, etc.
});
When RequiredFeature is set, the deployment editor only shows the parameter while the matching
feature checkbox is enabled, and clearing the feature also clears the dependent parameter so a
contradictory combination (for example a reasoningEffort value on a model that is not a reasoning
model) can never be saved. The relationship is also enforced in the framework:
IAIDeploymentCapabilityService.GetCapabilities excludes a parameter whose RequiredFeature is not
among the deployment's declared features, so ModelParametersAICompletionServiceHandler never applies
it — regardless of how the metadata was authored.
Parameter kinds
| Kind | Editor | Notes |
|---|---|---|
Choice | Drop-down | Requires AllowedValues. |
Number | Numeric input | Honors Minimum, Maximum, and Step. |
Integer | Numeric input | Honors Minimum, Maximum, and Step. |
Boolean | Drop-down of true / false | |
Text | Free-text input |
Custom binders
Implement IAIDeploymentParameterBinder when a parameter needs to shape the request beyond
AdditionalProperties:
public sealed class VerbosityModelParameterBinder : IAIDeploymentParameterBinder
{
public string ParameterName => "verbosity";
public ValueTask BindAsync(AIDeploymentParameterBindingContext context, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(context);
context.ChatOptions.AdditionalProperties ??= [];
context.ChatOptions.AdditionalProperties["verbosity"] = context.Value;
return ValueTask.CompletedTask;
}
}
services.AddScoped<IAIDeploymentParameterBinder, VerbosityModelParameterBinder>();
The binding context exposes the resolved Descriptor, the selected Value, the ChatOptions being
built, the CompletionContext, and the Deployment.
Sample host editors
Both sample hosts render the metadata rather than hardcoding options.
- AI Deployment editor — lists every registered feature as a checkbox under a Trained features
heading (features flagged
EnabledByDefaultare pre-checked on new deployments). A parameter that declares aRequiredFeatureis rendered inline beneath its feature checkbox and is only shown while that feature is enabled; the Model parameters heading only appears for parameters that are not linked to a feature. Each parameter offers a supported toggle, an allowed-values selector, a default value, and numeric bounds where applicable. - AI Profile, AI Profile Template, and Chat Interaction editors — render only the parameters the selected deployment supports, restricted to that deployment's allowed values. Nothing read-only is rendered: the editor is inputs only, and the whole block — its heading included — is hidden when the selected deployment declares no configurable parameter. Each screen renders two editors, one bound to the chat deployment and one bound to the utility deployment, so the two models are configured independently. They also validate the chosen deployment against the slot's required capability at save time (see Guardrails).
- AI Chat and Chat Interaction chat views — switch the input between a text box and an audio-only
Start speaking control based on the effective deployment's
realtimecapability, as described in Building a capability-aware chat UI.
In CrestApps.Core.Mvc.Web the server renders every registered parameter inside hidden, disabled
wrappers together with a deployment-to-capability JSON map; a small script shows, enables, and filters
the fields when the deployment selection changes. Disabled inputs are not posted, so an unsupported
value can never be submitted. Both the CrestApps.Core.Mvc.Web and CrestApps.Core.Blazor.Web
deployment editors render the allowed-values selectors with the
@crestapps/bootstrap-select picker for a searchable
multi-select experience, so the two hosts present the same editing UI. In CrestApps.Core.Blazor.Web
the picker is wrapped in an isolated BootstrapMultiSelect component that initializes the plugin once
through a small JS interop module and reports selection changes back to Blazor, and the components prune
values that the newly selected deployment does not support.
The GitHub Copilot and Claude orchestrators keep their own effort settings
(CopilotReasoningEffort, ClaudeEffortLevel). Those are orchestrator/session-level options rather
than deployment-level model parameters and are intentionally left unchanged.