using AIStudio.Models.Matching; using AIStudio.Provider; namespace AIStudio.Models; /// /// One rule, while it is being stated. /// /// /// Everything left unsaid stays unsaid: a rule which says nothing about the context window does not /// claim that nobody knows it, it simply makes no statement, and whatever else does gets to keep /// its answer. That is what lets a variant state one sentence instead of repeating its family. /// /// The name, or the part of it, this rule answers for. In normalized form. /// Whether the rule chooses the model or adjusts the choice. /// What the rule names as its origin, which is the family's name. public sealed class ModelRuleBuilder(string patternText, ModelRuleKind ruleKind, string origin) { private readonly List alsoContains = []; private readonly List notContains = []; private MatchKind matchKind = MatchKind.SEGMENT; private LLMProviders? onlyOn; private ModelVendor? onlyFrom; private int explicitRank; private bool inheritsFromPrevious; private string? inheritsFromText; private Capability adds; private Capability removes; private ReasoningSupport? reasoning; private ModelKind? modelKind; private ContextWindow? context; private TokenizerRef? tokenizer; private ImageLimits? images; /// /// The text this rule answers for, before anything was stated about it. /// /// /// Read by the family builder before it builds anything, to find the texts which name more /// than one rule. /// internal string PatternText => patternText; /// /// The text is the whole model name. /// /// The rule, to go on stating. public ModelRuleBuilder AsExact() => this.MatchingAs(MatchKind.EXACT); /// /// The name begins with the text, and a name part ends there. /// /// The rule, to go on stating. public ModelRuleBuilder AsPrefix() => this.MatchingAs(MatchKind.PREFIX); /// /// The text appears in the name as one or more whole name parts. This is the default. /// /// The rule, to go on stating. public ModelRuleBuilder AsSegment() => this.MatchingAs(MatchKind.SEGMENT); /// /// The text appears anywhere in the name, boundaries or not. /// /// /// The last resort, for the names where a vendor glues things together. It claims the least and /// therefore loses against every other kind. /// /// The rule, to go on stating. public ModelRuleBuilder AsSubstring() => this.MatchingAs(MatchKind.SUBSTRING); /// /// Further name parts the model's name has to carry. /// /// The name parts, each in normalized form. /// The rule, to go on stating. public ModelRuleBuilder AlsoContains(params string[] nameParts) { this.alsoContains.AddRange(nameParts); return this; } /// /// Name parts whose presence rules this rule out. /// /// The name parts, each in normalized form. /// The rule, to go on stating. public ModelRuleBuilder NotContains(params string[] nameParts) { this.notContains.AddRange(nameParts); return this; } /// /// Restricts this rule to one provider. /// /// The provider serving the model. /// The rule, to go on stating. public ModelRuleBuilder OnlyOn(LLMProviders provider) { this.onlyOn = provider; return this; } /// /// Restricts this rule to models of one vendor. /// /// Who built the model. /// The rule, to go on stating. public ModelRuleBuilder OnlyFrom(ModelVendor vendor) { this.onlyFrom = vendor; return this; } /// /// What the model can do. /// /// The capabilities, combined with the or operator. /// The rule, to go on stating. public ModelRuleBuilder Capabilities(Capability capabilities) { this.adds |= capabilities; return this; } /// /// Which APIs the model answers through. /// /// /// The same thing as stating a capability, said separately because it reads as a different kind /// of sentence: what a model is able to do, and how one talks to it. /// /// The API capabilities, combined with the or operator. /// The rule, to go on stating. public ModelRuleBuilder Apis(Capability apis) { this.adds |= apis; return this; } /// /// What the model cannot do, applied after everything it can. /// /// The capabilities, combined with the or operator. /// The rule, to go on stating. public ModelRuleBuilder Removes(Capability capabilities) { this.removes |= capabilities; return this; } /// /// How the model reasons. /// /// The way it reasons. /// The rule, to go on stating. public ModelRuleBuilder Reasoning(ReasoningSupport support) { this.reasoning = support; return this; } /// /// What the model is made for, when it is not a chat model. /// /// The kind of model. /// The rule, to go on stating. public ModelRuleBuilder Kind(ModelKind kind) { this.modelKind = kind; return this; } /// /// How much the model reads and writes in one conversation. /// /// What it does as it ships. /// What an operator can raise it to, where that is documented. /// The rule, to go on stating. public ModelRuleBuilder ContextWindow(int defaultTokens, int? raisableTo = null) { this.context = Models.ContextWindow.Of(defaultTokens, raisableTo); return this; } /// /// Takes back a context window this rule inherited, because nobody states one for this variant. /// /// /// A variant can already hand back a capability its family granted; a number has to be handed /// back too. Without this, a generation whose window nobody documents would quietly carry the /// number of the generation it inherits from -- and the app would then show a person that /// number as a fact about their model. /// /// The rule, to go on stating. public ModelRuleBuilder WithoutContextWindow() { this.context = Models.ContextWindow.UNKNOWN; return this; } /// /// Which tokenizer counts this model's tokens. /// /// What sort of tokenizer it is. /// Its name, in whatever spelling that sort uses. /// The rule, to go on stating. public ModelRuleBuilder Tokenizer(TokenizerKind kind, string id) { this.tokenizer = new TokenizerRef(kind, id); return this; } /// /// How many images the model accepts. /// /// How many fit into one message, where that is documented. /// How many fit into one request, where that is documented. /// The rule, to go on stating. public ModelRuleBuilder Images(int? maxPerMessage = null, int? maxPerRequest = null) { this.images = new ImageLimits(maxPerMessage, maxPerRequest); return this; } /// /// Takes everything the rule stated before this one and goes on from there. /// /// The rule, to go on stating. public ModelRuleBuilder Inherits() { this.inheritsFromPrevious = true; return this; } /// /// Takes everything one particular rule of this family stated and goes on from there. /// /// /// Worth preferring over the plain form in a family with more than one generation: naming the /// rule survives somebody reordering the file, while "the one before" does not. /// /// The text of the rule to inherit from. /// The rule, to go on stating. public ModelRuleBuilder InheritsFrom(string inheritedPatternText) { this.inheritsFromText = inheritedPatternText; return this; } /// /// Moves this rule ahead of, or behind, everything the computed specificity would decide. /// /// /// The emergency exit, and it is meant to stay unused. /// /// Positive to move the rule ahead, negative to push it back. /// /// What the computation gets wrong here. It is not kept: it stands in the source so that the /// next reader finds an explanation next to the rank instead of a number nobody can account for. /// /// The rule, to go on stating. public ModelRuleBuilder Rank(int rank, string reason) { // // Asking for a reason is what the second parameter does; insisting that it says something // is what keeps an empty string from passing for one. Without this, the way to write a rank // nobody can account for is still open, and it is the one thing the computed specificity // exists to get rid of. // if (string.IsNullOrWhiteSpace(reason)) throw new ArgumentException($"The rule \"{patternText}\" of {origin} sets the rank {rank} without saying what the computed specificity gets wrong here.", nameof(reason)); this.explicitRank = rank; return this; } /// /// What this rule goes on from, if it goes on from anything. /// /// What the rules stated so far, by their pattern text. /// The texts which name more than one rule of this family. /// What the rule stated right before this one, if there was one. /// The statement to start from, or null when the rule states everything itself. internal ModelProfileChange? InheritanceBasis(IReadOnlyDictionary byPatternText, IReadOnlySet statedMoreThanOnce, ModelProfileChange? previous) { if (this.inheritsFromText is not null) { // // A text stated twice names two rules, and taking whichever happened to come last // would be a coin toss nobody sees. The way out is the plain form, which says "the one // before" and means exactly one rule. // if (statedMoreThanOnce.Contains(this.inheritsFromText)) throw new InvalidOperationException($"The rule \"{patternText}\" of {origin} inherits from \"{this.inheritsFromText}\", which this family states more than once. Use Inherits() right after the rule to go on from, or give the rule a text of its own."); return byPatternText.TryGetValue(this.inheritsFromText, out var named) ? named : throw new InvalidOperationException($"The rule \"{patternText}\" of {origin} inherits from \"{this.inheritsFromText}\", which this family does not state before it."); } if (!this.inheritsFromPrevious) return null; return previous ?? throw new InvalidOperationException($"The rule \"{patternText}\" of {origin} inherits, but it is the first rule this family states."); } /// /// Turns the statement into a rule. /// /// What to go on from, or null to state everything from nothing. /// The rule. internal ModelRule Build(ModelProfileChange? basis) { var pattern = new MatchPattern { Kind = this.matchKind, Text = patternText, AlsoContains = this.alsoContains.ToArray(), NotContains = this.notContains.ToArray(), OnlyOn = this.onlyOn, OnlyFrom = this.onlyFrom, ExplicitRank = this.explicitRank, }; return new(pattern, ruleKind, this.ChangeOnTopOf(basis), origin); } private ModelProfileChange ChangeOnTopOf(ModelProfileChange? basis) => new() { // // What this rule states wins over what it inherited, in both directions: a variant may take // away what its family has, and it may hand back what its family took away. // Adds = ((basis?.Adds ?? Capability.NONE) | this.adds) & ~this.removes, Removes = ((basis?.Removes ?? Capability.NONE) | this.removes) & ~this.adds, Reasoning = this.reasoning ?? basis?.Reasoning, Kind = this.modelKind ?? basis?.Kind, Context = this.context ?? basis?.Context, Tokenizer = this.tokenizer ?? basis?.Tokenizer, Images = this.images ?? basis?.Images, }; private ModelRuleBuilder MatchingAs(MatchKind kind) { this.matchKind = kind; return this; } }