mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-04 15:24:28 +00:00
Rebuilt how AI Studio knows what a model can do (#960)
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Verify (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Verify (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
This commit is contained in:
1 parent
d21e09dd1e
commit
d85b4e71b6
287 files changed
+18341
-3677
No files matched your search
@@ -0,0 +1,404 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
|
||||
using AIStudio.Models.Matching;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
|
||||
using Lua;
|
||||
|
||||
namespace AIStudio.Models.Plugins;
|
||||
|
||||
/// <summary>
|
||||
/// What an organization states about a set of model names, read from one of its model plugins.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A declaration says exactly what a family in the source says, and it is measured by the same
|
||||
/// engine: a pattern, what the models matching it can do, and where that was read. What it must not
|
||||
/// be is half a statement. A declaration replaces what the built-in rules would have answered, so
|
||||
/// one which named a context window and nothing else would take away every capability the rules
|
||||
/// knew -- which is why stating the capabilities is not optional here.
|
||||
///
|
||||
/// Whoever only wants to correct one number for their own installation has the better tool already:
|
||||
/// the expert settings of the configured provider, which a configuration plugin writes as well.
|
||||
/// A model plugin is for the models the built-in rules do not know, or know wrongly.
|
||||
/// </remarks>
|
||||
public sealed record ModelDeclaration : ILivePluginContent
|
||||
{
|
||||
/// <summary>
|
||||
/// Which model names this declaration answers for.
|
||||
/// </summary>
|
||||
public required MatchPattern Pattern { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// What it states about them.
|
||||
/// </summary>
|
||||
public required ModelProfileChange Change { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Where that was read, and when somebody last looked.
|
||||
/// </summary>
|
||||
public required ModelSource Source { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// What the rule built from this declaration names as its origin, so a conflict can name both sides.
|
||||
/// </summary>
|
||||
public required string Origin { get; init; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid EnterpriseConfigurationPluginId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// What identifies this declaration when two plugins collide.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The pattern itself, because that is what a collision is here: two declarations claiming
|
||||
/// exactly the same names. They would otherwise both enter the index and tie there, and a tie
|
||||
/// is something only a person can settle. Two declarations about different names never meet.
|
||||
/// </remarks>
|
||||
public string Id => this.Pattern.Signature();
|
||||
|
||||
/// <summary>
|
||||
/// Turns the declaration into a rule of the matching engine.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Always a selector, never a modifier: a plugin states what a model is, not how to adjust
|
||||
/// somebody else's answer about it. And never with an explicit rank -- a declaration already
|
||||
/// comes before the built-in rules, so within the plugins the computed specificity decides,
|
||||
/// exactly as it does in the source.
|
||||
/// </remarks>
|
||||
/// <returns>The rule.</returns>
|
||||
public ModelRule ToRule() => new(this.Pattern, ModelRuleKind.SELECTOR, this.Change, this.Origin);
|
||||
|
||||
/// <summary>
|
||||
/// Reads one entry of a model plugin's MODELS table.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Anything it cannot read is rejected as a whole rather than read in part. A declaration is
|
||||
/// one statement, and half of one would answer for the models it matches just as firmly as a
|
||||
/// complete one -- with whatever the unreadable half was supposed to say silently missing.
|
||||
/// </remarks>
|
||||
/// <param name="index">Which entry of the table this is, so a warning can name it.</param>
|
||||
/// <param name="table">The entry.</param>
|
||||
/// <param name="pluginId">The plugin which declared it.</param>
|
||||
/// <param name="origin">What the resulting rule names as its origin.</param>
|
||||
/// <param name="logger">Where to report what could not be read.</param>
|
||||
/// <param name="declaration">The declaration, when the entry could be read.</param>
|
||||
/// <returns>True, when the entry could be read.</returns>
|
||||
public static bool TryParse(int index, LuaTable table, Guid pluginId, string origin, ILogger logger, [NotNullWhen(true)] out ModelDeclaration? declaration)
|
||||
{
|
||||
declaration = null;
|
||||
|
||||
if (!TryReadText(table, "PATTERN", out var patternText))
|
||||
{
|
||||
logger.LogWarning("The model declaration {DeclarationIndex} does not name a PATTERN. Every declaration has to say which model names it answers for. (model plugin id: {PluginId})", index, pluginId);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!MatchPattern.IsNormalized(patternText))
|
||||
{
|
||||
logger.LogWarning("The model declaration {DeclarationIndex} names the PATTERN '{Pattern}', which is not written the way a model name is written and can therefore never match anything. Write it as '{NormalizedPattern}'. (model plugin id: {PluginId})", index, patternText, new ModelId(patternText).Normalized, pluginId);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryReadEnum<MatchKind>(table, "MATCH", index, pluginId, logger, out var matchKind, MatchKind.SEGMENT))
|
||||
return false;
|
||||
|
||||
if (!TryReadNameParts(table, "ALSO_CONTAINS", index, pluginId, logger, out var alsoContains))
|
||||
return false;
|
||||
|
||||
if (!TryReadNameParts(table, "NOT_CONTAINS", index, pluginId, logger, out var notContains))
|
||||
return false;
|
||||
|
||||
if (!TryReadOptionalEnum<LLMProviders>(table, "ONLY_ON", index, pluginId, logger, out var onlyOn))
|
||||
return false;
|
||||
|
||||
if (!TryReadOptionalEnum<ModelVendor>(table, "ONLY_FROM", index, pluginId, logger, out var onlyFrom))
|
||||
return false;
|
||||
|
||||
if (!TryReadCapabilities(table, index, pluginId, logger, out var capabilities))
|
||||
return false;
|
||||
|
||||
if (!TryReadEnum<ReasoningSupport>(table, "REASONING", index, pluginId, logger, out var reasoning, ReasoningSupport.NONE))
|
||||
return false;
|
||||
|
||||
if (!TryReadEnum<ModelKind>(table, "KIND", index, pluginId, logger, out var modelKind, ModelKind.CHAT))
|
||||
return false;
|
||||
|
||||
if (!TryReadContextWindow(table, index, pluginId, logger, out var context))
|
||||
return false;
|
||||
|
||||
if (!TryReadTokenizer(table, index, pluginId, logger, out var tokenizer))
|
||||
return false;
|
||||
|
||||
if (!TryReadImageLimits(table, index, pluginId, logger, out var images))
|
||||
return false;
|
||||
|
||||
if (!TryReadSource(table, index, pluginId, logger, out var source))
|
||||
return false;
|
||||
|
||||
declaration = new()
|
||||
{
|
||||
Pattern = new()
|
||||
{
|
||||
Kind = matchKind,
|
||||
Text = patternText,
|
||||
AlsoContains = alsoContains,
|
||||
NotContains = notContains,
|
||||
OnlyOn = onlyOn,
|
||||
OnlyFrom = onlyFrom,
|
||||
},
|
||||
|
||||
Change = new()
|
||||
{
|
||||
Adds = capabilities,
|
||||
Reasoning = reasoning,
|
||||
Kind = modelKind,
|
||||
Context = context,
|
||||
Tokenizer = tokenizer,
|
||||
Images = images,
|
||||
},
|
||||
|
||||
Source = source,
|
||||
Origin = origin,
|
||||
EnterpriseConfigurationPluginId = pluginId,
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadText(LuaTable table, string key, out string text)
|
||||
{
|
||||
text = string.Empty;
|
||||
if (!table.TryGetValue(key, out var value) || !value.TryRead<string>(out var read))
|
||||
return false;
|
||||
|
||||
text = read;
|
||||
return !string.IsNullOrWhiteSpace(text);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a key which names one member of an enum, falling back to a default when it is absent.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A member is named, never combined and never numbered. Enum.TryParse accepts both of those,
|
||||
/// so the check that the value is actually a member of the enum is what rejects them -- writing
|
||||
/// two kinds into one key, or a number nobody can read back, would otherwise pass.
|
||||
/// </remarks>
|
||||
private static bool TryReadEnum<T>(LuaTable table, string key, int index, Guid pluginId, ILogger logger, out T parsed, T fallback) where T : struct, Enum
|
||||
{
|
||||
parsed = fallback;
|
||||
if (!table.TryGetValue(key, out var value))
|
||||
return true;
|
||||
|
||||
if (value.TryRead<string>(out var text) && Enum.TryParse(text, true, out parsed) && Enum.IsDefined(parsed))
|
||||
return true;
|
||||
|
||||
logger.LogWarning("The model declaration {DeclarationIndex} states an unknown {Key}. Valid values are: {ValidValues}. (model plugin id: {PluginId})", index, key, string.Join(", ", Enum.GetNames<T>()), pluginId);
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryReadOptionalEnum<T>(LuaTable table, string key, int index, Guid pluginId, ILogger logger, out T? parsed) where T : struct, Enum
|
||||
{
|
||||
parsed = null;
|
||||
if (!table.TryGetValue(key, out var value))
|
||||
return true;
|
||||
|
||||
if (value.TryRead<string>(out var text) && Enum.TryParse<T>(text, true, out var read) && Enum.IsDefined(read))
|
||||
{
|
||||
parsed = read;
|
||||
return true;
|
||||
}
|
||||
|
||||
logger.LogWarning("The model declaration {DeclarationIndex} states an unknown {Key}. Valid values are: {ValidValues}. (model plugin id: {PluginId})", index, key, string.Join(", ", Enum.GetNames<T>()), pluginId);
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryReadNameParts(LuaTable table, string key, int index, Guid pluginId, ILogger logger, out string[] nameParts)
|
||||
{
|
||||
nameParts = [];
|
||||
if (!table.TryGetValue(key, out var value))
|
||||
return true;
|
||||
|
||||
if (!value.TryRead<LuaTable>(out var partsTable))
|
||||
{
|
||||
logger.LogWarning("The model declaration {DeclarationIndex} states {Key}, but not as a list of name parts. (model plugin id: {PluginId})", index, key, pluginId);
|
||||
return false;
|
||||
}
|
||||
|
||||
var read = new string[partsTable.ArrayLength];
|
||||
for (var i = 1; i <= partsTable.ArrayLength; i++)
|
||||
{
|
||||
if (!partsTable[i].TryRead<string>(out var namePart) || !MatchPattern.IsNormalized(namePart))
|
||||
{
|
||||
logger.LogWarning("The model declaration {DeclarationIndex} states a {Key} entry which is not a name part written the way a model name is written. (model plugin id: {PluginId})", index, key, pluginId);
|
||||
return false;
|
||||
}
|
||||
|
||||
read[i - 1] = namePart;
|
||||
}
|
||||
|
||||
nameParts = read;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the capabilities, which every declaration has to state.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The three reasoning words are rejected rather than dropped. They are the vocabulary of the
|
||||
/// expert settings, where a person answers three questions with yes and no; here one key says
|
||||
/// how a model reasons, and the three of them together can state answers no model can give.
|
||||
/// </remarks>
|
||||
private static bool TryReadCapabilities(LuaTable table, int index, Guid pluginId, ILogger logger, out Capability capabilities)
|
||||
{
|
||||
capabilities = Capability.NONE;
|
||||
if (!table.TryGetValue("CAPABILITIES", out var value) || !value.TryRead<LuaTable>(out var capabilitiesTable) || capabilitiesTable.ArrayLength is 0)
|
||||
{
|
||||
logger.LogWarning("The model declaration {DeclarationIndex} does not state its CAPABILITIES. A declaration replaces what AI Studio would otherwise know about these models, so it has to say what they can do. (model plugin id: {PluginId})", index, pluginId);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 1; i <= capabilitiesTable.ArrayLength; i++)
|
||||
{
|
||||
if (!capabilitiesTable[i].TryRead<string>(out var capabilityText) || !Enum.TryParse<Capability>(capabilityText, true, out var capability) || !Enum.IsDefined(capability) || capability is Capability.NONE or Capability.UNKNOWN)
|
||||
{
|
||||
logger.LogWarning("The model declaration {DeclarationIndex} states an unknown capability. Name one capability per entry, e.g. TEXT_INPUT. (model plugin id: {PluginId})", index, pluginId);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((capability & ModelProfile.REASONING_VOCABULARY) is not Capability.NONE)
|
||||
{
|
||||
logger.LogWarning("The model declaration {DeclarationIndex} states the capability {Capability}, which says how a model reasons. Use the REASONING key instead, which takes exactly one of: {ValidValues}. (model plugin id: {PluginId})", index, capability, string.Join(", ", Enum.GetNames<ReasoningSupport>()), pluginId);
|
||||
return false;
|
||||
}
|
||||
|
||||
capabilities |= capability;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadContextWindow(LuaTable table, int index, Guid pluginId, ILogger logger, out ContextWindow? context)
|
||||
{
|
||||
context = null;
|
||||
var raisableIsStated = table.TryGetValue("CONTEXT_WINDOW_RAISABLE_TO", out var raisableValue);
|
||||
if (!table.TryGetValue("CONTEXT_WINDOW", out var value))
|
||||
{
|
||||
if (!raisableIsStated)
|
||||
return true;
|
||||
|
||||
logger.LogWarning("The model declaration {DeclarationIndex} states CONTEXT_WINDOW_RAISABLE_TO without stating the CONTEXT_WINDOW it can be raised from. (model plugin id: {PluginId})", index, pluginId);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!value.TryRead<int>(out var defaultTokens) || defaultTokens <= 0)
|
||||
{
|
||||
logger.LogWarning("The model declaration {DeclarationIndex} states a CONTEXT_WINDOW which is not a number of tokens greater than zero. (model plugin id: {PluginId})", index, pluginId);
|
||||
return false;
|
||||
}
|
||||
|
||||
int? raisableTo = null;
|
||||
if (raisableIsStated)
|
||||
{
|
||||
if (!raisableValue.TryRead<int>(out var raisable) || raisable < defaultTokens)
|
||||
{
|
||||
logger.LogWarning("The model declaration {DeclarationIndex} states a CONTEXT_WINDOW_RAISABLE_TO which is not a number of tokens of at least the CONTEXT_WINDOW itself. (model plugin id: {PluginId})", index, pluginId);
|
||||
return false;
|
||||
}
|
||||
|
||||
raisableTo = raisable;
|
||||
}
|
||||
|
||||
context = ContextWindow.Of(defaultTokens, raisableTo);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadTokenizer(LuaTable table, int index, Guid pluginId, ILogger logger, out TokenizerRef? tokenizer)
|
||||
{
|
||||
tokenizer = null;
|
||||
var kindIsStated = table.TryGetValue("TOKENIZER_KIND", out _);
|
||||
var idIsStated = TryReadText(table, "TOKENIZER_ID", out var tokenizerId);
|
||||
if (!kindIsStated && !idIsStated)
|
||||
return true;
|
||||
|
||||
if (!kindIsStated || !idIsStated)
|
||||
{
|
||||
logger.LogWarning("The model declaration {DeclarationIndex} states only one half of its tokenizer. A tokenizer reference needs both TOKENIZER_KIND and TOKENIZER_ID, because the kind is what says how the ID would be resolved. (model plugin id: {PluginId})", index, pluginId);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryReadEnum<TokenizerKind>(table, "TOKENIZER_KIND", index, pluginId, logger, out var tokenizerKind, TokenizerKind.UNKNOWN))
|
||||
return false;
|
||||
|
||||
tokenizer = new(tokenizerKind, tokenizerId);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadImageLimits(LuaTable table, int index, Guid pluginId, ILogger logger, out ImageLimits? images)
|
||||
{
|
||||
images = null;
|
||||
if (!TryReadImageLimit(table, "MAX_IMAGES_PER_MESSAGE", index, pluginId, logger, out var maxPerMessage))
|
||||
return false;
|
||||
|
||||
if (!TryReadImageLimit(table, "MAX_IMAGES_PER_REQUEST", index, pluginId, logger, out var maxPerRequest))
|
||||
return false;
|
||||
|
||||
if (maxPerMessage.HasValue || maxPerRequest.HasValue)
|
||||
images = new(maxPerMessage, maxPerRequest);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads one of the two image limits.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Zero is a real answer, not a way of saying that nobody knows: an engine can be configured to
|
||||
/// take no images at all. Unknown is the key being absent.
|
||||
/// </remarks>
|
||||
private static bool TryReadImageLimit(LuaTable table, string key, int index, Guid pluginId, ILogger logger, out int? limit)
|
||||
{
|
||||
limit = null;
|
||||
if (!table.TryGetValue(key, out var value))
|
||||
return true;
|
||||
|
||||
if (!value.TryRead<int>(out var read) || read < 0)
|
||||
{
|
||||
logger.LogWarning("The model declaration {DeclarationIndex} states a {Key} which is not a number of images of zero or more. (model plugin id: {PluginId})", index, key, pluginId);
|
||||
return false;
|
||||
}
|
||||
|
||||
limit = read;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads where the declaration was read from, which it has to name.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The compiler asks a family in the source for its source, and the same reasoning holds here:
|
||||
/// a model card changes without telling anybody, and a statement nobody can check ages into a
|
||||
/// defect. An organization's declaration outlives whoever wrote it, so the page and the day are
|
||||
/// what lets the next administrator find out whether it still holds.
|
||||
/// </remarks>
|
||||
private static bool TryReadSource(LuaTable table, int index, Guid pluginId, ILogger logger, out ModelSource source)
|
||||
{
|
||||
source = new(string.Empty, default, string.Empty);
|
||||
if (!TryReadText(table, "SOURCE_URL", out var url))
|
||||
{
|
||||
logger.LogWarning("The model declaration {DeclarationIndex} does not name a SOURCE_URL. State where these models are described, e.g. a model card or a page of your own documentation. (model plugin id: {PluginId})", index, pluginId);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryReadText(table, "SOURCE_CHECKED_ON", out var checkedOnText) || !DateOnly.TryParseExact(checkedOnText, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var checkedOn))
|
||||
{
|
||||
logger.LogWarning("The model declaration {DeclarationIndex} does not name a SOURCE_CHECKED_ON as a date of the form YYYY-MM-DD. State the day somebody last read that page. (model plugin id: {PluginId})", index, pluginId);
|
||||
return false;
|
||||
}
|
||||
|
||||
TryReadText(table, "SOURCE_NOTE", out var note);
|
||||
source = new(url, checkedOn, note);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user