mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-06 19:09:40 +00:00
Resolve model profiles through the registry
This commit is contained in:
8 files changed
+551
-6
No files matched your search
@@ -0,0 +1,41 @@
|
||||
using static AIStudio.Provider.Capability;
|
||||
|
||||
namespace AIStudio.Models.Baidu;
|
||||
|
||||
/// <summary>
|
||||
/// ERNIE, from Baidu.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The line calls functions and the thinking checkpoints keep the channel open whatever the request
|
||||
/// says. The vision checkpoints are the exception, and the reason this family is written down at
|
||||
/// all: they run in a thinking and a non-thinking mode, and tool calling is not documented for them.
|
||||
/// Left to the assumption they would be offered tools nobody has said they can use.
|
||||
///
|
||||
/// The vision rule wins over the thinking rule by the latter stepping aside rather than by being
|
||||
/// less specific: ERNIE ships a checkpoint which is both, and two rules claiming it with the same
|
||||
/// right would be a coin toss.
|
||||
/// </remarks>
|
||||
public sealed class ErnieFamily : ModelFamily
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override ModelVendor Vendor => ModelVendor.BAIDU;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ModelSource Source => new("https://ernie.baidu.com/blog/", new DateOnly(2026, 9, 12), "Ported unchanged from the ERNIE block of ProviderExtensions.OpenSource.cs.");
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Declare(ModelFamilyBuilder builder)
|
||||
{
|
||||
builder.Rule("ernie").AsSubstring()
|
||||
.Capabilities(TEXT_INPUT | TEXT_OUTPUT | FUNCTION_CALLING)
|
||||
.Apis(CHAT_COMPLETION_API);
|
||||
|
||||
builder.Rule("ernie").AsSubstring().AlsoContains("thinking").NotContains("vl").Inherits()
|
||||
.Reasoning(ReasoningSupport.ALWAYS);
|
||||
|
||||
builder.Rule("ernie").AsSubstring().AlsoContains("vl")
|
||||
.Capabilities(TEXT_INPUT | MULTIPLE_IMAGE_INPUT | TEXT_OUTPUT)
|
||||
.Apis(CHAT_COMPLETION_API)
|
||||
.Reasoning(ReasoningSupport.OPTIONAL);
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,31 @@ public readonly record struct ModelProfile
|
||||
/// </remarks>
|
||||
public static readonly ModelProfile UNKNOWN = new();
|
||||
|
||||
/// <summary>
|
||||
/// What the app assumes about a model when no rule says anything about it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Hugging Face alone carries more than a hundred thousand models, so falling through here is
|
||||
/// the normal case rather than a gap somebody forgot to close. The assumption describes what an
|
||||
/// instruction-tuned model of the last few years does: it reads and writes text, it speaks the
|
||||
/// chat completion API, and it calls functions.
|
||||
///
|
||||
/// Tool calling is the part that was weighed rather than observed. Counted over the corpus, 17
|
||||
/// of the models which reach this answer would be described wrongly without it and 8 with it --
|
||||
/// and those 8 are named, in WithoutToolCallingFamily. A model that is offered tools it cannot
|
||||
/// use fails visibly, and the person turns tool calling off in the expert settings; a model
|
||||
/// that is never offered any fails invisibly, because nothing ever asks it. On top of that, a
|
||||
/// model released from here on is far more likely to call functions than not.
|
||||
///
|
||||
/// This is the whole assumption. Everything else stays unknown on purpose: a context window
|
||||
/// nobody stated is not 4096 tokens, and a model whose name says nothing about images does not
|
||||
/// get image input for free -- that is what the expert settings and the model plugins are for.
|
||||
/// </remarks>
|
||||
public static readonly ModelProfile ASSUMED = new()
|
||||
{
|
||||
Capabilities = Capability.TEXT_INPUT | Capability.TEXT_OUTPUT | Capability.CHAT_COMPLETION_API | Capability.FUNCTION_CALLING,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// What the model can do.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
using static AIStudio.Provider.Capability;
|
||||
|
||||
namespace AIStudio.Models.ServiceNow;
|
||||
|
||||
/// <summary>
|
||||
/// Apriel, from ServiceNow.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The Thinker models see, and they always reason: their default chat template opens the thinking
|
||||
/// channel, so there is nothing to switch on and nothing to switch off.
|
||||
///
|
||||
/// This family exists although the line is a small one, and the reason is the tool tokens. They
|
||||
/// arrived with 1.6; 1.5 has none. Left to the assumption, 1.5 would be offered tools it cannot
|
||||
/// use -- which is the one direction the switch-over must not take, because nobody decided it and
|
||||
/// nothing would show it until a request comes back as an error.
|
||||
/// </remarks>
|
||||
public sealed class AprielFamily : ModelFamily
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override ModelVendor Vendor => ModelVendor.SERVICE_NOW;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ModelSource Source => new("https://huggingface.co/ServiceNow-AI/Apriel-1.5-15b-Thinker", new DateOnly(2026, 9, 12), "Ported unchanged from the Apriel block of ProviderExtensions.OpenSource.cs.");
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Declare(ModelFamilyBuilder builder)
|
||||
{
|
||||
builder.Rule("apriel").AsSubstring()
|
||||
.Capabilities(TEXT_INPUT | MULTIPLE_IMAGE_INPUT | TEXT_OUTPUT | FUNCTION_CALLING)
|
||||
.Apis(CHAT_COMPLETION_API)
|
||||
.Reasoning(ReasoningSupport.ALWAYS);
|
||||
|
||||
builder.Rule("apriel-1.5").AsSubstring().Inherits()
|
||||
.Removes(FUNCTION_CALLING);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user