Resolve model profiles through the registry

This commit is contained in:
Thorsten Sommer committed 2026-09-12 10:50:12 +02:00
1 parent 9307f2c363
commit 429ca8c739
8 files changed
+551 -6

No files matched your search

@@ -1,6 +1,7 @@
using System.Text;
using System.Text.Json.Serialization;
using AIStudio.Models;
using AIStudio.Provider;
using Lua;
@@ -15,6 +16,23 @@ namespace AIStudio.Settings;
/// </summary>
public sealed record ProviderCapabilityOverrides
{
/// <summary>
/// The capabilities a person switches on or off directly, without the reasoning words.
/// </summary>
/// <remarks>
/// How a model reasons is one answer out of four, not three flags which can contradict each
/// other, so it is resolved on its own below. The three words stay in the list above because
/// that is the vocabulary a settings file and a configuration plugin are written in.
/// </remarks>
private static readonly IReadOnlyList<Capability> DIRECTLY_SETTABLE_CAPABILITIES =
[
Capability.AUDIO_INPUT,
Capability.FUNCTION_CALLING,
Capability.MULTIPLE_IMAGE_INPUT,
Capability.SPEECH_INPUT,
Capability.VIDEO_INPUT,
];
private static readonly IReadOnlyList<Capability> SUPPORTED_CAPABILITIES =
[
Capability.AUDIO_INPUT,
@@ -96,6 +114,85 @@ public sealed record ProviderCapabilityOverrides
_ => this
};
/// <summary>
/// Applies what a person said about their own installation to what the rules worked out.
/// </summary>
/// <remarks>
/// The topmost link of the chain: an explicit statement about one's own provider wins over
/// everything the rules could know, because the person can see the installation and the rules
/// cannot.
/// </remarks>
/// <param name="profile">What the rules worked out.</param>
/// <returns>The profile as this provider instance was told it is.</returns>
public ModelProfile ApplyTo(in ModelProfile profile) => profile with
{
Capabilities = this.ApplyToCapabilities(profile.Capabilities),
Reasoning = this.ResolveReasoning(profile.Reasoning),
};
/// <summary>
/// Switches the plain capabilities on and off.
/// </summary>
/// <param name="stated">What the rules worked out.</param>
/// <returns>The capabilities after the overrides.</returns>
private Capability ApplyToCapabilities(Capability stated)
{
var capabilities = stated;
foreach (var capability in DIRECTLY_SETTABLE_CAPABILITIES)
switch (this.GetOverride(capability))
{
case true:
capabilities |= capability;
break;
case false:
capabilities &= ~capability;
break;
}
return capabilities;
}
/// <summary>
/// Works out how a model reasons, out of what the rules say and what a person said.
/// </summary>
/// <remarks>
/// This replaces thirty lines which repaired states that could not exist -- a model both always
/// reasoning and reasoning on request -- by an answer which cannot be in two of them at once.
/// The expert dialog writes all three words together, so the five combinations it produces are
/// answered exactly as they are today.
///
/// One thing changes, and it is a defect going away. A word nobody said anything about used to
/// destroy the answer: a provider carrying any override at all, say tool calling turned off, lost
/// "reasoning on by default" on the way through, because the old repair took the word away unless
/// "reasoning on request" stood next to it -- which no rule ever states. Here a "no" only takes
/// away what it names.
/// </remarks>
/// <param name="stated">How the rules say the model reasons.</param>
/// <returns>How it reasons after the overrides.</returns>
private ReasoningSupport ResolveReasoning(ReasoningSupport stated)
{
// A "yes" is the whole answer, whatever else is written next to it:
if (this.AlwaysReasoning is true)
return ReasoningSupport.ALWAYS;
if (this.ReasoningByDefault is true)
return ReasoningSupport.ON_BY_DEFAULT;
if (this.OptionalReasoning is true)
return ReasoningSupport.OPTIONAL;
// A "no" only contradicts the state it names:
return stated switch
{
ReasoningSupport.ALWAYS => this.AlwaysReasoning is false ? ReasoningSupport.NONE : ReasoningSupport.ALWAYS,
ReasoningSupport.ON_BY_DEFAULT => this.ReasoningByDefault is false || this.OptionalReasoning is false ? ReasoningSupport.NONE : ReasoningSupport.ON_BY_DEFAULT,
ReasoningSupport.OPTIONAL => this.OptionalReasoning is false ? ReasoningSupport.NONE : ReasoningSupport.OPTIONAL,
_ => ReasoningSupport.NONE,
};
}
public List<Capability> ApplyTo(IEnumerable<Capability> automaticCapabilities)
{
var mergedCapabilities = automaticCapabilities.Distinct().ToList();
@@ -1,4 +1,6 @@
using AIStudio.Provider;
using AIStudio.Models;
using AIStudio.Models.Registry;
using AIStudio.Provider;
using AIStudio.Provider.HuggingFace;
namespace AIStudio.Settings;
@@ -69,6 +71,53 @@ public static partial class ProviderExtensions
return new string(normalized[..length]);
}
/// <summary>
/// Everything the app knows about the model this provider instance is configured with.
/// </summary>
/// <remarks>
/// The one door to that question. Behind it stand the links of the chain, in the order they
/// win: what the person said about their own installation, then what the rules worked out from
/// the name, then what the app assumes when nothing else said anything.
/// </remarks>
/// <param name="provider">The configured provider.</param>
/// <returns>The profile of the configured model.</returns>
public static ModelProfile GetModelProfile(this Provider provider)
{
var stated = provider.UsedLLMProvider.GetModelProfile(provider.Model);
return provider.CapabilityOverrides?.ApplyTo(stated) ?? stated;
}
/// <summary>
/// Everything the rules know about a model at a provider, without anybody's own settings.
/// </summary>
/// <remarks>
/// What the expert dialog shows next to each switch as the automatic answer, so that a person
/// can see what they are overriding.
///
/// The assumed profile fills in where no rule stated a single capability. It fills in the
/// capabilities only: a modifier may well have said what the model is made for without any rule
/// saying what it can do, and an embedding model nobody wrote a rule for stays an embedding
/// model rather than turning into a chat model with an assumption attached.
/// </remarks>
/// <param name="provider">The LLM provider the model is reached through.</param>
/// <param name="model">The model, named the way that provider names it.</param>
/// <returns>The profile, which knows nothing when there is nothing to reach.</returns>
public static ModelProfile GetModelProfile(this LLMProviders provider, Model model)
{
//
// Without a provider there is nothing to reach the model through, and an empty name is what
// a provider reports before anybody picked one. Neither is a model we could assume anything
// about, so neither gets the assumption.
//
if (provider is LLMProviders.NONE || string.IsNullOrWhiteSpace(model.Id))
return ModelProfile.UNKNOWN;
var stated = ModelRegistry.Shared.Profile(provider, model.Id);
return stated.Capabilities is Capability.NONE
? stated with { Capabilities = ModelProfile.ASSUMED.Capabilities }
: stated;
}
/// <summary>
/// Get the capabilities of the model used by the configured provider.
/// </summary>