Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using CrestApps.Core.AI.Models;
using Microsoft.Extensions.AI;

namespace CrestApps.Core.AI.Capabilities;

/// <summary>
/// Carries the information required by an <see cref="IAIModelParameterBinder"/> to apply a selected
/// model parameter value to the outgoing request.
/// </summary>
public sealed class AIModelParameterBindingContext
{
/// <summary>
/// Initializes a new instance of the <see cref="AIModelParameterBindingContext"/> class.
/// </summary>
/// <param name="descriptor">The effective descriptor of the parameter being applied.</param>
/// <param name="value">The value selected by the operator.</param>
/// <param name="chatOptions">The chat options to mutate.</param>
/// <param name="completionContext">The completion context of the current request.</param>
public AIModelParameterBindingContext(
AIModelParameterDescriptor descriptor,
string value,
ChatOptions chatOptions,
AICompletionContext completionContext)
{
ArgumentNullException.ThrowIfNull(descriptor);
ArgumentNullException.ThrowIfNull(chatOptions);
ArgumentNullException.ThrowIfNull(completionContext);

Descriptor = descriptor;
Value = value;
ChatOptions = chatOptions;
CompletionContext = completionContext;
}

/// <summary>
/// Gets the effective descriptor of the parameter being applied.
/// </summary>
public AIModelParameterDescriptor Descriptor { get; }

/// <summary>
/// Gets the value selected by the operator.
/// </summary>
public string Value { get; }

/// <summary>
/// Gets the chat options to mutate.
/// </summary>
public ChatOptions ChatOptions { get; }

/// <summary>
/// Gets the completion context of the current request.
/// </summary>
public AICompletionContext CompletionContext { get; }

/// <summary>
/// Gets or sets the deployment resolved for the current request.
/// </summary>
public AIDeployment Deployment { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using CrestApps.Core.AI.Models;

namespace CrestApps.Core.AI.Capabilities;

/// <summary>
/// Resolves the metadata-driven capabilities of an <see cref="AIDeployment"/> by merging the globally
/// registered model features and parameters with the metadata stored on the deployment.
/// </summary>
public interface IAIModelCapabilityService
{
/// <summary>
/// Gets every model feature registered by the application.
/// </summary>
IReadOnlyList<AIModelFeatureDescriptor> GetRegisteredFeatures();

/// <summary>
/// Gets every model parameter registered by the application.
/// </summary>
IReadOnlyList<AIModelParameterDescriptor> GetRegisteredParameters();

/// <summary>
/// Gets the effective capabilities exposed by the given deployment.
/// </summary>
/// <param name="deployment">The deployment to inspect.</param>
AIDeploymentCapabilities GetCapabilities(AIDeployment deployment);

/// <summary>
/// Gets the effective capabilities exposed by the deployment with the given technical name.
/// </summary>
/// <param name="deploymentName">The technical name of the deployment.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
ValueTask<AIDeploymentCapabilities> GetCapabilitiesAsync(string deploymentName, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace CrestApps.Core.AI.Capabilities;

/// <summary>
/// Applies the value selected for a registered model parameter to the outgoing chat request.
/// Modules register a binder for every parameter they contribute so runtime behavior stays
/// provider-agnostic and free of model name detection.
/// </summary>
public interface IAIModelParameterBinder
{
/// <summary>
/// Gets the technical name of the parameter this binder applies.
/// </summary>
string ParameterName { get; }

/// <summary>
/// Applies the selected value to the request represented by the given context.
/// </summary>
/// <param name="context">The binding context.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
Task BindAsync(AIModelParameterBindingContext context, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using CrestApps.Core.AI.Models;

namespace CrestApps.Core.AI.Completions;

/// <summary>
/// Provides extension methods for <see cref="AICompletionContext"/>.
/// </summary>
public static class AICompletionContextExtensions
{
/// <summary>
/// Copies the model parameter values held by the given metadata onto the completion context.
/// Empty values are ignored so a stored blank never overrides a deployment default.
/// </summary>
/// <param name="context">The completion context to populate.</param>
/// <param name="metadata">The metadata holding the selected model parameter values.</param>
public static void ApplyModelParameters(this AICompletionContext context, AIModelParametersMetadata metadata)
{
ArgumentNullException.ThrowIfNull(context);

if (metadata?.Values is not { Count: > 0 })
{
return;
}

foreach (var (name, value) in metadata.Values)
{
if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(value))
{
continue;
}

context.ModelParameters[name] = value;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,13 @@ private string _utilityDeploymentIdBackingField
set => UtilityDeploymentName = value;
}

/// <summary>
/// Gets the model parameter values selected for this request, keyed by the registered
/// parameter technical name. Values for parameters that the resolved deployment does not
/// expose are discarded before the request is sent to the provider.
/// </summary>
public Dictionary<string, string> ModelParameters { get; } = new(StringComparer.OrdinalIgnoreCase);

/// <summary>
/// Gets the additional provider-specific properties applied to the completion request.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
namespace CrestApps.Core.AI.Models;

/// <summary>
/// Represents the effective capabilities of an <see cref="AIDeployment"/> after the registered
/// definitions have been merged with the deployment specific metadata.
/// </summary>
public sealed class AIDeploymentCapabilities
{
/// <summary>
/// Gets an instance that exposes no features and no parameters.
/// </summary>
public static AIDeploymentCapabilities Empty { get; } = new AIDeploymentCapabilities([], []);

private readonly Dictionary<string, AIModelParameterDescriptor> _parameters;
private readonly HashSet<string> _features;

/// <summary>
/// Initializes a new instance of the <see cref="AIDeploymentCapabilities"/> class.
/// </summary>
/// <param name="features">The features exposed by the deployment.</param>
/// <param name="parameters">The effective parameters exposed by the deployment.</param>
public AIDeploymentCapabilities(
IEnumerable<AIModelFeatureDescriptor> features,
IEnumerable<AIModelParameterDescriptor> parameters)
{
ArgumentNullException.ThrowIfNull(features);
ArgumentNullException.ThrowIfNull(parameters);

Features = [.. features.OrderBy(feature => feature.Order).ThenBy(feature => feature.Name, StringComparer.OrdinalIgnoreCase)];
Parameters = [.. parameters.OrderBy(parameter => parameter.Order).ThenBy(parameter => parameter.Name, StringComparer.OrdinalIgnoreCase)];
_features = new HashSet<string>(Features.Select(feature => feature.Name), StringComparer.OrdinalIgnoreCase);
_parameters = Parameters.ToDictionary(parameter => parameter.Name, StringComparer.OrdinalIgnoreCase);
}

/// <summary>
/// Gets the features exposed by the deployment.
/// </summary>
public IReadOnlyList<AIModelFeatureDescriptor> Features { get; }

/// <summary>
/// Gets the effective parameters exposed by the deployment.
/// </summary>
public IReadOnlyList<AIModelParameterDescriptor> Parameters { get; }

/// <summary>
/// Determines whether the deployment exposes the given feature.
/// </summary>
/// <param name="featureName">The technical name of the feature.</param>
public bool SupportsFeature(string featureName)
{
return !string.IsNullOrWhiteSpace(featureName) && _features.Contains(featureName);
}

/// <summary>
/// Gets the effective descriptor of the given parameter, or <see langword="null"/> when the
/// deployment does not expose it.
/// </summary>
/// <param name="parameterName">The technical name of the parameter.</param>
public AIModelParameterDescriptor GetParameter(string parameterName)
{
if (string.IsNullOrWhiteSpace(parameterName))
{
return null;
}

return _parameters.TryGetValue(parameterName, out var descriptor)
? descriptor
: null;
}

/// <summary>
/// Determines whether the deployment exposes the given parameter.
/// </summary>
/// <param name="parameterName">The technical name of the parameter.</param>
public bool SupportsParameter(string parameterName)
{
return GetParameter(parameterName) is not null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
namespace CrestApps.Core.AI.Models;

/// <summary>
/// Metadata stored on <see cref="AIDeployment"/> describing the features and configurable parameters
/// exposed by the underlying model. Editors, validation, and runtime request generation are driven from
/// this metadata instead of provider or model name detection.
/// </summary>
public sealed class AIDeploymentModelMetadata
{
/// <summary>
/// Gets or sets the technical names of the registered model features supported by this deployment.
/// </summary>
public string[] Features { get; set; } = [];

/// <summary>
/// Gets or sets the supported model parameters keyed by their registered technical name.
/// A parameter that is not present in this dictionary is not supported by the deployment and is
/// never rendered by editors or sent to the provider.
/// </summary>
public Dictionary<string, AIDeploymentModelParameter> Parameters { get; set; } = new(StringComparer.OrdinalIgnoreCase);

/// <summary>
/// Determines whether the deployment supports the given registered feature.
/// </summary>
/// <param name="featureName">The technical name of the feature.</param>
public bool SupportsFeature(string featureName)
{
if (string.IsNullOrWhiteSpace(featureName) || Features is not { Length: > 0 })
{
return false;
}

return Features.Any(feature => string.Equals(feature, featureName, StringComparison.OrdinalIgnoreCase));
}

/// <summary>
/// Determines whether the deployment supports the given registered parameter.
/// </summary>
/// <param name="parameterName">The technical name of the parameter.</param>
public bool SupportsParameter(string parameterName)
{
return !string.IsNullOrWhiteSpace(parameterName) &&
Parameters is not null &&
Parameters.ContainsKey(parameterName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
namespace CrestApps.Core.AI.Models;

/// <summary>
/// Describes the per-deployment metadata of a supported model parameter. Every member is optional and,
/// when supplied, narrows the globally registered <see cref="AIModelParameterDescriptor"/> so a deployment
/// can describe the exact behavior of its underlying model.
/// </summary>
public sealed class AIDeploymentModelParameter
{
/// <summary>
/// Gets or sets the subset of allowed values supported by this deployment.
/// When empty, the registered allowed values are used.
/// </summary>
public string[] AllowedValues { get; set; }

/// <summary>
/// Gets or sets the value applied when an operator does not select one.
/// </summary>
public string DefaultValue { get; set; }

/// <summary>
/// Gets or sets the inclusive minimum accepted value for numeric parameters.
/// </summary>
public double? Minimum { get; set; }

/// <summary>
/// Gets or sets the inclusive maximum accepted value for numeric parameters.
/// </summary>
public double? Maximum { get; set; }

/// <summary>
/// Gets or sets the increment applied by numeric editors.
/// </summary>
public double? Step { get; set; }

/// <summary>
/// Creates a copy of this instance.
/// </summary>
public AIDeploymentModelParameter Clone()
{
return new AIDeploymentModelParameter
{
AllowedValues = AllowedValues is null
? null
: [.. AllowedValues],
DefaultValue = DefaultValue,
Minimum = Minimum,
Maximum = Maximum,
Step = Step,
};
}
}
Loading
Loading